Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions cl/caller_frame_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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")
}
Expand Down
25 changes: 25 additions & 0 deletions cl/caller_tracking_precompute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")] {
Expand All @@ -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 {
Expand All @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 14 additions & 12 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1335,22 +1336,22 @@ 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
}
// 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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading