kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit f8164e2795d7e8f74d270efd23968f1dace98db8
parent e51cf4693982cac804e78807d5e31a9293b67a66
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  9 Jun 2026 12:15:42 -0700

link: reject local-exec TLS against non-local thread-locals

kit emits local-exec TLS only: a thread-local's offset within the image's
TLS block is fixed at link time, valid only for a thread-local defined in
the image being linked. Nothing checked that, so a TLS access that bound to
a DSO import or (cross-TU) to a non-thread-local definition silently baked a
bogus tp-relative offset into the binary.

Add link_require_local_tls() and call it at the two points that commit a
tp-relative offset: the TLS-IE GOT-slot fill (link_emit_internal_tpoff64,
shared by the AOT writer and the JIT mapper via link_layout_got) and the
ELF apply pass (apply_all_relocs, for direct local-exec relocs). An imported
or non-thread-local target now produces a clear diagnostic instead of a
bogus offset; kit has no initial-exec/global-dynamic fallback to relax to.

test/link/bad/31_tls_non_local exercises a cross-TU TLS-vs-non-TLS mismatch
(rejected on both the E and J lanes). Teach bad_read_case the per-tuple
'targets' gating the cases reader already has, and scope the case to ELF
(the IE-GOT-slot premise has no Mach-O/COFF analogue).

Diffstat:
Msrc/link/link_internal.h | 11+++++++++++
Msrc/link/link_reloc_layout.c | 23+++++++++++++++++++++++
Msrc/obj/elf/link.c | 7+++++++
Atest/link/bad/31_tls_non_local/a.c | 13+++++++++++++
Atest/link/bad/31_tls_non_local/b.c | 3+++
Atest/link/bad/31_tls_non_local/expect | 1+
Atest/link/bad/31_tls_non_local/targets | 3+++
Mtest/link/run.sh | 15+++++++++++++++
8 files changed, 76 insertions(+), 0 deletions(-)

