From d8d57d02a81b90435df57f30692e682f102eece4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 11 Aug 2026 07:02:46 +0800 Subject: [PATCH 1/8] test: cover recover-visible panic locations --- cl/caller_frame_test.go | 72 ++++++++++++++++++++++++++ test/go/runtime_statement_line_test.go | 38 ++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index b9d78eea23..66f45087a8 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -266,6 +266,78 @@ func FuncForPC(pc uintptr) uintptr { return 0 } } } +func TestRuntimeCallerFuncSetKeepsRecoverObservableCallees(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func inspect() { + recover() + runtime.Caller(0) +} + +func staticOwner() { + defer inspect() + staticLeaf() +} + +func staticLeaf() { staticNested() } +func staticNested() {} + +func dynamicOwner(fn func()) { + defer inspect() + fn() +} + +func dynamicEntry() { dynamicOwner(dynamicLeaf) } +func dynamicLeaf() {} +func unrelated() {} +`) + set := runtimeCallerFuncSet(NewCallerTracking(), ssapkg) + for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicEntry", "dynamicLeaf"} { + if !set[ssapkg.Func(name)] { + t.Fatalf("%s must keep a frame because a recovering defer can inspect its panic pc", name) + } + } + if set[ssapkg.Func("unrelated")] { + t.Fatal("an unrelated function must not be pinned by recover-visible frame tracking") + } +} + +func TestCompileRuntimeCallerPanicPCLineMetadata(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func inspect() { + recover() + runtime.Caller(0) +} + +func owner() { + defer inspect() + panicLeaf() +} + +func panicLeaf() { + var p *int +//line panic_site.go:123 + _ = *p +} +`) + prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{`!"example.com/foo.panicLeaf"`, `!"panic_site.go"`, `i32 123`} { + if !strings.Contains(ir, want) { + t.Fatalf("recover-visible nil dereference is missing panic-site metadata %q:\n%s", want, ir) + } + } +} + func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { callerCaches := NewCallerTracking() if fnUsesRuntimeCaller(callerCaches, nil) { diff --git a/test/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go index fd3a79cc00..051133757a 100644 --- a/test/go/runtime_statement_line_test.go +++ b/test/go/runtime_statement_line_test.go @@ -49,6 +49,8 @@ func main() { checkClosureIndirectCaller() checkAdjacentRuntimeStack() checkRecoveredDebugStackBounds() + checkRecoveredStaticPanicLine() + checkRecoveredIndirectPanicLine() } //go:noinline @@ -167,6 +169,40 @@ func checkRecoveredDebugStackBounds() { _ = foo.Get(3) // BOUNDS_MARK } +func checkRecoveredStaticPanicLine() { + defer expectRecoveredPanicLine("main.staticNilPanic", STATIC_NIL_PANIC_LINE) + staticNilPanic() +} + +func staticNilPanic() { + var p *int + _ = *p // STATIC_NIL_PANIC_MARK +} + +func checkRecoveredIndirectPanicLine() { + runRecoveredPanic("main.indirectBoundsPanic", INDIRECT_BOUNDS_PANIC_LINE, indirectBoundsPanic) +} + +func runRecoveredPanic(name string, want int, fn func()) { + defer expectRecoveredPanicLine(name, want) + fn() +} + +func indirectBoundsPanic() { + v := []int{0} + _ = v[1] // INDIRECT_BOUNDS_PANIC_MARK +} + +func expectRecoveredPanicLine(name string, want int) { + if recover() == nil { + panic("missing panic for " + name) + } + stack := string(debug.Stack()) + if got := stackLineFor(stack, name); got != want { + panic("bad recovered panic line for " + name + ": " + strconv.Itoa(got) + ", want " + strconv.Itoa(want) + "\n" + stack) + } +} + func stackLineFor(stack, fn string) int { lines := strings.Split(stack, "\n") for i := 0; i+1 < len(lines); i++ { @@ -198,6 +234,8 @@ func TestRuntimeStatementLineInfo(t *testing.T) { source = strings.ReplaceAll(source, "STACK_ONE_LINE", strconv.Itoa(markerLine(source, "STACK_ONE_MARK"))) source = strings.ReplaceAll(source, "STACK_TWO_LINE", strconv.Itoa(markerLine(source, "STACK_TWO_MARK"))) source = strings.ReplaceAll(source, "BOUNDS_LINE", strconv.Itoa(markerLine(source, "BOUNDS_MARK"))) + source = strings.ReplaceAll(source, "STATIC_NIL_PANIC_LINE", strconv.Itoa(markerLine(source, "STATIC_NIL_PANIC_MARK"))) + source = strings.ReplaceAll(source, "INDIRECT_BOUNDS_PANIC_LINE", strconv.Itoa(markerLine(source, "INDIRECT_BOUNDS_PANIC_MARK"))) dir := t.TempDir() file := filepath.Join(dir, "main.go") From 76e2f41220fa66656a1b78e23ce48cae56799e27 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 11 Aug 2026 07:50:03 +0800 Subject: [PATCH 2/8] cl: preserve recover-visible panic locations --- cl/caller_frame_test.go | 29 ++++++++++- cl/compile.go | 24 ++++----- cl/instr.go | 109 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 145 insertions(+), 17 deletions(-) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index 66f45087a8..ef789a2216 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -278,10 +278,12 @@ func inspect() { func staticOwner() { defer inspect() staticLeaf() + go goroutineLeaf() } func staticLeaf() { staticNested() } func staticNested() {} +func goroutineLeaf() {} func dynamicOwner(fn func()) { defer inspect() @@ -290,10 +292,30 @@ func dynamicOwner(fn func()) { func dynamicEntry() { dynamicOwner(dynamicLeaf) } func dynamicLeaf() {} + +func unresolvedOwner(fn func(int)) { + defer inspect() + fn(1) +} +func unresolvedCandidate(int) {} +func unresolvedWrong(string) {} + +func directCallerOwner() { + runtime.Caller(0) + directCallerLeaf() +} +func directCallerLeaf() {} + +func noRecoverInspect() { runtime.Caller(0) } +func noRecoverOwner() { + defer noRecoverInspect() + noRecoverLeaf() +} +func noRecoverLeaf() {} func unrelated() {} `) set := runtimeCallerFuncSet(NewCallerTracking(), ssapkg) - for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicEntry", "dynamicLeaf"} { + for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicEntry", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate"} { if !set[ssapkg.Func(name)] { t.Fatalf("%s must keep a frame because a recovering defer can inspect its panic pc", name) } @@ -301,6 +323,11 @@ func unrelated() {} if set[ssapkg.Func("unrelated")] { t.Fatal("an unrelated function must not be pinned by recover-visible frame tracking") } + for _, name := range []string{"goroutineLeaf", "directCallerLeaf", "noRecoverLeaf", "unresolvedWrong"} { + if set[ssapkg.Func(name)] { + t.Fatalf("%s must not be pinned without a recover-visible synchronous call path", name) + } + } } func TestCompileRuntimeCallerPanicPCLineMetadata(t *testing.T) { diff --git a/cl/compile.go b/cl/compile.go index 64d6387f44..9bf40e236f 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1335,7 +1335,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if skipUnusedArrayDeref(v) { x := p.compileValue(b, v.X) if effectfulArrayDeref { - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) b.AssertNilDeref(x) } return @@ -1343,14 +1343,14 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue // Elide the unused load, but keep an explicit nil check so the // Go dereference still panics instead of relying on a trapping load. x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return } if effectfulArrayDeref { x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) b.AssertNilDeref(x) } if refs, ok := nonDebugReferrers(v); ok && len(refs) == 1 { @@ -1379,7 +1379,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValue(b, v.X) if v.Op != token.ARROW { - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) } if shouldAssertDirectNilDeref(v) { b.AssertNilDeref(x) @@ -1419,7 +1419,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue ret = b.Convert(p.type_(t, llssa.InGo), x) case *ssa.FieldAddr: x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) if p.isAddressOfFieldAddr(v) { b.AssertNilDeref(x) } @@ -1446,12 +1446,12 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValue(b, vx) idx := p.compileValue(b, v.Index) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) ret = b.IndexAddr(x, idx) case *ssa.Index: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) ret = b.Index(x, idx, func() (addr llssa.Expr, zero bool) { switch n := v.X.(type) { case *ssa.Const: @@ -1485,7 +1485,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Max != nil { max = p.compileValue(b, v.Max) } - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) ret = b.Slice(x, low, high, max) ret.Type = p.type_(v.Type(), llssa.InGo) case *ssa.MakeInterface: @@ -1546,7 +1546,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.TypeAssert: x := p.compileValue(b, v.X) t := p.type_(v.AssertedType, llssa.InGo) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) ret = b.TypeAssert(x, t, v.CommaOk) case *ssa.Extract: x := p.compileValue(b, v.Tuple) @@ -1587,7 +1587,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.SliceToArrayPointer: t := p.type_(v.Type(), llssa.InGo) x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) ret = b.SliceToArrayPointer(x, t) default: panic(fmt.Sprintf("compileInstrAndValue: unknown instr - %T\n", iv)) @@ -1846,7 +1846,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { m := p.compileValue(b, v.Map) key := p.compileValue(b, v.Key) val := p.compileValue(b, v.Value) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) b.MapUpdate(m, key, val) case *ssa.Defer: if v.DeferStack != nil { @@ -1872,7 +1872,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { case *ssa.Send: ch := p.compileValue(b, v.Chan) x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) + p.recordPanicSite(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: if p.options.DebugSymbols && v.Parent().Origin() == nil { diff --git a/cl/instr.go b/cl/instr.go index 3c9bd4bd12..7d05f5dddd 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -916,6 +916,9 @@ func fnUsesRuntimeCaller(c *CallerTracking, fn *ssa.Function) bool { // what the callee's fixed Caller depth attributes, so inlining it // would both mis-attribute the location and, on ELF, drop the // function symbol its pcline sections are link-ordered to. +// 5. the function can run below a defer that consumes panic pcs — recover +// exposes the panicked call chain after longjmp has removed those physical +// frames, so the compiler must keep and annotate the possible callees. // // Criterion 2 tests membership against the callee package's *base* set // (criterion 1 alone), so tracking extends exactly one call level past a @@ -932,15 +935,15 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function panic("caller-tracking function set was not precomputed") } base := runtimeCallerBaseSet(c, pkg) - _, trackable := collectRuntimeCallerFunctions(pkg) - out := computeRuntimeCallerFuncSet(pkg, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + funcs, trackable := collectRuntimeCallerFunctions(pkg) + out := computeRuntimeCallerFuncSet(pkg, funcs, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { return runtimeCallerBaseSet(c, dep) }) c.extended[pkg] = out return out } -func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { +func computeRuntimeCallerFuncSet(pkg *ssa.Package, funcs, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { out := make(map[*ssa.Function]bool, len(base)) for fn := range base { out[fn] = true @@ -978,12 +981,103 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Func } }) } + addRecoverObservableCallees(c.recoverAnalysis(), pkg, funcs, base, out, trackable) if len(out) == 0 { out = nil } return out } +// addRecoverObservableCallees keeps the same-package call subtree below a +// defer that can inspect caller pcs. These frames are no longer physically +// live when the deferred function runs after recover; runtime reconstructs +// them from the panic snapshot, so allowing LLVM to inline them would lose the +// function identity and its panic-site line. +func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, base, out, trackable map[*ssa.Function]bool) { + if pkg == nil || len(base) == 0 || len(trackable) == 0 { + return + } + analysis := &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + } + queue := make([]*ssa.Function, 0) + seen := make(map[*ssa.Function]bool) + add := func(fn *ssa.Function) { + if !trackable[fn] || seen[fn] { + return + } + seen[fn] = true + out[fn] = true + queue = append(queue, fn) + } + for fn := range trackable { + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + deferInstr, ok := instr.(*ssa.Defer) + if !ok { + continue + } + targets, resolved := analysis.callTargets(fn, &deferInstr.Call) + if !resolved { + continue + } + for target := range targets { + if (isRuntimeCallerFrameFunc(target) || base[target]) && recover.needsRecoverScope(target) { + add(fn) + break + } + } + } + } + } + for len(queue) != 0 { + fn := queue[0] + queue = queue[1:] + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + callInstr, ok := instr.(*ssa.Call) + if !ok { + continue + } + if _, builtin := callInstr.Call.Value.(*ssa.Builtin); builtin { + continue + } + targets, resolved := analysis.callTargets(fn, &callInstr.Call) + if !resolved { + // The call can target any same-package function compatible + // with the value. Retaining the package-local candidates is + // conservative; cross-package callees keep their own policy. + for candidate := range trackable { + if types.Identical(callInstr.Call.Signature(), candidate.Signature) { + add(candidate) + } + } + continue + } + for target := range targets { + add(target) + } + } + } + } +} + +func (a *runtimeCallerAnalysis) callTargets(fn *ssa.Function, call *ssa.CallCommon) (map[*ssa.Function]bool, bool) { + if call == nil { + return nil, false + } + if callee := call.StaticCallee(); callee != nil { + return map[*ssa.Function]bool{callee: true}, true + } + if call.Method != nil { + return a.interfaceMethodTargets(fn, call.Value, call.Method) + } + return a.functionValueTargets(fn, call.Value) +} + // CallerTracking memoizes frontend analyses for one compilation. Like Patches, // it is compilation-scoped state owned by the driver: create one per // compilation and pass it to every NewPackageExWithEmbed call of that @@ -1049,7 +1143,7 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { base[i] = analyses[i].base } for i := range pkgs { - extended[i] = computeRuntimeCallerFuncSet(pkgs[i], base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + extended[i] = computeRuntimeCallerFuncSet(pkgs[i], analyses[i].funcs, base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { j, ok := index[dep] if !ok { panic("caller-tracking dependency was not precomputed") @@ -1068,6 +1162,7 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { } type callerTrackingPackageAnalysis struct { + funcs map[*ssa.Function]bool base map[*ssa.Function]bool trackable map[*ssa.Function]bool } @@ -1083,6 +1178,7 @@ func analyzeCallerTrackingPackage(pkg *ssa.Package, methods []*ssa.Function) cal visiting: make(map[*ssa.Function]bool), } return callerTrackingPackageAnalysis{ + funcs: funcs, base: computeRuntimeCallerBaseSetFromAnalysis(analysis), trackable: trackable, } @@ -1711,6 +1807,11 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { p.recordRuntimeLocation(b, pos, "RecordPanicLocation") } +func (p *context) recordPanicSite(b llssa.Builder, pos token.Pos) { + p.recordPanicLocation(b, pos) + p.emitPCLineLabel(b, pos) +} + func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { if !p.options.ShadowStack || !p.shouldTrackCallerFrames() { return From 1eb1653476b3e7ba233d3c9134549c5a61331f78 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 11 Aug 2026 08:33:54 +0800 Subject: [PATCH 3/8] cl: document caller tracking criteria --- cl/instr.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cl/instr.go b/cl/instr.go index 7d05f5dddd..6e266f40af 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -907,7 +907,7 @@ func fnUsesRuntimeCaller(c *CallerTracking, fn *ssa.Function) bool { // runtimeCallerFuncSet is the per-package tracking set: functions that // must keep physical frames (noinline, no tail calls) and get statement -// anchors at their call sites. Two criteria feed it: +// anchors at their call sites. Five criteria feed it: // // 1. the function (transitively, within the package) reaches a // runtime.Caller/Callers call — it consumes caller pcs itself; @@ -916,6 +916,10 @@ func fnUsesRuntimeCaller(c *CallerTracking, fn *ssa.Function) bool { // what the callee's fixed Caller depth attributes, so inlining it // would both mis-attribute the location and, on ELF, drop the // function symbol its pcline sections are link-ordered to. +// 3. program-unique functions (main.main and package init functions) +// already have one logical instance, so retaining their frame is free; +// 4. //go:noinline functions already retain their frame, so emitting +// statement anchors adds no further inlining cost; // 5. the function can run below a defer that consumes panic pcs — recover // exposes the panicked call chain after longjmp has removed those physical // frames, so the compiler must keep and annotate the possible callees. From 7a40c6a90b00e967c79fbc9a51144d95c5411b0d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 11 Aug 2026 08:33:58 +0800 Subject: [PATCH 4/8] test/goroot: enable recover-visible panic line cases --- test/goroot/xfail.yaml | 74 +++--------------------------------------- 1 file changed, 4 insertions(+), 70 deletions(-) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 58e80c90bb..07f5648916 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -1283,86 +1283,20 @@ xfails: directive: run case: heapsampling.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/bug347.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - platform: darwin/arm64 - directive: run - case: fixedbugs/bug348.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue27201.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue29504.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4562.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) + - version: go1.24 platform: linux/amd64 directive: run case: heapsampling.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/bug347.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue29504.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/bug348.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue27201.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4562.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/bug348.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue27201.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4562.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) + + - version: go1.26 platform: linux/amd64 directive: run case: heapsampling.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/bug347.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue29504.go - reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) + - version: go1.26 platform: darwin/arm64 directive: rundir From 9695eb4b43cb7fc53f2b5e357955ec61b343219d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 11 Aug 2026 19:48:26 +0800 Subject: [PATCH 5/8] cl: cache recover-visible signature candidates --- cl/caller_frame_test.go | 1 + cl/instr.go | 32 ++++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index ef789a2216..e249c22ffe 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -296,6 +296,7 @@ func dynamicLeaf() {} func unresolvedOwner(fn func(int)) { defer inspect() fn(1) + fn(2) } func unresolvedCandidate(int) {} func unresolvedWrong(string) {} diff --git a/cl/instr.go b/cl/instr.go index 6e266f40af..354339e2a5 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1009,6 +1009,32 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, } queue := make([]*ssa.Function, 0) seen := make(map[*ssa.Function]bool) + type signatureCandidates struct { + signature *types.Signature + candidates []*ssa.Function + } + candidatesBySignature := make(map[string][]signatureCandidates) + compatibleCandidates := func(signature *types.Signature) []*ssa.Function { + key := types.TypeString(signature, func(pkg *types.Package) string { + return pkg.Path() + }) + for _, cached := range candidatesBySignature[key] { + if types.Identical(signature, cached.signature) { + return cached.candidates + } + } + candidates := make([]*ssa.Function, 0) + for candidate := range trackable { + if types.Identical(signature, candidate.Signature) { + candidates = append(candidates, candidate) + } + } + candidatesBySignature[key] = append(candidatesBySignature[key], signatureCandidates{ + signature: signature, + candidates: candidates, + }) + return candidates + } add := func(fn *ssa.Function) { if !trackable[fn] || seen[fn] { return @@ -1054,10 +1080,8 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, // The call can target any same-package function compatible // with the value. Retaining the package-local candidates is // conservative; cross-package callees keep their own policy. - for candidate := range trackable { - if types.Identical(callInstr.Call.Signature(), candidate.Signature) { - add(candidate) - } + for _, candidate := range compatibleCandidates(callInstr.Call.Signature()) { + add(candidate) } continue } From 98216c2ae214302483b5876b92dbd0631a177152 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 13 Aug 2026 12:01:53 +0800 Subject: [PATCH 6/8] cl: preserve caller precomputation for recover analysis --- cl/instr.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cl/instr.go b/cl/instr.go index 354339e2a5..5f38a24678 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -940,14 +940,14 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function } base := runtimeCallerBaseSet(c, pkg) funcs, trackable := collectRuntimeCallerFunctions(pkg) - out := computeRuntimeCallerFuncSet(pkg, funcs, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + out := computeRuntimeCallerFuncSet(c.recoverAnalysis(), pkg, funcs, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { return runtimeCallerBaseSet(c, dep) }) c.extended[pkg] = out return out } -func computeRuntimeCallerFuncSet(pkg *ssa.Package, funcs, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { +func computeRuntimeCallerFuncSet(recover *recoverFacts, pkg *ssa.Package, funcs, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { out := make(map[*ssa.Function]bool, len(base)) for fn := range base { out[fn] = true @@ -985,7 +985,7 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, funcs, base, trackable map[*s } }) } - addRecoverObservableCallees(c.recoverAnalysis(), pkg, funcs, base, out, trackable) + addRecoverObservableCallees(recover, pkg, funcs, base, out, trackable) if len(out) == 0 { out = nil } @@ -1171,7 +1171,7 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { base[i] = analyses[i].base } for i := range pkgs { - extended[i] = computeRuntimeCallerFuncSet(pkgs[i], analyses[i].funcs, base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + extended[i] = computeRuntimeCallerFuncSet(c.recoverAnalysis(), pkgs[i], analyses[i].funcs, base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { j, ok := index[dep] if !ok { panic("caller-tracking dependency was not precomputed") From cae7bbd7597bfba8ebfbe3bb7263088cdd3e1f14 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 13 Aug 2026 23:51:55 +0800 Subject: [PATCH 7/8] cl: scope recover panic-site anchors --- cl/caller_frame_test.go | 35 ++++++++++++- cl/caller_tracking_precompute_test.go | 14 +++++ cl/compile.go | 2 + cl/instr.go | 74 ++++++++++++++++++--------- 4 files changed, 99 insertions(+), 26 deletions(-) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index e249c22ffe..565f215d15 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -313,14 +313,22 @@ func noRecoverOwner() { noRecoverLeaf() } func noRecoverLeaf() {} + +//go:noinline +func pinned() {} + func unrelated() {} `) - set := runtimeCallerFuncSet(NewCallerTracking(), ssapkg) + tracking := NewCallerTracking() + set := runtimeCallerFuncSet(tracking, ssapkg) for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicEntry", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate"} { if !set[ssapkg.Func(name)] { t.Fatalf("%s must keep a frame because a recovering defer can inspect its panic pc", name) } } + if !set[ssapkg.Func("pinned")] { + t.Fatal("//go:noinline function must keep its frame") + } if set[ssapkg.Func("unrelated")] { t.Fatal("an unrelated function must not be pinned by recover-visible frame tracking") } @@ -329,6 +337,18 @@ func unrelated() {} t.Fatalf("%s must not be pinned without a recover-visible synchronous call path", name) } } + + panicSites := recoverPanicSiteFuncSet(tracking, ssapkg) + for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate"} { + if !panicSites[ssapkg.Func(name)] { + t.Fatalf("%s needs implicit panic-site anchors below a recovering defer", name) + } + } + for _, name := range []string{"inspect", "dynamicEntry", "goroutineLeaf", "directCallerOwner", "directCallerLeaf", "noRecoverInspect", "noRecoverOwner", "noRecoverLeaf", "pinned", "unrelated", "unresolvedWrong"} { + if panicSites[ssapkg.Func(name)] { + t.Fatalf("%s must not get implicit panic-site anchors outside a recover-visible synchronous call subtree", name) + } + } } func TestCompileRuntimeCallerPanicPCLineMetadata(t *testing.T) { @@ -350,6 +370,13 @@ func panicLeaf() { //line panic_site.go:123 _ = *p } + +//go:noinline +func pinnedPanicSite() { + var p *int +//line non_recover_site.go:321 + _ = *p +} `) prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) prog.EnableFuncInfoMetadata(true) @@ -364,6 +391,9 @@ func panicLeaf() { t.Fatalf("recover-visible nil dereference is missing panic-site metadata %q:\n%s", want, ir) } } + if strings.Contains(ir, `!"non_recover_site.go"`) { + t.Fatalf("ordinary pinned function unexpectedly received implicit panic-site metadata:\n%s", ir) + } } func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { @@ -377,6 +407,9 @@ func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { if runtimeCallerFuncSet(callerCaches, nil) != nil { t.Fatal("nil package should have no runtime caller set") } + if recoverPanicSiteFuncSet(callerCaches, nil) != nil { + t.Fatal("nil package should have no recover panic-site set") + } if fnHasDirectRuntimeCaller(nil) { t.Fatal("nil function should not have direct runtime caller use") } diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go index 3524d9a05a..8eca5d5101 100644 --- a/cl/caller_tracking_precompute_test.go +++ b/cl/caller_tracking_precompute_test.go @@ -34,6 +34,9 @@ func TestCallerTrackingPrecomputeSupportsConcurrentReads(t *testing.T) { import "runtime" func Where() { runtime.Caller(0) } func Recovering() any { return recover() } +func Inspect() { recover(); runtime.Caller(0) } +func Owner() { defer Inspect(); Leaf() } +func Leaf() {} func Plain() {} `, "example.com/root", `package root @@ -48,6 +51,13 @@ func Logs() { dep.Where() } if !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { t.Fatal("precomputed extended set lost cross-package caller") } + panicSites := recoverPanicSiteFuncSet(tracking, dep) + if !panicSites[dep.Func("Owner")] || !panicSites[dep.Func("Leaf")] { + t.Fatal("precomputed recover panic-site set lost synchronous callees") + } + if panicSites[dep.Func("Where")] { + t.Fatal("ordinary caller-tracked function entered recover panic-site set") + } recovering := dep.Func("Recovering") plain := dep.Func("Plain") if needs, ok := tracking.recover.scopes[recovering]; !ok || !needs { @@ -64,6 +74,7 @@ func Logs() { dep.Where() } defer wg.Done() if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] || !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] || + !recoverPanicSiteFuncSet(tracking, dep)[dep.Func("Leaf")] || !tracking.recover.needsRecoverScope(recovering) || tracking.recover.needsRecoverScope(plain) { t.Error("concurrent read lost precomputed caller tracking data") @@ -124,6 +135,9 @@ func Logs() { dep.Where() } {name: "extended", lookup: func(c *CallerTracking, pkg *gossa.Package) { runtimeCallerFuncSet(c, pkg) }}, + {name: "recover-panic-sites", lookup: func(c *CallerTracking, pkg *gossa.Package) { + recoverPanicSiteFuncSet(c, pkg) + }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/cl/compile.go b/cl/compile.go index 9bf40e236f..f381a7a3bb 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -165,6 +165,7 @@ type context struct { debugDIVars map[*types.Var]llssa.DIVar debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool + panicSiteFuncs map[*ssa.Function]bool pcLineSeq uint64 options Options recoverSlots map[*ssa.Alloc]none @@ -2322,6 +2323,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri trackCallerFrames: filesUseRuntimeCaller(files) || packageUsesRuntimeCaller(ct, pkg), runtimeCallerFuncs: runtimeCallerFuncSet(ct, pkg), + panicSiteFuncs: recoverPanicSiteFuncSet(ct, pkg), } if embedMap != nil { ctx.embedMap = *embedMap diff --git a/cl/instr.go b/cl/instr.go index 5f38a24678..dd224fda15 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -929,31 +929,48 @@ func fnUsesRuntimeCaller(c *CallerTracking, fn *ssa.Function) bool { // pc-consuming package and does not cascade through arbitrary wrapper // layers; multi-package wrapper chains remain the P4 inline-tree's job. func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function]bool { + return callerTrackingFuncSetsForPackage(c, pkg).frames +} + +// recoverPanicSiteFuncSet is the subset whose implicit panic instructions +// need exact PC-line anchors. Caller consumers, program-unique functions, and +// //go:noinline functions can require stable frames without needing an anchor +// at every potentially panicking instruction. +func recoverPanicSiteFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function]bool { + return callerTrackingFuncSetsForPackage(c, pkg).recoverPanicSites +} + +func callerTrackingFuncSetsForPackage(c *CallerTracking, pkg *ssa.Package) callerTrackingFuncSets { if pkg == nil { - return nil + return callerTrackingFuncSets{} } - if set, ok := c.extended[pkg]; ok { - return set + if sets, ok := c.extended[pkg]; ok { + return sets } if c.precomputed { panic("caller-tracking function set was not precomputed") } base := runtimeCallerBaseSet(c, pkg) funcs, trackable := collectRuntimeCallerFunctions(pkg) - out := computeRuntimeCallerFuncSet(c.recoverAnalysis(), pkg, funcs, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + sets := computeRuntimeCallerFuncSets(c.recoverAnalysis(), pkg, funcs, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { return runtimeCallerBaseSet(c, dep) }) - c.extended[pkg] = out - return out + c.extended[pkg] = sets + return sets +} + +type callerTrackingFuncSets struct { + frames map[*ssa.Function]bool + recoverPanicSites map[*ssa.Function]bool } -func computeRuntimeCallerFuncSet(recover *recoverFacts, pkg *ssa.Package, funcs, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { - out := make(map[*ssa.Function]bool, len(base)) +func computeRuntimeCallerFuncSets(recover *recoverFacts, pkg *ssa.Package, funcs, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) callerTrackingFuncSets { + frames := make(map[*ssa.Function]bool, len(base)) for fn := range base { - out[fn] = true + frames[fn] = true } for fn := range trackable { - if out[fn] { + if frames[fn] { continue } // Criterion 3: pin program-unique frames. main.main and package @@ -961,7 +978,7 @@ func computeRuntimeCallerFuncSet(recover *recoverFacts, pkg *ssa.Package, funcs, // bottom frames of almost every panic traceback, where an // approximate declaration-adjacent line is most visible. if isProgramUniqueFrame(pkg, fn) { - out[fn] = true + frames[fn] = true continue } // Criterion 4: //go:noinline functions already keep their frames, @@ -969,7 +986,7 @@ func computeRuntimeCallerFuncSet(recover *recoverFacts, pkg *ssa.Package, funcs, // panic-traceback lines become exact instead of // declaration-adjacent. if hasNoInlineDirective(fn) { - out[fn] = true + frames[fn] = true continue } forEachCall(fn, func(call *ssa.CallCommon) { @@ -981,25 +998,26 @@ func computeRuntimeCallerFuncSet(recover *recoverFacts, pkg *ssa.Package, funcs, return } if baseSet(callee.Pkg)[callee] { - out[fn] = true + frames[fn] = true } }) } - addRecoverObservableCallees(recover, pkg, funcs, base, out, trackable) - if len(out) == 0 { - out = nil + recoverPanicSites := addRecoverObservableCallees(recover, pkg, funcs, base, frames, trackable) + if len(frames) == 0 { + frames = nil } - return out + return callerTrackingFuncSets{frames: frames, recoverPanicSites: recoverPanicSites} } // addRecoverObservableCallees keeps the same-package call subtree below a // defer that can inspect caller pcs. These frames are no longer physically // live when the deferred function runs after recover; runtime reconstructs // them from the panic snapshot, so allowing LLVM to inline them would lose the -// function identity and its panic-site line. -func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, base, out, trackable map[*ssa.Function]bool) { +// function identity and its panic-site line. The returned set is kept separate +// from out: only these functions need anchors at implicit panic instructions. +func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, base, out, trackable map[*ssa.Function]bool) map[*ssa.Function]bool { if pkg == nil || len(base) == 0 || len(trackable) == 0 { - return + return nil } analysis := &runtimeCallerAnalysis{ pkg: pkg, @@ -1091,6 +1109,10 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, } } } + if len(seen) == 0 { + return nil + } + return seen } func (a *runtimeCallerAnalysis) callTargets(fn *ssa.Function, call *ssa.CallCommon) (map[*ssa.Function]bool, bool) { @@ -1117,7 +1139,7 @@ func (a *runtimeCallerAnalysis) callTargets(fn *ssa.Function, call *ssa.CallComm // for nested and synthetic functions that are not package members. type CallerTracking struct { base map[*ssa.Package]map[*ssa.Function]bool - extended map[*ssa.Package]map[*ssa.Function]bool + extended map[*ssa.Package]callerTrackingFuncSets recover *recoverFacts precomputed bool } @@ -1160,7 +1182,7 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { methods := callerTrackingMethods(pkgs, runtimeTypes) analyses := make([]callerTrackingPackageAnalysis, len(pkgs)) base := make([]map[*ssa.Function]bool, len(pkgs)) - extended := make([]map[*ssa.Function]bool, len(pkgs)) + extended := make([]callerTrackingFuncSets, len(pkgs)) index := make(map[*ssa.Package]int, len(pkgs)) for i, pkg := range pkgs { index[pkg] = i @@ -1171,7 +1193,7 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { base[i] = analyses[i].base } for i := range pkgs { - extended[i] = computeRuntimeCallerFuncSet(c.recoverAnalysis(), pkgs[i], analyses[i].funcs, base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + extended[i] = computeRuntimeCallerFuncSets(c.recoverAnalysis(), pkgs[i], analyses[i].funcs, base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { j, ok := index[dep] if !ok { panic("caller-tracking dependency was not precomputed") @@ -1267,7 +1289,7 @@ func uniqueCallerTrackingPackages(pkgs []*ssa.Package) []*ssa.Package { func NewCallerTracking() *CallerTracking { return &CallerTracking{ base: make(map[*ssa.Package]map[*ssa.Function]bool), - extended: make(map[*ssa.Package]map[*ssa.Function]bool), + extended: make(map[*ssa.Package]callerTrackingFuncSets), recover: newRecoverFacts(), } } @@ -1837,7 +1859,9 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { func (p *context) recordPanicSite(b llssa.Builder, pos token.Pos) { p.recordPanicLocation(b, pos) - p.emitPCLineLabel(b, pos) + if p.panicSiteFuncs[p.goFn] { + p.emitPCLineLabel(b, pos) + } } func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { From ca71598e7e1abe49effd9e6184bf3acc1db083b7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 14 Aug 2026 06:50:16 +0800 Subject: [PATCH 8/8] cl: refine recover-visible panic tracking --- cl/caller_frame_test.go | 39 ++++++- cl/caller_tracking_precompute_test.go | 11 ++ cl/instr.go | 151 ++++++++++++++++++------- test/go/runtime_statement_line_test.go | 46 ++++++++ 4 files changed, 198 insertions(+), 49 deletions(-) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index 565f215d15..f70bbaaca1 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -277,14 +277,26 @@ func inspect() { func staticOwner() { defer inspect() + defer deferredLeaf() staticLeaf() go goroutineLeaf() } func staticLeaf() { staticNested() } func staticNested() {} +func deferredLeaf() { deferredNested() } +func deferredNested() {} func goroutineLeaf() {} +func closureObserverOwner() { + defer func() { + recover() + runtime.Caller(0) + }() + defer closureDeferredLeaf() +} +func closureDeferredLeaf() {} + func dynamicOwner(fn func()) { defer inspect() fn() @@ -293,12 +305,15 @@ func dynamicOwner(fn func()) { func dynamicEntry() { dynamicOwner(dynamicLeaf) } func dynamicLeaf() {} -func unresolvedOwner(fn func(int)) { +type unresolvedArg int + +func unresolvedOwner(fn func(unresolvedArg)) { defer inspect() fn(1) fn(2) } -func unresolvedCandidate(int) {} +func unresolvedCandidate(unresolvedArg) {} +func unresolvedCandidate2(unresolvedArg) {} func unresolvedWrong(string) {} func directCallerOwner() { @@ -321,7 +336,7 @@ func unrelated() {} `) tracking := NewCallerTracking() set := runtimeCallerFuncSet(tracking, ssapkg) - for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicEntry", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate"} { + for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "deferredLeaf", "deferredNested", "closureObserverOwner", "closureDeferredLeaf", "dynamicOwner", "dynamicEntry", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate", "unresolvedCandidate2"} { if !set[ssapkg.Func(name)] { t.Fatalf("%s must keep a frame because a recovering defer can inspect its panic pc", name) } @@ -339,7 +354,7 @@ func unrelated() {} } panicSites := recoverPanicSiteFuncSet(tracking, ssapkg) - for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "dynamicOwner", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate"} { + for _, name := range []string{"staticOwner", "staticLeaf", "staticNested", "deferredLeaf", "deferredNested", "closureObserverOwner", "closureDeferredLeaf", "dynamicOwner", "dynamicLeaf", "unresolvedOwner", "unresolvedCandidate", "unresolvedCandidate2"} { if !panicSites[ssapkg.Func(name)] { t.Fatalf("%s needs implicit panic-site anchors below a recovering defer", name) } @@ -362,6 +377,7 @@ func inspect() { func owner() { defer inspect() + defer deferredPanicLeaf() panicLeaf() } @@ -371,6 +387,12 @@ func panicLeaf() { _ = *p } +func deferredPanicLeaf() { + var p *int +//line deferred_panic_site.go:234 + _ = *p +} + //go:noinline func pinnedPanicSite() { var p *int @@ -386,7 +408,10 @@ func pinnedPanicSite() { t.Fatal(err) } ir := pkg.Module().String() - for _, want := range []string{`!"example.com/foo.panicLeaf"`, `!"panic_site.go"`, `i32 123`} { + for _, want := range []string{ + `!"example.com/foo.panicLeaf"`, `!"panic_site.go"`, `i32 123`, + `!"example.com/foo.deferredPanicLeaf"`, `!"deferred_panic_site.go"`, `i32 234`, + } { if !strings.Contains(ir, want) { t.Fatalf("recover-visible nil dereference is missing panic-site metadata %q:\n%s", want, ir) } @@ -410,6 +435,10 @@ func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { if recoverPanicSiteFuncSet(callerCaches, nil) != nil { t.Fatal("nil package should have no recover panic-site set") } + emptySignature := types.NewSignatureType(nil, nil, nil, nil, nil, false) + if candidates := newRecoverPanicCandidateIndex(nil).compatible(emptySignature); candidates != nil { + t.Fatal("empty recover panic candidate index should have no compatible functions") + } if fnHasDirectRuntimeCaller(nil) { t.Fatal("nil function should not have direct runtime caller use") } diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go index 8eca5d5101..4505862286 100644 --- a/cl/caller_tracking_precompute_test.go +++ b/cl/caller_tracking_precompute_test.go @@ -42,7 +42,13 @@ func Plain() {} "example.com/root", `package root import "example.com/dep" func Logs() { dep.Where() } +func CrossOwner() { defer dep.Inspect(); CrossLeaf() } +func CrossLeaf() {} `) + lazyCrossPackageSites := recoverPanicSiteFuncSet(NewCallerTracking(), root) + if !lazyCrossPackageSites[root.Func("CrossOwner")] || !lazyCrossPackageSites[root.Func("CrossLeaf")] { + t.Fatal("lazy analysis lost the subtree below a cross-package recovering defer") + } tracking := NewCallerTracking() tracking.Precompute([]*gossa.Package{dep, root}) if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] { @@ -58,6 +64,10 @@ func Logs() { dep.Where() } if panicSites[dep.Func("Where")] { t.Fatal("ordinary caller-tracked function entered recover panic-site set") } + rootPanicSites := recoverPanicSiteFuncSet(tracking, root) + if !rootPanicSites[root.Func("CrossOwner")] || !rootPanicSites[root.Func("CrossLeaf")] { + t.Fatal("precomputed analysis lost the subtree below a cross-package recovering defer") + } recovering := dep.Func("Recovering") plain := dep.Func("Plain") if needs, ok := tracking.recover.scopes[recovering]; !ok || !needs { @@ -75,6 +85,7 @@ func Logs() { dep.Where() } if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] || !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] || !recoverPanicSiteFuncSet(tracking, dep)[dep.Func("Leaf")] || + !recoverPanicSiteFuncSet(tracking, root)[root.Func("CrossLeaf")] || !tracking.recover.needsRecoverScope(recovering) || tracking.recover.needsRecoverScope(plain) { t.Error("concurrent read lost precomputed caller tracking data") diff --git a/cl/instr.go b/cl/instr.go index dd224fda15..adb7d933d4 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1002,21 +1002,23 @@ func computeRuntimeCallerFuncSets(recover *recoverFacts, pkg *ssa.Package, funcs } }) } - recoverPanicSites := addRecoverObservableCallees(recover, pkg, funcs, base, frames, trackable) + recoverPanicSites := addRecoverObservableCallees(recover, pkg, funcs, base, frames, trackable, baseSet) if len(frames) == 0 { frames = nil } return callerTrackingFuncSets{frames: frames, recoverPanicSites: recoverPanicSites} } -// addRecoverObservableCallees keeps the same-package call subtree below a -// defer that can inspect caller pcs. These frames are no longer physically -// live when the deferred function runs after recover; runtime reconstructs -// them from the panic snapshot, so allowing LLVM to inline them would lose the -// function identity and its panic-site line. The returned set is kept separate -// from out: only these functions need anchors at implicit panic instructions. -func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, base, out, trackable map[*ssa.Function]bool) map[*ssa.Function]bool { - if pkg == nil || len(base) == 0 || len(trackable) == 0 { +// addRecoverObservableCallees keeps the same-package synchronous call/defer +// subtree below a defer that can inspect caller pcs. These frames are no +// longer physically live when the deferred function runs after recover; +// runtime reconstructs them from the panic snapshot, so allowing LLVM to +// inline them would lose the function identity and its panic-site line. These +// functions are added to frames and also returned as a distinct set: only the +// returned set needs +// anchors at implicit panic instructions. +func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, base, frames, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { + if pkg == nil || len(trackable) == 0 { return nil } analysis := &runtimeCallerAnalysis{ @@ -1027,40 +1029,35 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, } queue := make([]*ssa.Function, 0) seen := make(map[*ssa.Function]bool) - type signatureCandidates struct { - signature *types.Signature - candidates []*ssa.Function - } - candidatesBySignature := make(map[string][]signatureCandidates) - compatibleCandidates := func(signature *types.Signature) []*ssa.Function { - key := types.TypeString(signature, func(pkg *types.Package) string { - return pkg.Path() - }) - for _, cached := range candidatesBySignature[key] { - if types.Identical(signature, cached.signature) { - return cached.candidates - } + isRecoverObserver := func(target *ssa.Function) bool { + if target == nil || !recover.needsRecoverScope(target) { + return false } - candidates := make([]*ssa.Function, 0) - for candidate := range trackable { - if types.Identical(signature, candidate.Signature) { - candidates = append(candidates, candidate) - } + if isRuntimeCallerFrameFunc(target) { + return true } - candidatesBySignature[key] = append(candidatesBySignature[key], signatureCandidates{ - signature: signature, - candidates: candidates, - }) - return candidates + targetBase := base + if target.Pkg != nil && target.Pkg != pkg { + targetBase = baseSet(target.Pkg) + } + return targetBase[target] } add := func(fn *ssa.Function) { if !trackable[fn] || seen[fn] { return } seen[fn] = true - out[fn] = true + frames[fn] = true queue = append(queue, fn) } + addCallee := func(fn *ssa.Function, deferred bool) { + // A recovering defer is the observer at the edge of this subtree, + // not another possible panic site below itself. + if deferred && isRecoverObserver(fn) { + return + } + add(fn) + } for fn := range trackable { for _, block := range fn.Blocks { for _, instr := range block.Instrs { @@ -1073,7 +1070,7 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, continue } for target := range targets { - if (isRuntimeCallerFrameFunc(target) || base[target]) && recover.needsRecoverScope(target) { + if isRecoverObserver(target) { add(fn) break } @@ -1081,40 +1078,106 @@ func addRecoverObservableCallees(recover *recoverFacts, pkg *ssa.Package, funcs, } } } + if len(queue) == 0 { + return nil + } + + candidateIndex := newRecoverPanicCandidateIndex(trackable) for len(queue) != 0 { fn := queue[0] queue = queue[1:] for _, block := range fn.Blocks { for _, instr := range block.Instrs { - callInstr, ok := instr.(*ssa.Call) - if !ok { + var call *ssa.CallCommon + isDefer := false + switch instr := instr.(type) { + case *ssa.Call: + call = &instr.Call + case *ssa.Defer: + // Non-observing deferred calls still run synchronously in + // this goroutine while the panic unwinds. Their panic sites + // can therefore be seen by a later recovering defer. + call = &instr.Call + isDefer = true + case *ssa.Go: + // A go edge starts an independent goroutine with its own + // panic/recover chain and cannot be observed by this defer. + continue + default: continue } - if _, builtin := callInstr.Call.Value.(*ssa.Builtin); builtin { + if _, builtin := call.Value.(*ssa.Builtin); builtin { continue } - targets, resolved := analysis.callTargets(fn, &callInstr.Call) + targets, resolved := analysis.callTargets(fn, call) if !resolved { // The call can target any same-package function compatible // with the value. Retaining the package-local candidates is // conservative; cross-package callees keep their own policy. - for _, candidate := range compatibleCandidates(callInstr.Call.Signature()) { - add(candidate) + for _, candidate := range candidateIndex.compatible(call.Signature()) { + addCallee(candidate, isDefer) } continue } for target := range targets { - add(target) + addCallee(target, isDefer) } } } } - if len(seen) == 0 { - return nil - } return seen } +type recoverPanicCandidateGroup struct { + signature *types.Signature + candidates []*ssa.Function +} + +type recoverPanicCandidateIndex map[string][]recoverPanicCandidateGroup + +// newRecoverPanicCandidateIndex scans trackable once and buckets candidates by +// fully qualified structural signature. The identical-signature groups retain +// a collision guard without making each unresolved call rescan the package. +func newRecoverPanicCandidateIndex(trackable map[*ssa.Function]bool) recoverPanicCandidateIndex { + index := make(recoverPanicCandidateIndex) + for candidate := range trackable { + signature := candidate.Signature + key := recoverPanicSignatureKey(signature) + groups := index[key] + matched := false + for i := range groups { + if types.Identical(signature, groups[i].signature) { + groups[i].candidates = append(groups[i].candidates, candidate) + matched = true + break + } + } + if !matched { + groups = append(groups, recoverPanicCandidateGroup{ + signature: signature, + candidates: []*ssa.Function{candidate}, + }) + } + index[key] = groups + } + return index +} + +func (index recoverPanicCandidateIndex) compatible(signature *types.Signature) []*ssa.Function { + for _, group := range index[recoverPanicSignatureKey(signature)] { + if types.Identical(signature, group.signature) { + return group.candidates + } + } + return nil +} + +func recoverPanicSignatureKey(signature *types.Signature) string { + return types.TypeString(signature, func(pkg *types.Package) string { + return pkg.Path() + }) +} + func (a *runtimeCallerAnalysis) callTargets(fn *ssa.Function, call *ssa.CallCommon) (map[*ssa.Function]bool, bool) { if call == nil { return nil, false diff --git a/test/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go index 051133757a..63c8b3cc9a 100644 --- a/test/go/runtime_statement_line_test.go +++ b/test/go/runtime_statement_line_test.go @@ -251,3 +251,49 @@ func TestRuntimeStatementLineInfo(t *testing.T) { t.Fatalf("llgo statement line probe failed: %v\n%s", err, out) } } + +const runtimeDeferredPanicLineProbe = `package main + +import "runtime/debug" + +func main() { + defer func() { + recover() + println(string(debug.Stack())) + }() + defer deferredPanic() + panic("start unwinding") +} + +func deferredPanic() { + var p *int + _ = *p // DEFERRED_PANIC_MARK +} +` + +func TestRuntimeDeferredPanicLine(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(runtimeDeferredPanicLineProbe), 0644); err != nil { + t.Fatal(err) + } + + repoRoot := findRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) + cmd.Dir = repoRoot + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("llgo deferred panic line probe failed: %v\n%s", err, out) + } + stack := string(out) + frame := strings.Index(stack, "main.deferredPanic()") + want := "main.go:" + strconv.Itoa(markerLine(runtimeDeferredPanicLineProbe, "DEFERRED_PANIC_MARK")) + if frame < 0 { + t.Fatalf("deferred panic stack is missing main.deferredPanic:\n%s", stack) + } + lines := strings.SplitN(stack[frame:], "\n", 3) + if len(lines) < 2 || !strings.Contains(lines[1], want) { + t.Fatalf("deferred panic stack is missing %s:\n%s", want, stack) + } +}