Skip to content

runtime: replace superseded panics across nested defer frames - #2295

Merged
xushiwei merged 3 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-nested-panic-20260811
Aug 11, 2026
Merged

runtime: replace superseded panics across nested defer frames#2295
xushiwei merged 3 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-nested-panic-20260811

Conversation

@cpunion

@cpunion cpunion commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • advance the active panic and goroutine defer cursors together when a longjmp rethrows into the parent defer frame
  • discard an older panic once a newer panic reaches the same defer frame, matching Go panic replacement semantics
  • add a three-scenario LLGo black-box regression and retire the three platform/version flake records for fixedbugs/issue43942.go

Root cause

The generated normal defer cleanup restores the goroutine defer head, but a panic raised by a nested deferred call can longjmp directly to the parent rethrow block and bypass that cleanup. The panic node then reaches the parent frame while the goroutine cursor still points at the child frame. The old link == gp.defer_ guard consequently skipped panic replacement, allowing the older panic to resume after the newer panic was recovered.

Keep this transition in the runtime rethrow abstraction: moving to the parent frame now updates both cursors and removes older panic nodes already unwinding that frame. The same invariant is maintained for Goexit and the bare-metal rethrow implementation.

Coverage

The new LLGo-executed fixture covers:

  1. a nested deferred panic replacing the panic already unwinding the parent frame (the issue43942 shape)
  2. recovery of an inner panic followed by resumption and recovery of the outer panic
  3. recovery of one panic followed by a new deferred panic escaping to an outer recovery point

Validation

Runtime sources were exercised only through LLGo-built programs; host go test commands below are compiler/test drivers.

  • macOS/arm64, Go 1.26.5: 13 related LLGo black-box cases passed
  • macOS/arm64, Go 1.26.5: official fixedbugs/issue43942.go and adjacent issue48898.go passed when run with LLGo
  • Linux/amd64, Go 1.26.5: the new fixture plus related defer/recover cases passed, and official fixedbugs/issue43942.go passed with LLGo
  • macOS/arm64 and Linux/amd64: LLGo-built issue43942 binaries passed 20/20 runs on Go 1.26.5; the Go 1.24.2 source also passed 20/20 on both platforms
  • 15 additional related GOROOT defer/recover cases passed on macOS/arm64
  • go test ./ssa -count=1 -timeout=20m

This fixes one unique GOROOT case and removes three records that represented that case on different Go versions/platforms.

Embedded scope

The new regression fixture is excluded from the existing ESP32 and ESP32-C3 emulator suites. Their bare-metal runtime still terminates after an outermost panic is recovered (the same limitation already documented for other panic fixtures); changing that runtime and its constrained panic-node allocator is outside this native GOROOT fix. Native macOS and Linux execution remains mandatory.

@cpunion cpunion added the go-test-compat Go standard-library and GOROOT test compatibility label Aug 11, 2026

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Clean, well-reasoned fix for the nested-defer-panic case. The core change — advancing gp.defer_ together with node.defer_ inside movePanicToDefer, and calling it unconditionally when a panic exists instead of only when link == gp.defer_ — correctly addresses the case where a siglongjmp reaches a parent's rethrow block before the child's normal defer cleanup restored gp.defer_. Duplicate (*panicNode)(ptr) casts were consolidated, the default/baremetal Rethrow paths now consistently set gp.defer_ = link, and no references to the old moveToDefer name remain.

I verified the change across four dimensions (correctness, memory safety, performance, docs/comments):

  • Memory safety: the movePanicToDefer free loop only unlinks-then-frees prev nodes strictly below node, never node/gp.panic_ itself, and clears the live gp.recoverPanic when it points at a freed node — no double-free or dangling gp.panic_.
  • Performance: the free loop is amortized O(active-panics) over an unwind (each node unlinked before free), not O(n²); the added assignment is trivial on a non-hot path.
  • Docs/comments & test: the new movePanicToDefer doc comment accurately describes updating both cursors; the new cl/_testgo/nesteddeferpanic test's CHECK-LABEL/LITTEST directives and expect.txt ordering are correct.

Non-blocking observations

  • runtime/internal/runtime/z_default.go (Rethrow, link == nil path): movePanicToDefer now also runs on the unrecovered-fatal path before TracePanic(node.arg) / c.Exit(2). This is safe — the loop frees only the prev chain, so node.arg remains valid for TracePanic — but it is a newly-reached code path for the fatal case and worth an explicit confirmation.
  • The baremetal Rethrow change (z_baremetal.go) mirrors the default path but is not exercised by a dedicated test; the new nesteddeferpanic test only runs under the non-baremetal build. Consider noting this coverage gap or adding baremetal coverage if feasible.

No blocking issues found.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

ae34bfee2fa1 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18656 B +0.0% 302.387 ms -3.8% (better) 1.364 ms +0.9% (worse)
Linux fmtprintf 1881632 B -0.0% (better) 2.898 s +1.0% (worse) 3.625 ms +0.4% (worse)
Linux println 68480 B -0.0% (better) 294.916 ms +0.1% (worse) 1.706 ms -1.2% (better)
macOS cprintf 84672 B +0.0% 296.815 ms -8.7% (better) 2.354 ms +1.8% (worse)
macOS fmtprintf 1889248 B +0.0% 2.297 s -12.4% (better) 10.723 ms -7.5% (better)
macOS println 121216 B +0.0% 303.390 ms +3.6% (worse) 3.475 ms +11.0% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 12.260 ns/op +0.1% (worse)
Linux BenchmarkMergeCompilerFlags 144.900 ns/op +0.2% (worse)
Linux BenchmarkMergeLinkerFlags 94.410 ns/op +0.3% (worse)
Linux BenchmarkChannelBuffered 36.310 ns/op -0.2% (better)
Linux BenchmarkChannelHandoff 23392 ns/op -4.8% (better)
Linux BenchmarkDefer 47.510 ns/op -3.2% (better)
Linux BenchmarkDirectCall 1.758 ns/op +0.0%
Linux BenchmarkGlobalRead 1.758 ns/op -0.2% (better)
Linux BenchmarkGlobalWrite 2.809 ns/op +0.1% (worse)
Linux BenchmarkGoroutine 29725 ns/op -14.4% (better)
Linux BenchmarkInterfaceCall 8.481 ns/op +0.5% (worse)
Linux BenchmarkRuntimeGetG 2.111 ns/op -14.3% (better)
macOS BenchmarkLookupPCRandom 11.650 ns/op -6.9% (better)
macOS BenchmarkMergeCompilerFlags 101.100 ns/op -3.6% (better)
macOS BenchmarkMergeLinkerFlags 70.460 ns/op -14.4% (better)
macOS BenchmarkChannelBuffered 20.710 ns/op -6.5% (better)
macOS BenchmarkChannelHandoff 6573 ns/op -0.4% (better)
macOS BenchmarkDefer 24.800 ns/op -7.7% (better)
macOS BenchmarkDirectCall 0.969 ns/op +2.7% (worse)
macOS BenchmarkGlobalRead 0.955 ns/op -0.8% (better)
macOS BenchmarkGlobalWrite 0.976 ns/op +3.5% (worse)
macOS BenchmarkGoroutine 27180 ns/op +0.7% (worse)
macOS BenchmarkInterfaceCall 4.404 ns/op +0.1% (worse)
macOS BenchmarkRuntimeGetG 1.885 ns/op -0.2% (better)

Compared with c454c0e65b71 measured in the same runner job.

@codecov

codecov Bot commented Aug 11, 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 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

The failed Go workflow was caused by the new native regression fixture being picked up by both embedded emulator suites. Both targets stop after the first outermost recovered panic (fatal error), while all native LLGo and GOROOT jobs passed. Commit 53a1cc5 now excludes this native-only fixture from the two embedded target lists with an explicit reason. Verified locally with the CI Espressif QEMU version: both embedded subtests skip, while the native nested defer/panic/recover regression group still passes.

@cpunion
cpunion force-pushed the codex/goroot-nested-panic-20260811 branch from 53a1cc5 to ae34bfe Compare August 11, 2026 10:47
@xushiwei
xushiwei merged commit d600859 into xgo-dev:main Aug 11, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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