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  }