runtime: don't crash on a stuck traceback when errors are tolerated
sigprof unwinds with unwindSilentErrors so that a profiling signal can
never take down the process. (*unwinder).next honors that flag when the
caller PC is invalid, but the adjacent check for a frame that cannot make
progress throws unconditionally, so a single unsymbolizable sample kills
a healthy program.
A stuck frame does not always mean the stack is corrupt. A signal can land
in machine code the runtime has no unwind information for and never could:
a JIT, or an assembly blob entered by a jump from a frameless Go symbol,
whose C prologue leaves pc == lr and sp == fp partway through. There is no
root cause to find there and nothing further unwinding can recover, so
give up and report a truncated stack instead.
Where a stuck unwind does indicate corruption the diagnostic is still worth
having, so keep it reachable: GODEBUG=tracebackstuckcrash=1 restores the
unconditional throw.
Fixes #80771
For #62086
diff --git a/src/runtime/crash_test.go b/src/runtime/crash_test.go
index f7891a9..4a2fdf1 100644
--- a/src/runtime/crash_test.go
+++ b/src/runtime/crash_test.go
@@ -1238,3 +1238,22 @@
})
}
}
+
+func TestProfileStuckTraceback(t *testing.T) {
+ if runtime.GOARCH != "arm64" {
+ t.Skip("test assembly is arm64-only")
+ }
+ t.Run("truncates", func(t *testing.T) {
+ output := runTestProg(t, "testprog", "ProfileStuckTraceback")
+ if !strings.Contains(output, "OK") {
+ t.Fatalf("output:\n%s\n\nwant output containing: OK", output)
+ }
+ })
+ t.Run("crashes under GODEBUG", func(t *testing.T) {
+ output := runTestProg(t, "testprog", "ProfileStuckTraceback", "GODEBUG=tracebackstuckcrash=1")
+ want := "fatal error: traceback stuck"
+ if !strings.Contains(output, want) {
+ t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
+ }
+ })
+}
diff --git a/src/runtime/extern.go b/src/runtime/extern.go
index a69f4aa..ed03fcd 100644
--- a/src/runtime/extern.go
+++ b/src/runtime/extern.go
@@ -211,6 +211,13 @@
report. This also extends the information returned by runtime.Stack.
Setting N to 0 will report no ancestry information.
+ tracebackstuckcrash: setting tracebackstuckcrash=1 restores the pre-Go 1.27
+ behavior of throwing a fatal "traceback stuck" error when the unwinder cannot
+ make progress, even for unwinds that tolerate errors such as CPU profiling.
+ By default such unwinds now stop early and report a truncated stack. Enable
+ this to diagnose a stuck unwind that is suspected to indicate stack corruption
+ rather than machine code the runtime has no unwind information for.
+
tracefpunwindoff: setting tracefpunwindoff=1 forces the execution tracer
and block and mutex profilers to use the runtime's default stack
unwinder instead of frame pointer unwinding. This increases their
diff --git a/src/runtime/runtime1.go b/src/runtime/runtime1.go
index fc26e9b..261131a 100644
--- a/src/runtime/runtime1.go
+++ b/src/runtime/runtime1.go
@@ -320,6 +320,7 @@
scheddetail int32
schedtrace int32
tracebackancestors int32
+ tracebackstuckcrash int32
updatemaxprocs int32
asyncpreemptoff int32
harddecommit int32
@@ -386,6 +387,7 @@
{name: "tracecheckstackownership", value: &debug.traceCheckStackOwnership},
{name: "tracebackancestors", value: &debug.tracebackancestors},
{name: "tracebacklabels", atomic: &debug.tracebacklabels, def: 1},
+ {name: "tracebackstuckcrash", value: &debug.tracebackstuckcrash},
{name: "tracefpunwindoff", value: &debug.tracefpunwindoff},
{name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1},
}
diff --git a/src/runtime/testdata/testprog/tracebackstuck_arm64.go b/src/runtime/testdata/testprog/tracebackstuck_arm64.go
new file mode 100644
index 0000000..d140a48
--- /dev/null
+++ b/src/runtime/testdata/testprog/tracebackstuck_arm64.go
@@ -0,0 +1,39 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "runtime"
+ "runtime/pprof"
+ "sync"
+)
+
+func init() {
+ register("ProfileStuckTraceback", ProfileStuckTraceback)
+}
+
+func spinStuck(n int64)
+
+func ProfileStuckTraceback() {
+ if err := pprof.StartCPUProfile(io.Discard); err != nil {
+ panic(err)
+ }
+ defer pprof.StopCPUProfile()
+
+ var wg sync.WaitGroup
+ for range runtime.GOMAXPROCS(0) {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for range 100 {
+ spinStuck(50_000_000)
+ }
+ }()
+ }
+ wg.Wait()
+ fmt.Println("OK")
+}
diff --git a/src/runtime/testdata/testprog/tracebackstuck_arm64.s b/src/runtime/testdata/testprog/tracebackstuck_arm64.s
new file mode 100644
index 0000000..05490d9
--- /dev/null
+++ b/src/runtime/testdata/testprog/tracebackstuck_arm64.s
@@ -0,0 +1,21 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "textflag.h"
+
+// func spinStuck(n int64)
+//
+// A frameless leaf that points LR at its own loop body. A signal landing in the
+// loop therefore observes pc == lr and sp == fp, the same shape the unwinder
+// sees when a signal lands in machine code it has no unwind information for,
+// such as a JIT or an assembly blob entered by a jump from a frameless symbol.
+TEXT ·spinStuck(SB), NOSPLIT|NOFRAME, $0-8
+ MOVD n+0(FP), R1
+ MOVD R30, R19
+ WORD $0x1000003e // adr x30, .+4
+loop:
+ SUB $1, R1, R1
+ CBNZ R1, loop
+ MOVD R19, R30
+ RET
diff --git a/src/runtime/traceback.go b/src/runtime/traceback.go
index 58161de..3702b49 100644
--- a/src/runtime/traceback.go
+++ b/src/runtime/traceback.go
@@ -481,9 +481,28 @@
if frame.pc == frame.lr && frame.sp == frame.fp {
// If the next frame is identical to the current frame, we cannot make progress.
- print("runtime: traceback stuck. pc=", hex(frame.pc), " sp=", hex(frame.sp), "\n")
- tracebackHexdump(gp.stack, frame, frame.sp)
- throw("traceback stuck")
+ // Like the invalid-caller-PC case above, this happens if you get a profiling
+ // interrupt at just the wrong time, so it is okay to stop early. But if no
+ // error flags are set, we're doing a garbage collection and must get
+ // everything, so crash loudly.
+ //
+ // A stuck frame does not always mean the stack is corrupt: a signal can land
+ // in machine code the runtime has no unwind information for, such as a JIT
+ // or an assembly blob entered by a jump from a frameless Go symbol, whose
+ // prologue leaves pc == lr and sp == fp. No amount of unwinding can recover
+ // from that, so killing the process serves no purpose. GODEBUG=
+ // tracebackstuckcrash=1 restores the unconditional throw for debugging.
+ fail := u.flags&(unwindPrintErrors|unwindSilentErrors) == 0 || debug.tracebackstuckcrash != 0
+ if fail || u.flags&unwindSilentErrors == 0 {
+ print("runtime: traceback stuck. pc=", hex(frame.pc), " sp=", hex(frame.sp), "\n")
+ tracebackHexdump(gp.stack, frame, frame.sp)
+ }
+ if fail {
+ throw("traceback stuck")
+ }
+ frame.lr = 0
+ u.finishInternal()
+ return
}
injectedCall := isInjectedCall(f.funcID)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Thanks for the CL. The change to the unwinder looks good.
A stuck frame does not always mean the stack is corrupt. A signal can landWhile it may not always corrupt, it is an ABI violation, so still a bug. The user assembly code should not do that. We probably don't want to add a test for this specific bad behavior.
Where a stuck unwind does indicate corruption the diagnostic is still worth
having, so keep it reachable: GODEBUG=tracebackstuckcrash=1 restores the
unconditional throw.While it may be valuable to keep the diagnostic scheme, a GODEBUG specifically for this may be overkill. Perhaps just remove. Or maybe it is more valuable to have a more general purpose GODEBUG, that controls whether to crash on unwinding errors in sigprof? (Maybe a separate CL, then.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Done, removed the test, and switched to a global flag for noisily crashing on traceback failure.
A stuck frame does not always mean the stack is corrupt. A signal can landWhile it may not always corrupt, it is an ABI violation, so still a bug. The user assembly code should not do that. We probably don't want to add a test for this specific bad behavior.
Acknowledged
Where a stuck unwind does indicate corruption the diagnostic is still worth
having, so keep it reachable: GODEBUG=tracebackstuckcrash=1 restores the
unconditional throw.While it may be valuable to keep the diagnostic scheme, a GODEBUG specifically for this may be overkill. Perhaps just remove. Or maybe it is more valuable to have a more general purpose GODEBUG, that controls whether to crash on unwinding errors in sigprof? (Maybe a separate CL, then.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
| Commit-Queue | +1 |
Thanks.
// runtime has no unwind information for, such as a JIT or an assembly blob
// entered by a jump from a frameless Go symbol, whose prologue leaves
// pc == lr and sp == fp. No amount of unwinding can recover from that.Minor: maybe mention that this is still technically a bug.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Added the prose about it being an ABI violation per your request.
// runtime has no unwind information for, such as a JIT or an assembly blob
// entered by a jump from a frameless Go symbol, whose prologue leaves
// pc == lr and sp == fp. No amount of unwinding can recover from that.Minor: maybe mention that this is still technically a bug.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
runtime: don't crash on a stuck traceback when errors are tolerated
sigprof unwinds with unwindSilentErrors so that a profiling signal can
never take down the process. (*unwinder).next already honored that flag
when the caller PC was invalid, but the adjacent check for a frame that
cannot make progress threw unconditionally, so a single unsymbolizable
sample killed a healthy program.
A stuck frame does not always mean the stack is corrupt. A signal can
land in machine code the runtime has no unwind information for and
never could: a JIT, or an assembly blob entered by a jump from a
frameless Go symbol, whose C prologue leaves pc == lr and sp == fp
partway through. There is no root cause to find there and nothing
further unwinding can recover, so give up and report a truncated stack
instead, the same as the existing invalid-caller-PC case.
GODEBUG=tracebackcrash=1 restores the pre-Go 1.28 behavior of throwing
on any unwinding error that these unwinds would otherwise tolerate, for
diagnosing a stuck or unresolvable unwind suspected to indicate real
stack corruption.
Fixes #80771
For #62086
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
tracebackcrash: setting tracebackcrash=1 restores the pre-Go 1.28 behavior ofThis is slightly inaccurate as the `!flr.valid()` case (L465 in traceback.go) will crash in cases where 1.27 would not. I don't think this needs to say it restores old behavior, just that it crashes.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
tracebackcrash: setting tracebackcrash=1 restores the pre-Go 1.28 behavior ofThis is slightly inaccurate as the `!flr.valid()` case (L465 in traceback.go) will crash in cases where 1.27 would not. I don't think this needs to say it restores old behavior, just that it crashes.
Ah, unfortunate this feedback landed right after merge. Do you want a follow-up or is this ok to leave
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
tracebackcrash: setting tracebackcrash=1 restores the pre-Go 1.28 behavior ofLiz Fong-JonesThis is slightly inaccurate as the `!flr.valid()` case (L465 in traceback.go) will crash in cases where 1.27 would not. I don't think this needs to say it restores old behavior, just that it crashes.
Ah, unfortunate this feedback landed right after merge. Do you want a follow-up or is this ok to leave
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
tracebackcrash: setting tracebackcrash=1 restores the pre-Go 1.28 behavior ofLiz Fong-JonesThis is slightly inaccurate as the `!flr.valid()` case (L465 in traceback.go) will crash in cases where 1.27 would not. I don't think this needs to say it restores old behavior, just that it crashes.
Michael PrattAh, unfortunate this feedback landed right after merge. Do you want a follow-up or is this ok to leave
A follow-up cleanup would be appreciated, thanks!
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |