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 }