← board

GS-relative TLS vs the FS-relative psABI

The fork

__thread needs per-thread storage. pxx has a per-thread block and it is on GS. Standard x86-64 ELF TLS is on FS. Which one do pxx's thread-locals use, and what do we promise at the boundary?

What is already settled, and by whom

thread_emit.inc:142 chose GS deliberately and stated the reason in the code: "GS, not fs: fs belongs to libc, and a pxx program may link one." That is a good reason and this ticket is not reopening it for pxx-internal use. Measured 2026-09-06 (with a sentinel control that separates "base is 0" from "arch_prctl never ran"), a pxx-native BeginThread thread gets a distinct non-zero GS base while FS is 0 in both threads. The mechanism works.

The options

1. GS-relative, pxx-only. Thread-locals work in pxx-compiled code. An --emit-obj object's __thread variables are NOT reachable by a gcc caller's TLS relocations, and a gcc object's __thread is not reachable by ours. State it as a documented boundary.

2. FS-relative, psABI-conformant. Interoperates. Conflicts with the reason GS was chosen: a pxx program that links libc has FS owned by that libc, and the two would fight over it.

3. Both, by output mode. GS for a standalone pxx program, FS/psABI under --emit-obj and --shared. Correct at both boundaries and the most work — two relocation models and two accessor shapes.

Recommendation

Option 1 for now, with the boundary written down, and option 3 named as the upgrade path if a real consumer appears. The deciding question is whether anyone actually links a pxx object containing __thread into a gcc program — if nobody does, 2 and 3 buy nothing and cost a fight with libc over FS.

What would settle it is a real consumer, not an argument.

A PROOF OF CONCEPT THAT FITS IN THE SLACK IS NOT A SMALLER VERSION OF THE FEATURE

Framing owed to frankA, who says they would have walked into it. The existing per-thread block is exactly full — 144 slots, 0..12 taken, the heap magazine at 16..79 and 80..143 — leaving three. Three is enough to demonstrate __thread working on one variable and nothing like enough for the feature, and a demo is how a design gets ratified.

So a working one-variable proof would be evidence for the wrong proposition: it would show that GS-relative access works (true, and not the open question) while saying nothing about allocation, which is the part that has to change. Whoever implements this must size the area first and demonstrate second.

Same shape as the discriminating-fixture problem on the layout ticket next door: {int a; double y} "works" under both candidate rules and therefore separates neither. A fixture that fits the slack cannot test the thing the slack is hiding.

And a second thing to rule on at the same time

A thread pxx did NOT create inherits the parent's GS base rather than getting its own. Measured: glibc pthread_create gives main and child the identical 4298F0, because GS base is inherited across clone and such a thread never passes through pxx's stub.

So under any GS scheme, a foreign-created thread silently shares the creator's thread-locals. That is the hazard the clone stub's own comment cites as its reason for installing GS first — a valid-looking pointer into someone else's storage, not a null you could test for. Options are to detect it and refuse, to install lazily on first access, or to document it. Whichever way this fork goes, that question is part of it and should not be answered separately by whoever writes the code.

Blocks [[bug-c-__thread-is-accepted-and-silently-ignored-so-thread-local-storage-is-shared]].

7a166c995 (frankH, Track P, feature-p-threadvar-is-not-supported-at-any-scope) shipped Pascal threadvar on the GS block. That settles this fork as option 1 — GS-only with the boundary enforced — and it enforces it in code rather than documenting it: --emit-obj and --shared are REFUSED (AssignThreadVarStorage, pasparser_decl.inc), as is every target but x86-64.

AND THE SHIPPED REASON IS SHARPER THAN THE ONE THIS TICKET ARGUED. I framed the boundary as an interop limitation — a feature you do not get across an --emit-obj edge. The refusal's own text names it as a correctness hazard:

"gs: with no base does not read 0, it FAULTS -- and inside a glibc host it succeeds and returns glibc's TCB, which is worse."

That is the difference between an absent feature and a silent wrong one. An object with no ELF entry point has nothing to install the block, so the failure is not "thread-locals are process-wide" but "thread-locals resolve into another runtime's thread control block". Refusing is therefore not conservatism about interop; it is the only answer that is not silently wrong, and the reasoning this ticket offered would have supported a weaker guard.

The residual question that remains is unchanged and has no consumer: whether to grow an FS/psABI path so a pxx object carrying thread-locals can be linked into a gcc program. Nobody has produced such a consumer, which was this ticket's own deciding test. It reopens if one appears; it is not open now.

(b) IS NOT MINE AND IS NOT NEW — it already has a ticket and a worse measurement

The foreign-created-thread hazard bundled in above is [[bug-a-a-foreign-thread-shares-the-main-thread-s-heap-magazine]], measured 2026-09-01, before the reading here. ir_codegen.inc:265 carries it: gdb reports gs_base = 0x411f98 for ALL FIVE threads of test/test_multithreading.pasBSS_TLS_MAIN, the main thread's block — because a libc-pthread_create thread never runs the clone stub that carves and installs one. It cost 18 SIGSEGV in 100 runs through a shared heap magazine, and the pinned v399 compiler crashes at the same rate, so it is not a regression.

My probe reproduced the same fact by a different route (identical GS across threads, via arch_prctl(ARCH_GET_GS) rather than gdb), which is corroboration rather than a finding — two instruments that fail differently, agreeing. Bundling it here was right at the time and is wrong now: it has an owner, and a second copy of a measured hazard is how one of them goes stale. Follow that ticket, not this section.

Resolving. The fork was ruled by code, and the residual has an owner.

Log