|
| 1 | +# Authentic generic scalar CALLXS checkpoint (2026-07-18) |
| 2 | + |
| 3 | +## Status and scope |
| 4 | + |
| 5 | +This checkpoint executes the real generic x64 `IR_CALLXS` seam between the |
| 6 | +XSAVE-consuming exact-frame entry/leave helpers. Activation is deliberately |
| 7 | +test-only under `LJ_FFI_CALLXS_TEST_ACTIVATE`; the default recorder still raises |
| 8 | +`LJ_TRERR_BLACKL`. No `plan/` file is changed. |
| 9 | + |
| 10 | +The path is declaration-independent. It adds no signature enum, C declaration |
| 11 | +matcher, wrapper catalogue, or per-shape dispatcher. CType-driven argument |
| 12 | +conversion and the existing x64 ABI classifier continue to build one generic |
| 13 | +`CARG` tree and lower one `CALLXS`. |
| 14 | + |
| 15 | +## Generated lifecycle |
| 16 | + |
| 17 | +For the currently admitted nonallocating scalar result-handoff classes, |
| 18 | +recording builds every CType argument conversion before the native boundary, |
| 19 | +then emits: |
| 20 | + |
| 21 | +```text |
| 22 | +XSAVE |
| 23 | +CALLS lj_ffi_native_trace_enter(L, exact_trace, raw_function) |
| 24 | +guard enter != 0 -- pre-call/replaying snapshot |
| 25 | +CALLXS generic_CARG_tree |
| 26 | +pure result normalization |
| 27 | +caller-state snapshot |
| 28 | +CALLS lj_ffi_native_trace_leave(L) -- first C call after the foreign call |
| 29 | +guard leave == 0 -- post-call/non-replaying snapshot |
| 30 | +``` |
| 31 | + |
| 32 | +The admitted result normalizations are nonthrowing machine conversions and do |
| 33 | +not alter the foreign error pair. Leave remains the first C call after the |
| 34 | +foreign return. Its `CCI_T` unwind and its forced guard now share the caller |
| 35 | +snapshot because that snapshot's IR reference names leave itself. The enter and |
| 36 | +leave IRCALL descriptors intentionally do not claim preserved FP registers; |
| 37 | +normal register allocation must move or spill the normalized result across |
| 38 | +leave. The authentic matrix proves both GPR and XMM result survival. |
| 39 | + |
| 40 | +Both native-boundary guard snapshots carry `SNAPCOUNT_DONE`, used here as an |
| 41 | +explicit never-record marker. LOOP copy-substitution preserves this marker. |
| 42 | +The pre-call rejection stays in the interpreter, while the completed-call exit |
| 43 | +must pass through central trace-exit cleanup; linking either exit to a side |
| 44 | +trace could otherwise cross or bypass native lifecycle ownership. |
| 45 | + |
| 46 | +The opt-in boundary currently accepts ordinary `CALL`, `CALLM`, and `ITERC` |
| 47 | +Lua frames only. Bool, pointer, enum, i64 and u64 results stay interpreted: |
| 48 | +their current result paths branch or allocate cdata after the foreign side |
| 49 | +effect. Protected, continuation, vararg-frame and tail-return shapes also stay |
| 50 | +interpreted until each has an exact post-return snapshot contract. These are |
| 51 | +return/frame-class safety boundaries, not declaration matching. |
| 52 | + |
| 53 | +## Exact trace constant on loop traces |
| 54 | + |
| 55 | +The first authentic loop showed that KTRACE patching in `asm_tail_link()` was |
| 56 | +not sufficient: normal loop traces skip tail linking, so their placeholder |
| 57 | +remained null and native entry rejected every execution. That looked correct at |
| 58 | +the Lua level because the pre-call exit replayed the call in the interpreter. |
| 59 | + |
| 60 | +KTRACE is now patched immediately after each assembly attempt selects the |
| 61 | +copied `J->curfinal->ir`. This covers loop and non-loop traces plus IR-growth, |
| 62 | +alignment and RENAME retries which allocate a fresh final body. The generated |
| 63 | +fixture inspects the enter CARG and proves its KGC is the exact finalized body. |
| 64 | + |
| 65 | +## Caller-state post-call snapshot |
| 66 | + |
| 67 | +The first forced leave exit exposed a real continuation bug. `lj_record_ret()` |
| 68 | +had already moved `J->base`, `baseslot`, `framedepth` and result slots back to |
| 69 | +the Lua caller, but `L->base/top` and `J->pc/fn/pt` still described the live |
| 70 | +fast-function retry. The snapshot therefore combined caller values with the |
| 71 | +synthetic `BC_FUNCC` PC. Its side trace started at source line zero and restore |
| 72 | +resumed `lj_BC_FUNCC` with a Lua closure where a C closure was expected, |
| 73 | +jumping through a Lua upvalue word into non-executable GC memory. |
| 74 | + |
| 75 | +`ffrecord_postcall_snap()` now presents the matching Lua caller view only while |
| 76 | +the snapshot is built. It does not shift the recorder stack a second time. It |
| 77 | +validates the caller PC against the caller prototype, snapshots with caller |
| 78 | +`L->base/top` and `J->pc/fn/pt`, and then restores the still-live fast-function |
| 79 | +view. Terminal trace links and unsupported physical frame shapes fail recording |
| 80 | +instead of appending a guard to an invalid continuation. |
| 81 | + |
| 82 | +For its i32 side-effect target, the C fixture mechanically verifies that every |
| 83 | +activated CALLXS is immediately followed by native leave, every leave guard |
| 84 | +owns a snapshot inside the expected Lua prototype, and the root snapshot's |
| 85 | +frame/top encoding matches that prototype. It then forces a handshake-epoch |
| 86 | +change while the final frame sequence is odd. The real guard restores the |
| 87 | +caller, releases the retained pin once, preserves the exact foreign side-effect |
| 88 | +count, and leaves the root safely re-enterable. |
| 89 | + |
| 90 | +## One-shot XSAVE ownership |
| 91 | + |
| 92 | +Every native-entry attempt consumes all three XSAVE staging words. Earlier |
| 93 | +rejected entries left them intact for the pre-call snapshot exit, but a hot |
| 94 | +exit can link directly to side code and bypass central trace-exit cleanup. |
| 95 | +Authentic occupied-depth injection exposed the stale raw |
| 96 | +`ffi_xsave_root/baseslot/nslots` geometry after interpreter fallback. |
| 97 | + |
| 98 | +`lj_ffi_native_trace_enter()` now performs the complementary owner clear on |
| 99 | +every valid-carrier return, including capacity, occupied-depth, malformed |
| 100 | +geometry, trace-pin and publication rejection. This keeps dormant XSAVE-only |
| 101 | +instrumentation independent of unrelated trace exits. The injected rejection |
| 102 | +proves that CALLXS and generated leave do not run, the interpreter performs each |
| 103 | +foreign side effect exactly once, and all staging is zero afterward. |
| 104 | + |
| 105 | +## Deterministic evidence |
| 106 | + |
| 107 | +`m7_ffi_callxs_authentic` builds the runtime with the explicit activation macro |
| 108 | +and runs two fixtures: |
| 109 | + |
| 110 | +- `t-ffi-callxs-authentic.lua` covers void, signed and unsigned 8/16/32-bit |
| 111 | + scalar returns, float, double, mixed GPR/XMM/pointer/u64 arguments, C |
| 112 | + varargs, and generated-call `errno` preservation. It requires real XSAVE and |
| 113 | + CALLXS IR, while proving bool, pointer, enum, i64 and u64 results remain |
| 114 | + interpreted with their exact values intact. |
| 115 | +- `t-ffi-callxs-postcall.c` proves exact finalized KTRACE identity, immediate |
| 116 | + leave ordering, caller snapshot PC/frame geometry, permanent no-side-trace |
| 117 | + markers, repeated forced POSTCALL cleanup under `hotexit=1`, exact side |
| 118 | + effects, a deterministic fresh-STOPREQ injection between generated native |
| 119 | + leave and its throw decision from the same caller snapshot, re-entry, |
| 120 | + rejected-entry fallback, zero native depth, zero frame depth, zero trace pins, |
| 121 | + restored callback/function mirrors and cleared XSAVE staging. |
| 122 | + |
| 123 | +Focused validation passed: |
| 124 | + |
| 125 | +- `m7_ffi_callxs_authentic`; |
| 126 | +- default-gated `m7_ffi_ccall_native` and `m7_ffi_native_frames`; |
| 127 | +- `m6_jit_xsave`; |
| 128 | +- `m5_jit_trace_publish`; |
| 129 | +- Clang 19 ASan with fail-fast and leak checking disabled for both authentic |
| 130 | + fixtures; |
| 131 | +- Clang 19 UBSan with the repository's documented alignment, function, |
| 132 | + pointer-overflow and shift exclusions; |
| 133 | +- default, `LUAJIT_DISABLE_JIT`, and `LUAJIT_DISABLE_FFI` builds. |
| 134 | + |
| 135 | +## Production gate remains closed |
| 136 | + |
| 137 | +This checkpoint does not claim safe default generic FFI tracing. The remaining |
| 138 | +coupled gates are: |
| 139 | + |
| 140 | +1. callback `ACTIVE -> SUSPENDED -> ACTIVE` publication, nested callback and |
| 141 | + error-unwind cleanup, plus nonwaiting carrier leasing; |
| 142 | +2. pre-rooted boxed result storage and nonthrowing bool/result handoff; |
| 143 | +3. exact snapshot contracts for pcall, continuation, vararg and tail/RETF |
| 144 | + return shapes; |
| 145 | +4. remote trace-flush admission for a certified pinned ACTIVE frame, followed |
| 146 | + by forced POSTCALL exit from the retired exact body; |
| 147 | +5. descriptor-driven aggregate argument and multi-register result lowering; |
| 148 | +6. Linux stress and sanitizer proof followed by Win64/Wine and macOS/Darling |
| 149 | + ABI and error-state coverage. |
| 150 | + |
| 151 | +Unknown foreign functions can callback on their first invocation regardless of |
| 152 | +their declaration, so historical callback blacklisting cannot justify opening |
| 153 | +the default gate. The next lifecycle tranche is callback suspension, not a new |
| 154 | +signature allowlist. |
0 commit comments