Skip to content

[Based on #2090] runtime,cl: preserve panic-site frames across defer and recover#2026

Open
cpunion wants to merge 18 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-panic-snapshot
Open

[Based on #2090] runtime,cl: preserve panic-site frames across defer and recover#2026
cpunion wants to merge 18 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-panic-snapshot

Conversation

@cpunion

@cpunion cpunion commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Depends on #2090

Dependency

Stack: #2079 -> #2090 -> this PR.

#2079 provides //llgo:tls and //llgo:gls; #2090 stores goroutine runtime state in one GLS-backed g. This PR uses the same facilities and no longer adds a pthread key.

Problem

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

#2028 already owns recoverable hardware faults, the SA_SIGINFO handler, and fault-context unwinding. The missing layer is preserving ordinary panic snapshots and exposing both ordinary and fault snapshots through caller APIs.

Implementation

  • Capture the physical PC chain before longjmp and splice it below live deferred frames.
  • Store the fixed, pointer-free panicPCStore in //llgo:tls. Signal handling performs no allocation, pthread-key lookup, or first-use initialization.
  • Use an ordinary global fallback for host builds, bare-metal, and Nintendo Switch builds.
  • Use [Based on #2079] runtime: store goroutine state in //llgo:gls #2090's g state to determine whether a panic is still active.
  • Keep a recovered snapshot observable only while the 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.

The Linux validation found that the old frame liveness check inspected a C helper frame after it had returned; the next call could reuse that slot and terminate the walk. The final implementation synchronously consumes the existing helper's active FP links in Go and returns the stable Go caller frame. It adds no C symbol or cache ABI.

Conformance

Removed xfails for:

  • fixedbugs/issue14646.go
  • fixedbugs/issue5856.go
  • fixedbugs/issue33724.go

The remaining statement-line cases are still limited by line granularity in untracked functions and are not skipped by this PR.

Validation

Tests were serialized with GOMAXPROCS=2, GOMEMLIMIT=8GiB, and -p=1 where supported.

  • macOS arm64, Go 1.26.5: compiler PC-line tests, crosscompile tests, caller acceptance, three sequential recovered C faults, and the three focused GOROOT cases pass. Peak RSS for caller acceptance was 1.61 GiB.
  • Ubuntu arm64, Go 1.26.5, LLVM 19.1.7: the same affected compiler paths, recovered C-fault regression, and focused GOROOT cases pass in a container hard-limited to 15 GiB, 2 CPU, and 512 PIDs.
  • The root coverage run executes every new cl/compile.go branch; deferRunPos is 100% covered. The nested runtime module is exercised by LLGo integration tests because the root Codecov job does not instrument it.
  • runtime/internal/runtime and runtime/internal/lib/runtime compile 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

✅ All modified and coverable lines are covered by tests.

📢 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 3 times, most recently from fcc22f8 to 566b0d7 Compare July 19, 2026 00:13
cpunion and others added 18 commits July 20, 2026 12:09
Store each immutable Func as one atomic pointer and validate the queried PC from the Func itself. This prevents concurrent cache replacement from combining a PC and function from different writers. Apply the same publication rule to the prebuilt function cache and stress multiple PCs during concurrent first use.
Give each package-backed locality block an owner-local TLS cache so hot accesses are O(1) regardless of the number of touched packages. Keep the block list only for GC reachability and teardown.

Require locality variables to be unexported and reject go:linkname aliases. Add codegen, lifecycle, and 2/4/8-package performance coverage.
…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 566b0d7 to 78652f0 Compare July 20, 2026 06:11
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.

1 participant