github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/go/ir/exits.go (about)

     1  package ir
     2  
     3  import (
     4  	"go/types"
     5  )
     6  
     7  func (b *builder) buildExits(fn *Function) {
     8  	if obj := fn.Object(); obj != nil {
     9  		switch obj.Pkg().Path() {
    10  		case "runtime":
    11  			switch obj.Name() {
    12  			case "exit":
    13  				fn.NoReturn = AlwaysExits
    14  				return
    15  			case "throw":
    16  				fn.NoReturn = AlwaysExits
    17  				return
    18  			case "Goexit":
    19  				fn.NoReturn = AlwaysUnwinds
    20  				return
    21  			}
    22  		case "go.uber.org/zap":
    23  			switch obj.(*types.Func).FullName() {
    24  			case "(*go.uber.org/zap.Logger).Fatal",
    25  				"(*go.uber.org/zap.SugaredLogger).Fatal",
    26  				"(*go.uber.org/zap.SugaredLogger).Fatalw",
    27  				"(*go.uber.org/zap.SugaredLogger).Fatalf":
    28  				// Technically, this method does not unconditionally exit
    29  				// the process. It dynamically calls a function stored in
    30  				// the logger. If the function is nil, it defaults to
    31  				// os.Exit.
    32  				//
    33  				// The main intent of this method is to terminate the
    34  				// process, and that's what the vast majority of people
    35  				// will use it for. We'll happily accept some false
    36  				// negatives to avoid a lot of false positives.
    37  				fn.NoReturn = AlwaysExits
    38  			case "(*go.uber.org/zap.Logger).Panic",
    39  				"(*go.uber.org/zap.SugaredLogger).Panicw",
    40  				"(*go.uber.org/zap.SugaredLogger).Panicf":
    41  				fn.NoReturn = AlwaysUnwinds
    42  				return
    43  			case "(*go.uber.org/zap.Logger).DPanic",
    44  				"(*go.uber.org/zap.SugaredLogger).DPanicf",
    45  				"(*go.uber.org/zap.SugaredLogger).DPanicw":
    46  				// These methods will only panic in development.
    47  			}
    48  		case "github.com/sirupsen/logrus":
    49  			switch obj.(*types.Func).FullName() {
    50  			case "(*github.com/sirupsen/logrus.Logger).Exit":
    51  				// Technically, this method does not unconditionally exit
    52  				// the process. It dynamically calls a function stored in
    53  				// the logger. If the function is nil, it defaults to
    54  				// os.Exit.
    55  				//
    56  				// The main intent of this method is to terminate the
    57  				// process, and that's what the vast majority of people
    58  				// will use it for. We'll happily accept some false
    59  				// negatives to avoid a lot of false positives.
    60  				fn.NoReturn = AlwaysExits
    61  				return
    62  			case "(*github.com/sirupsen/logrus.Logger).Panic",
    63  				"(*github.com/sirupsen/logrus.Logger).Panicf",
    64  				"(*github.com/sirupsen/logrus.Logger).Panicln":
    65  
    66  				// These methods will always panic, but that's not
    67  				// statically known from the code alone, because they
    68  				// take a detour through the generic Log methods.
    69  				fn.NoReturn = AlwaysUnwinds
    70  				return
    71  			case "(*github.com/sirupsen/logrus.Entry).Panicf",
    72  				"(*github.com/sirupsen/logrus.Entry).Panicln":
    73  
    74  				// Entry.Panic has an explicit panic, but Panicf and
    75  				// Panicln do not, relying fully on the generic Log
    76  				// method.
    77  				fn.NoReturn = AlwaysUnwinds
    78  				return
    79  			case "(*github.com/sirupsen/logrus.Logger).Log",
    80  				"(*github.com/sirupsen/logrus.Logger).Logf",
    81  				"(*github.com/sirupsen/logrus.Logger).Logln":
    82  				// TODO(dh): we cannot handle these cases. Whether they
    83  				// exit or unwind depends on the level, which is set
    84  				// via the first argument. We don't currently support
    85  				// call-site-specific exit information.
    86  			}
    87  		case "github.com/golang/glog":
    88  			switch obj.(*types.Func).FullName() {
    89  			case "github.com/golang/glog.Exit",
    90  				"github.com/golang/glog.ExitDepth",
    91  				"github.com/golang/glog.Exitf",
    92  				"github.com/golang/glog.Exitln",
    93  				"github.com/golang/glog.Fatal",
    94  				"github.com/golang/glog.FatalDepth",
    95  				"github.com/golang/glog.Fatalf",
    96  				"github.com/golang/glog.Fatalln":
    97  				// all of these call os.Exit after logging
    98  				fn.NoReturn = AlwaysExits
    99  			}
   100  		case "k8s.io/klog":
   101  			switch obj.(*types.Func).FullName() {
   102  			case "k8s.io/klog.Exit",
   103  				"k8s.io/klog.ExitDepth",
   104  				"k8s.io/klog.Exitf",
   105  				"k8s.io/klog.Exitln",
   106  				"k8s.io/klog.Fatal",
   107  				"k8s.io/klog.FatalDepth",
   108  				"k8s.io/klog.Fatalf",
   109  				"k8s.io/klog.Fatalln":
   110  				// all of these call os.Exit after logging
   111  				fn.NoReturn = AlwaysExits
   112  			}
   113  		case "k8s.io/klog/v2":
   114  			switch obj.(*types.Func).FullName() {
   115  			case "k8s.io/klog/v2.Exit",
   116  				"k8s.io/klog/v2.ExitDepth",
   117  				"k8s.io/klog/v2.Exitf",
   118  				"k8s.io/klog/v2.Exitln",
   119  				"k8s.io/klog/v2.Fatal",
   120  				"k8s.io/klog/v2.FatalDepth",
   121  				"k8s.io/klog/v2.Fatalf",
   122  				"k8s.io/klog/v2.Fatalln":
   123  				// all of these call os.Exit after logging
   124  				fn.NoReturn = AlwaysExits
   125  			}
   126  		}
   127  	}
   128  
   129  	isRecoverCall := func(instr Instruction) bool {
   130  		if instr, ok := instr.(*Call); ok {
   131  			if builtin, ok := instr.Call.Value.(*Builtin); ok {
   132  				if builtin.Name() == "recover" {
   133  					return true
   134  				}
   135  			}
   136  		}
   137  		return false
   138  	}
   139  
   140  	both := NewBlockSet(len(fn.Blocks))
   141  	exits := NewBlockSet(len(fn.Blocks))
   142  	unwinds := NewBlockSet(len(fn.Blocks))
   143  	recovers := false
   144  	for _, u := range fn.Blocks {
   145  		for _, instr := range u.Instrs {
   146  		instrSwitch:
   147  			switch instr := instr.(type) {
   148  			case *Defer:
   149  				if recovers {
   150  					// avoid doing extra work, we already know that this function calls recover
   151  					continue
   152  				}
   153  				call := instr.Call.StaticCallee()
   154  				if call == nil {
   155  					// not a static call, so we can't be sure the
   156  					// deferred call isn't calling recover
   157  					recovers = true
   158  					break
   159  				}
   160  				if call.Package() == fn.Package() {
   161  					b.buildFunction(call)
   162  				}
   163  				if len(call.Blocks) == 0 {
   164  					// external function, we don't know what's
   165  					// happening inside it
   166  					//
   167  					// TODO(dh): this includes functions from
   168  					// imported packages, due to how go/analysis
   169  					// works. We could introduce another fact,
   170  					// like we've done for exiting and unwinding.
   171  					recovers = true
   172  					break
   173  				}
   174  				for _, y := range call.Blocks {
   175  					for _, instr2 := range y.Instrs {
   176  						if isRecoverCall(instr2) {
   177  							recovers = true
   178  							break instrSwitch
   179  						}
   180  					}
   181  				}
   182  
   183  			case *Panic:
   184  				both.Add(u)
   185  				unwinds.Add(u)
   186  
   187  			case CallInstruction:
   188  				switch instr.(type) {
   189  				case *Defer, *Call:
   190  				default:
   191  					continue
   192  				}
   193  				if instr.Common().IsInvoke() {
   194  					// give up
   195  					return
   196  				}
   197  				var call *Function
   198  				switch instr.Common().Value.(type) {
   199  				case *Function, *MakeClosure:
   200  					call = instr.Common().StaticCallee()
   201  				case *Builtin:
   202  					// the only builtins that affect control flow are
   203  					// panic and recover, and we've already handled
   204  					// those
   205  					continue
   206  				default:
   207  					// dynamic dispatch
   208  					return
   209  				}
   210  				// buildFunction is idempotent. if we're part of a
   211  				// (mutually) recursive call chain, then buildFunction
   212  				// will immediately return, and fn.WillExit will be false.
   213  				if call.Package() == fn.Package() {
   214  					b.buildFunction(call)
   215  				}
   216  				switch call.NoReturn {
   217  				case AlwaysExits:
   218  					both.Add(u)
   219  					exits.Add(u)
   220  				case AlwaysUnwinds:
   221  					both.Add(u)
   222  					unwinds.Add(u)
   223  				case NeverReturns:
   224  					both.Add(u)
   225  				}
   226  			}
   227  		}
   228  	}
   229  
   230  	// depth-first search trying to find a path to the exit block that
   231  	// doesn't cross any of the blacklisted blocks
   232  	seen := NewBlockSet(len(fn.Blocks))
   233  	var findPath func(root *BasicBlock, bl *BlockSet) bool
   234  	findPath = func(root *BasicBlock, bl *BlockSet) bool {
   235  		if root == fn.Exit {
   236  			return true
   237  		}
   238  		if seen.Has(root) {
   239  			return false
   240  		}
   241  		if bl.Has(root) {
   242  			return false
   243  		}
   244  		seen.Add(root)
   245  		for _, succ := range root.Succs {
   246  			if findPath(succ, bl) {
   247  				return true
   248  			}
   249  		}
   250  		return false
   251  	}
   252  	findPathEntry := func(root *BasicBlock, bl *BlockSet) bool {
   253  		if bl.Num() == 0 {
   254  			return true
   255  		}
   256  		seen.Clear()
   257  		return findPath(root, bl)
   258  	}
   259  
   260  	if !findPathEntry(fn.Blocks[0], exits) {
   261  		fn.NoReturn = AlwaysExits
   262  	} else if !recovers {
   263  		// Only consider unwinding and "never returns" if we don't
   264  		// call recover. If we do call recover, then panics don't
   265  		// bubble up the stack.
   266  
   267  		// TODO(dh): the position of the defer matters. If we
   268  		// unconditionally terminate before we defer a recover, then
   269  		// the recover is ineffective.
   270  
   271  		if !findPathEntry(fn.Blocks[0], unwinds) {
   272  			fn.NoReturn = AlwaysUnwinds
   273  		} else if !findPathEntry(fn.Blocks[0], both) {
   274  			fn.NoReturn = NeverReturns
   275  		}
   276  	}
   277  }
   278  
   279  func (b *builder) addUnreachables(fn *Function) {
   280  	var unreachable *BasicBlock
   281  
   282  	for _, bb := range fn.Blocks {
   283  	instrLoop:
   284  		for i, instr := range bb.Instrs {
   285  			if instr, ok := instr.(*Call); ok {
   286  				var call *Function
   287  				switch v := instr.Common().Value.(type) {
   288  				case *Function:
   289  					call = v
   290  				case *MakeClosure:
   291  					call = v.Fn.(*Function)
   292  				}
   293  				if call == nil {
   294  					continue
   295  				}
   296  				if call.Package() == fn.Package() {
   297  					// make sure we have information on all functions in this package
   298  					b.buildFunction(call)
   299  				}
   300  				switch call.NoReturn {
   301  				case AlwaysExits:
   302  					// This call will cause the process to terminate.
   303  					// Remove remaining instructions in the block and
   304  					// replace any control flow with Unreachable.
   305  					for _, succ := range bb.Succs {
   306  						succ.removePred(bb)
   307  					}
   308  					bb.Succs = bb.Succs[:0]
   309  
   310  					bb.Instrs = bb.Instrs[:i+1]
   311  					bb.emit(new(Unreachable), instr.Source())
   312  					addEdge(bb, fn.Exit)
   313  					break instrLoop
   314  
   315  				case AlwaysUnwinds:
   316  					// This call will cause the goroutine to terminate
   317  					// and defers to run (i.e. a panic or
   318  					// runtime.Goexit). Remove remaining instructions
   319  					// in the block and replace any control flow with
   320  					// an unconditional jump to the exit block.
   321  					for _, succ := range bb.Succs {
   322  						succ.removePred(bb)
   323  					}
   324  					bb.Succs = bb.Succs[:0]
   325  
   326  					bb.Instrs = bb.Instrs[:i+1]
   327  					bb.emit(new(Jump), instr.Source())
   328  					addEdge(bb, fn.Exit)
   329  					break instrLoop
   330  
   331  				case NeverReturns:
   332  					// This call will either cause the goroutine to
   333  					// terminate, or the process to terminate. Remove
   334  					// remaining instructions in the block and replace
   335  					// any control flow with a conditional jump to
   336  					// either the exit block, or Unreachable.
   337  					for _, succ := range bb.Succs {
   338  						succ.removePred(bb)
   339  					}
   340  					bb.Succs = bb.Succs[:0]
   341  
   342  					bb.Instrs = bb.Instrs[:i+1]
   343  					var c Call
   344  					c.Call.Value = &Builtin{
   345  						name: "ir:noreturnWasPanic",
   346  						sig: types.NewSignatureType(nil, nil, nil,
   347  							types.NewTuple(),
   348  							types.NewTuple(anonVar(types.Typ[types.Bool])),
   349  							false,
   350  						),
   351  					}
   352  					c.setType(types.Typ[types.Bool])
   353  
   354  					if unreachable == nil {
   355  						unreachable = fn.newBasicBlock("unreachable")
   356  						unreachable.emit(&Unreachable{}, nil)
   357  						addEdge(unreachable, fn.Exit)
   358  					}
   359  
   360  					bb.emit(&c, instr.Source())
   361  					bb.emit(&If{Cond: &c}, instr.Source())
   362  					addEdge(bb, fn.Exit)
   363  					addEdge(bb, unreachable)
   364  					break instrLoop
   365  				}
   366  			}
   367  		}
   368  	}
   369  }