Repository navigation
fix(amd64): lay out System V arguments as libffi does - #113
Merged
Merged
Conversation
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
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On System V amd64 (Linux, macOS, FreeBSD) a by-value struct argument over 72 bytes panicked inside
CallFunctionwithindex out of range [15] with length 15; webgpu.h's 96-byteWGPUAdapterInfohit 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:
PrepareCallInterfacecounted a struct over 16 bytes as using no stack;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;classifyEightbytedid not look into nested structs, sostruct { 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 byPrepareCallInterface(StackArgBytes), the call (call_unix.go) and the Unix amd64 callback decoder (ffi/callback.go).internal/syscall/syscall_unix_amd64.{go,s}:syscallNtakes 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.amd64.MaxStackBytes). More isErrTooManyArgumentsat prepare time, or an error at call time for a hand-builtCallInterface, never a panic. Windows amd64 gets the same call-time check of its 4 + 9 slots.NewCallbackcounts the signature's stack words and the decoder reads exactly that many.CallFunctionContextsafety notes andSECURITY.mdnow say that a Go address must stay pointer-typed all the way toavalue, binding wrappers included (auintptrwrapper parameter lost avkMapMemorymapping in a downstream binding when the stack moved). They also say that memory reachable fromrvalue/avaluethrough pointer-typed values stays in place for the call.ARCHITECTURE.mddescribes 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:
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-builtCallInterface, 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.GOFFI_REQUIRE_TESTLIB=1):go test ./...withCGO_ENABLED=0and-racewithCGO_ENABLED=1, green. Windows amd64go test ./...green. golangci-lint on all 8 targets: 0 issues.WGPUAdapterInfocall on lavapipe that panics with v0.6.3.BenchmarkGoffiOverheadon linux/amd64: 233.6 → ~240–245 ns/op, 0 allocations.Fixes #112