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:
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) {