feat: TLS runtime - PT_TLS loading and per-thread FS base

MontaukOS binaries could not use thread-local storage: the ELF loader
ignored PT_TLS and no FS base was ever programmed, so any %fs access
(initial-exec/local-exec TLS, e.g. libbfd's _Thread_local error state)
page faulted at address 0. This is the kernel prerequisite for
cc1plus/libstdc++ in the GCC port.

ELF loader: parse PT_TLS and build the main thread's TLS block above
the loaded image (x86-64 variant II ABI: block below the thread
pointer, TP = base + align_up(memsz, align), .tdata copied from the
template, .tbss zeroed, ABI self-pointer stored at [TP]). The
template description is kept on the process for thread spawns.

Scheduler: fsBase per thread, loaded into IA32_FS_BASE at every
dispatch site through a per-CPU cache - TLS-free processes cost one
MSR write per CPU ever, and userspace cannot desync the cache since
CR4.FSGSBASE stays off. SYS_THREAD_SPAWN gives each new thread its
own TLS block copied from the template (allocated from the process
heap; reclaimed at process teardown).

Ships tls-test.elf in the SDK (built by the devkit target): checks
.tdata values, .tbss zeroing, and the %fs:0 self-pointer, exiting
nonzero on failure. Boot-smoke verified; existing TLS-free programs
are unaffected.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-16 18:48:25 +02:00
co-authored by Claude Fable 5
parent 75184e0651
commit 5db2bd4c7c
8 changed files with 222 additions and 4 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 15
#define MONTAUK_BUILD_NUMBER 16
+1
View File
@@ -35,6 +35,7 @@ namespace Smp {
int cpuIndex; // 0-based CPU index
uint32_t lapicId; // local APIC ID
uint64_t idleSavedRsp; // RSP saved when switching from idle to process
uint64_t currentFsBase = ~0ULL; // last IA32_FS_BASE written on this CPU
volatile bool started; // set by AP after init is complete
Hal::TSS64* tss; // pointer to this CPU's TSS
+77 -1
View File
@@ -413,7 +413,14 @@ namespace Sched {
return true;
}
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) {
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls) {
if (outTls != nullptr) {
outTls->fsBase = 0;
outTls->templateVaddr = 0;
outTls->fileSize = 0;
outTls->memSize = 0;
outTls->align = 0;
}
Fs::Vfs::BackendFile file = {-1, -1, 0};
if (Fs::Vfs::OpenBackendFile(vfsPath, file) < 0) {
return 0;
@@ -449,9 +456,16 @@ namespace Sched {
}
// Process program headers
Elf64ProgramHeader* tlsPhdr = nullptr;
uint64_t maxVaddrEnd = 0;
for (uint16_t i = 0; i < hdr->e_phnum; i++) {
Elf64ProgramHeader* phdr = (Elf64ProgramHeader*)(fileData + hdr->e_phoff + i * hdr->e_phentsize);
if (phdr->p_type == PT_TLS && phdr->p_memsz > 0) {
tlsPhdr = phdr;
continue;
}
if (phdr->p_type != PT_LOAD) {
continue;
}
@@ -460,6 +474,10 @@ namespace Sched {
continue;
}
if (phdr->p_vaddr + phdr->p_memsz > maxVaddrEnd) {
maxVaddrEnd = phdr->p_vaddr + phdr->p_memsz;
}
// Allocate pages and map them in the process PML4 with User bit
uint64_t segBase = phdr->p_vaddr & ~0xFFFULL;
uint64_t segEnd = (phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFULL;
@@ -505,6 +523,64 @@ namespace Sched {
}
}
// Build the main thread's TLS block (x86-64 variant II): the block
// lives below the thread pointer, TP = base + S where
// S = align_up(memsz, align), template data at [base, base+filesz),
// .tbss zeroed, and [TP] holds the ABI-required self-pointer.
if (tlsPhdr != nullptr && outTls != nullptr) {
uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align;
uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1);
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
uint64_t total = S + 16; // + minimal TCB
uint64_t numPages = (total + 0xFFF) / 0x1000;
uint64_t tp = base + S;
bool ok = true;
for (uint64_t p = 0; p < numPages && ok; p++) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) {
ok = false;
break;
}
uint64_t physAddr = Memory::SubHHDM((uint64_t)page);
uint64_t virtAddr = base + p * 0x1000;
if (!Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, virtAddr)) {
ok = false;
break;
}
// Copy the .tdata template bytes that land in this page.
uint64_t pageStart = virtAddr;
uint64_t pageEnd = virtAddr + 0x1000;
uint64_t dataStart = base;
uint64_t dataEnd = base + tlsPhdr->p_filesz;
uint64_t copyStart = (pageStart > dataStart) ? pageStart : dataStart;
uint64_t copyEnd = (pageEnd < dataEnd) ? pageEnd : dataEnd;
if (copyStart < copyEnd) {
memcpy((uint8_t*)Memory::HHDM(physAddr) + (copyStart - pageStart),
fileData + tlsPhdr->p_offset + (copyStart - dataStart),
copyEnd - copyStart);
}
// Write the self-pointer if TP falls in this page.
if (tp >= pageStart && tp + 8 <= pageEnd) {
*(uint64_t*)((uint8_t*)Memory::HHDM(physAddr) + (tp - pageStart)) = tp;
}
}
if (ok) {
outTls->fsBase = tp;
outTls->templateVaddr = tlsPhdr->p_vaddr;
outTls->fileSize = tlsPhdr->p_filesz;
outTls->memSize = tlsPhdr->p_memsz;
outTls->align = align;
} else {
Kt::KernelLogStream(Kt::ERROR, "ELF")
<< "PT_TLS setup failed (out of pages?), "
<< "process will run without TLS";
}
}
uint64_t entryPoint = hdr->e_entry;
Memory::g_heap->Free(fileData);
+14 -1
View File
@@ -39,6 +39,7 @@ namespace Sched {
static constexpr uint32_t PT_LOAD = 1;
static constexpr uint32_t PT_DYNAMIC = 2;
static constexpr uint32_t PT_TLS = 7;
static constexpr uint16_t ET_EXEC = 2;
static constexpr uint16_t ET_DYN = 3;
static constexpr uint16_t EM_X86_64 = 62;
@@ -50,7 +51,19 @@ namespace Sched {
// Load an ELF64 binary into a per-process address space.
// pml4Phys = physical address of the process's PML4.
// Returns the entry point address, or 0 on failure.
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys);
// Thread-local storage description recovered from PT_TLS. fsBase is
// the main thread's thread pointer (x86-64 variant II: the TLS block
// sits below it, [fsBase] holds a self-pointer). The template fields
// let SpawnThread build per-thread copies from the loaded image.
struct TlsInfo {
uint64_t fsBase; // 0 when the binary has no PT_TLS
uint64_t templateVaddr; // source of .tdata in the loaded image
uint64_t fileSize; // initialized bytes
uint64_t memSize; // total bytes (incl. .tbss)
uint64_t align;
};
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
// Load a shared library into a fixed address range.
// Returns the relocation base / load bias on success, or 0 on failure.
+81 -1
View File
@@ -32,6 +32,22 @@
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
uint8_t* oldFpuArea, uint8_t* newFpuArea);
// Load a user thread's FS base (TLS thread pointer) before dispatching it.
// Cached per CPU: the MSR write is skipped when unchanged, so TLS-free
// processes (fsBase 0) cost one write per CPU ever. Userspace cannot move
// FS base itself (CR4.FSGSBASE is off), so the cache cannot go stale.
static inline void LoadUserFsBase(Smp::CpuData* cpu, uint64_t fsBase) {
if (cpu->currentFsBase == fsBase) {
return;
}
cpu->currentFsBase = fsBase;
asm volatile("wrmsr"
:
: "c"(0xC0000100u), /* IA32_FS_BASE */
"a"((uint32_t)fsBase),
"d"((uint32_t)(fsBase >> 32)));
}
// Assembly: jump to user mode via IRETQ.
// `arg` is delivered as the user-mode RDI (SystemV first argument).
// For freshly spawned processes this is 0; for SpawnThread it is the
@@ -129,6 +145,7 @@ namespace Sched {
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
LoadUserFsBase(cpu, processTable[next].fsBase);
SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp,
processTable[next].pml4Phys,
processTable[slot].fpuState, processTable[next].fpuState);
@@ -161,6 +178,8 @@ namespace Sched {
// Set up per-CPU TSS RSP0 for hardware interrupts from ring 3
cpu->tss->rsp0 = proc.kernelStackTop;
LoadUserFsBase(cpu, proc.fsBase);
// Jump to user mode (never returns).
// For main threads threadArg is 0 (libc _start ignores RDI);
// for sibling threads it carries the user-supplied argument.
@@ -261,7 +280,8 @@ namespace Sched {
uint64_t pml4Phys = Memory::VMM::Paging::CreateUserPML4();
// Load ELF into the process's address space
uint64_t entry = ElfLoad(vfsPath, pml4Phys);
TlsInfo tls{};
uint64_t entry = ElfLoad(vfsPath, pml4Phys, &tls);
if (entry == 0) {
Memory::VMM::Paging::FreeUserHalf(pml4Phys);
Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys));
@@ -387,6 +407,11 @@ namespace Sched {
proc.kernelStackTop = kernelStackTop;
proc.userStackTop = UserStackTop - 8;
proc.heapNext = UserHeapBase;
proc.fsBase = tls.fsBase;
proc.tlsTemplateVaddr = tls.templateVaddr;
proc.tlsFileSize = tls.fileSize;
proc.tlsMemSize = tls.memSize;
proc.tlsAlign = tls.align;
proc.readdirCursor = 0;
proc.runningOnCpu = -1;
proc.killPending = false;
@@ -499,6 +524,52 @@ namespace Sched {
int processPid = processTable[primarySlot_].pid;
if (sharedPml4 == 0) return -1;
// Give the new thread its own TLS block when the image has one.
// We are executing inside the process's address space, so the
// freshly mapped pages and the template are directly addressable.
// The block is process-heap memory; it is reclaimed with the rest
// of the user half at process teardown (a thread that exits before
// its process leaks its block until then - acceptable for now).
uint64_t threadFsBase = 0;
Process& primary = processTable[primarySlot_];
if (primary.tlsMemSize > 0) {
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1);
uint64_t total = S + 16;
uint64_t numPages = (total + 0xFFF) / 0x1000;
schedLock.Acquire();
uint64_t base = primary.heapNext;
primary.heapNext += numPages * 0x1000;
schedLock.Release();
bool ok = true;
for (uint64_t p = 0; p < numPages; p++) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) {
ok = false;
break;
}
uint64_t physAddr = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserIn(sharedPml4, physAddr,
base + p * 0x1000)) {
ok = false;
break;
}
}
if (!ok) {
Kt::KernelLogStream(Kt::ERROR, "Sched")
<< "Thread TLS allocation failed";
return -1;
}
uint64_t tp = base + S;
memcpy((void*)base, (const void*)primary.tlsTemplateVaddr,
primary.tlsFileSize);
*(uint64_t*)tp = tp; /* ABI self-pointer */
threadFsBase = tp;
}
// We do not write to the user stack from kernel mode (that would
// require validating the user VA against the process page tables).
// Userspace is responsible for ensuring the thread entry calls
@@ -566,6 +637,11 @@ namespace Sched {
thr.ioOutHandle = -1; thr.ioInHandle = -1;
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
thr.args[0] = '\0'; thr.user[0] = '\0'; thr.cwd[0] = '\0';
thr.fsBase = threadFsBase;
thr.tlsTemplateVaddr = 0;
thr.tlsFileSize = 0;
thr.tlsMemSize = 0;
thr.tlsAlign = 0;
// Derive a debug name from the primary's name.
{
@@ -609,6 +685,7 @@ namespace Sched {
if (readyCount > 0) {
KickOneIdleCpu(cpu->cpuIndex);
}
LoadUserFsBase(cpu, processTable[next].fsBase);
SchedContextSwitch(&thr.savedRsp, processTable[next].savedRsp,
processTable[next].pml4Phys,
thr.fpuState, processTable[next].fpuState);
@@ -930,6 +1007,8 @@ namespace Sched {
uint8_t* oldFpu = (oldSlot >= 0) ? processTable[oldSlot].fpuState : nullptr;
uint8_t* newFpu = processTable[next].fpuState;
LoadUserFsBase(cpu, processTable[next].fsBase);
// DO NOT release schedLock here! It is held across the context
// switch so that setting Ready + saving RSP is atomic. The
// resumed process releases it.
@@ -1207,6 +1286,7 @@ namespace Sched {
uint64_t newCR3 = processTable[next].pml4Phys;
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
LoadUserFsBase(cpu, processTable[next].fsBase);
// Lock held across context switch -- resumed process releases it
SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, newCR3,
+10
View File
@@ -58,6 +58,16 @@ namespace Sched {
char user[32]; // Owner user name (inherited from parent on spawn)
char cwd[256]; // Absolute current working directory
// Thread-local storage. fsBase is loaded into IA32_FS_BASE when
// this thread is dispatched (0 = no TLS). The tls* template fields
// live on the primary slot and describe PT_TLS so sibling threads
// can get their own block copies.
uint64_t fsBase = 0;
uint64_t tlsTemplateVaddr = 0;
uint64_t tlsFileSize = 0;
uint64_t tlsMemSize = 0;
uint64_t tlsAlign = 0;
int runningOnCpu; // CPU index running this process (-1 if not running)
bool killPending = false; // Set by Sys_Kill when target is running on another CPU
bool reapReady = false; // Set once teardown is complete and BSP may free slot resources