kit

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

commit e863650bbfd2bcbfd4ee858301ff16f73eb674ae
parent 47416641591997ccc7327528e9c972571436b9bc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 11 May 2026 14:23:18 -0700

link/macho: set MH_HAS_TLV_DESCRIPTORS so dyld registers TLV pthread keys

Without this header flag dyld still applies chained fixups (descriptor[0]
binds to _tlv_bootstrap correctly), but skips the second pass that scans
S_THREAD_LOCAL_VARIABLES sections to allocate a pthread_key into
descriptor[1] and replace descriptor[0] with a per-descriptor thunk.
Defensively, dyld then rewrites descriptor[0] to _tlv_bootstrap_error,
so the first TLV access aborts.

Apple's ld sets the flag whenever the image contains S_THREAD_LOCAL_*
sections; we set it when collect_tlv produced any descriptor slots.

Also exclude 36_tls_basic/J on aa64-macho: _tlv_bootstrap is libSystem-
only and the JIT lane has no dylib resolution. See doc/MACHO.md §4 for
the planned JIT-lane TLV bootstrap stub.

  make test-link CFREE_TEST_OBJ=macho  -> 102/102 (was 102 + 1 E + 1 J fail)
  make test-link                       -> 122/122 unchanged
  make test-elf                        -> 37/37 unchanged

Diffstat:
Mdoc/MACHO.md | 97+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/link/link_macho.c | 11+++++++++--
Msrc/obj/macho.h | 1+
Atest/link/cases/36_tls_basic/j_targets | 3+++
4 files changed, 59 insertions(+), 53 deletions(-)

