Skip to content

Commit bb28937

Browse files
committed
vulncheck: add witness search logic for call stacks
The CL adds API for traversal of call graph in search of call stacks that can serve as witnesses for uses of vulnerable symbols. Change-Id: Ib45c5c6aad347e1bcecb39ca4a62f589b4766677 Reviewed-on: https://go-review.googlesource.com/c/exp/+/381777 Reviewed-by: Jonathan Amsterdam <[email protected]> Trust: Zvonimir Pavlinovic <[email protected]> Run-TryBot: Zvonimir Pavlinovic <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent a36d682 commit bb28937

File tree

2 files changed

+246
-10
lines changed

2 files changed

+246
-10
lines changed

vulncheck/witness.go

Lines changed: 189 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
15
package vulncheck
26

37
import (
48
"container/list"
9+
"fmt"
10+
"sort"
11+
"strings"
512
"sync"
613
)
714

@@ -10,18 +17,18 @@ import (
1017
// known vulnerabilities.
1118
type ImportChain []*PkgNode
1219

13-
// ImportChains performs a BFS search of res.RequireGraph for imports of vulnerable
14-
// packages. Search is performed for each vulnerable package in res.Vulns. The search
15-
// starts at a vulnerable package and goes up until reaching an entry package in
16-
// res.ImportGraph.Entries, hence producing an import chain. During the search, a
17-
// package is visited only once to avoid analyzing every possible import chain.
18-
// Hence, not all possible vulnerable import chains are reported.
20+
// ImportChains lists import chains for each vulnerability in res. The
21+
// reported chains are ordered by how seemingly easy is to understand
22+
// them. Shorter import chains appear earlier in the returned slices.
1923
//
20-
// Note that the resulting map produces an import chain for each Vuln. Thus, a Vuln
21-
// with the same PkgPath will have the same list of identified import chains.
24+
// ImportChains does not list all import chains for a vulnerability.
25+
// It performs a BFS search of res.RequireGraph starting at a vulnerable
26+
// package and going up until reaching an entry package in res.ImportGraph.Entries.
27+
// During this search, a package is visited only once to avoid analyzing
28+
// every possible import chain.
2229
//
23-
// The reported import chains are ordered by how seemingly easy is to understand
24-
// them. Shorter import chains appear earlier in the returned slices.
30+
// Note that the resulting map produces an import chain for each Vuln. Vulns
31+
// with the same PkgPath will have the same list of identified import chains.
2532
func ImportChains(res *Result) map[*Vuln][]ImportChain {
2633
// Group vulns per package.
2734
vPerPkg := make(map[int][]*Vuln)
@@ -122,3 +129,175 @@ type StackEntry struct {
122129
// nil when the frame represents an entry point of the stack.
123130
Call *CallSite
124131
}
132+
133+
// CallStacks lists call stacks for each vulnerability in res. The listed call
134+
// stacks are ordered by how seemingly easy is to understand them. In general,
135+
// shorter call stacks with less dynamic call sites appear earlier in the returned
136+
// call stack slices.
137+
//
138+
// CallStacks does not report every possible call stack for a vulnerable symbol.
139+
// It performs a BFS search of res.CallGraph starting at the symbol and going up
140+
// until reaching an entry function or method in res.CallGraph.Entries. During
141+
// this search, each function is visited at most once to avoid potential
142+
// exponential explosion, thus skipping some call stacks.
143+
func CallStacks(res *Result) map[*Vuln][]CallStack {
144+
var (
145+
wg sync.WaitGroup
146+
mu sync.Mutex
147+
)
148+
stacksPerVuln := make(map[*Vuln][]CallStack)
149+
for _, vuln := range res.Vulns {
150+
vuln := vuln
151+
wg.Add(1)
152+
go func() {
153+
cs := callStacks(vuln.CallSink, res)
154+
// sort call stacks by the estimated value to the user
155+
sort.SliceStable(cs, func(i int, j int) bool { return stackLess(cs[i], cs[j]) })
156+
mu.Lock()
157+
stacksPerVuln[vuln] = cs
158+
mu.Unlock()
159+
wg.Done()
160+
}()
161+
}
162+
163+
wg.Wait()
164+
return stacksPerVuln
165+
}
166+
167+
// callStacks finds representative call stacks
168+
// for vulnerable symbol identified with vulnSinkID.
169+
func callStacks(vulnSinkID int, res *Result) []CallStack {
170+
if vulnSinkID == 0 {
171+
return nil
172+
}
173+
174+
entries := make(map[int]bool)
175+
for _, e := range res.Calls.Entries {
176+
entries[e] = true
177+
}
178+
179+
var stacks []CallStack
180+
seen := make(map[int]bool)
181+
182+
queue := list.New()
183+
queue.PushBack(&callChain{f: res.Calls.Functions[vulnSinkID]})
184+
185+
for queue.Len() > 0 {
186+
front := queue.Front()
187+
c := front.Value.(*callChain)
188+
queue.Remove(front)
189+
190+
f := c.f
191+
if seen[f.ID] {
192+
continue
193+
}
194+
seen[f.ID] = true
195+
196+
for _, cs := range f.CallSites {
197+
callee := res.Calls.Functions[cs.Parent]
198+
nStack := &callChain{f: callee, call: cs, child: c}
199+
if entries[callee.ID] {
200+
stacks = append(stacks, nStack.CallStack())
201+
}
202+
queue.PushBack(nStack)
203+
}
204+
}
205+
return stacks
206+
}
207+
208+
// callChain models a chain of function calls.
209+
type callChain struct {
210+
call *CallSite // nil for entry points
211+
f *FuncNode
212+
child *callChain
213+
}
214+
215+
// CallStack converts callChain to CallStack type.
216+
func (c *callChain) CallStack() CallStack {
217+
if c == nil {
218+
return nil
219+
}
220+
return append(CallStack{StackEntry{Function: c.f, Call: c.call}}, c.child.CallStack()...)
221+
}
222+
223+
// weight computes an approximate measure of how easy is to understand the call
224+
// stack when presented to the client as a witness. The smaller the value, the more
225+
// understandable the stack is. Currently defined as the number of unresolved
226+
// call sites in the stack.
227+
func weight(stack CallStack) int {
228+
w := 0
229+
for _, e := range stack {
230+
if e.Call != nil && !e.Call.Resolved {
231+
w += 1
232+
}
233+
}
234+
return w
235+
}
236+
237+
func isStdPackage(pkg string) bool {
238+
if pkg == "" {
239+
return false
240+
}
241+
// std packages do not have a "." in their path. For instance, see
242+
// Contains in pkgsite/+/refs/heads/master/internal/stdlbib/stdlib.go.
243+
if i := strings.IndexByte(pkg, '/'); i != -1 {
244+
pkg = pkg[:i]
245+
}
246+
return !strings.Contains(pkg, ".")
247+
}
248+
249+
// confidence computes an approximate measure of whether the stack
250+
// is realizeable in practice. Currently, it equals the number of call
251+
// sites in stack that go through standard libraries. Such call stacks
252+
// have been experimentally shown to often result in false positives.
253+
func confidence(stack CallStack) int {
254+
c := 0
255+
for _, e := range stack {
256+
if isStdPackage(e.Function.PkgPath) {
257+
c += 1
258+
}
259+
}
260+
return c
261+
}
262+
263+
// stackLess compares two call stacks in terms of their estimated
264+
// value to the user. Shorter stacks generally come earlier in the ordering.
265+
//
266+
// Two stacks are lexicographically ordered by:
267+
// 1) their estimated level of confidence in being a real call stack,
268+
// 2) their length, and 3) the number of dynamic call sites in the stack.
269+
func stackLess(s1, s2 CallStack) bool {
270+
if c1, c2 := confidence(s1), confidence(s2); c1 != c2 {
271+
return c1 < c2
272+
}
273+
274+
if len(s1) != len(s2) {
275+
return len(s1) < len(s2)
276+
}
277+
278+
if w1, w2 := weight(s1), weight(s2); w1 != w2 {
279+
return w1 < w2
280+
}
281+
// At this point we just need to make sure the ordering is deterministic.
282+
// TODO(zpavlinovic): is there a more meaningful additional ordering?
283+
return stackStrLess(s1, s2)
284+
}
285+
286+
// stackStrLess compares string representation of stacks.
287+
func stackStrLess(s1, s2 CallStack) bool {
288+
// Creates a unique string representation of a call stack
289+
// for comparison purposes only.
290+
stackStr := func(stack CallStack) string {
291+
var stackStr []string
292+
for _, cs := range stack {
293+
s := cs.Function.String()
294+
if cs.Call != nil && cs.Call.Pos != nil {
295+
p := cs.Call.Pos
296+
s = fmt.Sprintf("%s[%s:%d:%d:%d]", s, p.Filename, p.Line, p.Column, p.Offset)
297+
}
298+
stackStr = append(stackStr, s)
299+
}
300+
return strings.Join(stackStr, "->")
301+
}
302+
return strings.Compare(stackStr(s1), stackStr(s2)) <= 0
303+
}

vulncheck/witness_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
15
package vulncheck
26

37
import (
@@ -24,6 +28,24 @@ func chainsToString(chains map[*Vuln][]ImportChain) map[string][]string {
2428
return m
2529
}
2630

31+
// stacksToString converts map *Vuln:stacks to Vuln.Symbol:["f1->...->fN", ...]
32+
// string representation.
33+
func stacksToString(stacks map[*Vuln][]CallStack) map[string][]string {
34+
m := make(map[string][]string)
35+
for v, sts := range stacks {
36+
var stsStr []string
37+
for _, st := range sts {
38+
var stStr []string
39+
for _, call := range st {
40+
stStr = append(stStr, call.Function.Name)
41+
}
42+
stsStr = append(stsStr, strings.Join(stStr, "->"))
43+
}
44+
m[v.Symbol] = stsStr
45+
}
46+
return m
47+
}
48+
2749
func TestImportChains(t *testing.T) {
2850
// Package import structure for the test program
2951
// entry1 entry2
@@ -61,3 +83,38 @@ func TestImportChains(t *testing.T) {
6183
t.Errorf("want %v; got %v", want, got)
6284
}
6385
}
86+
87+
func TestCallStacks(t *testing.T) {
88+
// Call graph structure for the test program
89+
// entry1 entry2
90+
// | |
91+
// interm1(std) |
92+
// | \ /
93+
// | interm2(interface)
94+
// | / |
95+
// vuln1 vuln2
96+
e1 := &FuncNode{ID: 1, Name: "entry1"}
97+
e2 := &FuncNode{ID: 2, Name: "entry2"}
98+
i1 := &FuncNode{ID: 3, Name: "interm1", PkgPath: "net/http", CallSites: []*CallSite{&CallSite{Parent: 1, Resolved: true}}}
99+
i2 := &FuncNode{ID: 4, Name: "interm2", CallSites: []*CallSite{&CallSite{Parent: 2, Resolved: true}, &CallSite{Parent: 3, Resolved: true}}}
100+
v1 := &FuncNode{ID: 5, Name: "vuln1", CallSites: []*CallSite{&CallSite{Parent: 3, Resolved: true}, &CallSite{Parent: 4, Resolved: false}}}
101+
v2 := &FuncNode{ID: 6, Name: "vuln2", CallSites: []*CallSite{&CallSite{Parent: 4, Resolved: false}}}
102+
103+
cg := &CallGraph{
104+
Functions: map[int]*FuncNode{1: e1, 2: e2, 3: i1, 4: i2, 5: v1, 6: v2},
105+
Entries: []int{1, 2},
106+
}
107+
vuln1 := &Vuln{CallSink: 5, Symbol: "vuln1"}
108+
vuln2 := &Vuln{CallSink: 6, Symbol: "vuln2"}
109+
res := &Result{Calls: cg, Vulns: []*Vuln{vuln1, vuln2}}
110+
111+
want := map[string][]string{
112+
"vuln1": {"entry2->interm2->vuln1", "entry1->interm1->vuln1"},
113+
"vuln2": {"entry2->interm2->vuln2", "entry1->interm1->interm2->vuln2"},
114+
}
115+
116+
stacks := CallStacks(res)
117+
if got := stacksToString(stacks); !reflect.DeepEqual(want, got) {
118+
t.Errorf("want %v; got %v", want, got)
119+
}
120+
}

0 commit comments

Comments
 (0)