diff --git a/src/link/link_internal.h b/src/link/link_internal.h @@ -616,6 +616,17 @@ struct LinkImage { * the relocation site. Panics on unsupported kinds. */ void link_reloc_apply(Compiler*, RelocKind, u8* P_bytes, u64 S, i64 A, u64 P); +/* kit emits local-exec TLS only: a thread-local's offset within THIS image's + * TLS block is fixed at link time, so a local-exec access (or the TP-relative + * fill of a TLS initial-exec GOT slot) is valid only against a thread-local + * *defined in the image being linked*. Panic with a clear diagnostic if `tgt` + * is imported from a shared object (its TLS block belongs to another module, + * sized and placed by the dynamic loader) or resolved to a non-thread-local + * definition. There is no initial-exec/global-dynamic fallback to relax to, + * so the only alternative is a silently bogus tp-relative offset -- hence the + * hard error. A no-op when `tgt` is a thread-local defined here. */ +void link_require_local_tls(Compiler*, const LinkSymbol* tgt); + /* Public link_emit_image_writer dispatches by Compiler.target.obj. The * ELF and Mach-O writers get architecture identity from LinkArchDesc; * reloc application remains keyed by RelocKind. COFF arrives later. */ diff --git a/src/link/link_reloc_layout.c b/src/link/link_reloc_layout.c @@ -520,6 +520,24 @@ void link_layout_jit_stubs(Linker* l, LinkImage* img, u32 map_size, /* ---- pass 3c: GOT layout ---- */ +void link_require_local_tls(Compiler* c, const LinkSymbol* tgt) { + if (!tgt) return; + if (!tgt->imported && tgt->kind == SK_TLS) return; + { + Slice nm = tgt->name ? pool_slice(c->global, tgt->name) : SLICE_NULL; + const char* why = tgt->imported + ? "it is imported from a shared object" + : "it resolved to a definition that is not " + "thread-local"; + compiler_panic(c, SRCLOC_NONE, + "link: cannot resolve thread-local access to '%.*s': %s. " + "kit emits local-exec TLS only, so every thread-local must " + "be defined in the image being linked (initial-exec / " + "global-dynamic against a shared object are not supported)", + (int)nm.len, nm.s ? nm.s : "", why); + } +} + /* Fill a TLS Initial-Exec GOT slot with the target's TP-relative offset. * Emitted as an internal raw-64-bit tpoff reloc so apply_all_relocs computes * the offset in the same coordinate system it uses for ordinary local-exec @@ -531,6 +549,11 @@ static void link_emit_internal_tpoff64(LinkImage* img, Linker* l, u64 write_vaddr, LinkSymId target) { LinkRelocApply rrec; (void)l; + /* The slot holds a tp-relative offset, valid only for a thread-local + * defined in this image (see link_require_local_tls). A TLS-IE access + * against an imported or non-thread-local target would otherwise bake a + * bogus offset into the slot. */ + link_require_local_tls(img->c, LinkSyms_at(&img->syms, target - 1)); memset(&rrec, 0, sizeof(rrec)); rrec.input_id = LINK_INPUT_NONE; rrec.section_id = OBJ_SEC_NONE; diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c @@ -496,6 +496,13 @@ static void apply_all_relocs(LinkImage* img, u64 img_base) { continue; } seg = &img->segments[sec->segment_id - 1]; + if (reloc_is_tlsle(r->kind, tls_vi) || reloc_is_x64_tlsle(r->kind, tls_vi)) + /* Both the direct local-exec relocs and the internal R_TPOFF64 used to + * fill a TLS-IE GOT slot resolve to a tp-relative offset within THIS + * image's TLS block — only meaningful for a thread-local defined here. + * Reject an imported or non-thread-local target rather than emit a + * bogus offset (kit has no initial-exec/global-dynamic fallback). */ + link_require_local_tls(img->c, tgt); if (reloc_is_tlsle(r->kind, tls_vi)) { /* S is the target's TP-relative offset: distance from the TLS image * start plus the arch/OS TCB bias (see tls_tcb_bias). Both vaddrs are diff --git a/test/link/bad/31_tls_non_local/a.c b/test/link/bad/31_tls_non_local/a.c @@ -0,0 +1,13 @@ +/* Reference a thread-local symbol that is *not* a thread-local definition + * in the linked image. kit emits local-exec TLS only, so the symbol's + * tp-relative offset must be known at link time -- which holds only for a + * thread-local defined in this image. Here `shared` resolves to b.c's + * ordinary (non-TLS) global, so there is no valid TLS offset; the linker + * must reject this rather than bake in a bogus tp-relative offset. + * + * (clang lowers this external __thread reference to initial-exec on a + * non-PIC executable; kit's linker fills the IE GOT slot with the symbol's + * local-exec tp-offset, which is where the mismatch surfaces.) */ +extern _Thread_local int shared; + +int test_main(void) { return shared; } diff --git a/test/link/bad/31_tls_non_local/b.c b/test/link/bad/31_tls_non_local/b.c @@ -0,0 +1,3 @@ +/* Defines `shared` as an ordinary global -- NOT thread-local. The TLS + * reference in a.c therefore binds to a non-thread-local definition. */ +int shared = 5; diff --git a/test/link/bad/31_tls_non_local/expect b/test/link/bad/31_tls_non_local/expect @@ -0,0 +1 @@ +cannot resolve thread-local access diff --git a/test/link/bad/31_tls_non_local/targets b/test/link/bad/31_tls_non_local/targets @@ -0,0 +1,3 @@ +aa64-elf +x64-elf +rv64-elf diff --git a/test/link/run.sh b/test/link/run.sh @@ -642,6 +642,21 @@ bad_read_case() { CASE_DIR="${KIT_SRC%/}" NAME="bad/$KIT_BASE" + # Whole-case applicability via `targets` (same mechanism as link_read_case): + # a negative case may be format-specific (e.g. an ELF-only TLS scenario with + # no Mach-O/COFF analogue). Listed <arch>-<obj> tuples run; others SKIP-NA. + if [ -f "$CASE_DIR/targets" ]; then + applicable=0 + for tuple in $(cat "$CASE_DIR/targets"); do + [ "$tuple" = "$CUR_TUPLE" ] && applicable=1 + done + if [ $applicable -eq 0 ]; then + printf ' SKIP-NA %s — N/A on %s\n' "$NAME" "$CUR_TUPLE" + KIT_SKIP_NA_CASE=1 + return + fi + fi + EXPECT_FILE="$CASE_DIR/expect" if [ ! -f "$EXPECT_FILE" ]; then kit_fail "$NAME (missing $EXPECT_FILE)"