Skip to content

The Task State Segment

The Task State Segment is a structure protected mode briefly introduces as the source of the stack pointer used on a ring 3 to ring 0 transition, but the TSS is a larger structure than that single field, and one nearly every kernel supporting user-mode code ends up depending on for more than just that pointer. Unlike a GDT or IDT entry, a TSS isn’t consulted by index on every access; instead, exactly one is marked current at a time by loading its selector into the special TR register, and the CPU reads fields out of that current TSS automatically whenever specific hardware events, chiefly privilege-raising interrupts, occur.

The 64-bit TSS is a fixed 104-byte structure (plus, optionally, an appended I/O permission bitmap): three stack pointers (RSP0 through RSP2, one per privilege level a transition could land in from a lower one), seven Interrupt Stack Table pointers used by the mechanism the IDT article already covers, and a 16-bit offset field pointing to where the I/O permission bitmap begins, relative to the TSS’s own base. The 32-bit-mode TSS predates all of this and is considerably larger, since it also holds a full saved register set (EAX through EDI, segment selectors, EFLAGS) for the hardware task-switching mechanism the structure was originally designed around.

struct tss_64 {
uint32_t reserved0;
uint64_t rsp0, rsp1, rsp2;
uint64_t reserved1;
uint64_t ist1, ist2, ist3, ist4, ist5, ist6, ist7;
uint64_t reserved2;
uint16_t reserved3;
uint16_t iopb_offset;
} __attribute__((packed));

The stack-switch fields and privilege transitions

Section titled “The stack-switch fields and privilege transitions”

RSP0 (still commonly called ESP0/SS0, its 32-bit-mode name, in most kernel source) is the field that matters for essentially every kernel: whenever a syscall, interrupt, or exception raises the CPU’s privilege from ring 3 to ring 0, the CPU loads RSP from this field before pushing the interrupt frame, because a ring 3 stack cannot be trusted to have enough room, or even to be mapped at all, for the kernel to safely push data onto. Without a valid RSP0, there is no defined destination for that push at all; a transition attempted with the TSS unset or pointing at unmapped memory faults immediately, before a single instruction of the handler itself runs. RSP1 and RSP2 exist for symmetry with ring 1 and ring 2, but see essentially no practical use, since almost no kernel defines intermediate privilege levels between user and kernel code.

The bitmap iopb_offset points to is what determines whether ring 3 code can execute IN/OUT directly against a given port without faulting and trapping into the kernel first: one bit per I/O port, with a set bit meaning “this port is not accessible,” read at the moment ring 3 code attempts a port access. A kernel that wants user-mode code to have unrestricted port access, the common case, points iopb_offset past the end of the TSS’s actual limit, which the CPU treats as “no bitmap present” and denies all ports; a kernel wanting to grant a specific process access to specific ports (a userspace driver reading one particular device’s ports, for instance) instead allocates the full 8 KB bitmap and clears only the bits for the ports that process is permitted to touch. This is a per-TSS setting, not a per-process one directly, so switching which process’s I/O permissions are active on a single-TSS-per-core design means rewriting the relevant bitmap bits on every context switch between a process with elevated port access and one without, a cost some kernels avoid by keeping port I/O behind system calls entirely rather than ever granting ring 3 direct access.

TR is a single register, one per core, so a symmetric multiprocessing kernel cannot share a single TSS across CPUs: each core needs its own TSS, its own GDT entry describing that TSS (or its own dedicated slot in a shared GDT), and its own LTR load performed during that core’s bring-up, before the core can safely take an interrupt at all. Getting this wrong in a way that’s easy to miss during single-core testing shows up only once a second CPU is brought online: two cores sharing one TSS end up racing to overwrite the same RSP0 field, so whichever core’s write happened most recently silently determines where the other core’s next ring 0 transition pushes its interrupt frame, corrupting that core’s kernel stack in a way that looks, from the crash alone, unrelated to the actual cause.

A GDT TSS descriptor is 16 bytes on x86-64 rather than the usual 8, since it needs the full 64-bit base address a TSS living anywhere in a 64-bit address space requires; a kernel that copies the 32-bit 8-byte descriptor layout by habit ends up corrupting whatever descriptor follows it in the table. LTR also has a one-time restriction worth knowing before it causes a confusing fault: the TSS descriptor’s busy bit must be clear before the load, and LTR itself sets that bit as a side effect, so attempting to LTR the same selector twice without an intervening task switch (which, on the interrupt-based transitions covered above, never happens) faults on the second attempt.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 8: the complete TSS layout, the I/O permission bitmap, and the LTR/STR instructions.