Skip to content

fix(amd64): lay out System V arguments as libffi does - #113

Merged
kolkov merged 1 commit into
mainfrom
fix/amd64-stack-args
Nov 1, 2026
Merged

kolkov merged 1 commit into
mainfrom
fix/amd64-stack-args

Conversation

@kolkov

@kolkov kolkov commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

On System V amd64 (Linux, macOS, FreeBSD) a by-value struct argument over 72 bytes panicked inside CallFunction with index out of range [15] with length 15; webgpu.h's 96-byte WGPUAdapterInfo hit it. Reading the same path turned up two bugs that give wrong values without any error: a 9–16 byte struct split between the last register and the stack, and nested struct members classified as INTEGER.

Causes:

  • the call path copied stack arguments into a fixed 9-word area and checked its size after writing, and PrepareCallInterface counted a struct over 16 bytes as using no stack;
  • when fewer registers were left than a struct needs, its first eightbyte still took the last register. psABI §3.2.3 and libffi (src/x86/ffi64.c, ffi_prep_cif_machdep / ffi_call_int) pass the whole argument on the stack and keep the register for later arguments;
  • classifyEightbyte did not look into nested structs, so struct { struct { float x, y; } a, b; } went in GPRs and was returned from RAX:RDX instead of XMM0:XMM1.

Change:

  • internal/arch/amd64/layout_sysv.go: one System V argument allocator following libffi, used by PrepareCallInterface (StackArgBytes), the call (call_unix.go) and the Unix amd64 callback decoder (ffi/callback.go).
  • internal/syscall/syscall_unix_amd64.{go,s}: syscallN takes a pointer to the stack words and their count, reserves exactly that area (rounded to 16 bytes) and keeps the args pointer in callee-saved R12 across the call. The words are copied into the pooled heap block, so nothing on the goroutine stack is read after it may move.
  • The area is bounded at 64 KiB (amd64.MaxStackBytes). More is ErrTooManyArguments at prepare time, or an error at call time for a hand-built CallInterface, never a panic. Windows amd64 gets the same call-time check of its 4 + 9 slots.
  • Struct classification walks nested members (and arrays in callbacks) by offset, for arguments, results and callbacks. NewCallback counts the signature's stack words and the decoder reads exactly that many.
  • Docs: the CallFunctionContext safety notes and SECURITY.md now say that a Go address must stay pointer-typed all the way to avalue, binding wrappers included (a uintptr wrapper parameter lost a vkMapMemory mapping in a downstream binding when the stack moved). They also say that memory reachable from rvalue/avalue through pointer-typed values stays in place for the call. ARCHITECTURE.md describes the new layout.

arm64 and Windows amd64 keep their fixed stack areas (7 and 9 words) for now; they already return ErrTooManyArguments. Lifting them is planned for v0.6.9.

Verification:

  • New C-backed tests in ffi/stack_layout_e2e_test.go: 96-, 64- and 40-byte structs in webgpu.h shapes, two 96-byte structs (24 stack words), the split cases for {int64,int64}, {double,double} and {int64,double} in calls and callbacks, nested float structs as argument, result and callback, a hand-built CallInterface, and too many arguments. On main the 96-byte, 24-word, hand-built and too-many cases panic, and the split and nested cases return wrong values (e.g. split_s16_after_5i = 4207955, want 942755, take_nested_f32x4 = 0, want 35). The 64- and 40-byte cases fit the old area and stay as guards.
  • ffi/pointer_contract_e2e_test.go: C writes through an out-pointer after a Go callback has moved the goroutine stack.
  • linux/amd64 (gcc, GOFFI_REQUIRE_TESTLIB=1): go test ./... with CGO_ENABLED=0 and -race with CGO_ENABLED=1, green. Windows amd64 go test ./... green. golangci-lint on all 8 targets: 0 issues.
  • Downstream projects (go-webgpu/webgpu, gogpu/wgpu, gogpu/gogpu) ran their suites against this branch on Windows and linux/amd64, including a 96-byte WGPUAdapterInfo call on lavapipe that panics with v0.6.3.
  • BenchmarkGoffiOverhead on linux/amd64: 233.6 → ~240–245 ns/op, 0 allocations.
  • Not run locally: macOS, linux/arm64, FreeBSD and windows/arm64. CI covers them.

Fixes #112

A by-value struct over 72 bytes panicked with "index out of range [15]"
on Linux, macOS and FreeBSD amd64: stack arguments went into a fixed
9-word area whose bound was checked after the write, and
PrepareCallInterface counted a struct over 16 bytes as using no stack.
webgpu.h's 96-byte WGPUAdapterInfo hit it.

The argument layout now follows psABI 3.2.3 as libffi implements it
(src/x86/ffi64.c): an argument goes in registers only if every
eightbyte gets one, otherwise the whole argument goes on the stack and
the registers left stay free. Before, a 9-16 byte struct with too few
registers was split between a register and the stack, and the callee
read wrong values. One allocator (layout_sysv.go) serves
PrepareCallInterface, the call and the Unix callback decoder.

syscallN reserves a stack area sized from the arguments, up to 64 KiB
(MaxStackBytes); a larger argument list is an error at prepare time and,
for a hand-built CallInterface, at call time. The Windows call path
gets the same call-time check. Nested struct members are classified by
their offsets, for arguments, results and callbacks.

Document that a Go address must stay pointer-typed all the way to
avalue, binding wrappers included (wgpu's vkMapMemory binding lost a
mapping that way), and test that C can write through an out-pointer
after a callback moved the goroutine stack.

Tests: 96/64/40-byte structs (webgpu shapes), 24 stack words, the
split cases in calls and callbacks, nested float structs, too many
arguments. All but the 64- and 40-byte cases (which fit
the old 9 words) fail on b9cdfd7.

Fixes #112
@kolkov kolkov added type: bug Something isn't working priority: high Important for next release status: review In code review area: ffi Public API (PrepareCallInterface, CallFunction) area: assembly Platform-specific assembly (ABI) area: callbacks C-to-Go callback trampolines arch: amd64 x86-64 specific (System V / Win64 ABI) abi: system-v System V AMD64 ABI (Linux/macOS x86-64) labels Nov 1, 2026
@kolkov kolkov added this to the v0.6.8 milestone Nov 1, 2026
@codecov

codecov Bot commented Nov 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.51852% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ffi/callback.go 93.51% 5 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@kolkov
kolkov merged commit 7e464f6 into main Nov 1, 2026
49 checks passed
@kolkov
kolkov deleted the fix/amd64-stack-args branch November 1, 2026 00:52
@kolkov kolkov mentioned this pull request Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

abi: system-v System V AMD64 ABI (Linux/macOS x86-64) arch: amd64 x86-64 specific (System V / Win64 ABI) area: assembly Platform-specific assembly (ABI) area: callbacks C-to-Go callback trampolines area: ffi Public API (PrepareCallInterface, CallFunction) priority: high Important for next release status: review In code review type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System V amd64: struct arguments over 72 bytes panic; 9–16 byte structs are split between a register and the stack

1 participant