modify_ldt(2)

๐ŸŒธ NAME ๐ŸŒธ

modify_ldt โ€” get or set a per-process LDT entry ๐Ÿ’–

okay so real talk, this page comes to you straight from the System Calls Manual, and bestie, we are about to go deep into kernel lore. strap in. ๐ŸŽ€


๐Ÿ“š LIBRARY ๐Ÿ“š

Standard C library (libc, -lc)


โœจ SYNOPSIS โœจ

#include <asm/ldt.h>         /* Definition of struct user_desc */
#include <sys/syscall.h>     /* Definition of SYS_* constants */
#include <unistd.h>

int syscall(unsigned long bytecount;
            SYS_modify_ldt, int func, void ptr[bytecount],
            unsigned long bytecount);
spoiler alert bestie: glibc provides no wrapper for modify_ldt(), which means you're forced to use syscall(2). no cute little convenience function, no safety net, you're raw-dogging the syscall interface. iconic but also ๐Ÿ’€

๐Ÿ’– DESCRIPTION ๐Ÿ’–

okay so modify_ldt() reads or writes the local descriptor table (LDT) for a process. and what even is the LDT? it's an array of segment descriptors that user code can reference. Linux is like "yes queen, you may configure a per-process LDT" โ€” and when we say per-process we actually mean per-mm ๐Ÿซถ. if you want the full tea on the LDT, go read the Intel Software Developer's Manual or the AMD Architecture Programming Manual. yes, actual CPU manuals. this is that kind of party. ๐Ÿงš

Here's how func runs the show:

  • When func is 0, modify_ldt() reads the LDT into the memory pointed to by ptr. the number of bytes read is the smaller of bytecount and the actual size of the LDT โ€” although the kernel may act as though the LDT is padded with additional trailing zero bytes ~~mysterious~~. on success, modify_ldt() returns the number of bytes read. โœจ
  • When func is 1 or 0x11, modify_ldt() modifies the LDT entry indicated by ptr->entry_number. ptr points to a user_desc structure, and bytecount must equal the size of this structure. no wiggle room. ๐Ÿ’…

The user_desc structure is defined in <asm/ldt.h> as:

struct user_desc {
    unsigned int  entry_number;
    unsigned int  base_addr;
    unsigned int  limit;
    unsigned int  seg_32bit:1;
    unsigned int  contents:2;
    unsigned int  read_exec_only:1;
    unsigned int  limit_in_pages:1;
    unsigned int  seg_not_present:1;
    unsigned int  useable:1;
};

btw in Linux 2.4 and earlier, this structure was named modify_ldt_ldt_s. vintage. ๐Ÿ“

The contents field is the segment type โ€” data, expand-down data, non-conforming code, or conforming code. the other fields match their descriptions in the CPU manual, although modify_ldt() cannot set the hardware-defined "accessed" bit described in the CPU manual. nope. not allowed. ๐Ÿ™ˆ

A user_desc is considered "empty" if read_exec_only and seg_not_present are set to 1 and all of the other fields are 0. an LDT entry can be cleared by setting it to an "empty" user_desc โ€” or, if func is 1, by setting both base and limit to 0. โœจ

A conforming code segment (i.e., one with contents==3) will be rejected if func is 1, or if seg_not_present is 0. lowkey rude but okay. ๐Ÿ˜ญ

When func is 2, modify_ldt() will read zeros. this appears to be a leftover from Linux 2.4. it's giving fossil energy. ๐Ÿ’€


๐Ÿ”ฅ RETURN VALUE ๐Ÿ”ฅ

on success, modify_ldt() returns either the actual number of bytes read (for reading) or 0 (for writing). ๐Ÿซถ

on failure, it returns -1 and sets errno to indicate the error. as always bestie, check your errno. ๐Ÿ’…


๐Ÿ˜ญ ERRORS ๐Ÿ˜ญ

  • EFAULT โ€” ptr points outside the address space.
  • EINVAL โ€” ptr is 0, or func is 1 and bytecount is not equal to the size of the structure user_desc, or func is 1 or 0x11 and the new LDT entry has invalid values.
  • ENOSYS โ€” func is neither 0, 1, 2, nor 0x11.

๐ŸŽ€ STANDARDS ๐ŸŽ€

Linux. that's it, that's the standard. ๐Ÿ‡ฑ๐Ÿ‡ฎ๐Ÿ‡ณ๐Ÿ‡บ๐Ÿ‡ฝ


๐Ÿ’… NOTES ๐Ÿ’…

Do not use modify_ldt() for thread-local storage. big no. it slows down context switches and only supports a limited number of threads. threading libraries should use set_thread_area(2) or arch_prctl(2) instead โ€” except on extremely old kernels that do not support those system calls. iykyk. ๐ŸŒˆ

The normal use for modify_ldt() is to run legacy 16-bit or segmented 32-bit code. however, not all kernels allow 16-bit segments to be installed. sad but true. ๐Ÿฆ‹

And even on 64-bit kernels, modify_ldt() cannot be used to create a long mode (i.e., 64-bit) code segment. the undocumented field "lm" in user_desc is not useful, and, despite its name, does not result in a long mode segment. the name is a lie. the name is catfishing you. ๐Ÿ’€


๐Ÿ’€ BUGS ๐Ÿ’€

On 64-bit kernels before Linux 3.19, setting the "lm" bit in user_desc prevents the descriptor from being considered empty. keep in mind that the "lm" bit does not exist in the 32-bit headers โ€” but these buggy kernels will still notice the bit even when it's set in a 32-bit process. yeah. it's a whole situation. ๐Ÿ™ˆ


๐Ÿฆ‹ SEE ALSO ๐Ÿฆ‹

arch_prctl(2), set_thread_area(2), vm86(2)


okay bestie that's the whole page, every flag and bit and byte accounted for, no cap. ๐Ÿ‘‘

this came to you from Linux man-pages 6.20, dated 2026-02-08. mwah. ๐Ÿ’–โœจ๐Ÿซถ

๐Ÿ“ฆ where this page comes from
  • arch rolling: man-pages 6.20-1 ยท BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
  • nixpkgs unstable: man-pages 6.20
  • ๐Ÿ“œ the page's license: GPL-2.0-or-later
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms