Repository navigation
runtime: windows/amd64 exception recovery can write below a goroutine stack and corrupt the Go heap #81238
Description
Go version
go version go1.27.0 windows/amd64
Output of go env in your module/workspace:
set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=0
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=C:\Users\runneradmin\go\bin
set GOCACHE=C:\Users\runneradmin\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=adaptivestackstart=0
set GOENV=
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\RUNNER~1\AppData\Local\Temp\go-build508021546=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=D:\a\ptah-2365-repro\ptah-2365-repro\go.mod
set GOMODCACHE=C:\Users\runneradmin\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPACKAGESDRIVER=
set GOPATH=C:\Users\runneradmin\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\hostedtoolcache\windows\go\1.27.0\x64
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\runneradmin\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=local
set GOTOOLDIR=C:\hostedtoolcache\windows\go\1.27.0\x64\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.27.0
set GOWORK=off
set PKG_CONFIG=pkg-configWhat did you do?
On a GitHub-hosted Windows Server 2025/amd64 runner:
git clone https://github.com/denisvmedia/ptah-2365-repro
cd ptah-2365-repro
$env:GODEBUG = 'adaptivestackstart=0'
$env:GOGC = 'off'
$env:GOMEMLIMIT = 'off'
$env:GOTRACEBACK = 'system'
$env:CGO_ENABLED = '0'
$env:GOAMD64 = 'v1'
$env:GOTOOLCHAIN = 'local'
go build -trimpath -buildvcs=false -o repro.exe .
.\repro.exe panic 2> panic.log
.\repro.exe fault 2> fault.logThe panic mode is a negative control. Both modes use the same goroutine stack size, depth, heap adjacency, deferred recovery, and explicit post-recovery GC. The only difference is an ordinary Go panic versus a real read access violation at address 0x118.
The hosted pool is heterogeneous. In the instrumented four-job run, the exact failure occurred on the sole Intel/AMX host; the three AMD/non-AMX jobs were clean. Later uninstrumented runs reproduce intermittently on the same pool. The repository workflow runs four attempts and validates either result fail-closed.
Standalone source and workflow:
https://github.com/denisvmedia/ptah-2365-repro/tree/72cb4a8a27ef8c86347f8eee237c23e7b6681e85
What did you see happen?
The panic control exits 0 and the 32 bytes preceding stack.lo are unchanged:
REPRO_TAIL before=0000000000000000000000000000000000000000000000000000000000000004 after=0000000000000000000000000000000000000000000000000000000000000004
REPRO_RESULT clean
On an affected host the fault mode first records a write below stack.lo, then the next GC aborts:
REPRO_PAIR span=11771934179328 stack_lo=11771934187520
REPRO_TAIL before=0000000000000000000000000000000000000000000000000000000000000004 after=17c5ee29f77f000048a0f4ddb40a0000f192ec29f77f000020a0f4ddb40a0000
runtime: marked free object in span ..., elemsize=16 freeindex=0 (...)
...
fatal error: found pointer to free object
runtime.(*mspan).reportZombies(...)
panic exits 0; fault exits 2. The runtime report contains exactly 496 contiguous size-class-16 rows, starting at the REPRO_PAIR predecessor span. The witness and go env above are from run 33366178316, attempt 1.
What did you expect to see?
The read access violation should become a recoverable Go runtime panic without writing outside the goroutine stack. The ordinary panic and fault modes should both leave the adjacent heap page unchanged and exit normally after recovery.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
Activity
denisvmedia commentedon Aug 31, 2026
Analysis
Go 1.27 reserves an extra 4096 bytes for goroutine stacks on Windows:
The failing GitHub runner was Windows Server 2025. On another failing Intel host, XSTATE measurements showed that enabling the AMX
XTILEDATAcomponent increases the Windows context size by 8192 bytes:On AMD/non-AMX hosts, the required context was much smaller and the reproducer stayed clean. This is a strong size correlation, although it does not prove that Windows writes the entire 8192-byte XTILEDATA area during exception handling. The measurement is in run 33350325788.
The reproducer positions the faulting instruction near the bottom of a goroutine stack. After the access violation is recovered, the last 32 bytes immediately below
stack.lohave changed. The written values resolve to runtime stack/exception-handling addresses, including:This shows that the exception path writes at least 32 bytes below the goroutine stack before GC runs.
The memory directly below the stack is intentionally a live 8 KiB size-class-16 heap span. Under Green Tea GC, its mark metadata occupies that area. The overwrite corrupts those mark bits, and the next GC reports
found pointer to free object. The GC detects the corruption; it does not appear to cause it.The reproducer does not create this with a dangling pointer.
unsafeis used only to arrange the memory layout and inspect it. The same memory remains unchanged in an otherwise equivalentpanic/recovercontrol and changes only after the hardware access violation.This is also separate from golang/go#80799. Stock Go already produces both the memory overwrite and the runtime abort. #80799 only affects how
reportZombiesprints the already-detected corruption.This looks similar to golang/go#70288, where Windows exception handling required a larger system-stack reserve. The current evidence suggests that the fixed 4096-byte Windows reserve may be too small on hosts where AMX
XTILEDATAis enabled. It does not yet prove exactly which Windows/runtime component performs the overwrite or whether increasingstackSystemalone is the complete fix.gabyhelp commentedon Aug 31, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
gopherbot commentedon Aug 31, 2026
Change https://go.dev/cl/824724 mentions this issue:
runtime: crash if a Windows exception is dispatched below the goroutine stackqmuntal commentedon Aug 31, 2026
Thanks for reporting this issue, and for the reproducer. I've submitted https://go.dev/cl/824724 to turn the heal corruption into a deterministic panic.
On the other hand, I'm afraid the current design can't cope with such big CONTEXTs. We can't keep growing the system-reserved goroutine stack indefinitely. The long term fix is to stop using Vectored Exception Handling (VEH) and use Structured Error Handling (SEH) instead, tracked in #47576, given that SEH don't need a dedicated stack where to dispatch exceptions,
denisvmedia commentedon Sep 1, 2026
Thanks @qmuntal. I agree SEH may be the right long-term direction, but I tested the exact CL 824724 patch set 1, and it does not catch this reproducer.
The repro repo now includes the exact patch and a permanent stock-vs-patched A/B workflow:
https://github.com/denisvmedia/ptah-2365-repro
The result was:
MISSED_GUARD STOCK=EXACT PATCHED=EXACThttps://github.com/denisvmedia/ptah-2365-repro/actions/runs/33403553197/job/99525466135
On the same Intel Xeon Platinum 8573C runner, both stock and patched fault runs:
stack.lo, including pointers atstack.lo+0x48andstack.lo+0x20;runtime.(*mspan).reportZombies.The patched run did not emit
exception dispatched below goroutine stack bottom.The workflow also verifies the exact source blobs and confirms the new diagnostic string is present in the patched binary, so this is not a stale or missing overlay. A second run reproduced the same
MISSED_GUARDresult twice:https://github.com/denisvmedia/ptah-2365-repro/actions/runs/33403988570
REPRO_TAILrecords the overwrite before GC and before the #80799 reporting path, so this still appears to be actual memory corruption rather than just a reporter failure.It looks like
sys.GetCallerSP()is still abovegp.stack.lowhen the new check runs, even though the exception path subsequently writes belowstack.lo.So CL 824724 may still be useful as defense in depth, but it does not catch this failure. Could we keep this issue open?
prattmic commentedon Sep 4, 2026
cc @golang/runtime
35 remaining items
feat(server): surface unclean exits on the attention panel and exit o…
Merge pull request #103 from rodrigorjsf/ci/102-windows-go-runtime-retry
fix(review): U1 retry the Windows traceback-unwind runtime crash
logging: panic explicitly in the typed-nil error fixture
junioryono commentedon Oct 5, 2026
Another data point, from an ordinary test suite with no
unsafe, no cgo and noSetPanicOnFault. A plain nil-pointer write recovered withrecover()is enough to hit this on AMX hosts. It shows up as several different runtime fatals, among theminvalid g status.Go version
go version go1.27.1 windows/amd64(GitHub-hostedwindows-latest, Windows Server 2025,go test -race)What did you do?
Ran the root package tests of https://github.com/junioryono/godi (commit ec70c42) with
go test -race ./...onwindows-latest. The suite has one test that takes a hardware fault,TestLifetime/NilPointerUnmarshal. It is a parallel subtest that calls a pointer-receiver method on a nil pointer inside testify'sassert.Panics:Our CI crashed once in about 80 runs. To reproduce it, we looped the prebuilt race test binary on many fresh
windows-latestrunners and recorded each runner's CPU:The affected runner was 8573C job 1. On it, running only
-test.run=TestLifetime/NilPointerUnmarshal -test.count=500also killed the process: 2 of the first 4 runs died silently, with no output and a non-zero exit. The job hit its time limit before the "faulting test skipped" control and the standalone loops ran on that host. The second 8573C job ran clean, which matches the per-job variance reported above (73 of 162 jobs crashing on that model).What did you expect to see?
The nil dereference becomes a recoverable runtime panic, and the test passes, as it does on every non-AMX host.
What did you see happen?
The 10 crashes on the AMX host report corruption in assorted runtime structures:
The original CI crash (
invalid g status):The
gbeside the one being scanned (0xc00077eb40) holds what look like register values rather than goroutine state. We rebuilt the same binary on the same toolchain and checked these values against it:0x7ffa75bc6588is a system-DLL address.0x140085960is the entry ofruntime.initMetrics.func10.0x140a71240points intogo:funcdesc.One of the AMX crashes is in a sibling parallel subtest,
TestLifetime/ConcurrentAccess, which starts goroutines right after the nil-write subtest:Logs:
crashes-26artifact): https://github.com/junioryono/godi/actions/runs/37258698130junioryono commentedon Oct 5, 2026
Independent A/B validation of CL 828724 patch set 1 (
cf0ed477) against its parent38d1265e(tip, which already has CL 824724). This used a different workload from the one in the CL description.Setup. Both toolchains were built from source on
windows-latest(Windows Server 2025, 10.0.26100). Then 60 fresh runners each ran both builds on the same host, alternating stock and patched every round:godi-nil: the godi race test binary from my earlier comment,-test.run=TestLifetime/NilPointerUnmarshal -test.count=500(a plain nil-pointer write recovered byrecover)godi-suite: the full godi race suite,-test.count=1faultstress: theSetPanicOnFaultreproducer from this thread,-test.count=20AMX hosts looped for 40 minutes. Other hosts ran 3 rounds.
InitializeContext(CONTEXT_ALL|CONTEXT_XSTATE)On the hosts with an 11,567-byte context, every stock run died. 3840 runs died with
fatal error: exception dispatched below goroutine stack bottom, and 3 hung after it, for example a secondfatal error: stack not a power of 2fromgfget. All three workloads failed equally. The patched build ran 3843 times on the same hosts without a single failure. A dry run on another 8573C (context 11567) gave the same result: stock failed 81/81, patched 0/81.A few other observations:
TestExceptionStackReservepassed on all 60 hosts. With the patch,go test -short runtimepassed on all four AMX hosts, and so didgo test -short -race -run 'Stack|Exception|Fault|Panic|Signal|Goroutine' runtime.stackGuardbecomes a variable on every port, I also builtruntimeand its test binary for everygo tool dist listtarget. Everything compiled, apart from targets that require cgo linking, which were skipped.go test -short runtime runtime/debugpasses on darwin/arm64.fatal error: unsafe.Slice: len out of range/panic during panicintracebackHexdump, because thesigtrampframe is belowstack.lo. With this CL that path isn't reached.Workflow, scripts and per-host logs/CSVs: https://github.com/junioryono/go81238-amx-validation (run 37266360678).
client/thin, core/pki: return "<nil>" from String on a nil receiver
plumbing: transport, Render nil origins without a fault
client/thin, core/pki: return "<nil>" from String on a nil receiver
fix: no recovered nil dereference on each Telegram API call
fix: no recovered nil dereference on each Telegram API call (#30)
client/thin, core/pki: return "<nil>" from String on a nil receiver
security: wipe dropped secrets, bound the thin-client listener, keep …
fix(jobs): check River client settings instead of recovering panics
test(windows): skip nil-deref panic recovery tests