Skip to content

runtime: tighten recover to direct deferred calls#1918

Closed
cpunion wants to merge 50 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-fixedbugs-recover-coverage
Closed

runtime: tighten recover to direct deferred calls#1918
cpunion wants to merge 50 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-fixedbugs-recover-coverage

Conversation

@cpunion

@cpunion cpunion commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix recover/defer ownership during panic unwinding.

recover now only succeeds from the deferred function that is actually being run, while panics rethrown through nested defer and range-over-func frames remain recoverable by the correct outer defer.

Examples covered by this PR:

// A directly deferred function value can recover the panic.
defer func() {
	if recover() == nil {
		panic("not recovered")
	}
}()
panic("boom")
// A nested defer inside a deferred function must not recover the outer panic.
defer func() {
	defer func() {
		if recover() != nil {
			panic("nested recover should be nil")
		}
	}()
	if recover() != "outer" {
		panic("outer recover failed")
	}
}()
panic("outer")
// A panic crossing range-over-func/defer frames remains recoverable by the
// correct deferred function, instead of being claimed by an inner frame.
defer func() {
	if recover() != "loop panic" {
		panic("recover owner mismatch")
	}
}()
for range func(yield func(int) bool) {
	defer func() { _ = recover() }()
	yield(1)
} {
	panic("loop panic")
}

Main verification

After #1950 was merged, latest xgo-dev/llgo main still fails the nested defer/recover case above. The temporary LLGo test failed with:

outer recover = <nil>, want outer

The same case passes with go test, so this PR still covers a remaining LLGo recover/defer ownership bug.

@cpunion
cpunion force-pushed the codex/goroot-fixedbugs-recover-coverage branch from 6de90d9 to ad13f0e Compare May 24, 2026 12:52
@codecov-commenter

codecov-commenter commented May 24, 2026

Copy link
Copy Markdown

@cpunion
cpunion marked this pull request as ready for review June 1, 2026 02:30
@cpunion

cpunion commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

Added patch-coverage tests for the recover/direct-deferred-call helpers:

  • ssa/ssa_test.go: covers recover defer token classification for marked function decls, function pointers, non-function values, nil expressions, and the recover builtin helper.
  • cl/cgo_test.go: covers functionUsesRecover nil/direct-recover cases and callMayRecover conservative closure/default paths.

Local verification:

  • go test ./ssa -coverprofile=/tmp/llgo-recover-ssa-full-after.out (passes; ssa/eh.go recover helper functions now report 100% for callRecoverScopedDefer, recoverDeferToken, deferMayRecover, and isRecoverBuiltin)
  • go test ./cl -run 'Test(RecoverCallClassificationHelpers|EmitDo|CompileRangeFuncDeferModule|DeferStackOwner|RangeFuncDefer|NestedRangeFunc|Cgo)' -coverprofile=/tmp/llgo-recover-cl-after2.out (passes; functionUsesRecover and callMayRecover report 100%)
  • go test ./test/go -run TestRecoverFixedbug -count=1 (passes)
  • go run ./cmd/llgo test -run TestRecoverFixedbug -count=1 ./test/go/recover_defer_fixedbugs_test.go (passes)
  • git diff --check (passes)

Note: go run ./cmd/llgo test -run TestRecoverFixedbug -count=1 ./test/go still links the whole test/go package and failed before reaching the recover tests due unrelated generic-local-type undefined symbols, so I used the single recover test file for the focused LLGo runtime/codegen check.

@cpunion
cpunion force-pushed the codex/goroot-fixedbugs-recover-coverage branch 2 times, most recently from 747e47b to a16d798 Compare June 10, 2026 11:30
@cpunion
cpunion force-pushed the codex/goroot-fixedbugs-recover-coverage branch 2 times, most recently from 071f268 to 41dfd44 Compare June 19, 2026 14:44
@cpunion
cpunion force-pushed the codex/goroot-fixedbugs-recover-coverage branch 2 times, most recently from 7156fc2 to 80e2e83 Compare June 29, 2026 02:26
cpunion and others added 11 commits July 2, 2026 22:01
Pure-compute probes (recursive fib, JSON round-trip, sort.Ints, map
churn) with no runtime introspection, so one harness run covers both
the introspection extremes and what the funcinfo machinery costs code
that never asks for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go's pclntab pages are touched by its own runtime (traceback, GC) long
before user code queries it, so its first FuncForPC never pays page-in.
Mirror that: when the prebuilt table is present, init adopts it
(zero-copy, sub-µs), touches the pages the lookup path reads (blob,
funcinfo records, string offsets, strings), runs one synthetic lookup
to warm the code paths, and write-warms the FuncForPC cache pages.

First-in-process FuncForPC: darwin ~17µs -> ~2.8µs, linux ~6.6µs ->
~1.0µs. Startup cost is page-count-bound (tens of µs on stdlib-sized
tables, invisible next to ~3ms process startup; hello-world medians
unchanged). Non-prebuilt binaries stay fully lazy: first-use
construction allocates, which has no place in init, and programs that
never introspect pay nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-depths generates deep_<N> scenarios at configurable call depths;
-bigsizes generates bigfunc scenarios (funcs x statements) whose large
bodies stress statement-level pcline density, mid-function pc
symbolization, and ordinary performance of big method bodies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cpunion
cpunion force-pushed the codex/goroot-fixedbugs-recover-coverage branch from f867d84 to 8fe0ad3 Compare July 2, 2026 15:03
@cpunion

cpunion commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto #2016 (codex/pclntab-linkphase-p1, which includes #2012) per the review-order plan: #2012#2016 → this line of semantics fixes. Conflicts resolved were additive (context fields, the noinline condition set, and runtime.Panic now calls SavePanicCallerFrames() before the panic-node bookkeeping). Note the PR base is still main, so the diff shows #2012/#2016 commits until those merge.

@cpunion

cpunion commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #2033, which re-expresses the Defer-node recover-ownership model on the stage-5 base (#2023); the funcinfo draft carried here was superseded by #2012/#2016/#2019. #1882's recover-error-value work re-expresses on top of #2033 next.

@cpunion cpunion closed this Jul 6, 2026
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 8, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 9, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 16, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 17, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 18, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 18, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 19, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 20, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 23, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
cpunion added a commit to cpunion/llgo that referenced this pull request Jul 23, 2026
Re-expresses xgo-dev#1918 on the xgo-dev#2023 base (its remaining ~11k diff lines were
the pre-xgo-dev#2012 funcinfo draft, superseded by the stage-5 chain):

- recover() only succeeds when called directly by a deferred function
  (gc semantics): the panic node records the owning Defer frame at
  rethrow (panicKey/panicNode + GoDeferData), and Recover checks the
  caller is that frame's direct deferred call. Closure wraps carry
  StartRecoverFrameAlias/EndRecoverFrame so method-value and closure
  adapters stay transparent to the ownership check.
- Rethrow keeps the xgo-dev#2023 PanicTraceback hook on the unrecovered path.
- xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in
  ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover
  semantics), validated on darwin/arm64 go1.26.

Supersedes xgo-dev#1918.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants