kit

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

commit 03954357a132585b90688f2e6551880974081941
parent bb82ba8985a793406af848a2ba1b3171a509a525
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 19 Jul 2026 10:33:58 -0700

x64 backend bug fixes

Diffstat:
Adoc/BACKEND_REFACTOR_CROSS_TEST.md | 38++++++++++++++++++++++++++++++++++++++
Msrc/arch/x64/native.c | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/cg/native_direct_target.c | 1+
Msrc/opt/opt.c | 20++++++++++++++++++++
4 files changed, 170 insertions(+), 24 deletions(-)

diff --git a/doc/BACKEND_REFACTOR_CROSS_TEST.md b/doc/BACKEND_REFACTOR_CROSS_TEST.md @@ -0,0 +1,38 @@ +# Backend refactor cross-architecture findings + +Date: 2026-07-19 +Revision tested: `8d7927db` (`refactor(opt): make scratch and clobber ownership explicit`) + +## Run + +```sh +make test-cross TARGET=all DEPTH=full KIT_VM=1 RUN=1 +``` + +The run covered all 19 configured target selectors and completed in about 3h32m: + +- 233 lane checks passed, 26 failed, 0 skipped. +- All compile/link smoke cases passed. +- All basic smoke executions passed. +- Full corpora ran where wired; Android and 64-bit freestanding targets have smoke-only coverage. + +## Findings + +| Scope | Finding | +| --- | --- | +| Hosted x64, O0/O1 | Aggregate/sret returns are corrupt across Linux, FreeBSD, Windows, and macOS. `x64_copy_bytes()` resolves a stack source through R11 while R11 holds the sret destination, redirecting stores into the stack frame. | +| x64 and RV64, O1 | Overflow-result tests miscompile. | +| Arm32, O1 | Two aggregate-return cases abort emission with `no instruction-local native temporary`. | +| Windows x64 | Packed indirect musttail is rejected at O0 and returns 173 at O1. | +| Linux RV64 | Initialized C TLS returns zero under both glibc and musl at O0/O1. | +| FreeBSD RV64 dynamic | 334 cases per FreeBSD matrix fail before execution because `libc.so.7` uses System V ELF OSABI and carries FreeBSD identity in `NT_FREEBSD_ABI_TAG`; Kit classifies it as Linux. The other 10 failures are the x64 regressions above. | + +All actual AArch64 cases passed. RV32 toy/parser coverage, all glibc/musl libc suites, and non-x64 front/back amalgam executions also passed. + +Linux x64 QEMU-user processes reported SIGSEGV and then hung; the affected test containers were stopped, so the aggregate log records status 137. Native FreeBSD and macOS runs reproduced the segmentation fault. + +## Artifact + +Full log: `build/test-logs/test-cross-all-full.log` + +The tested binaries were built from clean revision `8d7927db`. Workspace edits made after that build were not included in this run. diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -2015,6 +2015,9 @@ static u32 x64_build_prologue(X64NativeTarget* a, u8* buf, u32 cap, return wi; } +static u32 x64_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, + int* variadic, u32* nparams); + static void x64_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { X64NativeTarget* a = x64_of(t); MCEmitter* mc = t->mc; @@ -2025,7 +2028,15 @@ static void x64_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { /* Shared frame bookkeeping: clears the slot table, cum_off, max_outgoing, * callee-save set, and known_frame/has_alloca/frame_final. */ native_frame_reset(&a->frame); - a->incoming_stack_size = 0; + /* Tail-call legality is queried while the single-pass frontend is emitting + * the body, so derive the complete incoming stack window from the signature + * up front rather than waiting for bind_param to walk every declaration. + * x64_signature_stack_bytes includes Win64 shadow space; keep only the + * reusable argument suffix here because x64_no_tail adds the common prefix + * on both sides of its comparison. */ + a->incoming_stack_size = + x64_signature_stack_bytes(t, fd->fn_type, NULL, NULL) - + a->abi->shadow_space; a->next_param_int = 0; a->next_param_fp = 0; a->next_param_stack = 0; @@ -2396,8 +2407,10 @@ static int x64_direct_to_stack(const X64ABIRegs* abi, const ABIArgInfo* ai, next_fp + need_fp > abi->n_fp_args; } -/* Outgoing stack bytes a call uses (16-aligned), per the ABI. */ -static u32 x64_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) { +/* Raw outgoing argument bytes a call uses, including Win64 shadow space but + * excluding the final call-site alignment padding. */ +static u32 x64_call_stack_raw_size(NativeTarget* t, + const NativeCallDesc* desc) { const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); const X64ABIRegs* aregs = x64_abi_for_os(t->c->target.os); u32 next_int = (abi && abi->has_sret) ? 1u : 0u; @@ -2439,11 +2452,42 @@ static u32 x64_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) { x64_sync_slot(aregs, &next_int, &next_fp); } } - return align_up_u32(stack, 16u); + return stack; +} + +/* Outgoing stack bytes a call uses (16-aligned), per the ABI. */ +static u32 x64_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) { + return align_up_u32(x64_call_stack_raw_size(t, desc), 16u); +} + +/* A normal indirect/byval argument can point into the caller's local frame, + * because that frame remains live through a call. A sibling call tears the + * frame down first, so every such payload needs stable storage in the reusable + * incoming argument window. Pack those payloads after the raw outgoing ABI + * slots (using their natural alignment); final call alignment may provide all + * the space, as in a packed three-byte Win64 argument after one stack slot. */ +static u32 x64_tail_call_stack_size(NativeTarget* t, + const NativeCallDesc* desc) { + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); + u32 payload = x64_call_stack_raw_size(t, desc); + u32 i; + for (i = 0; i < desc->nargs; ++i) { + ABIArgInfo tmp; + const ABIArgInfo* ai = x64_param_abi(t, abi, desc, i, &tmp); + u32 align, size; + if (ai->kind != ABI_ARG_INDIRECT) continue; + align = ai->indirect_align; + if (!align) align = native_type_align(t, desc->args[i].type); + size = native_type_size(t, desc->args[i].type); + payload = align_up_u32(payload, align ? align : 1u); + payload += size; + } + return align_up_u32(payload, 16u); } static u32 x64_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) { - return x64_call_stack_size(t, desc); + return (desc->flags & CG_CALL_TAIL) ? x64_tail_call_stack_size(t, desc) + : x64_call_stack_size(t, desc); } static u32 x64_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, @@ -2669,7 +2713,12 @@ static void x64_bind_native_param(NativeTarget* t, const CGParamDesc* p, if (a->next_param_int < a->abi->n_int_args) { ptr_reg = a->abi->int_args[a->next_param_int++]; } else { - ptr_reg = X64_R11; + /* Keep a stack-homed byval pointer out of R10/R11 while copying from + * it. x64_copy_bytes owns that private bank for transfer/address + * resolution; in particular, resolving the frame destination through + * R11 must not overwrite the source pointer between packed chunks. RAX + * is reserved from allocation and has no live entry value here. */ + ptr_reg = X64_RAX; emit_mov_load(t->mc, 8, 0, ptr_reg, X64_RBP, incoming_bias + (i32)a->next_param_stack); a->next_param_stack += 8u; @@ -2769,7 +2818,6 @@ static void x64_bind_native_param(NativeTarget* t, const CGParamDesc* p, tloc, 0, part->size); } } - a->incoming_stack_size = align_up_u32(a->next_param_stack, 16u); } /* Flush the deferred register-destination param binds as a parallel copy (the @@ -2871,7 +2919,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, plan->flags = desc->flags; plan->has_sret = abi && abi->has_sret; plan->is_variadic = abi && abi->variadic; - plan->stack_arg_size = x64_call_stack_size(t, desc); + plan->stack_arg_size = x64_call_stack_bytes(t, desc); if (plan->stack_arg_size > a->frame.max_outgoing) a->frame.max_outgoing = plan->stack_arg_size; for (c = 0; c < NATIVE_CALL_PLAN_CLASSES; ++c) { @@ -2895,6 +2943,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, u32 next_int = (abi && abi->has_sret) ? 1u : 0u; u32 next_fp = 0, stack = aregs->shadow_space, nmoves = 0, i; int tail = (desc->flags & CG_CALL_TAIL) != 0; + u32 tail_payload = tail ? x64_call_stack_raw_size(t, desc) : 0u; u16 p; X64ArgMove moves[X64_MAX_REG_ARG_MOVES]; x64_sync_slot(aregs, &next_int, &next_fp); @@ -2904,17 +2953,36 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, int variadic_arg = abi && i >= abi->nparams; if (ai->kind == ABI_ARG_IGNORE) continue; if (ai->kind == ABI_ARG_INDIRECT) { + NativeLoc src = desc->args[i]; + if (tail) { + AggregateAccess access; + NativeAddr from, to; + u32 align = ai->indirect_align; + u32 size = native_type_size(t, src.type); + if (!align) align = native_type_align(t, src.type); + tail_payload = align_up_u32(tail_payload, align ? align : 1u); + src = x64_incoming_stack_loc( + src.type, NATIVE_REG_INT, (i32)(16u + tail_payload)); + from = x64_storage_addr(a, desc->args[i], 0); + to = x64_storage_addr(a, src, 0); + memset(&access, 0, sizeof access); + access.type = src.type; + access.size = size; + access.align = align ? align : 1u; + x64_copy_bytes(t, to, from, access); + tail_payload += size; + } if (next_int < aregs->n_int_args) { X64ArgMove* m = &moves[nmoves++]; memset(m, 0, sizeof *m); m->dst = native_loc_reg(i64t, NATIVE_REG_INT, aregs->int_args[next_int++]); - m->src = desc->args[i]; + m->src = src; m->size = 8; m->is_addr = 1; } else { NativeLoc ptr = native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX); - x64_addr_of_loc(t, ptr, desc->args[i]); + x64_addr_of_loc(t, ptr, src); x64_store_outgoing_part(t, tail, stack, ptr, 8); stack += 8u; } @@ -3092,17 +3160,21 @@ static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, u32 nr = 0; if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { - /* sret: reload destination pointer (spilled at entry) into r11, memcpy the - * source aggregate into [r11], and convention-return the pointer in rax. */ + /* sret: reload the destination pointer (spilled at entry) into rax and + * copy the source aggregate into [rax]. Keep it out of R10/R11: those are + * x64_copy_bytes' private transfer/address bank, and resolving a stack + * source through R11 would otherwise overwrite the destination. RAX is + * also the ABI's sret return register, so it can stay live through the + * copy and needs no reload afterwards. */ KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); - NativeLoc dstp = native_loc_reg(i64t, NATIVE_REG_INT, X64_R11); + NativeLoc dstp = native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX); NativeLoc saved = native_loc_stack(i64t, a->sret_ptr_slot, 0); NativeAddr dst_addr, src_addr; AggregateAccess access; x64_load_part(t, dstp, saved, 0, 8); memset(&dst_addr, 0, sizeof dst_addr); dst_addr.base_kind = NATIVE_ADDR_BASE_REG; - dst_addr.base.reg = X64_R11; + dst_addr.base.reg = X64_RAX; dst_addr.base_type = value->type; src_addr = x64_storage_addr(a, *value, 0); src_addr.base_type = value->type; @@ -3111,9 +3183,6 @@ static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, access.size = (u32)cg_type_size(t->c, value->type); access.align = native_type_align(t, value->type); x64_copy_bytes(t, dst_addr, src_addr, access); - /* rax = sret pointer. Reload it (copy_bytes clobbered r11/rax). */ - x64_load_part(t, native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX), saved, 0, - 8); *out_rets = NULL; *out_nrets = 0; return; @@ -3900,6 +3969,21 @@ static void emit_stack_guard(MCEmitter* mc, u32 rd) { emit_u32le(mc, 0x28u); } +/* Optimizer folding may put an immediate directly in a native intrinsic even + * when the instruction form itself requires a register. Materialize those + * arithmetic operands in the backend-private bank; ordinary MIR/NDT operand + * locations never occupy R10/R11. Callers pass a distinct scratch for each + * simultaneously-live argument. */ +static u32 x64_intrinsic_arg_reg(X64NativeTarget* a, NativeLoc arg, + u32 scratch, int w) { + if (arg.kind == NATIVE_LOC_REG) return loc_reg(arg); + if (arg.kind == NATIVE_LOC_IMM) { + x64_emit_load_imm(a->base.mc, w, scratch, arg.v.imm); + return scratch; + } + x64_panic(a, "arithmetic intrinsic operand is not register/immediate"); +} + static void x64_intrinsic(NativeTarget* t, IntrinKind kind, const NativeLoc* dsts, u32 ndst, const NativeLoc* args, u32 narg) { @@ -4027,7 +4111,8 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_USUB_OVERFLOW: { int w = x64_is_64(t, dsts[0].type) ? 1 : 0; u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]); - u32 ra = loc_reg(args[0]), rb = loc_reg(args[1]); + u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w); + u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w); u8 op = (kind == INTRIN_SADD_OVERFLOW || kind == INTRIN_UADD_OVERFLOW) ? X64_OPC_ALU_ADD : X64_OPC_ALU_SUB; @@ -4043,7 +4128,8 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_SMUL_OVERFLOW: { int w = x64_is_64(t, dsts[0].type) ? 1 : 0; u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]); - u32 ra = loc_reg(args[0]), rb = loc_reg(args[1]); + u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w); + u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w); if (rd != ra) emit_mov_rr(mc, w, rd, ra); emit_imul_rr(mc, w, rd, rb); emit_setcc(mc, X64_CC_O, rovf); @@ -4053,7 +4139,8 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_UMUL_OVERFLOW: { int w = x64_is_64(t, dsts[0].type) ? 1 : 0; u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]); - u32 ra = loc_reg(args[0]), rb = loc_reg(args[1]); + u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w); + u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w); if (rb == X64_RAX || rb == X64_RDX) { emit_mov_rr(mc, w, X64_R11, rb); rb = X64_R11; @@ -4069,8 +4156,8 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_UMUL_HIGH: { int w = x64_is_64(t, dsts[0].type) ? 1 : 0; u32 rd = loc_reg(dsts[0]); - u32 ra = loc_reg(args[0]); - u32 rb = loc_reg(args[1]); + u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w); + u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w); if (rb == X64_RAX || rb == X64_RDX) { emit_mov_rr(mc, w, X64_R11, rb); rb = X64_R11; @@ -4726,8 +4813,8 @@ static const char* x64_no_tail(NativeDirectTarget* d, const CGCallDesc* call) { if (a->frame.ncallee_saves) return "x64 tail call: callee-saved registers in use"; native_direct_project_tail_call_desc(d, call, &nd); - stack = x64_call_stack_size(d->native, &nd); - /* x64_call_stack_size includes the shadow-space prefix; the caller's incoming + stack = x64_tail_call_stack_size(d->native, &nd); + /* The tail footprint includes the shadow-space prefix; the caller's incoming * window has the same prefix, so compare against incoming_stack_size + it. */ if (stack > a->incoming_stack_size + a->abi->shadow_space) return "x64 tail call: stack argument area too small"; diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -661,6 +661,7 @@ static void nd_store_operand_from_reg(NativeDirectTarget* d, Operand dst, static int nd_local_cacheable(NativeDirectTarget* d, const NativeDirectLocal* l) { return !l->address_taken && !l->memory_required && l->size != 0 && + !cg_type_is_aggregate(d->base.c, l->type) && l->size <= (u32)d->base.c->target.ptr_size; } diff --git a/src/opt/opt.c b/src/opt/opt.c @@ -998,6 +998,26 @@ static const char* opt_on_tail_call_unrealizable_reason( if (caller_va) return "variadic caller cannot host a sibling call"; if (callee_va && call->nargs > callee_nparams) return "variadic tail call arguments are not realizable as a sibling call"; + /* A backend may need tail-only stable storage in the reusable incoming + * window in addition to the ABI argument slots themselves (x64 indirect / + * byval payloads are one example). Once variadic-extra arguments have been + * excluded above, synthesize the pure NativeCallDesc shape from the fixed + * signature so call_stack_bytes can include that target-specific tail cost. + */ + if (o->native->call_stack_bytes) { + NativeCallDesc d; + NativeLoc* args = NULL; + memset(&d, 0, sizeof d); + if (call->nargs) + args = arena_zarray(o->c->tu, NativeLoc, call->nargs); + for (u32 i = 0; i < call->nargs; ++i) + args[i].type = cg_type_func_param_id(o->c, call->fn_type, i); + d.fn_type = call->fn_type; + d.args = args; + d.nargs = call->nargs; + d.flags = CG_CALL_TAIL; + outgoing = o->native->call_stack_bytes(o->native, &d); + } if (outgoing > incoming) return "tail call stack arguments exceed the caller's parameter area"; return NULL;