Skip to content

runtime: memory corruption from stack-allocated defer on 32-bit #41795

Closed
@aclements

Description

@aclements

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions