Description
On 32-bit architectures, a stack-allocated defer of a function with a 16-byte or smaller argument frame including a non-empty result area can corrupt memory if the deferred function grows the stack and is invoked during a panic. This has been possible since stack-allocated defers were implemented in Go 1.13.
This happens because the Go signature of runtime.call16
(the function used for reflectcall
with argument frames 16 bytes or smaller) is wrong. Specifically, it is
func call16(fn, arg unsafe.Pointer, n, retoffset uint32)
but it should be
func call16(typ, fn, arg unsafe.Pointer, n, retoffset uint32)
As a result of this mismatch, the stack copier thought call16
's third argument, arg
, was a scalar rather than a pointer and thus would not adjust it even if it pointed into the stack. There's only one case where this happens: stack-allocated defers. All other uses of reflectcall
pass a heap-allocated arg
(which is kept reachable from other stack roots). If the deferred function grows the stack, then upon return, call16
would copy its results back to a stale arg
frame that still points to where the stack used to be, potentially corrupting that memory.
It's basically impossible to write a unit test for this, but I confirmed it manually by stepping through the following example compiled for GOARCH=386:
package main
func main() {
defer func() (x byte) {
growStack(1000)
return 42
}()
// Force the above defer to the stack.
for i := 0; i < 1; i++ {
defer func() {}()
}
// panic to invoke deferred functions reflectively.
panic("x")
}
func growStack(n int) {
if n != 0 {
growStack(n - 1)
}
}
$ GOARCH=386 go build x.go
$ gdb ./x
(gdb) br x.go:5
(gdb) br x.go:6
(gdb) r
Thread 1 "x" hit Breakpoint 1, main.main.func1 (x=<optimized out>) at /tmp/x.go:5
5 growStack(1000)
(gdb) print/x $getg().stack
$1 = {lo = 0x8442000, hi = 0x8442800}
# ^ Stack prior to call16
(gdb) c
Thread 1 "x" hit Breakpoint 2, main.main.func1 (x=<optimized out>) at /tmp/x.go:6
6 return 42
(gdb) print/x $getg().stack
$2 = {lo = 0x8476000, hi = 0x847a000}
# ^ Stack after stack move
(gdb) br 'runtime.reflectcallmove'
Breakpoint 3 at 0x8054140: file /home/austin/go.dev/src/runtime/mbarrier.go, line 226.
(gdb) c
Thread 1 "x" hit Breakpoint 3, runtime.reflectcallmove (typ=0x0, dst=0x84427c4, src=0x8479f18, size=0)
(gdb)
In reflectcallmove
, the dst
argument refers to the stack's address from before the stack move, which could have been reallocated.