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
+
1
5
package vulncheck
2
6
3
7
import (
4
8
"container/list"
9
+ "fmt"
10
+ "sort"
11
+ "strings"
5
12
"sync"
6
13
)
7
14
@@ -10,18 +17,18 @@ import (
10
17
// known vulnerabilities.
11
18
type ImportChain []* PkgNode
12
19
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.
19
23
//
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.
22
29
//
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 .
25
32
func ImportChains (res * Result ) map [* Vuln ][]ImportChain {
26
33
// Group vulns per package.
27
34
vPerPkg := make (map [int ][]* Vuln )
@@ -122,3 +129,175 @@ type StackEntry struct {
122
129
// nil when the frame represents an entry point of the stack.
123
130
Call * CallSite
124
131
}
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
+ }
0 commit comments