kit

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

commit 30873143c411b1f4c5fd73db6ecb79b0b0fb4c32
parent 13677b4af13ca7c0336624e9f49d44c792de6201
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 18:39:54 -0700

feat(x64-macos): thread-locals via Mach-O TLV descriptor model

x86_64-macos _Thread_local accesses SIGSEGV'd: the x64 backend emitted the
ELF Local-Exec idiom (mov %fs:0 + R_X64_TPOFF32, mis-mapped onto
X86_64_RELOC_TLV) instead of the macOS TLV descriptor model, so the
linker's TLV collection (keyed on RELOC_IS_TLVP) found nothing and the
load dereferenced garbage. arm64-macos already used the descriptor model.

- New R_X64_TLV reloc kind (RIP-relative disp32, RELOC_IS_TLVP), the
  x86_64 peer of arm64 TLVP_LOAD_PAGE21/PAGEOFF12; maps to/from
  X86_64_RELOC_TLV. Applied via the neutral RIP-relative path (-4 bias in
  the addend, like the other x64 RIP kinds). The existing arch-neutral
  __thread_ptrs slot machinery + obj_define_tls descriptor now serve both
  arches unchanged.
- x64_tls_addr_of: descriptor branch emits
    movq sym@TLVP(%rip), %rdi ; callq *(%rdi) ; [add %rax, addend] ; mov rd, %rax
  modeling the thunk's {rax, rdi} clobbers in x64_machine_op_clobbers.
- Leaf analysis: a descriptor-model IR_TLS_ADDR_OF is a hidden call (the
  resolver thunk). Mark has_call so the x64 SysV red-zone leaf tier is not
  selected -- otherwise callee-saved spills land in the red zone and the
  thunk's call instruction corrupts them (wrong results at -O1; -O0 was fine).

Full x86_64-macos toy corpus now 330 pass / 0 fail / 1 skip (was 324/330);
kit TLV objects link under system ld64 too. Removes both x86_64-macos
entries from doc/plan/TODO.md.

Diffstat:
Mdoc/plan/TODO.md | 32--------------------------------
Msrc/arch/x64/native.c | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/arch/x64/reloc.c | 1+
Msrc/obj/macho/reloc_x86_64.c | 6+++---
Msrc/obj/obj.c | 1+
Msrc/obj/obj.h | 6++++++
Msrc/obj/reloc_apply.c | 7++++++-
Msrc/opt/pass_native_emit.c | 8++++++++
Mtest/link/reloc_desc_test.c | 8++++++--
9 files changed, 102 insertions(+), 39 deletions(-)

diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -85,38 +85,6 @@ Add new deferred fixes below as they are discovered. the existing scratch between mem-staging and operand-staging — a register-policy decision, hence "reviewed change." -- **x86_64-macos chained-fixups rebase is off by one page (SIGBUS at dyld load).** - A global pointer initialized to another global's address (`var gp: *i64 = &g`, - or C `long g; long *gp = &g;`) crashes at process start *inside dyld* — - `applyFixupsGeneric` does `movq` to e.g. `0x100008000`, exactly **one 16 KB - page past** `__data` (`0x100004000`), faulting on unmapped memory. The rebased - pointer *value* kit writes is correct (gp holds g's address, same bytes as the - working arm64-macos build); the bug is the **`dyld_chained_starts_in_segment` - page table** kit emits for x86_64 — a wrong `page_start` / `segment_offset` - (or page indexing) sends dyld's chain walk into the next page. arm64-macos is - fine. Reproduces both freestanding and hosted (`-lc`). Location: - `src/obj/macho/link.c` ~1544–1700 (the chained-fixups emit; `MZ_PAGE=0x4000`). - Repro / guard: toy case `15_cg_api_types_bytes_globals` (SIGBUS, want exit 92) - under `make test-cross TARGET=macos-x64 DEPTH=full` (test/toy/vm.sh macos). - -- **x86_64-macos thread-locals are not linked (TLV pass is arm64-only).** An - extern/`_Thread_local` access on x86_64-macos SIGSEGVs at runtime: the Mach-O - linker's TLV collection + apply passes (`src/obj/macho/link.c` ~251–264, - 465–468, plus the `__thread_ptrs` slot pass ~1638) key off the **arm64** TLV - relocs (`ARM64_RELOC_TLVP_LOAD_PAGE21/PAGEOFF12`); x86_64's single - `X86_64_RELOC_TLV` is never collected, so no `__thread_ptrs` slot / descriptor - is set up and the thread-local load dereferences garbage. (Freestanding x86_64 - thread-locals additionally can't even link — `undefined reference to - '_tlv_bootstrap'`, a libSystem symbol — which is why these cases carry a - `.link.hosted` sidecar and must link `-lc`.) arm64-macos is fine. Repro / guard: - toy cases `141_threadlocal_mutate` (want 43) + `142_threadlocal_multi` - (want 134), both SIGSEGV, in the `macos-x64 DEPTH=full` lane above. - - (Context: x86_64-macos compile+link+execute via Rosetta otherwise works as of - `feat(macho): x86_64-macos compile+link+execute via Rosetta`; the full toy - corpus is 324/330 green and these two gaps are the only failures. The lane is - committed RED to track them.) - ## Deferred dedups & abstraction cleanups ("use the shared seam") - **A.2 — optimizer-path inline-asm dedup.** The three `aa_/x64_/rv_asm_block_native` diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -2949,6 +2949,63 @@ static void x64_tls_addr_of_win64(NativeTarget* t, NativeLoc dst, ObjSymId sym, } } +/* x86-64 Mach-O TLV (descriptor model): load the __thread_ptrs slot holding + * the TLV descriptor address into %rdi, then call the descriptor's resolver + * thunk (descriptor[0]); it returns the variable's storage address in %rax. + * Peer of arm64's aa_tls_addr_of descriptor path. The slot is addressed by an + * R_X64_TLV reloc (RIP-relative disp32) that the Mach-O linker routes through + * __thread_ptrs. The Apple TLV thunk preserves every register except %rax, so + * the only clobbers are %rdi (we load it) and %rax (the result) — modeled in + * x64_machine_op_clobbers so no value lives across the access in them. Any + * addend is a byte offset into the variable, applied to %rax after the call. */ +static void x64_tls_addr_of_macho(NativeTarget* t, NativeLoc dst, ObjSymId sym, + i64 addend) { + MCEmitter* mc = t->mc; + u32 sec = mc->section_id; + u32 rd = loc_reg(dst); + u32 disp_pos; + /* movq sym@TLVP(%rip), %rdi (48 8B 3D disp32). */ + emit_rex(mc, 1, X64_RDI, 0, 0); + { + u8 op = X64_OPC_MOV_R_RM; + mc->emit_bytes(mc, &op, 1); + } + { + u8 mr = modrm(0u, X64_RDI & 7u, 5u); /* [rip + disp32] */ + mc->emit_bytes(mc, &mr, 1); + } + disp_pos = mc->pos(mc); + emit_u32le(mc, 0); + mc->emit_reloc_at(mc, sec, disp_pos, R_X64_TLV, sym, -4, 1, 0); + /* callq *(%rdi) (FF /2, mem [rdi]) -> %rax = &storage. */ + { + u8 call[2] = {0xFFu, modrm(0u, 2u, X64_RDI & 7u)}; + mc->emit_bytes(mc, call, 2); + } + /* %rax += addend (byte offset into the variable). */ + if (addend) { + i32 a = (i32)addend; + if (a == addend) { + emit_rex(mc, 1, 0, 0, X64_RAX); + if (imm_fits_i8(a)) { + u8 buf[3] = {X64_OPC_ALU_IMM8, modrm(3u, X64_ALU_SUB_ADD, X64_RAX & 7u), + (u8)a}; + mc->emit_bytes(mc, buf, 3); + } else { + u8 buf[2] = {X64_OPC_ALU_IMM32, + modrm(3u, X64_ALU_SUB_ADD, X64_RAX & 7u)}; + mc->emit_bytes(mc, buf, 2); + emit_u32le(mc, (u32)a); + } + } else { + /* Wide addend: materialize in r11 then add. */ + x64_emit_load_imm(mc, 1, X64_R11, addend); + emit_alu_rr(mc, 1, X64_OPC_ALU_ADD, X64_RAX, X64_R11); + } + } + if (rd != X64_RAX) emit_mov_rr(mc, 1, rd, X64_RAX); +} + /* x86-64 TLS Local-Exec: mov rd, fs:0; lea rd, [rd + sym@tpoff]. */ static void x64_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym, i64 addend) { @@ -2956,6 +3013,10 @@ static void x64_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym, u32 sec = mc->section_id; u32 rd = loc_reg(dst); u32 disp_pos; + if (obj_format_tls_via_descriptor(t->c)) { + x64_tls_addr_of_macho(t, dst, sym, addend); + return; + } if (obj_format_tls_model(t->c) == OBJ_TLS_WINDOWS_TEB) { x64_tls_addr_of_win64(t, dst, sym, addend); return; @@ -4113,9 +4174,18 @@ static void x64_set_loc(NativeTarget* t, SrcLoc loc) { * nothing). */ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, u32 mask[NATIVE_CALL_PLAN_CLASSES]) { - (void)t; mask[0] = mask[1] = mask[2] = 0; switch ((NativeMachineOpKind)op->kind) { + case NATIVE_MOP_TLS_ADDR: + /* ELF Local-Exec / Windows TEB materialize the address using only the + * destination register (no fixed clobbers). The Mach-O TLV descriptor + * sequence loads the descriptor into %rdi and calls its resolver thunk, + * which returns the storage in %rax and preserves every other register; + * model the %rdi/%rax clobbers so no value lives across the access in + * them. */ + if (!obj_format_tls_via_descriptor(t->c)) return 0; + mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDI); + return 1; case NATIVE_MOP_BINOP: switch ((BinOp)op->binop) { case BO_SDIV: diff --git a/src/arch/x64/reloc.c b/src/arch/x64/reloc.c @@ -28,6 +28,7 @@ static const RelocDescRow x64_rows[] = { {R_X64_GOTPC32, {4, 0}}, {R_X64_GOTTPOFF, {4, RELOC_IS_TLS_GOT}}, {R_X64_TPOFF32, {4, RELOC_IS_TLS_LE}}, + {R_X64_TLV, {4, RELOC_IS_TLVP}}, {R_X64_GLOB_DAT, {8, 0}}, {R_X64_JUMP_SLOT, {8, 0}}, {R_X64_RELATIVE, {8, 0}}, diff --git a/src/obj/macho/reloc_x86_64.c b/src/obj/macho/reloc_x86_64.c @@ -22,7 +22,7 @@ u32 macho_x86_64_reloc_to(u32 kind /* RelocKind */) { return X86_64_RELOC_GOT_LOAD; case R_X64_GOTPCREL: return X86_64_RELOC_GOT; - case R_X64_TPOFF32: + case R_X64_TLV: return X86_64_RELOC_TLV; default: return (u32)-1; @@ -41,7 +41,7 @@ u32 macho_x86_64_reloc_pcrel(u32 kind /* RelocKind */) { case R_X64_GOTPCREL: case R_X64_GOTPCRELX: case R_X64_REX_GOTPCRELX: - case R_X64_TPOFF32: + case R_X64_TLV: return 1; default: return 0; @@ -77,7 +77,7 @@ u32 macho_x86_64_reloc_from(u32 macho_type) { case X86_64_RELOC_GOT: return R_X64_GOTPCREL; case X86_64_RELOC_TLV: - return R_X64_TPOFF32; + return R_X64_TLV; default: return (u32)-1; } diff --git a/src/obj/obj.c b/src/obj/obj.c @@ -1308,6 +1308,7 @@ const char* reloc_kind_name(RelocKind k) { _CASE(R_X64_JUMP_SLOT); _CASE(R_X64_RELATIVE); _CASE(R_X64_COPY); + _CASE(R_X64_TLV); _CASE(R_RV_HI20); _CASE(R_RV_LO12_I); _CASE(R_RV_LO12_S); diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -209,6 +209,12 @@ typedef enum RelocKind { R_X64_JUMP_SLOT, R_X64_RELATIVE, R_X64_COPY, + /* Mach-O x86_64 TLV access: RIP-relative disp32 in `movq sym@TLVP(%rip), + * %rdi` addressing the __thread_ptrs slot that holds the TLV descriptor + * address (the descriptor-model peer of arm64's TLVP_LOAD_PAGE21/PAGEOFF12; + * maps to X86_64_RELOC_TLV). Distinct from the ELF R_X64_TPOFF32 Local-Exec + * kind. */ + R_X64_TLV, R_RV_HI20, R_RV_LO12_I, R_RV_LO12_S, diff --git a/src/obj/reloc_apply.c b/src/obj/reloc_apply.c @@ -127,11 +127,16 @@ int reloc_apply_neutral(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, case R_X64_GOTPCRELX: case R_X64_REX_GOTPCRELX: case R_X64_GOTPC32: - case R_X64_GOTTPOFF: { + case R_X64_GOTTPOFF: + case R_X64_TLV: { /* GOTTPOFF (TLS Initial-Exec) is a RIP-relative load of a GOT slot * that the linker fills with the symbol's TP-relative offset; the * fixup is identical to GOTPCREL once the target has been redirected * to that slot (see link_layout_got). */ + /* TLV (Mach-O x86_64 descriptor model): RIP-relative disp32 in + * `movq sym@TLVP(%rip), %rdi`, redirected by the linker to address the + * __thread_ptrs slot (S = slot vaddr). The -4 RIP bias rides in the + * addend, same as the other RIP-relative kinds here. */ /* AArch64 ELF: PREL32 maps to either of these; both encode a * 32-bit signed PC-relative displacement. The kit-canonical * distinction (section-relative vs PC-relative) collapses on diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -1435,6 +1435,14 @@ static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) { if (aux && (aux->kind == INTRIN_FRAME_ADDRESS || aux->kind == INTRIN_RETURN_ADDRESS)) reads_frame = 1; + } else if ((IROp)in->op == IR_TLS_ADDR_OF) { + /* The Mach-O TLV descriptor access calls the resolver thunk — a hidden + * call the leaf analysis must see, or the frame-eliding leaf tiers (the + * x64 SysV red zone, which keeps spills below sp; any return-address + * elision) miscompile: the thunk's `call`/`blr` clobbers the return + * address and the bytes below sp. ELF Local-Exec and Windows TEB + * accesses make no call, so only the descriptor model disqualifies. */ + if (obj_format_tls_via_descriptor(t->c)) has_call = 1; } } } diff --git a/test/link/reloc_desc_test.c b/test/link/reloc_desc_test.c @@ -52,6 +52,9 @@ static u8 oracle_width(RelocKind k) { case R_X64_REX_GOTPCRELX: case R_X64_GOTPC32: case R_X64_GOTTPOFF: + /* R_X64_TLV: post-freeze addition — the Mach-O x86_64 TLV descriptor + * access (RIP-relative disp32), peer of arm64's TLVP_LOAD_PAGE21/12. */ + case R_X64_TLV: return 4; case R_ABS64: case R_REL64: @@ -186,10 +189,11 @@ static int oracle_rv_branch(RelocKind k) { return k == R_RV_CALL || k == R_PLT32; } -/* is_tlvp_reloc (Mach-O aa64 only). */ +/* is_tlvp_reloc (Mach-O). */ static int oracle_aa64_tlvp(RelocKind k) { return k == R_AARCH64_TLVP_LOAD_PAGE21 || k == R_AARCH64_TLVP_LOAD_PAGEOFF12; } +static int oracle_x64_tlvp(RelocKind k) { return k == R_X64_TLV; } /* is_direct_page_reloc (Mach-O aa64 only). */ static int oracle_aa64_direct_page(RelocKind k) { @@ -227,7 +231,7 @@ static const ArchOracle kArchOracles[] = { {"aarch64", KIT_ARCH_ARM_64, oracle_aa64_got_use, oracle_aa64_tls_got, oracle_aa64_branch, oracle_aa64_tlvp, oracle_aa64_direct_page}, {"x86_64", KIT_ARCH_X86_64, oracle_x64_got_use, oracle_x64_tls_got, - oracle_x64_branch, zero_oracle, zero_oracle}, + oracle_x64_branch, oracle_x64_tlvp, zero_oracle}, {"rv64", KIT_ARCH_RV64, oracle_rv_got_use, oracle_rv_tls_got, oracle_rv_branch, zero_oracle, zero_oracle}, {"rv32", KIT_ARCH_RV32, oracle_rv_got_use, oracle_rv_tls_got,