The Thread Control Block
Every thread needs a private scratchpad of bookkeeping the runtime can reach instantly and without knowing the thread’s identity — its kernel thread ID, its list of held robust locks, its thread-local-storage pointers, its cancellation state, its stack canary. That scratchpad is the Thread Control Block (TCB), which glibc implements as
struct pthread(defined innptl/descr.h). It is anchored by the thread pointer: the%fssegment base on x86-64, which the code reaches with a single segment-relative memory access. The first bytes of the block are the ABI-definedtcbhead_t— holding the self-pointer, the DTV pointer, and, at fixed offsets, the security cookiesstack_guardandpointer_guard. The rest is glibc’s private per-thread state. This note dissects that structure and how the thread pointer is set; the storage it anchors is Thread-Local Storage and the addressing that reaches it is TLS Models and Access.
Mental Model
Picture the thread pointer as a this pointer for the running thread. Just as an object method reaches its fields through this, every thread reaches its own descriptor through %fs — and because %fs is set per thread by the kernel, the same instruction in shared code lands on a different descriptor in each thread. On x86-64 (a Variant II layout, see TLS Models and Access) the TCB sits at the top of the per-thread block with the TLS blocks below it, and the thread pointer points at the TCB itself. The very first word, %fs:0, is a self-pointer back to the descriptor, so code that needs the descriptor’s address (not just to index off it) can load it in one instruction.
flowchart TB subgraph BLOCK["Per-thread memory block (x86-64, Variant II)"] direction TB TLSN["…other modules' TLS blocks…"] TLS1["executable's TLS block<br/>(negative offsets from TP)"] subgraph PTHREAD["struct pthread (the TCB)"] HEAD["tcbhead_t (ABI header):<br/>[0x00] tcb [0x08] dtv → DTV<br/>[0x10] self [0x18] multiple_threads/gscope<br/>[0x20] sysinfo [0x28] stack_guard (canary)<br/>[0x30] pointer_guard (PTR_MANGLE)"] PRIV["glibc private fields:<br/>list, tid, robust_head,<br/>cancelhandling, stackblock,<br/>specific[] (pthread_key TSD),<br/>start_routine, result, joinstate…"] end end FS["%fs base (thread pointer)"] --> HEAD FS -. "%fs:0 = self-pointer" .-> HEAD HEAD -->|dtv| DTV["Dynamic Thread Vector"] TLS1 -.->|"%fs:x@tpoff (negative)"| PTHREAD
The x86-64 thread control block. What it shows: the %fs base points at struct pthread; its ABI header tcbhead_t exposes the DTV pointer and the security cookies at fixed offsets (stack_guard at %fs:0x28, pointer_guard at %fs:0x30), with static TLS blocks living at negative offsets below and glibc’s private per-thread fields following. The insight to take: the fixed offsets are a contract — the compiler emits %fs:0x28 to read the stack canary and %fs:(dtv) to reach TLS without ever computing the descriptor’s address, which is why TLS and stack-protector accesses are single instructions.
The ABI Header: tcbhead_t
The head of the block is fixed by the ABI so that compiler-generated code and the dynamic linker can find well-known fields at well-known offsets, independent of glibc’s private layout. On x86-64 it is (sysdeps/x86_64/nptl/tls.h, verbatim):
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int unused_vgetcpu_cache[2];
unsigned int feature_1;
int __glibc_unused1;
void *__private_tm[4];
void *__private_ss;
unsigned long long int ssp_base;
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;Walking the load-bearing fields:
tcb(offset0x00) andself(0x10) both point back at the descriptor.selfis “Pointer to the thread descriptor,” so%fs:0(which istcb) is the self-pointer that makesmov %fs:0, %raxload the TP into a register. The comment “For now the thread descriptor is at the same address” reflects that on x86-64 the TCB andstruct pthreadcoincide.dtv(0x08) is the pointer to this thread’s Dynamic Thread Vector — the array__tls_get_addrand the dynamic TLS models index to find module blocks.multiple_threads(0x18) is a fast single-threaded flag: glibc checks it to skip locking and atomic overhead while a process is still single-threaded (see the mutex fast path in pthread Synchronization and Futexes).sysinfo(0x20) historically cached the vDSO/__kernel_vsyscallentry point for fast system calls.stack_guard(0x28) andpointer_guard(0x30) are the two security cookies, dissected below.
The tcb/self split and the placement of stack_guard at exactly %fs:0x28 are ABI promises the compiler hard-codes: GCC’s stack-protector reads %fs:0x28 directly, so this offset cannot change without breaking every stack-protected binary.
The Security Cookies
Two words in the header are secrets the loader randomizes per process to harden against memory-corruption exploits.
stack_guard (%fs:0x28) is the stack canary used by -fstack-protector. GCC’s prologue copies this value between a function’s locals and its saved return address; the epilogue re-reads %fs:0x28 and compares. A linear buffer overflow that smashes the return address must first overwrite the canary, and since the attacker cannot know the randomized value, the mismatch is caught and __stack_chk_fail aborts the process (Wikipedia: Buffer overflow protection). Storing the canary in the TCB rather than a global is what makes it per-thread and cheap to read — details in Thread Stacks and Stack Guards, which contrasts this software canary with the hardware guard page.
pointer_guard (%fs:0x30) backs glibc’s PTR_MANGLE/PTR_DEMANGLE macros. Sensitive function pointers stored in writable memory — atexit handlers, longjmp targets, some GOT-adjacent state — are XOR-scrambled with this per-process cookie (and rotated) before being stored, and unscrambled before use. An attacker who overwrites such a pointer cannot substitute a useful address without knowing the cookie. Both cookies are seeded from the kernel’s auxiliary vector entry AT_RANDOM during process startup in [[_start and __libc_start_main|__libc_start_main]].
struct pthread: glibc’s Private State
Following the ABI header (which appears as the header member, overlaid via a union with the raw multiple_threads/gscope_flag fields for the non-threaded TLS-only case), struct pthread in nptl/descr.h carries glibc’s per-thread bookkeeping. The load-bearing members:
list_t list— links this descriptor into glibc’s internal list of all threads (used for teardown andpthread_keycleanup).pid_t tid— the kernel thread ID, filled by the kernel atclonetime viaCLONE_PARENT_SETTID. This is the same word the kernel zeroes and futex-wakes on exit underCLONE_CHILD_CLEARTID, which is precisely how [[POSIX Threads and NPTL|pthread_join]] detects termination.struct robust_list_head robust_head— the head of this thread’s robust-futex list, registered with the kernel viaset_robust_list. If the thread dies holding robust mutexes, the kernel walks this list to mark them owner-dead (see pthread Synchronization and Futexes).int cancelhandling— a bitfield packing cancellation and lifecycle state (CANCELSTATE_BIT,CANCELTYPE_BIT,CANCELING_BIT,CANCELED_BIT,EXITING_BIT,TERMINATED_BIT,SETXID_BIT).void *stackblock; size_t stackblock_size; size_t guardsize; size_t reported_guardsize— describe the thread’s stack allocation, including the guard region, and are what glibc’s stack cache reuses.struct pthread_key_data specific_1stblock[…]andstruct pthread_key_data *specific[…]— the two-level sparse array backing POSIX thread-specific data (pthread_key_create/pthread_getspecific), the older key-based per-thread storage that predates__thread.void *(*start_routine)(void *); void *arg; void *result— the entry function, its argument, and the eventual return value thatpthread_joinretrieves.unsigned int joinstate— the lifecycle state (joinable, detached, exiting, exited).struct sched_param schedparam; int schedpolicy— the thread’s scheduling parameters.struct _pthread_cleanup_buffer *cleanup; struct pthread_unwind_buf *cleanup_jmp_buf— the cleanup-handler stack (pthread_cleanup_push) and the unwind buffer that cancellation uses.td_eventbuf_t eventbuf— a slot the thread-debugging library (libthread_db, used by GDB) reads to track thread events.
A crucial identity: the opaque pthread_t handle is simply the address of this struct pthread. Drepper’s NPTL paper is explicit — “the thread handle is simply the pointer to the thread descriptor,” which is why “successively created threads will get the same handle” when glibc reuses a cached stack (Drepper & Molnar 2005 §5.6). That is the concrete reason POSIX forbids comparing pthread_t with == (use pthread_equal) and why using a handle after join/detach is undefined.
The gscope Flag and Safe dlclose
One header field deserves more than a line because it solves a genuinely hard problem: gscope_flag (offset 0x1c, sharing space with multiple_threads). “gscope” is glibc’s global scope lock, and it exists to make [[dlopen and Runtime Loading|dlclose]] safe in the presence of lazy PLT resolution.
The problem: when the dynamic linker resolves a symbol lazily, it walks the global lookup scope — the ordered list of loaded objects. If another thread calls dlclose and unmaps an object mid-walk, the resolving thread reads freed memory and crashes. glibc cannot take a heavyweight lock around every lazy resolution (that would serialize all symbol lookups), so it uses a per-thread, almost-free flag instead. Each thread sets its gscope_flag while it is inside a scope walk and clears it afterward — a plain store, no atomic in the fast path. When dlclose wants to free an object, it must wait until no thread is walking the scope: it flips the objects out of the scope, then calls __thread_gscope_wait, which scans every thread’s gscope_flag (reachable through the thread list) and spins/futex-waits until each in-flight walker has cleared its flag. This is a classic asymmetric synchronization scheme: the common operation (a scope walk) pays almost nothing, while the rare operation (dlclose) pays the full cost of polling all threads. Placing the flag in the ABI header, reachable via %fs, is what makes the common side a single un-contended store.
Finding the Descriptor from Outside: Debuggers
The descriptor is not only reached by the thread itself. libthread_db — the helper library GDB and other debuggers load to understand a live or core-dumped multithreaded process — walks these structures from outside the process, where %fs is not available. It finds the thread list and each struct pthread by reading exported offsets and the td_eventbuf_t eventbuf field, which glibc updates on thread creation/death so a debugger can set “new thread” and “thread exit” breakpoints. This is why the layout of struct pthread, though “private,” is not entirely free to change: libthread_db is compiled against specific offsets, and glibc coordinates the two. It is also why a corrupted TCB often makes GDB’s info threads misbehave — the debugger is chasing pointers through the very structure the bug damaged.
How the Thread Pointer Gets Set
For the TCB to work, %fs must be pointed at the descriptor before the thread runs any TLS-using code. There are two mechanisms, one for the main thread and one for new threads.
The main thread is set up during process startup. glibc allocates the initial descriptor and calls its TLS_INIT_TP macro, which “sets _head->tcb and _head->self to the thread descriptor” and then “invokes arch_prctl(ARCH_SET_FS, thrdescr)… setting the x86-64 FS segment register to point to the TCB base” (sysdeps/x86_64/nptl/tls.h). arch_prctl(ARCH_SET_FS, addr) is the x86-64 system call that writes the %fs base MSR; before it existed, this required a costly GDT/LDT descriptor manipulation, which was one of the pain points the NPTL redesign eliminated.
For a new thread, glibc avoids a second arch_prctl by folding the TP setup into thread creation: pthread_create allocates the descriptor, then issues clone with CLONE_SETTLS, whose tls argument on x86-64 “is the new value to be set for the %fs base register” (clone(2)). The kernel installs the %fs base as part of creating the task, so the new thread wakes up already able to reach its TCB and TLS — no window in which TLS is unusable. This is exactly the “signal-safe loading of the thread register” the NPTL kernel work added: “the new thread must be started by the kernel with the thread register already loaded… what… the CLONE_TLS flag” provides (Drepper & Molnar 2005 §6).
Failure Modes and Common Misunderstandings
- Overwriting
%fs:0x28breaks stack protection silently. Inline assembly or a wild write that clobbers the TCB header can corrupt the canary or DTV pointer; the symptom is a spurious*** stack smashing detected ***abort or a TLS segfault far from the real bug. - Assuming
pthread_tis a small integer. It is a pointer into this structure; printing it as an ID or hashing it across process boundaries is meaningless. Stale handles alias reused descriptors (see the==warning above). tcbhead_tlayout is arch-specific. The offsets quoted here (stack_guardat0x28,pointer_guardat0x30) are x86-64. i386, AArch64, and others have differenttcbhead_tlayouts; the concepts transfer but the numeric offsets do not.- DTV pointer corruption after
dlopen. If the header’sdtvfield is stale relative to the global generation (see Thread-Local Storage), a TLS access triggers a DTV resize; a bug in adlopened module that corrupts the DTV pointer manifests as garbage TLS reads.
See Also
- Thread-Local Storage — the DTV the
dtvheader field points at, and static/dynamic TLS blocks - TLS Models and Access — how code reaches this block via
%fs/TPIDR_EL0and the two variants - POSIX Threads and NPTL —
pthread_tis the address of this descriptor;tidpowerspthread_join - Thread Stacks and Stack Guards — the
stackblock/guardsizefields and thestack_guardcanary - pthread Synchronization and Futexes — the
robust_headrobust-futex list andmultiple_threadsflag - _start and __libc_start_main — where the main thread’s TCB and security cookies are set up
- The Auxiliary Vector —
AT_RANDOM, the seed forstack_guardandpointer_guard - Linux Userspace Runtime MOC — parent map (§7, Threads and Thread-Local Storage)