Skip to content

runtime,cl: preserve panic-site frames across defer and recover - #2026

Merged
xushiwei merged 7 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-panic-snapshot
Jul 30, 2026
Merged

runtime,cl: preserve panic-site frames across defer and recover#2026
xushiwei merged 7 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-panic-snapshot

Conversation

@cpunion

@cpunion cpunion commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

LLGo implements panic/recover with longjmp. The jump removes panicking frames before deferred functions run, while Go keeps those logical frames visible to runtime.Caller, runtime.Callers/CallersFrames, and debug.Stack, including after recover until the recovering defer returns.

#2028 owns recoverable hardware faults and fault-context unwinding. This PR preserves ordinary panic snapshots and exposes ordinary and fault snapshots through caller APIs.

Implementation

  • capture the physical PC chain before longjmp and splice it below live deferred frames;
  • keep the fixed, pointer-free panic snapshot in the current g;
  • share the same getg path for ordinary panic and hardware-fault snapshots;
  • keep a recovered snapshot observable only while its recovering deferred frame is live;
  • attribute deferred execution to the function closing brace and emit an explicit PC/line anchor for panic;
  • compile C with frame pointers so fault chains can cross C frames;
  • keep wasm and baremetal source sets buildable with no-frame-pointer unwind hooks; snapshot splicing remains disabled there;
  • allocate tiny finalizer-test objects on a short-lived goroutine so conservative stack roots cannot make the Go 1.24 Linux CI check intermittent.

The diff against main is +524/-197 across 17 files.

Conformance

  • Restored the GOROOT cases directly covered by this PR: issue14646, issue5856, issue33724, devirtualization_nil_panics, and issue11656.
  • Removed stale expectations for cases fixed by already-merged changes.
  • Reclassified issue25897a, issue45045, and issue54343 only on the platform/version combinations where repeated runs show GC or timeout nondeterminism.

Validation

  • macOS arm64, Go 1.26.5: real panic traceback, three sequential recovered C faults, and shallow panic execution at O0/O2 with DWARF both enabled and disabled pass.
  • Repeated GOROOT expectation checks pass across Go 1.24, 1.25, and 1.26 on Darwin arm64 and Linux amd64 for the changed cases.
  • Standard-runtime builds pass for js/wasm and wasip1/wasm; a cortex-m4 baremetal executable also links successfully.
  • The CI-equivalent Go 1.24.2 Linux test/go command passes; the previously intermittent tiny-finalizer test also passes 100 consecutive runs.
  • Full local coverage passes: 75.1% across instrumented packages and 97.3% for cl; the complete test/go suite passes.
  • Linux amd64, Go 1.26.5: c-shared and c-archive both link into and execute the C export demo; explicit -w=false DWARF source lookup succeeds in both modes.
  • The Go export demo passes all 27 assertions.
  • runtime/internal/runtime compiles under the host Go toolchain.

Limits

  • Statement-level fault lines in untracked functions still need the prebuilt PC-line follow-up.
  • Linux cannot reliably name non-dynamic C symbols; captured PCs are retained, but a C frame can be attributed to a neighboring symbol.
  • Alternate signal stacks remain outside this PR.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/compile.go 92.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 9c33770 to 157d48d Compare July 4, 2026 12:29
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch 2 times, most recently from 9a4beda to f102328 Compare July 4, 2026 14:05
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 4, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from f102328 to 2de72d5 Compare July 4, 2026 16:12
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 2de72d5 to f9e5617 Compare July 8, 2026 05:27
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from f9e5617 to 1c0db95 Compare July 8, 2026 07:02
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 1c0db95 to 63bbc3d Compare July 8, 2026 11:16
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch 2 times, most recently from 78cd80e to 96a7100 Compare July 9, 2026 05:30
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 9, 2026
…libunwind + FP chain)

Hardware faults — SIGSEGV/SIGBUS and previously-fatal SIGFPE — now
convert to ordinary recoverable Go panics, and the unrecovered traceback
shows the fault-site chain: C frames down through the Go callers.

- A SA_SIGINFO handler captures the interrupted context; the handler's
  own frame-pointer chain dead-ends at the signal trampoline, which is
  why the ucontext pc/fp is required.
- The capture prefers a dynamically-resolved libunwind (dlopen/dlsym at
  install time only — no link-time -lunwind; LLGO_DYNUNWIND=0 disables):
  DWARF/compact-unwind stepping survives C frames compiled without frame
  pointers, and the nongnu flavor's unw_get_proc_name reads .symtab,
  naming static C symbols dladdr cannot see (they otherwise display
  under a neighboring Go function via nearest-below). Where unwind info
  runs out, the walk resumes along the FP chain from libunwind's final
  cursor. Flavors: darwin libSystem, linux nongnu (arch-prefixed
  symbols), linux LLVM (context translated).
- Only man-page async-signal-safe unw_* calls run in the handler
  (resolution and a lazy-state warm-up happen at install). Fault-context
  walks probe page readability (msync) before dereferencing — an
  arithmetic-valid frame pointer can still point into an unmapped hole,
  and faulting inside the fault path would recurse; a re-entered handler
  restores the default disposition for one clean core.
- The unrecovered dump goes through a new PanicTraceback hook (gc-style
  frames via the funcinfo tables; libunwind's name for dot-less C
  symbols); non-fault panics keep the existing clite dump.

Verified: darwin/arm64 (libSystem flavor); linux/amd64 with a
-fomit-frame-pointer C chain — the FP-only walk recovers 2 frames with a
misattributed name, the dynamic path recovers the full chain with
correct static names.

Overlaps with the fault half of xgo-dev#2026 (panic-site snapshots); whichever
lands second rebases to drop its duplicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 96a7100 to 05aafd4 Compare July 11, 2026 03:04
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 05aafd4 to 4486503 Compare July 12, 2026 12:13
@cpunion cpunion changed the title runtime,cl: panic-site pc snapshots — deferred callers and fault stacks see the panic frames runtime,cl: preserve panic-site frames across defer and recover Jul 12, 2026
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 65b284f to 65919ea Compare July 15, 2026 06:06
@cpunion cpunion added go-test-compat Go standard-library and GOROOT test compatibility bug Something isn't working and removed bug Something isn't working labels Jul 16, 2026
@cpunion cpunion added bugfix Fixes an existing bug or regression go1.26 Go 1.26 compatibility and support labels Jul 16, 2026
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 65919ea to 370694c Compare July 16, 2026 04:37
@cpunion cpunion changed the title runtime,cl: preserve panic-site frames across defer and recover [Based on #2090] runtime,cl: preserve panic-site frames across defer and recover Jul 16, 2026
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch 8 times, most recently from 4e494fa to 34d6829 Compare July 24, 2026 03:34
@cpunion cpunion changed the title [Based on #2090] runtime,cl: preserve panic-site frames across defer and recover runtime,cl: preserve panic-site frames across defer and recover Jul 26, 2026
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch 4 times, most recently from f5085b6 to 8825b8b Compare July 28, 2026 12:25
cpunion and others added 7 commits July 29, 2026 09:34
…ks see the panic frames

gc runs deferred functions on top of the panicked stack; LLGo's longjmp
unwinding removes those frames physically, so runtime.Caller /
CallersFrames / debug.Stack from a deferred function (before or after
recover) could not see the panic site. Now:

- Panic() captures the physical pc chain (the existing
  SavePanicCallerFrames hook, empty since the shadow stack left) into a
  per-thread snapshot; Recover() marks the recovering frame so the
  snapshot stays observable exactly while that frame is live.
- Caller-info walks splice the snapshot below the live deferred frames at
  the defer-owner junction, keeping one panic-machinery frame where gc
  has runtime.gopanic (fixed Caller depths count it).
- Hardware faults (SIGSEGV/SIGBUS and previously-fatal SIGFPE) install a
  SA_SIGINFO handler that captures from the interrupted ucontext pc/fp —
  the handler's own chain dead-ends at the signal trampoline — so fault
  tracebacks start at the fault site, through C frames into the Go
  callers. C is compiled with -fno-omit-frame-pointer so x86-64 chains
  hold.
- Defer execution is attributed to the function's closing brace like gc,
  and explicit panic statements get their own statement anchor.

Signal-path robustness (the reflectmake flake, ~7% -> 0 over 300 runs):
- The recover mark reads the frame-pointer chain, which after siglongjmp
  can reach a stale/unmapped slot; the guarded read (msync page probe)
  lives in the public runtime via a RecoverMark hook, and the core just
  calls it — an unguarded read self-faulted and corrupted the value the
  recover was extracting.
- The fault handler does no async-signal-unsafe work: the snapshot buffer
  is preallocated (no bdwgc malloc in signal context) and the page size
  is primed at install (no sysconf). SA_NODEFER + an unblock on capture
  keep a savemask=0 longjmp escape from leaving the fault signal blocked,
  and a re-entered handler restores the default disposition for one clean
  core; fault-context walks probe page readability before dereferencing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove issue14646/issue5856/issue33724 xfails; the C-fault regression
runs three sequential faults (a handler leaving the signal blocked after
the longjmp escape cores on the second) and asserts the fault-site chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/stage5-panic-snapshot branch from 8825b8b to bf9389f Compare July 29, 2026 01:50
@xushiwei
xushiwei merged commit 77a7205 into xgo-dev:main Jul 30, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes an existing bug or regression go1.26 Go 1.26 compatibility and support go-test-compat Go standard-library and GOROOT test compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants