Summary
An unrecovered panic can fall back from LLGo's Go-style traceback to the raw clite PC dump when the physical frame-pointer walk starts from an invalid frame. The failure was first exposed by combining a logging dependency graph with a panic probe, but the minimized reproducer only needs a shallow panic, the public runtime package, DWARF, and -O0.
This is independent of #2115: funcinfo and runtime.Stack remain available; only the panic-time physical walk returns no frames.
Reproduction
On Darwin/arm64 with current main:
package main
import "runtime"
//go:noinline
func panicSite() {
panic("shallow-panic")
}
func main() {
_ = runtime.NumCPU()
panicSite()
}
llgo run -a -O0 -ldflags=-w=false main.go
Actual output falls back to raw addresses:
panic: shallow-panic
[0x... command-line-arguments.panicSite+0x20, SP = ...]
[0x... command-line-arguments.main+..., SP = ...]
[0x... main+..., SP = ...]
Expected output starts with goroutine 1 [running]: and contains Go-style main.panicSite(...) and main.main(...) frames.
Root cause
runtime/internal/lib/runtime/_wrap/runtime.c:llgo_framepointer returns __builtin_frame_address(0), the C helper's own frame address. The Go walker dereferences that address after the helper has returned, so it is a dangling stack-frame pointer. In the failing O0/DWARF layout its saved-FP slot has already been overwritten (observed value: 64), and the monotonic-chain guard stops immediately.
Optimized layouts often leave the stale bytes intact long enough to hide the bug. This explains why #2143's O2 pipeline appears to fix the larger probe while -O0 still fails.
The helper should read its saved caller FP while its own frame is alive and return that FP value. That matches the Go-side walker's existing assumption and skip++ accounting.
Acceptance
- A shallow unrecovered panic emits a Go-style traceback under
-O0 -ldflags=-w=false.
- Caller/Callers/Stack and panic traceback tests pass at O0/O2 with and without DWARF.
- Darwin and Linux native builds pass.
Proposed fix
Summary
An unrecovered panic can fall back from LLGo's Go-style traceback to the raw clite PC dump when the physical frame-pointer walk starts from an invalid frame. The failure was first exposed by combining a logging dependency graph with a panic probe, but the minimized reproducer only needs a shallow panic, the public runtime package, DWARF, and
-O0.This is independent of #2115: funcinfo and
runtime.Stackremain available; only the panic-time physical walk returns no frames.Reproduction
On Darwin/arm64 with current
main:Actual output falls back to raw addresses:
Expected output starts with
goroutine 1 [running]:and contains Go-stylemain.panicSite(...)andmain.main(...)frames.Root cause
runtime/internal/lib/runtime/_wrap/runtime.c:llgo_framepointerreturns__builtin_frame_address(0), the C helper's own frame address. The Go walker dereferences that address after the helper has returned, so it is a dangling stack-frame pointer. In the failing O0/DWARF layout its saved-FP slot has already been overwritten (observed value:64), and the monotonic-chain guard stops immediately.Optimized layouts often leave the stale bytes intact long enough to hide the bug. This explains why #2143's O2 pipeline appears to fix the larger probe while
-O0still fails.The helper should read its saved caller FP while its own frame is alive and return that FP value. That matches the Go-side walker's existing assumption and
skip++accounting.Acceptance
-O0 -ldflags=-w=false.Proposed fix