kit

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

commit 3684877141bf927383d6e46b8fd1d2c54dd5af34
parent f595bf99aca8e36b7e2874f6b03cdd6174c25b1e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 09:43:14 -0700

fix(aa64): widen scalar variadic stack args to their full slot

On Apple arm64 every variadic argument is passed in an 8-byte stack slot, but
aa_plan_call's force_stack path loaded chunk=min(8,n) bytes from the value's
home for the whole slot. A scalar narrower than 8 bytes (e.g. an int) thus read
4 garbage high bytes; when the callee va_arg's it as a wider type — the common
`(int)0` read back as a null pointer, as in sqlite3_db_config — the result was a
garbage pointer and a crash. Load the value at its own width (a sub-slot integer
load zero-extends into the temp) and store the full slot, matching clang. -O0
NativeDirectTarget only; the -O1 path was already correct.

Diffstat:
Msrc/arch/aa64/native.c | 42+++++++++++++++++++++++++++++++++---------
1 file changed, 33 insertions(+), 9 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -2886,18 +2886,42 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, abi && abi->variadic && abi->vararg_on_stack && i >= abi->nparams; if (ai->kind == ABI_ARG_IGNORE) continue; if (force_stack) { - NativeLoc tmpreg = - native_loc_reg(desc->args[i].type, NATIVE_REG_INT, AA_TMP0); u32 n = aa_class_vararg_stack_size(abi, ai); - u32 off = 0; stack = aa_vararg_stack_start(abi, stack); - while (off < n) { - u32 chunk = (n - off > 8u) ? 8u : (n - off); - aa_load_part(t, tmpreg, desc->args[i], off, chunk); - aa_store_outgoing_part(t, tail_call, stack + off, tmpreg, chunk); - off += chunk; + if (ai->kind == ABI_ARG_DIRECT && ai->nparts == 1 && n <= 8u) { + /* Scalar variadic argument in one <=8-byte slot. Load the value at + * its OWN width — a sub-slot integer load zero-extends into the + * 64-bit temp — then store the whole slot. Loading the slot width + * from the value's home would pull garbage high bits, and storing + * only the value width would leave them undefined; either way a + * callee that va_arg's a wider type than was passed (the common + * `(int)0` read back as a null pointer) would see garbage. Zero- + * extend-then-store-the-slot is what clang emits and makes it + * well-defined. */ + const ABIArgPart* part = &ai->parts[0]; + NativeAllocClass cls = + part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; + NativeLoc tmpreg = native_loc_reg(desc->args[i].type, cls, + cls == NATIVE_REG_FP ? 16u : AA_TMP0); + aa_load_part(t, tmpreg, desc->args[i], part->src_offset, part->size); + aa_store_outgoing_part(t, tail_call, stack, tmpreg, n); + stack += n; + continue; + } + { + /* Aggregate / multi-word variadic argument: copy its bytes in + * 8-byte chunks (each chunk's home bytes are all live). */ + NativeLoc tmpreg = + native_loc_reg(desc->args[i].type, NATIVE_REG_INT, AA_TMP0); + u32 off = 0; + while (off < n) { + u32 chunk = (n - off > 8u) ? 8u : (n - off); + aa_load_part(t, tmpreg, desc->args[i], off, chunk); + aa_store_outgoing_part(t, tail_call, stack + off, tmpreg, chunk); + off += chunk; + } + stack += n; } - stack += n; continue; } if (ai->kind == ABI_ARG_INDIRECT) {