Repository navigation
runtime: native SEH unwinding faults in mcall on windows/amd64 since Go 1.25 #81885
Description
Go version
go1.27.1 windows/amd64, and master at 1e963f8. Go 1.25.0 and later are affected; go1.24.13 is not.
Output of go env in your module/workspace:
set GOARCH=amd64
set GOOS=windows
(cross-compiled from linux/amd64; the target is Windows 11 25H2, build 26200.9457)What did you do?
Walked the stack of a Go thread that is parked in the scheduler using the Windows x64 native unwinder (RtlLookupFunctionEntry + RtlVirtualUnwind), the way debuggers, profilers, crash reporters and injected tools do.
Reproducer: a goroutine locks itself to its thread and blocks on a channel, so the thread parks via gopark -> mcall(park_m) -> schedule -> stoplockedm. main then suspends that thread and unwinds it.
package main
import (
"fmt"
"os"
"runtime"
"syscall"
"time"
"unsafe"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
procOpenThread = kernel32.NewProc("OpenThread")
procSuspendThread = kernel32.NewProc("SuspendThread")
procResumeThread = kernel32.NewProc("ResumeThread")
procGetThreadContext = kernel32.NewProc("GetThreadContext")
procRtlLookupFunctionEntry = kernel32.NewProc("RtlLookupFunctionEntry")
procRtlVirtualUnwind = kernel32.NewProc("RtlVirtualUnwind")
)
const (
threadSuspendResume = 0x0002
threadGetContext = 0x0008
contextFull = 0x10000b
ctxSize = 1232
offContextFlags = 0x30
offRsp = 0x98
offRbp = 0xa0
offRip = 0xf8
)
func main() {
tid := make(chan uintptr)
block := make(chan struct{})
go func() {
runtime.LockOSThread()
id, _, _ := procGetCurrentThreadId.Call()
tid <- id
<-block
}()
id := <-tid
time.Sleep(200 * time.Millisecond) // let it park
h, _, err := procOpenThread.Call(threadSuspendResume|threadGetContext, 0, id)
if h == 0 {
fmt.Println("OpenThread:", err)
os.Exit(2)
}
procSuspendThread.Call(h)
var buf [ctxSize + 16]byte
ctx := unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + 15) &^ 15)
field := func(off uintptr) *uintptr { return (*uintptr)(unsafe.Add(ctx, off)) }
*(*uint32)(unsafe.Add(ctx, offContextFlags)) = contextFull
if r, _, err := procGetThreadContext.Call(h, uintptr(ctx)); r == 0 {
fmt.Println("GetThreadContext:", err)
os.Exit(2)
}
for i := 0; i < 64; i++ {
pc := *field(offRip)
if pc == 0 {
break
}
name := "?"
if f := runtime.FuncForPC(pc - 1); f != nil {
name = f.Name()
}
fmt.Printf(" %#x bp=%#x %s\n", pc, *field(offRbp), name)
var base uintptr
fn, _, _ := procRtlLookupFunctionEntry.Call(pc, uintptr(unsafe.Pointer(&base)), 0)
if fn == 0 { // leaf: return address at RSP
sp := *field(offRsp)
*field(offRip) = *(*uintptr)(unsafe.Pointer(sp))
*field(offRsp) = sp + 8
continue
}
var handlerData, establisher uintptr
procRtlVirtualUnwind.Call(0, base, pc, fn, uintptr(ctx),
uintptr(unsafe.Pointer(&handlerData)), uintptr(unsafe.Pointer(&establisher)), 0)
}
procResumeThread.Call(h)
fmt.Println("OK")
close(block)
}What did you see happen?
With Go 1.25 and later, RtlVirtualUnwind faults while unwinding runtime.mcall, and the Go exception handler turns that into a fatal crash:
Exception 0xc0000005 0x0 0x0 0x7ff9c275587a
PC=0x7ff9c275587a
runtime.cgocall(...)
syscall.SyscallN(...)
syscall.(*LazyProc).Call(...)
main.main()
The faulting address is 0. runtime.mcall has SEH unwind info that declares BP as its frame register (cmd/internal/obj/x86/seh.go emits UWOP_SET_FPREG for every function with a frame pointer). But since 9c1d19a ("runtime: clear frame pointer in mcall", CL 669615, for #63630), mcall runs MOVQ $0, BP after switching to g0. So when a thread is stopped inside fn (for example park_m), the native unwinder reaches mcall's frame, takes RSP from BP = 0, and reads the return address from address 0.
This is not only a debugging concern. Any tool that walks the stacks of all threads in a process kills every Go 1.25+ program on the machine. I found it because Windhawk walks every suspended thread's stack when it unloads a mod, and each time Proton Mail Bridge (a Go program) died with the exception above. Profilers, crash reporters and debuggers that capture all threads take the same path.
What did you expect to see?
The unwind ends cleanly at runtime.mcall, as it does with go1.24.13:
go1.24.13
...
0x75289a bp=0xe1c43ffaa8 runtime.schedule
0x752d85 bp=0xe1c43ffb08 runtime.park_m
0x77c4b3 bp=0xc00006fec8 runtime.mcall
0x777aae bp=0xc00006fee8 runtime.gopark
...
OK
I have a fix that keeps the intent of 9c1d19a on Windows: instead of zeroing BP, mcall pushes an empty frame (saved BP 0, return PC 0) on g0 and points BP at it. Frame-pointer unwinding still stops there, and native unwinding now ends cleanly at mcall rather than faulting. I'll send it as a CL with a regression test in runtime-seh_windows_test.go. The test fails with the exact crash above without the fix and passes with it, and the Windows runtime short tests pass.
cc @qmuntal
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
runtime: keep a valid frame pointer in mcall on windows/amd64
gopherbot commentedon Sep 30, 2026
Change https://go.dev/cl/842085 mentions this issue:
runtime: keep a valid frame pointer in mcall on windows/amd64dmitshur commentedon Oct 5, 2026
CC @golang/runtime, @golang/windows.