271 lines
7 KiB
Go
271 lines
7 KiB
Go
package ir
|
|
|
|
import (
|
|
"go/types"
|
|
)
|
|
|
|
func (b *builder) buildExits(fn *Function) {
|
|
if obj := fn.Object(); obj != nil {
|
|
switch obj.Pkg().Path() {
|
|
case "runtime":
|
|
switch obj.Name() {
|
|
case "exit":
|
|
fn.WillExit = true
|
|
return
|
|
case "throw":
|
|
fn.WillExit = true
|
|
return
|
|
case "Goexit":
|
|
fn.WillUnwind = true
|
|
return
|
|
}
|
|
case "github.com/sirupsen/logrus":
|
|
switch obj.(*types.Func).FullName() {
|
|
case "(*github.com/sirupsen/logrus.Logger).Exit":
|
|
// Technically, this method does not unconditionally exit
|
|
// the process. It dynamically calls a function stored in
|
|
// the logger. If the function is nil, it defaults to
|
|
// os.Exit.
|
|
//
|
|
// The main intent of this method is to terminate the
|
|
// process, and that's what the vast majority of people
|
|
// will use it for. We'll happily accept some false
|
|
// negatives to avoid a lot of false positives.
|
|
fn.WillExit = true
|
|
return
|
|
case "(*github.com/sirupsen/logrus.Logger).Panic",
|
|
"(*github.com/sirupsen/logrus.Logger).Panicf",
|
|
"(*github.com/sirupsen/logrus.Logger).Panicln":
|
|
|
|
// These methods will always panic, but that's not
|
|
// statically known from the code alone, because they
|
|
// take a detour through the generic Log methods.
|
|
fn.WillUnwind = true
|
|
return
|
|
case "(*github.com/sirupsen/logrus.Entry).Panicf",
|
|
"(*github.com/sirupsen/logrus.Entry).Panicln":
|
|
|
|
// Entry.Panic has an explicit panic, but Panicf and
|
|
// Panicln do not, relying fully on the generic Log
|
|
// method.
|
|
fn.WillUnwind = true
|
|
return
|
|
case "(*github.com/sirupsen/logrus.Logger).Log",
|
|
"(*github.com/sirupsen/logrus.Logger).Logf",
|
|
"(*github.com/sirupsen/logrus.Logger).Logln":
|
|
// TODO(dh): we cannot handle these case. Whether they
|
|
// exit or unwind depends on the level, which is set
|
|
// via the first argument. We don't currently support
|
|
// call-site-specific exit information.
|
|
}
|
|
}
|
|
}
|
|
|
|
buildDomTree(fn)
|
|
|
|
isRecoverCall := func(instr Instruction) bool {
|
|
if instr, ok := instr.(*Call); ok {
|
|
if builtin, ok := instr.Call.Value.(*Builtin); ok {
|
|
if builtin.Name() == "recover" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// All panics branch to the exit block, which means that if every
|
|
// possible path through the function panics, then all
|
|
// predecessors of the exit block must panic.
|
|
willPanic := true
|
|
for _, pred := range fn.Exit.Preds {
|
|
if _, ok := pred.Control().(*Panic); !ok {
|
|
willPanic = false
|
|
}
|
|
}
|
|
if willPanic {
|
|
recovers := false
|
|
recoverLoop:
|
|
for _, u := range fn.Blocks {
|
|
for _, instr := range u.Instrs {
|
|
if instr, ok := instr.(*Defer); ok {
|
|
call := instr.Call.StaticCallee()
|
|
if call == nil {
|
|
// not a static call, so we can't be sure the
|
|
// deferred call isn't calling recover
|
|
recovers = true
|
|
break recoverLoop
|
|
}
|
|
if len(call.Blocks) == 0 {
|
|
// external function, we don't know what's
|
|
// happening inside it
|
|
//
|
|
// TODO(dh): this includes functions from
|
|
// imported packages, due to how go/analysis
|
|
// works. We could introduce another fact,
|
|
// like we've done for exiting and unwinding,
|
|
// but it doesn't seem worth it. Virtually all
|
|
// uses of recover will be in closures.
|
|
recovers = true
|
|
break recoverLoop
|
|
}
|
|
for _, y := range call.Blocks {
|
|
for _, instr2 := range y.Instrs {
|
|
if isRecoverCall(instr2) {
|
|
recovers = true
|
|
break recoverLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !recovers {
|
|
fn.WillUnwind = true
|
|
return
|
|
}
|
|
}
|
|
|
|
// TODO(dh): don't check that any specific call dominates the exit
|
|
// block. instead, check that all calls combined cover every
|
|
// possible path through the function.
|
|
exits := NewBlockSet(len(fn.Blocks))
|
|
unwinds := NewBlockSet(len(fn.Blocks))
|
|
for _, u := range fn.Blocks {
|
|
for _, instr := range u.Instrs {
|
|
if instr, ok := instr.(CallInstruction); ok {
|
|
switch instr.(type) {
|
|
case *Defer, *Call:
|
|
default:
|
|
continue
|
|
}
|
|
if instr.Common().IsInvoke() {
|
|
// give up
|
|
return
|
|
}
|
|
var call *Function
|
|
switch instr.Common().Value.(type) {
|
|
case *Function, *MakeClosure:
|
|
call = instr.Common().StaticCallee()
|
|
case *Builtin:
|
|
// the only builtins that affect control flow are
|
|
// panic and recover, and we've already handled
|
|
// those
|
|
continue
|
|
default:
|
|
// dynamic dispatch
|
|
return
|
|
}
|
|
// buildFunction is idempotent. if we're part of a
|
|
// (mutually) recursive call chain, then buildFunction
|
|
// will immediately return, and fn.WillExit will be false.
|
|
if call.Package() == fn.Package() {
|
|
b.buildFunction(call)
|
|
}
|
|
dom := u.Dominates(fn.Exit)
|
|
if call.WillExit {
|
|
if dom {
|
|
fn.WillExit = true
|
|
return
|
|
}
|
|
exits.Add(u)
|
|
} else if call.WillUnwind {
|
|
if dom {
|
|
fn.WillUnwind = true
|
|
return
|
|
}
|
|
unwinds.Add(u)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// depth-first search trying to find a path to the exit block that
|
|
// doesn't cross any of the blacklisted blocks
|
|
seen := NewBlockSet(len(fn.Blocks))
|
|
var findPath func(root *BasicBlock, bl *BlockSet) bool
|
|
findPath = func(root *BasicBlock, bl *BlockSet) bool {
|
|
if root == fn.Exit {
|
|
return true
|
|
}
|
|
if seen.Has(root) {
|
|
return false
|
|
}
|
|
if bl.Has(root) {
|
|
return false
|
|
}
|
|
seen.Add(root)
|
|
for _, succ := range root.Succs {
|
|
if findPath(succ, bl) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if exits.Num() > 0 {
|
|
if !findPath(fn.Blocks[0], exits) {
|
|
fn.WillExit = true
|
|
return
|
|
}
|
|
}
|
|
if unwinds.Num() > 0 {
|
|
seen.Clear()
|
|
if !findPath(fn.Blocks[0], unwinds) {
|
|
fn.WillUnwind = true
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *builder) addUnreachables(fn *Function) {
|
|
for _, bb := range fn.Blocks {
|
|
for i, instr := range bb.Instrs {
|
|
if instr, ok := instr.(*Call); ok {
|
|
var call *Function
|
|
switch v := instr.Common().Value.(type) {
|
|
case *Function:
|
|
call = v
|
|
case *MakeClosure:
|
|
call = v.Fn.(*Function)
|
|
}
|
|
if call == nil {
|
|
continue
|
|
}
|
|
if call.Package() == fn.Package() {
|
|
// make sure we have information on all functions in this package
|
|
b.buildFunction(call)
|
|
}
|
|
if call.WillExit {
|
|
// This call will cause the process to terminate.
|
|
// Remove remaining instructions in the block and
|
|
// replace any control flow with Unreachable.
|
|
for _, succ := range bb.Succs {
|
|
succ.removePred(bb)
|
|
}
|
|
bb.Succs = bb.Succs[:0]
|
|
|
|
bb.Instrs = bb.Instrs[:i+1]
|
|
bb.emit(new(Unreachable), instr.Source())
|
|
addEdge(bb, fn.Exit)
|
|
break
|
|
} else if call.WillUnwind {
|
|
// This call will cause the goroutine to terminate
|
|
// and defers to run (i.e. a panic or
|
|
// runtime.Goexit). Remove remaining instructions
|
|
// in the block and replace any control flow with
|
|
// an unconditional jump to the exit block.
|
|
for _, succ := range bb.Succs {
|
|
succ.removePred(bb)
|
|
}
|
|
bb.Succs = bb.Succs[:0]
|
|
|
|
bb.Instrs = bb.Instrs[:i+1]
|
|
bb.emit(new(Jump), instr.Source())
|
|
addEdge(bb, fn.Exit)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|