github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa5011/sa5011.go (about) 1 package sa5011 2 3 import ( 4 "go/types" 5 6 "github.com/amarpal/go-tools/analysis/lint" 7 "github.com/amarpal/go-tools/analysis/report" 8 "github.com/amarpal/go-tools/go/ir" 9 "github.com/amarpal/go-tools/go/types/typeutil" 10 "github.com/amarpal/go-tools/internal/passes/buildir" 11 12 "golang.org/x/tools/go/analysis" 13 ) 14 15 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 16 Analyzer: &analysis.Analyzer{ 17 Name: "SA5011", 18 Run: run, 19 Requires: []*analysis.Analyzer{buildir.Analyzer}, 20 }, 21 Doc: &lint.Documentation{ 22 Title: `Possible nil pointer dereference`, 23 24 Text: `A pointer is being dereferenced unconditionally, while 25 also being checked against nil in another place. This suggests that 26 the pointer may be nil and dereferencing it may panic. This is 27 commonly a result of improperly ordered code or missing return 28 statements. Consider the following examples: 29 30 func fn(x *int) { 31 fmt.Println(*x) 32 33 // This nil check is equally important for the previous dereference 34 if x != nil { 35 foo(*x) 36 } 37 } 38 39 func TestFoo(t *testing.T) { 40 x := compute() 41 if x == nil { 42 t.Errorf("nil pointer received") 43 } 44 45 // t.Errorf does not abort the test, so if x is nil, the next line will panic. 46 foo(*x) 47 } 48 49 Staticcheck tries to deduce which functions abort control flow. 50 For example, it is aware that a function will not continue 51 execution after a call to \'panic\' or \'log.Fatal\'. However, sometimes 52 this detection fails, in particular in the presence of 53 conditionals. Consider the following example: 54 55 func Log(msg string, level int) { 56 fmt.Println(msg) 57 if level == levelFatal { 58 os.Exit(1) 59 } 60 } 61 62 func Fatal(msg string) { 63 Log(msg, levelFatal) 64 } 65 66 func fn(x *int) { 67 if x == nil { 68 Fatal("unexpected nil pointer") 69 } 70 fmt.Println(*x) 71 } 72 73 Staticcheck will flag the dereference of \'x\', even though it is perfectly 74 safe. Staticcheck is not able to deduce that a call to 75 Fatal will exit the program. For the time being, the easiest 76 workaround is to modify the definition of Fatal like so: 77 78 func Fatal(msg string) { 79 Log(msg, levelFatal) 80 panic("unreachable") 81 } 82 83 We also hard-code functions from common logging packages such as 84 logrus. Please file an issue if we're missing support for a 85 popular package.`, 86 Since: "2020.1", 87 Severity: lint.SeverityWarning, 88 MergeIf: lint.MergeIfAny, 89 }, 90 }) 91 92 var Analyzer = SCAnalyzer.Analyzer 93 94 func run(pass *analysis.Pass) (interface{}, error) { 95 // This is an extremely trivial check that doesn't try to reason 96 // about control flow. That is, phis and sigmas do not propagate 97 // any information. As such, we can flag this: 98 // 99 // _ = *x 100 // if x == nil { return } 101 // 102 // but we cannot flag this: 103 // 104 // if x == nil { println(x) } 105 // _ = *x 106 // 107 // but we can flag this, because the if's body doesn't use x: 108 // 109 // if x == nil { println("this is bad") } 110 // _ = *x 111 // 112 // nor many other variations of conditional uses of or assignments to x. 113 // 114 // However, even this trivial implementation finds plenty of 115 // real-world bugs, such as dereference before nil pointer check, 116 // or using t.Error instead of t.Fatal when encountering nil 117 // pointers. 118 // 119 // On the flip side, our naive implementation avoids false positives in branches, such as 120 // 121 // if x != nil { _ = *x } 122 // 123 // due to the same lack of propagating information through sigma 124 // nodes. x inside the branch will be independent of the x in the 125 // nil pointer check. 126 // 127 // 128 // We could implement a more powerful check, but then we'd be 129 // getting false positives instead of false negatives because 130 // we're incapable of deducing relationships between variables. 131 // For example, a function might return a pointer and an error, 132 // and the error being nil guarantees that the pointer is not nil. 133 // Depending on the surrounding code, the pointer may still end up 134 // being checked against nil in one place, and guarded by a check 135 // on the error in another, which would lead to us marking some 136 // loads as unsafe. 137 // 138 // Unfortunately, simply hard-coding the relationship between 139 // return values wouldn't eliminate all false positives, either. 140 // Many other more subtle relationships exist. An abridged example 141 // from real code: 142 // 143 // if a == nil && b == nil { return } 144 // c := fn(a) 145 // if c != "" { _ = *a } 146 // 147 // where `fn` is guaranteed to return a non-empty string if a 148 // isn't nil. 149 // 150 // We choose to err on the side of false negatives. 151 152 isNilConst := func(v ir.Value) bool { 153 if typeutil.IsPointerLike(v.Type()) { 154 if k, ok := v.(*ir.Const); ok { 155 return k.IsNil() 156 } 157 } 158 return false 159 } 160 161 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 162 maybeNil := map[ir.Value]ir.Instruction{} 163 for _, b := range fn.Blocks { 164 for _, instr := range b.Instrs { 165 // Originally we looked at all ir.BinOp, but that would lead to calls like 'assert(x != nil)' causing false positives. 166 // Restrict ourselves to actual if statements, as these are more likely to affect control flow in a way we can observe. 167 if instr, ok := instr.(*ir.If); ok { 168 if cond, ok := instr.Cond.(*ir.BinOp); ok { 169 if isNilConst(cond.X) { 170 maybeNil[cond.Y] = cond 171 } 172 if isNilConst(cond.Y) { 173 maybeNil[cond.X] = cond 174 } 175 } 176 } 177 } 178 } 179 180 for _, b := range fn.Blocks { 181 for _, instr := range b.Instrs { 182 var ptr ir.Value 183 switch instr := instr.(type) { 184 case *ir.Load: 185 ptr = instr.X 186 case *ir.Store: 187 ptr = instr.Addr 188 case *ir.IndexAddr: 189 ptr = instr.X 190 if typeutil.All(ptr.Type(), func(term *types.Term) bool { 191 if _, ok := term.Type().Underlying().(*types.Slice); ok { 192 return true 193 } 194 return false 195 }) { 196 // indexing a nil slice does not cause a nil pointer panic 197 // 198 // Note: This also works around the bad lowering of range loops over slices 199 // (https://github.com/dominikh/go-tools/issues/1053) 200 continue 201 } 202 case *ir.FieldAddr: 203 ptr = instr.X 204 } 205 if ptr != nil { 206 switch ptr.(type) { 207 case *ir.Alloc, *ir.FieldAddr, *ir.IndexAddr: 208 // these cannot be nil 209 continue 210 } 211 if r, ok := maybeNil[ptr]; ok { 212 report.Report(pass, instr, "possible nil pointer dereference", 213 report.Related(r, "this check suggests that the pointer can be nil")) 214 } 215 } 216 } 217 } 218 } 219 220 return nil, nil 221 }