github.com/google/capslock@v0.2.3-0.20240517042941-dac19fc347c0/testing/analyzepackages_test.go (about) 1 // Copyright 2023 Google LLC 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file or at 5 // https://developers.google.com/open-source/licenses/bsd 6 7 package analyzepackages_test 8 9 import ( 10 "bytes" 11 "fmt" 12 "os" 13 "os/exec" 14 "regexp" 15 "strings" 16 "testing" 17 18 cpb "github.com/google/capslock/proto" 19 "google.golang.org/protobuf/encoding/protojson" 20 ) 21 22 type expectedPath struct { 23 Fn []string 24 Cap string 25 } 26 27 // matches returns true if for one of the CapabilityInfo objects in the input: 28 // 29 // - The object's path contains functions which match each of the elements of 30 // path.Fn. The matching functions do not have to be consecutive, but they 31 // do need to be in order. 32 // 33 // - path.Cap is either empty, or matches the capability in the CapabilityInfo. 34 func (path expectedPath) matches(cil *cpb.CapabilityInfoList) (bool, error) { 35 if len(path.Fn) == 0 { 36 return false, fmt.Errorf("empty path") 37 } 38 for _, ci := range cil.GetCapabilityInfo() { 39 if len(path.Cap) != 0 && ci.GetCapability().String() != path.Cap { 40 continue 41 } 42 i := 0 43 for _, f := range ci.GetPath() { 44 if matches, err := regexp.MatchString(path.Fn[i], f.GetName()); matches { 45 i++ 46 if i == len(path.Fn) { 47 return true, nil 48 } 49 } else if err != nil { 50 return false, fmt.Errorf("parsing expression %q: %v", path.Fn[i], err) 51 } 52 } 53 } 54 return false, nil 55 } 56 57 func TestExpectedOutput(t *testing.T) { 58 // Run analyzepackages, get its stdout in output. 59 cmd := exec.Command( 60 "go", "run", "../cmd/capslock", "-packages=../testpkgs/...", "-output=json") 61 var output bytes.Buffer 62 cmd.Stdout = &output 63 cmd.Stderr = os.Stderr 64 if err := cmd.Run(); err != nil { 65 t.Errorf("exec.Command.Run: %v. stdout:", err) 66 if _, err := os.Stderr.Write(output.Bytes()); err != nil { 67 t.Errorf("couldn't write analyzepackages' output to stderr: %v", err) 68 } 69 t.Fatalf("failed to run analyzepackages.") 70 } 71 72 cil := new(cpb.CapabilityInfoList) 73 err := protojson.Unmarshal(output.Bytes(), cil) 74 if err != nil { 75 t.Fatalf("Couldn't parse analyzer output: %v", err) 76 } 77 78 expectedPaths := []expectedPath{ 79 {Fn: []string{"buildtags.Foo", "net.LookupIP"}}, 80 {Fn: []string{"callnet.Foo", "net.LookupIP"}}, 81 {Fn: []string{"callos.Foo", "os.Getpid"}}, 82 {Fn: []string{"callos.Bar", "os/exec"}}, 83 {Fn: []string{"callos.Baz", "os/user.Current"}}, 84 {Fn: []string{"callruntime.Interesting", "runtime.CPUProfile"}}, 85 {Fn: []string{"importname.CallTheWrongSort", "os.ReadFile"}}, 86 {Fn: []string{`indirectcalls.AccessMethodViaTypeAssertion`, `\(\*os.File\).Chown`}}, 87 {Fn: []string{"indirectcalls.CallOs", "os.Getuid"}}, 88 {Fn: []string{"indirectcalls.CallOsViaFuncVariable", "os.Getuid"}}, 89 {Fn: []string{"indirectcalls.CallOsViaInterfaceMethod", "os.Getuid"}}, 90 {Fn: []string{"indirectcalls.CallOsViaStructField", "os.Getuid"}}, 91 {Fn: []string{`indirectcalls.myStruct\).foo`, `os.Getuid`}}, 92 {Fn: []string{"initfn.init"}, Cap: "CAPABILITY_REFLECT"}, 93 {Fn: []string{"initfn.init"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 94 {Fn: []string{"initfn.init", "net.LookupIP"}}, 95 {Fn: []string{"initfn.init", "os.Getpid"}}, 96 {Fn: []string{"initfn.init", "runtime/debug.SetMaxThreads"}}, 97 {Fn: []string{"transitive.Asm", "useasm.Foo", "useasm.bar"}}, 98 {Fn: []string{`transitive.CallGenericFunction`, `usegenerics.Foo\[.*/transitive.a\]`, `\(.*/transitive.a\).Baz`, `net.Dial`}}, 99 {Fn: []string{`transitive.CallGenericFunction`, `usegenerics.Foo\[.*/transitive.a\]`, `os.Rename`}}, 100 {Fn: []string{`transitive.CallGenericFunctionTransitively`, `usegenerics.Bar`, `usegenerics.Foo\[.*/usegenerics.a\]`, `\(.*/usegenerics.a\).Baz`, `net.Interfaces`}}, 101 {Fn: []string{`transitive.CallGenericFunctionTransitively`, `usegenerics.Bar`, `usegenerics.Foo\[.*/usegenerics.a\]`, `os.Rename`}}, 102 {Fn: []string{"transitive.CallViaStdlib", "callnet.Foo", "net.LookupIP"}}, 103 {Fn: []string{"transitive.Cgo", "usecgo._cgo_runtime_cgocall"}}, 104 {Fn: []string{"transitive.Indirect", "os.Getuid"}}, 105 {Fn: []string{"transitive.InterestingOnceDo"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 106 {Fn: []string{"transitive.OnceInStruct"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 107 {Fn: []string{"transitive.ComplicatedExpressionWithOnce"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 108 {Fn: []string{"transitive.InterestingSort"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 109 {Fn: []string{"transitive.InterestingSortViaFunction", "sort.Sort"}, Cap: "CAPABILITY_UNANALYZED"}, 110 {Fn: []string{"transitive.InterestingSortSlice"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 111 {Fn: []string{"transitive.InterestingSortSliceNested"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 112 {Fn: []string{"transitive.InterestingSortSliceStable"}, Cap: "CAPABILITY_READ_SYSTEM_STATE"}, 113 {Fn: []string{"transitive.InterestingSyncPool"}, Cap: "CAPABILITY_UNANALYZED"}, 114 {Fn: []string{"transitive.Linkname", "uselinkname.Foo", "uselinkname.runtime_fastrand64"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 115 {Fn: []string{"transitive.MultipleCapabilities", "usecgo._cgo_runtime_cgocall"}}, 116 {Fn: []string{"transitive.MultipleCapabilities", "os.Getpid"}}, 117 {Fn: []string{"transitive.Net", "net.LookupIP"}}, 118 {Fn: []string{"transitive.Os", "os.Getpid"}}, 119 {Fn: []string{"transitive.UninterestingSyncPool"}, Cap: "CAPABILITY_UNANALYZED"}, 120 {Fn: []string{"transitive.Unsafe"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 121 {Fn: []string{`transitive.UseBigIntRand`, `big.Int..Rand`, `transitive.src..Int63`, `net.LookupIP`}}, 122 {Fn: []string{"transitive.init", "initfn.init"}, Cap: "CAPABILITY_REFLECT"}, 123 {Fn: []string{"transitive.init", "initfn.init"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 124 {Fn: []string{"transitive.init", "initfn.init", "net.LookupIP"}}, 125 {Fn: []string{"transitive.init", "initfn.init", "os.Getpid"}}, 126 {Fn: []string{"transitive.init", "initfn.init", "runtime/debug.SetMaxThreads"}}, 127 {Fn: []string{"useasm.Foo", "useasm.bar"}}, 128 {Fn: []string{"useasm.bar"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 129 {Fn: []string{"usecgo.CallCBytes", ""}}, 130 {Fn: []string{"usecgo.CallCString", ""}}, 131 {Fn: []string{"usecgo.CallGoBytes", ""}}, 132 {Fn: []string{"usecgo.CallGoString", ""}}, 133 {Fn: []string{"usecgo.CallGoStringN", ""}}, 134 {Fn: []string{"usecgo.Foo", "usecgo._cgo_runtime_cgocall"}}, 135 {Fn: []string{"usecgo._Cfunc_acfunction", "usecgo._cgo_runtime_cgocall"}}, 136 {Fn: []string{`usegenerics.Bar`, `usegenerics.Foo\[.*/usegenerics.a\]`, `\(.*/usegenerics.a\).Baz`, `net.Interfaces`}}, 137 {Fn: []string{`usegenerics.Bar`, `usegenerics.Foo\[.*/usegenerics.a\]`, `os.Rename`}}, 138 {Fn: []string{`usegenerics.a\).Baz`, `net.Interfaces`}}, 139 {Fn: []string{`usegenerics.CallNestedFunction`, `usegenerics.NestedFunction\[.*/usegenerics.a\]\$1`, `\(.*/usegenerics.a\).Baz`, `net.Interfaces`}}, 140 {Fn: []string{"uselinkname.CallExplicitlyCategorizedFunction", "syscall.Getpagesize"}, Cap: "CAPABILITY_SYSTEM_CALLS"}, 141 {Fn: []string{"uselinkname.Foo", "uselinkname.runtime_fastrand64"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 142 {Fn: []string{"uselinkname.runtime_fastrand64"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 143 {Fn: []string{`usereflect.CopyValueConcurrently\$1`}, Cap: `CAPABILITY_REFLECT`}, 144 {Fn: []string{`usereflect.CopyValueConcurrently\$2`}, Cap: `CAPABILITY_REFLECT`}, 145 {Fn: []string{`usereflect.CopyValueConcurrently`, `usereflect.CopyValueConcurrently\$[12]`}}, 146 {Fn: []string{"usereflect.CopyValueContainingStructViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 147 {Fn: []string{"usereflect.CopyValueEquivalentViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 148 {Fn: []string{"usereflect.CopyValueGlobal"}, Cap: "CAPABILITY_REFLECT"}, 149 {Fn: []string{"usereflect.CopyValueInArrayViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 150 {Fn: []string{"usereflect.CopyValueInMultipleAssignmentViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 151 {Fn: []string{"usereflect.CopyValueInStructFieldViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 152 {Fn: []string{"usereflect.CopyValueViaPointer"}, Cap: "CAPABILITY_REFLECT"}, 153 {Fn: []string{`usereflect.RangeValueTwo\$1`}, Cap: `CAPABILITY_REFLECT`}, 154 {Fn: []string{`usereflect.RangeValueTwo\$2`}, Cap: `CAPABILITY_REFLECT`}, 155 {Fn: []string{`usereflect.RangeValueTwo`, `usereflect.RangeValueTwo\$[12]`}}, 156 {Fn: []string{"useunsafe.Bar"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 157 {Fn: []string{"useunsafe.Baz"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 158 {Fn: []string{`useunsafe.CallNestedFunctions`, `useunsafe.NestedFunctions\$1\$1\$1`}}, 159 {Fn: []string{"useunsafe.Foo"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 160 {Fn: []string{`useunsafe.Indirect`, `useunsafe.ReturnFunction\$1`}}, 161 {Fn: []string{`useunsafe.Indirect2`, `useunsafe.init\$1`}}, 162 {Fn: []string{`useunsafe.NestedFunctions\$1\$1\$1`}, Cap: `CAPABILITY_UNSAFE_POINTER`}, 163 {Fn: []string{`useunsafe.ReturnFunction\$1`}, Cap: `CAPABILITY_UNSAFE_POINTER`}, 164 {Fn: []string{`useunsafe.T\).M`}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 165 {Fn: []string{`useunsafe.init$`}, Cap: `CAPABILITY_UNSAFE_POINTER`}, 166 {Fn: []string{`useunsafe.init\$1`}, Cap: `CAPABILITY_UNSAFE_POINTER`}, 167 } 168 for _, path := range expectedPaths { 169 if matches, err := path.matches(cil); err != nil { 170 t.Fatalf("TestExpectedOutput: internal error: %v", err) 171 } else if !matches { 172 t.Errorf("TestExpectedOutput: did not find expected path %v", path) 173 } 174 } 175 unexpectedPaths := []expectedPath{ 176 {Fn: []string{"indirectcalls.ShouldHaveNoCapabilities"}}, 177 {Fn: []string{"callos.init"}}, 178 {Fn: []string{"callruntime.Uninteresting"}}, 179 {Fn: []string{"transitive.AllowedAsmInStdlib"}}, 180 {Fn: []string{"usegenerics.Foo"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 181 {Fn: []string{"uselinkname.CallExplicitlyCategorizedFunction", "syscall.Getpagesize"}, Cap: "CAPABILITY_ARBITRARY_EXECUTION"}, 182 {Fn: []string{"useunsafe.Ok"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 183 {Fn: []string{"useunsafe.ReturnFunction$"}, Cap: "CAPABILITY_UNSAFE_POINTER"}, 184 {Fn: []string{"usegenerics.AtomicPointer"}}, 185 186 // Currently we don't include functions called by these functions. 187 {Fn: []string{"^sort.Sort", ".*"}}, // need ^ to avoid matching notsort.go 188 {Fn: []string{"sort.Slice", ".*"}}, 189 {Fn: []string{`\(\*sync.Once\).Do`, ".*"}}, 190 {Fn: []string{`\(\*sync.Pool\).Get`, ".*"}}, 191 192 // We do not expect the following call paths, as they are avoided by the 193 // syntax-tree-rewriting code. 194 {Fn: []string{`transitive.InterestingOnceDo`, `\(\*sync.Once\).Do`}}, 195 {Fn: []string{`transitive.OnceInStruct`, `\(\*sync.Once\).Do`}}, 196 {Fn: []string{`transitive.ComplicatedExpressionWithOnce`, `\(\*sync.Once\).Do`}}, 197 {Fn: []string{"transitive.UninterestingOnceDo", ".*"}}, 198 {Fn: []string{"transitive.UninterestingSort", ".*"}}, 199 {Fn: []string{"transitive.UninterestingSortSlice", ".*"}}, 200 {Fn: []string{"transitive.UninterestingSortSliceNested", ".*"}}, 201 {Fn: []string{"transitive.UninterestingSortSliceStable", ".*"}}, 202 203 // These functions copy reflect.Value objects, but the destinations are 204 // only local variables which do not escape, so we do not need to warn 205 // about them. 206 {Fn: []string{"usereflect.CopyValue$"}}, 207 {Fn: []string{"usereflect.CopyValueContainingStruct$"}}, 208 {Fn: []string{"usereflect.CopyValueEquivalent$"}}, 209 {Fn: []string{"usereflect.CopyValueInArray$"}}, 210 {Fn: []string{"usereflect.CopyValueInCommaOk"}}, 211 {Fn: []string{"usereflect.CopyValueInMultipleAssignment$"}}, 212 {Fn: []string{"usereflect.CopyValueInStructField$"}}, 213 {Fn: []string{"usereflect.RangeValue$"}}, 214 215 // MaybeChmod type-asserts an io.Reader parameter to an interface whose 216 // method set contains Chmod, so that (*os.File).Chmod can be called if 217 // the user passes an argument with dynamic type *os.File. No code in 218 // our testdata does this, so we do not warn about this code having an 219 // interesting capability, but perhaps it would be good to do so. 220 {Fn: []string{`indirectcalls.MaybeChmod`, `\(*os.File\).Chmod`}}, 221 } 222 for _, path := range unexpectedPaths { 223 if matches, err := path.matches(cil); err != nil { 224 t.Fatalf("TestExpectedOutput: internal error: %v", err) 225 } else if matches { 226 t.Errorf("TestExpectedOutput: expected not to see match for %v", path) 227 } 228 } 229 if t.Failed() { 230 t.Log(output.String()) 231 } 232 } 233 234 func TestGraph(t *testing.T) { 235 // Run analyzepackages, get its stdout in output. 236 cmd := exec.Command( 237 "go", "run", "../cmd/capslock", "-packages=../testpkgs/useunsafe", "-output=graph") 238 var output bytes.Buffer 239 cmd.Stdout = &output 240 cmd.Stderr = os.Stderr 241 if err := cmd.Run(); err != nil { 242 t.Errorf("exec.Command.Run: %v. stdout:", err) 243 if _, err := os.Stderr.Write(output.Bytes()); err != nil { 244 t.Errorf("couldn't write analyzepackages' output to stderr: %v", err) 245 } 246 t.Fatalf("failed to run analyzepackages.") 247 } 248 // map from expected strings to the number of times each is seen in the output. 249 m := map[string]int{ 250 `digraph {`: 0, 251 `"github.com/google/capslock/testpkgs/useunsafe.Bar" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 252 `"github.com/google/capslock/testpkgs/useunsafe.Baz" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 253 `"github.com/google/capslock/testpkgs/useunsafe.CallNestedFunctions" -> "github.com/google/capslock/testpkgs/useunsafe.NestedFunctions$1$1$1"`: 0, 254 `"github.com/google/capslock/testpkgs/useunsafe.Foo" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 255 `"github.com/google/capslock/testpkgs/useunsafe.Indirect2" -> "github.com/google/capslock/testpkgs/useunsafe.init$1"`: 0, 256 `"github.com/google/capslock/testpkgs/useunsafe.Indirect" -> "github.com/google/capslock/testpkgs/useunsafe.ReturnFunction$1"`: 0, 257 `"github.com/google/capslock/testpkgs/useunsafe.init$1" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 258 `"github.com/google/capslock/testpkgs/useunsafe.init" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 259 `"github.com/google/capslock/testpkgs/useunsafe.NestedFunctions$1$1$1" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 260 `"github.com/google/capslock/testpkgs/useunsafe.ReturnFunction$1" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 261 `"(github.com/google/capslock/testpkgs/useunsafe.T).M" -> "CAPABILITY_UNSAFE_POINTER"`: 0, 262 `}`: 0, 263 } 264 for _, s := range strings.Split(output.String(), "\n") { 265 s = strings.TrimSpace(s) 266 if s == "" { 267 continue 268 } 269 if _, ok := m[s]; !ok { 270 t.Errorf("TestGraph: saw unexpected output line %q", s) 271 continue 272 } 273 m[s]++ 274 } 275 for s, c := range m { 276 if c != 1 { 277 t.Errorf("TestGraph: got output line %q %d times, want 1", s, c) 278 } 279 } 280 if t.Failed() { 281 t.Log(output.String()) 282 } 283 }