goffi v0.5.6 - Callback Stack Move Corruption Fix


goffi v0.5.6, published on July 5, 2026, fixes callback stack move corruption in the Go and C boundary. The important change is small in surface area but serious in effect: return values from C callbacks could be written to freed or recycled memory after Go stack growth.

The full release notes and downloads are on the GitHub release page.

This release is about correctness under runtime pressure, not a feature surface change. The bug appears when a C callback enters Go again and the Go code in that callback grows the goroutine stack through copystack.

In the broken path, syscallArgs lived on the goroutine stack because of the v0.5.4 //go:noescape optimization. When the stack moved, syscallN was still running on g0 and still held the old address. The return values then went to memory that could already be freed or reused.

For data engineering and web processing services that call native code from Go, this is the kind of failure that is worse than a clean crash. A callback can return a plausible value while corrupting state outside the immediate call site. That can show up later as wrong records, unstable native bridge behavior, or a failure that does not reproduce from the visible input data.

The release notes tie the runtime behavior to Go issues #8771 and #11089. Those references matter because the failure depends on Go runtime callback and stack mechanics, not just a local pointer lifetime mistake.

goffi v0.5.6 moves syscallArgs to heap allocation through sync.Pool. That keeps the argument storage stable when the goroutine stack moves during callback execution. It also removes the //go:noescape directive from hot path runtime_cgocall declarations.

The tradeoff is explicit. The project gives up the stack placement optimization that caused the corruption risk. In return, it gets stable storage across callback stack growth. The release notes state that pool reuse gives 0 allocs/op in steady state, so the fix is not described as a new allocation on every call once the pool is warm.

That detail is useful for operators who watch allocation rate and latency in Go services. The change should still be tested under local workload shape, especially for services that make heavy native calls in tight loops. But the risk model is better: bounded pool behavior is easier to observe than silent return value corruption.

The release also cleans up the sret path for struct returns larger than 16B. The notes say the project simplified the path and removed the redundant sretBuf. That is mostly internal cleanup, but it is adjacent to the same class of native boundary behavior. Return value handling needs to be boring and predictable.

Two tests are called out. TestCallbackGrowStack is a deterministic reproducer for the stack move bug. That matters because callback stack growth bugs often sit in the gap between runtime scheduling, stack copying, and native call state. A test that forces the condition is more valuable than a broad stress test that sometimes gets lucky.

TestStructReturn24B covers a larger struct return through sret from end to end. For consumers, the practical takeaway is that this release does not only change storage for syscallArgs; it also adds coverage around the return paths most likely to hide bad memory writes.

The release credits @tie for finding the issue, writing the reproducer, and contributing the fix. That is worth noting because the final patch is compact, but the hard part is usually reducing a runtime boundary failure into a deterministic case.

The release notes do not list breaking API changes or migration steps. This is still a high priority patch if go-webgpu/goffi is used in a service where Go callbacks can call enough code to grow a goroutine stack.

The main validation path is workload focused. Run callback heavy tests, include paths that return structs larger than 16B, and watch for allocation and latency changes after warmup. If prior incidents looked like unexplained data drift or impossible return values across a native boundary, compare behavior before and after v0.5.6 using the same inputs.