internal/govulncheck: do not show anonymous functions in call stacks
Their names are likely to be confusing to users. Instead, replace a
call to an anonymous function A$1 with "B calls A", where B is the
previous non-anonymous function in the call stack leading to call of
A$1 and A is the function creating A$1. This is motivated by real
world example that can be simplified to
func B() {
A()()
}
func A() {
return func() {
//...
vuln()
}
}
Change-Id: Ie353b7cfa8d96e2e7a8d67dfb4e7f4619145a56d
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/454195
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/cmd/govulncheck/testdata/manystacks.ct b/cmd/govulncheck/testdata/manystacks.ct
index b35ce36..0348de0 100644
--- a/cmd/govulncheck/testdata/manystacks.ct
+++ b/cmd/govulncheck/testdata/manystacks.ct
@@ -11,7 +11,7 @@
Call stacks in your code:
.../main.go:16:14: example.com/manystacks.main calls github.com/shiyanhui/dht.New
- .../main.go:30:28: example.com/manystacks.main$2 calls example.com/manystacks/otherpkg.GetPeers, which eventually calls github.com/shiyanhui/dht.DHT.GetPeers
+ .../main.go:27:2: example.com/manystacks.main calls example.com/manystacks/otherpkg.GetPeers, which eventually calls github.com/shiyanhui/dht.DHT.GetPeers
.../main.go:44:7: example.com/manystacks.main calls github.com/shiyanhui/dht.DHT.Run
Found in: github.com/shiyanhui/dht@v0.0.0-20201219151056-5a20f3199263
diff --git a/internal/govulncheck/callstacks.go b/internal/govulncheck/callstacks.go
index f97e1bc..b66c4a8 100644
--- a/internal/govulncheck/callstacks.go
+++ b/internal/govulncheck/callstacks.go
@@ -116,41 +116,148 @@
}
// summarizeCallStack returns a short description of the call stack.
-// It uses one of two forms, depending on what the lowest function F in topPkgs
-// calls:
-// - If it calls a function V from the vulnerable package, then summarizeCallStack
-// returns "F calls V".
-// - If it calls a function G in some other package, which eventually calls V,
-// it returns "F calls G, which eventually calls V".
+// It uses one of four forms, depending on what the lowest function F
+// in topPkgs calls and what is the highest function V of vulnPkg:
+// - If F calls V directly and F as well as V are not anonymous functions:
+// "F calls V"
+// - The same case as above except F calls function G in some other package:
+// "F calls G, which eventually calls V"
+// - If F is an anonymous function, created by function G, and H is the
+// lowest non-anonymous function in topPkgs:
+// "H calls G, which eventually calls V"
+// - If V is an anonymous function, created by function W:
+// "F calls W, which eventually calls V"
//
// If it can't find any of these functions, summarizeCallStack returns the empty string.
func summarizeCallStack(cs CallStack, topPkgs map[string]bool, vulnPkg string) string {
- // Find the lowest function in the top packages.
- iTop := lowest(cs.Frames, func(e *StackFrame) bool {
- return topPkgs[e.PkgPath]
- })
+ iTop, iTopEnd, topFunc, topEndFunc := summarizeTop(cs.Frames, topPkgs)
if iTop < 0 {
return ""
}
- // Find the highest function in the vulnerable package that is below iTop.
- iVuln := highest(cs.Frames[iTop+1:], func(e *StackFrame) bool {
- return e.PkgPath == vulnPkg
- })
- if iVuln < 0 {
+
+ iVulnStart, vulnStartFunc, vulnFunc := summarizeVuln(cs.Frames, iTopEnd, vulnPkg)
+ if iVulnStart < 0 {
return ""
}
- iVuln += iTop + 1 // adjust for slice in call to highest.
- topName := cs.Frames[iTop].Name()
+
topPos := internal.AbsRelShorter(cs.Frames[iTop].Pos())
if topPos != "" {
topPos += ": "
}
- vulnName := cs.Frames[iVuln].Name()
- if iVuln == iTop+1 {
- return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName)
+
+ // The invariant is that the summary will always mention at most three functions
+ // and never mention an anonymous function. It prioritizes summarizing top of the
+ // stack as that is what the user has the most control of. For instance, if both
+ // the top and vuln portions of the stack are each summarized with two functions,
+ // then the final summary will mention the two functions of the top segment and
+ // only one from the vuln segment.
+ if topFunc != topEndFunc {
+ // The last function of the top segment is anonymous.
+ return fmt.Sprintf("%s%s calls %s, which eventually calls %s", topPos, topFunc, topEndFunc, vulnFunc)
}
- return fmt.Sprintf("%s%s calls %s, which eventually calls %s",
- topPos, topName, cs.Frames[iTop+1].Name(), vulnName)
+ if iVulnStart != iTopEnd+1 {
+ // If there is something in between top and vuln segments of
+ // the stack, then also summarize that intermediate segment.
+ return fmt.Sprintf("%s%s calls %s, which eventually calls %s", topPos, topFunc, cs.Frames[iTopEnd+1].Name(), vulnFunc)
+ }
+ if vulnStartFunc != vulnFunc {
+ // The first function of the vuln segment is anonymous.
+ return fmt.Sprintf("%s%s calls %s, which eventually calls %s", topPos, topFunc, vulnStartFunc, vulnFunc)
+ }
+ return fmt.Sprintf("%s%s calls %s", topPos, topFunc, vulnFunc)
+}
+
+// summarizeTop returns summary information for the beginning segment
+// of call stack frames that belong to topPkgs. It returns the latest,
+// e.g., lowest function in this segment and its index in frames. If
+// that function is anonymous, then summarizeTop also returns the
+// lowest non-anonymous function and its index in frames. In that case,
+// the anonymous function is replaced by the function that created it.
+//
+// [p.V p.W q.Q ...] -> (1, 1, p.W, p.W)
+// [p.V p.W p.Z$1 q.Q ...] -> (1, 2, p.W, p.Z)
+func summarizeTop(frames []*StackFrame, topPkgs map[string]bool) (iTop, iTopEnd int, topFunc, topEndFunc string) {
+ iTopEnd = lowest(frames, func(e *StackFrame) bool {
+ return topPkgs[e.PkgPath]
+ })
+ if iTopEnd < 0 {
+ return -1, -1, "", ""
+ }
+
+ topEndFunc = frames[iTopEnd].Name()
+ if !isAnonymousFunction(topEndFunc) {
+ iTop = iTopEnd
+ topFunc = topEndFunc
+ return
+ }
+
+ topEndFunc = creatorName(topEndFunc)
+
+ iTop = lowest(frames, func(e *StackFrame) bool {
+ return topPkgs[e.PkgPath] && !isAnonymousFunction(e.FuncName)
+ })
+ if iTop < 0 {
+ iTop = iTopEnd
+ topFunc = topEndFunc // for sanity
+ return
+ }
+
+ topFunc = frames[iTop].Name()
+ return
+}
+
+// summarizeVuln returns summary information for the final segment
+// of call stack frames that belong to vulnPkg. It returns the earliest,
+// e.g., highest function in this segment and its index in frames. If
+// that function is anonymous, then summarizeVuln also returns the
+// highest non-anonymous function. In that case, the anonymous function
+// is replaced by the function that created it.
+//
+// [x x q.Q v.V v.W] -> (3, v.V, v.V)
+// [x x q.Q v.V$1 v.W] -> (3, v.V, v.W)
+func summarizeVuln(frames []*StackFrame, iTop int, vulnPkg string) (iVulnStart int, vulnStartFunc, vulnFunc string) {
+ iVulnStart = highest(frames[iTop+1:], func(e *StackFrame) bool {
+ return e.PkgPath == vulnPkg
+ })
+ if iVulnStart < 0 {
+ return -1, "", ""
+ }
+ iVulnStart += iTop + 1 // adjust for slice in call to highest.
+
+ vulnStartFunc = frames[iVulnStart].Name()
+ if !isAnonymousFunction(vulnStartFunc) {
+ vulnFunc = vulnStartFunc
+ return
+ }
+
+ vulnStartFunc = creatorName(vulnStartFunc)
+
+ iVuln := highest(frames[iVulnStart:], func(e *StackFrame) bool {
+ return e.PkgPath == vulnPkg && !isAnonymousFunction(e.FuncName)
+ })
+ if iVuln < 0 {
+ vulnFunc = vulnStartFunc // for sanity
+ return
+ }
+
+ vulnFunc = frames[iVuln+iVulnStart].Name()
+ return
+}
+
+// creatorName returns the name of the function that created
+// the anonymous function anonFuncName. Assumes anonFuncName
+// is of the form <name>$1...
+func creatorName(anonFuncName string) string {
+ vs := strings.Split(anonFuncName, "$")
+ if len(vs) == 1 {
+ return anonFuncName
+ }
+ return vs[0]
+}
+
+func isAnonymousFunction(funcName string) bool {
+ // anonymous functions have $ sign in their name (naming done by ssa)
+ return strings.ContainsRune(funcName, '$')
}
// uniqueCallStack returns the first unique call stack among css, if any.
diff --git a/internal/govulncheck/callstacks_test.go b/internal/govulncheck/callstacks_test.go
index 8e6aef8..2ff2e6e 100644
--- a/internal/govulncheck/callstacks_test.go
+++ b/internal/govulncheck/callstacks_test.go
@@ -87,9 +87,25 @@
"t2.G calls v.V1",
},
{
+ "t1.F t2.G v.V$1 v.V1",
+ "t2.G calls v.V, which eventually calls v.V1",
+ },
+ {
+ "t1.F t2.G$1 v.V1",
+ "t1.F calls t2.G, which eventually calls v.V1",
+ },
+ {
+ "t1.F t2.G$1 v.V$1 v.V1",
+ "t1.F calls t2.G, which eventually calls v.V1",
+ },
+ {
"t1.F x.Y t2.G a.H b.I c.J v.V",
"t2.G calls a.H, which eventually calls v.V",
},
+ {
+ "t1.F x.Y t2.G a.H b.I c.J v.V$1 v.V1",
+ "t2.G calls a.H, which eventually calls v.V1",
+ },
} {
in := stringToCallStack(test.in)
got := summarizeCallStack(in, topPkgs, vulnPkg)
diff --git a/internal/govulncheck/util.go b/internal/govulncheck/util.go
index aba3adb..25a10c6 100644
--- a/internal/govulncheck/util.go
+++ b/internal/govulncheck/util.go
@@ -73,7 +73,7 @@
return -1
}
-// lowest returns the lowest (one with the largets index) entry in the call
+// lowest returns the lowest (one with the largest index) entry in the call
// stack for which f returns true.
func lowest(cs []*StackFrame, f func(e *StackFrame) bool) int {
for i := len(cs) - 1; i >= 0; i-- {