diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index b9d78eea23..f70bbaaca1 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -266,6 +266,161 @@ 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() + 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() +} + +func dynamicEntry() { dynamicOwner(dynamicLeaf) } +func dynamicLeaf() {} + +type unresolvedArg int + +func unresolvedOwner(fn func(unresolvedArg)) { + defer inspect() + fn(1) + fn(2) +} +func unresolvedCandidate(unresolvedArg) {} +func unresolvedCandidate2(unresolvedArg) {} +func unresolvedWrong(string) {} + +func directCallerOwner() { + runtime.Caller(0) + directCallerLeaf() +} +func directCallerLeaf() {} + +func noRecoverInspect() { runtime.Caller(0) } +func noRecoverOwner() { + defer noRecoverInspect() + noRecoverLeaf() +} +func noRecoverLeaf() {} + +//go:noinline +func pinned() {} + +func unrelated() {} +`) + tracking := NewCallerTracking() + set := runtimeCallerFuncSet(tracking, ssapkg) + 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) + } + } + 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") + } + 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) + } + } + + panicSites := recoverPanicSiteFuncSet(tracking, ssapkg) + 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) + } + } + 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) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func inspect() { + recover() + runtime.Caller(0) +} + +func owner() { + defer inspect() + defer deferredPanicLeaf() + panicLeaf() +} + +func panicLeaf() { + var p *int +//line panic_site.go:123 + _ = *p +} + +func deferredPanicLeaf() { + var p *int +//line deferred_panic_site.go:234 + _ = *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) + 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`, + `!"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) + } + } + 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) { callerCaches := NewCallerTracking() if fnUsesRuntimeCaller(callerCaches, nil) { @@ -277,6 +432,13 @@ 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") + } + 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 3524d9a05a..4505862286 100644 --- a/cl/caller_tracking_precompute_test.go +++ b/cl/caller_tracking_precompute_test.go @@ -34,12 +34,21 @@ 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 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")] { @@ -48,6 +57,17 @@ 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") + } + 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 { @@ -64,6 +84,8 @@ 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")] || + !recoverPanicSiteFuncSet(tracking, root)[root.Func("CrossLeaf")] || !tracking.recover.needsRecoverScope(recovering) || tracking.recover.needsRecoverScope(plain) { t.Error("concurrent read lost precomputed caller tracking data") @@ -124,6 +146,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 64d6387f44..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 @@ -1335,7 +1336,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 +1344,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 +1380,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 +1420,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 +1447,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 +1486,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 +1547,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 +1588,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 +1847,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 +1873,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 { @@ -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 3c9bd4bd12..adb7d933d4 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,37 +916,61 @@ 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. // // Criterion 2 tests membership against the callee package's *base* set // (criterion 1 alone), so tracking extends exactly one call level past a // 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) - _, trackable := collectRuntimeCallerFunctions(pkg) - out := computeRuntimeCallerFuncSet(pkg, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + funcs, trackable := collectRuntimeCallerFunctions(pkg) + 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(pkg *ssa.Package, 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 @@ -954,7 +978,7 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Func // 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, @@ -962,7 +986,7 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Func // 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) { @@ -974,14 +998,197 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Func return } if baseSet(callee.Pkg)[callee] { - out[fn] = true + frames[fn] = true } }) } - if len(out) == 0 { - out = nil + recoverPanicSites := addRecoverObservableCallees(recover, pkg, funcs, base, frames, trackable, baseSet) + if len(frames) == 0 { + frames = nil } - return out + return callerTrackingFuncSets{frames: frames, recoverPanicSites: recoverPanicSites} +} + +// 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{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + } + queue := make([]*ssa.Function, 0) + seen := make(map[*ssa.Function]bool) + isRecoverObserver := func(target *ssa.Function) bool { + if target == nil || !recover.needsRecoverScope(target) { + return false + } + if isRuntimeCallerFrameFunc(target) { + return true + } + 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 + 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 { + deferInstr, ok := instr.(*ssa.Defer) + if !ok { + continue + } + targets, resolved := analysis.callTargets(fn, &deferInstr.Call) + if !resolved { + continue + } + for target := range targets { + if isRecoverObserver(target) { + add(fn) + break + } + } + } + } + } + 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 { + 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 := call.Value.(*ssa.Builtin); builtin { + continue + } + 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 candidateIndex.compatible(call.Signature()) { + addCallee(candidate, isDefer) + } + continue + } + for target := range targets { + addCallee(target, isDefer) + } + } + } + } + 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 + } + 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, @@ -995,7 +1202,7 @@ func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Func // 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 } @@ -1038,7 +1245,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 @@ -1049,7 +1256,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] = 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") @@ -1068,6 +1275,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 +1291,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, } @@ -1143,7 +1352,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(), } } @@ -1711,6 +1920,13 @@ 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) + if p.panicSiteFuncs[p.goFn] { + p.emitPCLineLabel(b, pos) + } +} + func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { if !p.options.ShadowStack || !p.shouldTrackCallerFrames() { return diff --git a/test/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go index fd3a79cc00..63c8b3cc9a 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") @@ -213,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) + } +} 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