fix(verify): gate JIT verification to amd64 until trampolines are hardened
Test / vet (push) Successful in 48s
Test / test (push) Successful in 2m34s
Test / build (push) Successful in 41s

This commit is contained in:
2026-08-30 22:48:48 +02:00
parent 9cb1666b35
commit a5a59d6503
15 changed files with 206 additions and 30 deletions
+10 -9
View File
@@ -364,15 +364,16 @@ and returns). A 64-byte pad below the return address accommodates the
ABIInternal wrapper that the Go runtime interposes on assembly functions.
Every supported architecture carries its own hand-written trampoline pair
(`trampoline_amd64.s`, `trampoline_arm64.s`, `trampoline_riscv64.s`,
`trampoline_loong64.s`), so `Call` works wherever the toolkit runs. The
ABI-checked variant `CallChecked` exists for every architecture too:
`enterJITChecked` plants sentinels in the registers the Go ABI fixes across
calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64 `X27`, loong64 `R22`;
the latter two keep no hardware frame pointer) and the raw return trampoline
`leaveJITCheckedRaw` verifies them, so `-abi` reports frame-pointer,
goroutine-pointer and below-SP violations on every supported host. `gasm
verify` dispatches by host: the JIT checks run when the host matches the
kernel's architecture, and only the toolchain comparisons run elsewhere.
`trampoline_loong64.s`). The ABI-checked variant `CallChecked` exists for
every architecture too: `enterJITChecked` plants sentinels in the registers
the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
`X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
saved registers before Go code resumes. At present only the amd64 JIT path
is runtime-proven: the non-amd64 trampolines compile and their kernels are
correct, but the return into Go code still fails under emulation, so `gasm
verify` gates JIT execution to amd64 kernels on amd64 hosts and runs only
the toolchain comparisons elsewhere (see docs/DECISIONS.md).
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
+31
View File
@@ -33,6 +33,37 @@ runs `go list` as a subprocess (consistent with `toolchainObjectPreamble`
which already calls `go tool asm`). All symbol data is cached per package
for the lifetime of the GOOBJ emission.
## 2026-08-30 non-amd64 JIT execution trampolines
**Status:** open (blocks runtime verification on arm64, riscv64 and
loong64 hosts).
**State.** The per-architecture trampolines compile for all targets, the
kernels they execute are byte-for-byte correct against `go tool asm`, and
under `qemu-aarch64` the arm64 kernel demonstrably executes and stores its
result correctly. The failure is on the return path into Go code: arm64
and loong64 take a SIGSEGV after the kernel's RET (the Go-side unwind
through `leaveJIT` and its interposed ABIInternal wrapper is the suspect),
and riscv64 returns cleanly but with an untouched result area. amd64 is
unaffected (the checked trampoline saves and restores BP/R14 and the flow
is validated end to end).
**Evidence harness.** `verify/jit_arch_test.go` (plain call) and
`verify/abi_arch_test.go` (checked call) are GOARCH-guarded tests; build
the test binary per target (`GOARCH=arm64 go test -c -o v.test ./verify/`)
and run it under `qemu-aarch64-static` from the `verify/` directory. A
minimal reproducer pattern lives in the qemu exploration notes: verify
loads, the kernel executes, the fault follows the return.
**Fix direction.** Compare the amd64 checked trampoline (GLOBL/DATA raw
address, explicit SP/BP/R14 save-restore) against the arm64/riscv64/
loong64 `leaveJIT` unwind, in particular the interaction with the
ABIInternal wrapper that `reflect.ValueOf(leaveJIT).Pointer()` returns.
The plain-call path (no sentinels) fails the same way, so the checked
path is not the variable.
---
## 2026-08-29 tooling round
- `lint abi0-register-args`: flags kernels whose `// func` parameters are