Skip to content

runtime: native SEH unwinding faults in mcall on windows/amd64 since Go 1.25 #81885

Open

Description

@UltraVolta425

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

Activity

  1. added a commit that references this issue on Sep 30, 2026
    1d010de
  2. gopherbot commented on Sep 30, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/842085 mentions this issue: runtime: keep a valid frame pointer in mcall on windows/amd64

  3. dmitshur commented on Oct 5, 2026

    @dmitshur
    Member

    CC @golang/runtime, @golang/windows.

  4. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Oct 5, 2026
  5. added this to the Backlog milestone on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions