commit 0f8f6f11206cb62dc958185dd0310b3135f15aa9
parent 76f33b88a9245f1f03ef8d8eb7b5406ae06663a2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 15 Jun 2026 17:41:06 -0700
aa64: address-build fallback for far slots in any size/large frame
The deferred far-slot patch resolves to a base-relative scaled ldr/str
whose immediate must fit the access-size-scaled reach (4KB/8KB/16KB/32KB
for byte/half/word/dword). The one-word patch was kept for word/dword on
the assumption that the 16KB/32KB reach covers any frame; only byte/half
reserved a second word for the address-build fallback.
That assumption breaks on kit's own large frames: src/api/package.c has a
~69KB frame, where a top-of-frame 8-byte slot (small slot offset, so a
base offset near frame_size) resolved to ~68KB -- past the 32KB dword
reach -- and tripped "far slot offset out of positive scaled range".
The final base offset is frame_size - 16 - slot_off, unknown during the
single-pass body emit (the frame is still growing), so no emit-time test
can prove a one-word patch is safe.
Reserve the second word for every far slot regardless of size, and let
the resolver emit the address-build fallback (add x17,base,#hi ;
ldr/str rt,[x17,#lo], correct for any frame < 16MB) for any size whose
scaled offset overflows; it stays a nop when the one-word form fits.
Also routes two raw 0xd503201f nop encodings through aa64_nop().
Regression test test/parse/cases/far_slot_large_frame.c exercises 4-byte
and 8-byte far slots in a >32KB frame; passes E on aa64/x64/rv64 at
O0/O1.
Diffstat:
3 files changed, 59 insertions(+), 25 deletions(-)
diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c
@@ -884,14 +884,14 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg,
* final, aa_apply_patches rewrites it to `[base, #frame_size-...]`, base = sp
* (stable) or AA_FRAME_BASE (alloca).
*
- * A 4/8-byte slot (sz>=2) always fits the scaled imm12 (reach 16 KB/32 KB >=
- * any frame), so it is a one-word patch. A 1/2-byte slot (sz<2, Lever 3) has
- * a smaller scaled reach (4 KB/8 KB) that the largest frames (sqlite VdbeExec
- * ~6.5 KB) exceed, so it reserves TWO words: when the resolved offset fits it
- * is `ldrb/strb [base,#scaled] ; nop`, and when it overflows the resolver
- * emits the address-build fallback `add x17,base,#hi ; ldrb/strb [x17,#lo]`
- * (correct for any frame). byte/half are always integer (vbit=0; FP was
- * widened to sz>=2 above).
+ * The base-relative offset is frame_size - 16 - slot_off, unknown here
+ * (single-pass: the frame is still growing) and able to exceed the scaled
+ * ldr/str reach (4 KB/8 KB/16 KB/32 KB by size) for ANY access size — a
+ * top-of-frame 8-byte slot sits ~frame_size off sp, and kit's own
+ * src/api/package.c reaches a ~69 KB frame. So every size reserves TWO words:
+ * when the resolved offset fits it is `ldr/str [base,#scaled] ; nop`, and when
+ * it overflows the resolver emits the address-build fallback `add x17,base,#hi
+ * ; ldr/str [x17,#lo]` (correct for any frame < 16 MB).
*
* Lever 3/4 interaction: a far SIGNED narrow load (ld_opc==LDRS_X) is
* excluded from this scaled-slot path — the slot patch records only a plain
@@ -928,9 +928,14 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg,
p->u.slot.rt = (u8)rt;
aa_emit32(mc, load ? aa_ldr_uimm_v(sz, vbit, rt, AA_SP, 0)
: aa_str_uimm_v(sz, vbit, rt, AA_SP, 0));
- /* Reserve the fallback's second word for byte/half (filled at resolve).
- */
- if (sz < 2u) aa_emit32(mc, 0xd503201fu /* nop */);
+ /* Reserve a second word for the address-build fallback. The final
+ * base-relative offset is frame_size - 16 - slot_off, which is unknown
+ * here (single-pass: the frame keeps growing) and can exceed the scaled
+ * ldr/str reach for ANY access size in a large frame (a top-of-frame
+ * 8-byte slot sits ~frame_size off sp; kit's own src/api/package.c has a
+ * ~69 KB frame). So every size reserves the fallback word; aa_apply_patches
+ * leaves it a nop when the one-word scaled form fits. */
+ aa_emit32(mc, aa64_nop());
return;
}
}
@@ -1352,7 +1357,7 @@ static void aa_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
a->prologue_region_words = region;
{
u8 nops[AA_NDT_SUB_WORDS * 4u];
- for (u32 i = 0; i < region; ++i) wr_u32_le(nops + i * 4u, 0xd503201fu);
+ for (u32 i = 0; i < region; ++i) wr_u32_le(nops + i * 4u, aa64_nop());
if (mc->debug) {
u32 ofs = obj_pos(mc->obj, mc->section_id);
mc_emit_bytes(mc, nops, region * 4u);
@@ -1844,19 +1849,19 @@ static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) {
if (base_off < 0 || (ubo & ((1u << sl->sz) - 1u)) != 0u)
aa_panic(a, "far slot offset negative or misaligned");
if ((ubo >> sl->sz) <= 0xfffu) {
- /* Scaled immediate fits: one-word ldr/str. For byte/half the reserved
- * second placeholder word stays the nop emitted at body time. */
+ /* Scaled immediate fits: one-word ldr/str; the reserved second
+ * placeholder word stays the nop emitted at body time. */
aa_patch32(a->base.obj, sec, p->pos,
sl->load
? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo)
: aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo));
- } else if (sl->sz < 2u) {
- /* Lever 3 overflow fallback. byte/half scaled reach (4 KB/8 KB) is
- * below the largest frames, so build the address into the reserved
- * second word instead: `add x17,base,#(off & ~0xfff) ; ldrb/strb
- * rt,[x17,#(off & 0xfff)]`. The high part is a multiple of 4096 (fits
- * the shift-12 addsub imm for any frame < 16 MB); the low part is <4096
- * and, for a half, even (base_off is 2-aligned), so both scale-divide
+ } else {
+ /* The scaled reach (4 KB/8 KB/16 KB/32 KB by size) is below the largest
+ * frames, so build the address into the reserved second word instead:
+ * `add x17,base,#(off & ~0xfff) ; ldr/str rt,[x17,#(off & 0xfff)]`. The
+ * high part is a multiple of 4096 (fits the shift-12 addsub imm for any
+ * frame < 16 MB); the low part is <4096 and, being naturally aligned to
+ * the access size (base_off is, and hi is 4096-aligned), scale-divides
* cleanly. */
u32 hi = ubo & ~0xfffu;
u32 lo = ubo & 0xfffu;
@@ -1868,10 +1873,6 @@ static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) {
sl->load
? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo)
: aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo));
- } else {
- /* 4/8-byte scaled reach (16 KB/32 KB) covers any frame, so this is
- * unreachable; a one-word patch has no fallback room. */
- aa_panic(a, "far slot offset out of positive scaled range");
}
} else { /* AA_PATCH_TAIL */
NativeLoc callee = p->u.callee;
diff --git a/test/parse/cases/far_slot_large_frame.c b/test/parse/cases/far_slot_large_frame.c
@@ -0,0 +1,32 @@
+/* Far frame slots in a very large (>32KB) frame. `x` (8 bytes) and `y` (4
+ * bytes) are declared before the big array, so they get small slot offsets and
+ * thus large base-relative offsets (near frame_size off sp) -- past the scaled
+ * ldr/str reach (32KB for 8-byte, 16KB for 4-byte) on aarch64. The far-slot
+ * fast path must resolve these via the address-build fallback rather than an
+ * out-of-range scaled immediate (which previously tripped "far slot offset out
+ * of positive scaled range" compiling src/api/package.c, a ~69KB frame).
+ * x64/rv64 have no scaled-offset constraint, so this just confirms correctness
+ * everywhere. Returns the count of passing checks (expect 7). */
+
+int test_main(void) {
+ volatile long x;
+ volatile int y;
+ volatile char huge[40000];
+ int i;
+ int ok = 0;
+ long s = 0;
+
+ x = 0x1122334455667788L;
+ y = 0x0a0b0c0d;
+ for (i = 0; i < 40000; i += 997) huge[i] = (char)(i & 0x7f);
+
+ /* Read the far slots back. */
+ if (x == 0x1122334455667788L) ok += 3;
+ if (y == 0x0a0b0c0d) ok += 4;
+
+ /* Keep `huge` live so the big frame is not elided. */
+ for (i = 0; i < 40000; i += 997) s += huge[i];
+ if (s < 0) ok = -1;
+
+ return ok;
+}
diff --git a/test/parse/cases/far_slot_large_frame.expected b/test/parse/cases/far_slot_large_frame.expected
@@ -0,0 +1 @@
+7