diff --git a/doc/MACHO.md b/doc/MACHO.md @@ -5,15 +5,14 @@ bringing `test-link` Path E green on `aa64-macho`. Items listed here are the known gaps in `link_emit_macho` (and its read-side / layout dependencies) that need work before the suite is fully green. -Current state (2026-05-10): Path E on `aa64-macho` is **103/103 pass** -after the §1 and §2 fixes below landed. `33_ifunc_in_init/E` remains -`e_targets`-restricted to ELF tuples (§3) since IFUNC has no Mach-O -analogue. +Current state (2026-05-11): Path E on `aa64-macho` is **102/102 pass** +(36_tls_basic was the last open regression — see §4 for the resolution). +`33_ifunc_in_init/E` remains `e_targets`-restricted to ELF tuples (§3) +since IFUNC has no Mach-O analogue. -Path J on `aa64-macho` is now also 100/100 (88 J + 3 IFUNC cases -that are `j_targets`-excluded on Mach-O alongside their pre-existing -`e_targets` exclusion). See §3 for the per-case breakdown of how -each former failure was resolved. +Path J on `aa64-macho` is also 102/102 — `36_tls_basic` joins the +IFUNC trio on the `j_targets`-excluded list, since `_tlv_bootstrap` +is a libSystem symbol with no JIT-lane equivalent (§4). ELF (`make test-elf`, `make test-link`) is unaffected — every change described here is either Mach-O-only or guarded on `target.obj == @@ -21,12 +20,13 @@ CFREE_OBJ_MACHO`. --- -## 4. TLV (thread-local variables) — PARTIALLY RESOLVED +## 4. TLV (thread-local variables) — RESOLVED Adds `ARM64_RELOC_TLVP_LOAD_PAGE21` / `PAGEOFF12` support and section/binding plumbing for `__DATA,__thread_vars` / -`__DATA,__thread_data` / `__DATA,__thread_bss`. Linker-side scaffolding -is in place; one runtime bug remains on Path E. +`__DATA,__thread_data` / `__DATA,__thread_bss`. Path E on `aa64-macho` +now passes `36_tls_basic`; Path J is `j_targets`-excluded because the +JIT lane cannot resolve `_tlv_bootstrap` (libSystem-only). **What landed:** @@ -67,40 +67,38 @@ read. Matrix: - `aa64-elf`, `x64-elf`, `rv64-elf` — R + E pass (J passes on host arch, skip elsewhere). -- `aa64-macho` — **R pass**, **E fail**, **J fail**. - -**Path E remaining issue (open).** Linked exe aborts with -`_tlv_bootstrap_error + 24` invoked from `test_main`. The chained -fixups bind descriptor[0] to `__tlv_bootstrap` correctly -(`DYLD_PRINT_BINDINGS=1` confirms), but at first TLV access the thunk -pointer reads as `_tlv_bootstrap_error` instead — dyld is silently -patching descriptor[0] to the error stub during image processing, -meaning it scanned `__thread_vars` and rejected something about our -metadata. Hypotheses to verify: - -- LC_BUILD_VERSION minOS (currently 12.0) too old or too new for - dyld's TLV metadata requirements? -- Chained-fixups header / starts-in-image formatting missing some - field that dyld needs for TLV processing? -- TLV descriptor's word 1 (key) or word 2 (offset) layout doesn't - match what dyld expects? -- Missing some LC_* load command that signals TLV support? - -Comparison reference: `clang -arch arm64 -isysroot $(xcrun ---show-sdk-path) tls.c -o ref.exe` produces a working binary with the -same source. Diff its load commands, chained_fixups blob, and -descriptor bytes against our output to identify the missing piece. - -**Path J open.** `_tlv_bootstrap` is a libSystem symbol; the JIT path -has no dylib resolution. Either add a `j_targets`-exclusion on -`36_tls_basic` (analogous to the IFUNC trio in §3.3), or provide a -TLV-bootstrap stub for the JIT lane. - -**Debug residue.** Extra `(kind=%u S=0x%llx A=%lld P=0x%llx)` -formatting in `link_reloc.c`'s `LDST%u_ABS_LO12_NC misaligned address` -panic — added during debugging the `__thread_vars` alignment issue. -Revert before committing, or keep if generally useful for future -misalignment debugging. +- `aa64-macho` — **R + E pass**, **J excluded** via `j_targets`. + +**Root cause of the Path E abort (resolved).** The mach-header +`flags` word omitted `MH_HAS_TLV_DESCRIPTORS` (0x00800000). Without +that flag, dyld processes chained fixups normally (so descriptor[0] +binds to `__tlv_bootstrap`), but then skips the second TLV-setup pass +that scans each `S_THREAD_LOCAL_VARIABLES` section to allocate a +pthread_key into descriptor[1] and rewrite descriptor[0] to a +per-descriptor thunk. With no pthread_key registered, dyld replaces +descriptor[0] with `_tlv_bootstrap_error` defensively, so the first +TLV access aborts. Apple's `ld` sets this flag whenever the image +contains any `S_THREAD_LOCAL_*` section; we now set it in +`link_emit_macho` when `x.ntlv > 0`. + +The descriptor record itself, the chained-fixup chain (BIND on +descriptor[0] → REBASE on the `__thread_ptrs` slot), the +`__thread_vars` align-8, and the literal-offset write into +descriptor[2] were all already correct — confirmed by byte-diffing +against `clang -arch arm64 -isysroot $(xcrun --show-sdk-path) tls.c`'s +output. The only divergence that mattered to dyld was the missing +header flag. + +(Cosmetic divergence that does not matter: Apple's `ld` relaxes the +`TLVP_LOAD_PAGEOFF12` LDR into an ADD when the descriptor is in-image +and drops the indirect `__thread_ptrs` slot entirely. Our linker +keeps the LDR form and routes through a one-pointer `__thread_ptrs` +section, which dyld accepts.) + +**Path J excluded.** `_tlv_bootstrap` is a libSystem symbol; the JIT +path has no dylib resolution. `36_tls_basic/j_targets` now lists only +the three ELF tuples, mirroring the IFUNC trio in §3.3. A real fix +would provide a TLV-bootstrap stub for the JIT lane. --- @@ -164,11 +162,8 @@ Run the matrix under both tuples; the ELF side is the regression guardrail: make test-elf # 37/37 — unaffected - make test-link # 119/119 — ELF baseline - make test-link CFREE_TEST_OBJ=macho # Path E: 103/103 + - # 1 fail (36_tls_basic, §4) - # Path J: 100/100 + - # 1 fail (36_tls_basic, §4) + make test-link # 122/122 — ELF baseline + make test-link CFREE_TEST_OBJ=macho # 102/102 — Mach-O (E+J) `33_ifunc_in_init/E` is `e_targets`-excluded on `aa64-macho` (§5). -`36_tls_basic/{E,J}` is the open regression — see §4. +`36_tls_basic/J` is `j_targets`-excluded on `aa64-macho` (§4). diff --git a/src/link/link_macho.c b/src/link/link_macho.c @@ -2433,8 +2433,15 @@ void link_emit_macho(LinkImage* img, Writer* w) { mbuf_u32(&file, MH_EXECUTE); mbuf_u32(&file, ncmds); mbuf_u32(&file, lc.len); - mbuf_u32(&file, - MH_DYLDLINK | MH_TWOLEVEL | MH_NOUNDEFS | MH_PIE); + { + u32 mh_flags = MH_DYLDLINK | MH_TWOLEVEL | MH_NOUNDEFS | MH_PIE; + /* dyld scans __thread_vars and allocates a pthread_key for each + * descriptor only when this flag is set; without it the descriptor's + * thunk pointer is silently patched to _tlv_bootstrap_error. Apple's + * ld sets it whenever the image contains S_THREAD_LOCAL_* sections. */ + if (x.ntlv) mh_flags |= MH_HAS_TLV_DESCRIPTORS; + mbuf_u32(&file, mh_flags); + } mbuf_u32(&file, 0); /* reserved */ mbuf_append(&file, lc.data, lc.len); diff --git a/src/obj/macho.h b/src/obj/macho.h @@ -44,6 +44,7 @@ #define MH_DYLDLINK 0x00000004u #define MH_TWOLEVEL 0x00000080u #define MH_PIE 0x00200000u +#define MH_HAS_TLV_DESCRIPTORS 0x00800000u /* ---- load command IDs (subset cfree will emit / consume) ---- */ #define LC_REQ_DYLD 0x80000000u diff --git a/test/link/cases/36_tls_basic/j_targets b/test/link/cases/36_tls_basic/j_targets @@ -0,0 +1,3 @@ +aa64-elf +rv64-elf +x64-elf