fix: consider vendored golang packages in module attribution (#5093)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow 2026-07-27 15:20:49 -04:00 committed by GitHub
parent 1286689419
commit 16223e6dd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 106 additions and 9 deletions

View File

@ -16,6 +16,10 @@ import (
// mainPackage is the import path the linker assigns to the binary's main package.
const mainPackage = "main"
// vendorPrefix is the import-path prefix carried by vendored packages (e.g. from `go mod vendor`, or the
// standard library's own vendored dependencies such as "vendor/golang.org/x/net/http2").
const vendorPrefix = "vendor/"
// binarySymbol represents a single function symbol extracted from a go binary's pclntab.
type binarySymbol struct {
// packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz")
@ -319,8 +323,13 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main"
// import path the linker assigns. Standard-library symbols (which belong to no module) are collected
// separately and returned as the second value, grouped by import path, so they can be attached to the
// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
// recognizable stdlib import path are dropped.
// synthetic "stdlib" package. Vendored packages carry a "vendor/" import-path prefix: such symbols match
// both modules whose own path carries the prefix and modules without it (matched with the prefix trimmed),
// and the prefix is retained in the group key only when the owning module itself is named "vendor/...".
// Module-less vendored packages (the stdlib's own vendored dependencies, e.g.
// "vendor/golang.org/x/net/http2") are dropped: stdlib vulnerabilities seem to be reported against the public
// packages (e.g. "crypto/x509"), not the vendored internal copies. Compiler/runtime-internal symbols that
// are neither module-owned nor a recognizable stdlib import path are likewise dropped.
func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) {
if len(symbols) == 0 {
return nil, nil
@ -348,16 +357,17 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
attrPath = main.Path
}
var best string
for _, modPath := range modulePaths {
if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) {
best = modPath
}
best := findBestMatch(modulePaths, attrPath)
// the vendor/ prefix is only retained when the owning module itself is named "vendor/...";
// in all other cases (non-vendored modules and vendored stdlib) the recorded import path is trimmed
if !strings.HasPrefix(best, vendorPrefix) {
importPath = strings.TrimPrefix(importPath, vendorPrefix)
}
local := localSymbolName(sym.name, importPath)
if best == "" {
if importPath != mainPackage && isStandardImportPath(importPath) {
if importPath != mainPackage && isStandardImportPath(importPath) { // drop stdlib vendored packages
stdlib[importPath] = append(stdlib[importPath], local)
}
continue
@ -379,11 +389,36 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
return results, stdlib
}
// findBestMatch returns the module path that owns the given package path taking into account vendor/ prefixes: a vendor/ import path
// will take precedence and continue to match, non-vendored imports will match against their vendored equivalent
func findBestMatch(modulePaths []string, importPath string) string {
trimmedPath, trimmed := strings.CutPrefix(importPath, vendorPrefix)
candidatePaths := []string{importPath, trimmedPath}
if !trimmed {
candidatePaths = candidatePaths[:1]
}
var best string
for _, candidate := range candidatePaths {
for _, modPath := range modulePaths {
// the prefix must end at a path-segment boundary in candidate, so that e.g. the module
// "github.com/foo/bar" matches the package "github.com/foo/bar/baz" but not "github.com/foo/barbaz"
if len(modPath) > len(best) && strings.HasPrefix(candidate, modPath) && (candidate == modPath || candidate[len(modPath)] == '/') {
best = modPath
}
}
}
return best
}
// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g.
// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned
// unchanged when it does not carry the expected prefix.
func localSymbolName(name, importPath string) string {
if importPath != "" && strings.HasPrefix(name, importPath+".") {
if !strings.HasPrefix(importPath, vendorPrefix) {
name = strings.TrimPrefix(name, vendorPrefix)
}
if len(importPath) < len(name) && strings.HasPrefix(name, importPath) && name[len(importPath)] == '.' {
return name[len(importPath)+1:]
}
return name

View File

@ -16,6 +16,8 @@ func Test_moduleSymbols(t *testing.T) {
deps := []*debug.Module{
{Path: "github.com/foo/bar"},
{Path: "github.com/foo/bar/v2"},
{Path: "vendor/github.com/vendored/mod"},
{Path: "github.com/vendored/mod"},
nil,
}
@ -74,6 +76,63 @@ func Test_moduleSymbols(t *testing.T) {
"runtime": {"main"},
},
},
{
name: "vendored packages match non-vendored modules and are recorded under the canonical import path",
symbols: []binarySymbol{
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
{packagePath: "vendor/github.com/foo/bar/internal/util", name: "vendor/github.com/foo/bar/internal/util.(*Helper).Do"},
},
expected: map[string]map[string][]string{
"github.com/foo/bar": {
"github.com/foo/bar": {"Parse"},
"github.com/foo/bar/internal/util": {"(*Helper).Do"},
},
},
},
{
name: "stdlib-vendored packages are dropped",
symbols: []binarySymbol{
{packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.(*Framer).ReadFrame"},
},
expected: map[string]map[string][]string{},
},
{
name: "modules whose own path carries the vendor/ prefix match exactly and win over the trimmed match",
symbols: []binarySymbol{
{packagePath: "vendor/github.com/vendored/mod", name: "vendor/github.com/vendored/mod.Run"},
{packagePath: "github.com/vendored/mod", name: "github.com/vendored/mod.Run"},
},
expected: map[string]map[string][]string{
"vendor/github.com/vendored/mod": {
"vendor/github.com/vendored/mod": {"Run"},
},
"github.com/vendored/mod": {
"github.com/vendored/mod": {"Run"},
},
},
},
{
name: "vendored and unvendored symbols for the same package are merged and deduplicated under the canonical import path",
symbols: []binarySymbol{
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
},
expected: map[string]map[string][]string{
"github.com/foo/bar": {
"github.com/foo/bar": {"Parse"},
},
},
},
{
// "github.com/foo/barbaz" shares a string prefix with the "github.com/foo/bar" module but not a
// path-segment boundary, so it must not be attributed to it (and, having a dotted first element,
// it is not a stdlib path either — it is dropped)
name: "module paths only match the package path at a path-segment boundary",
symbols: []binarySymbol{
{packagePath: "github.com/foo/barbaz", name: "github.com/foo/barbaz.Parse"},
},
expected: map[string]map[string][]string{},
},
{
name: "duplicate symbols are deduplicated",
symbols: []binarySymbol{
@ -291,6 +350,9 @@ func Test_packagePathFromSymbolName(t *testing.T) {
// module paths that begin with "go." are not compiler-generated
{"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"},
{"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"},
// vendored packages retain their "vendor/" import-path prefix in symbol names
{"vendor/golang.org/x/net/http2.(*Framer).ReadFrame", "vendor/golang.org/x/net/http2"},
{"vendor/github.com/foo/bar.Parse", "vendor/github.com/foo/bar"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {