Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Christopher Phillips 2026-07-01 02:26:46 -04:00
parent 46f9974fec
commit 9b6fc99727
No known key found for this signature in database
3 changed files with 82 additions and 10 deletions

View File

@ -82,11 +82,12 @@ func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols
c.stdlibSymbols[coord] = slices.Compact(merged) c.stdlibSymbols[coord] = slices.Compact(merged)
} }
// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. // stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy
// so callers cannot alias (and later mutate or race on) the map's internal slice.
func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string { func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string {
c.stdlibSymbolsMu.Lock() c.stdlibSymbolsMu.Lock()
defer c.stdlibSymbolsMu.Unlock() defer c.stdlibSymbolsMu.Unlock()
return c.stdlibSymbols[coord] return slices.Clone(c.stdlibSymbols[coord])
} }
// parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler. // parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler.
@ -429,7 +430,7 @@ func getExperimentsFromVersion(version string) (string, []string) {
version, rest, ok := strings.Cut(version, " ") version, rest, ok := strings.Cut(version, " ")
if ok { if ok {
// Assume they may add more non-version chunks in the future, so only look for "X:". // Assume they may add more non-version chunks in the future, so only look for "X:".
for _, chunk := range strings.Split(rest, " ") { for chunk := range strings.SplitSeq(rest, " ") {
if strings.HasPrefix(rest, "X:") { if strings.HasPrefix(rest, "X:") {
csv := strings.TrimPrefix(chunk, "X:") csv := strings.TrimPrefix(chunk, "X:")
experiments = append(experiments, strings.Split(csv, ",")...) experiments = append(experiments, strings.Split(csv, ",")...)

View File

@ -47,7 +47,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, fn := range table.Funcs { for _, fn := range table.Funcs {
if fn.Sym == nil { if fn.Sym == nil || isCompilerGeneratedName(fn.Name) {
continue continue
} }
seen[fn.Name] = struct{}{} seen[fn.Name] = struct{}{}
@ -79,8 +79,11 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
// packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name. // packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name.
// The package path is everything up to the first "." that follows the final "/" — e.g. // The package path is everything up to the first "." that follows the final "/" — e.g.
// "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" -> // "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" ->
// "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot. // "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot or is compiler-generated.
func packagePathFromSymbolName(name string) string { func packagePathFromSymbolName(name string) string {
if isCompilerGeneratedName(name) {
return ""
}
slash := strings.LastIndex(name, "/") slash := strings.LastIndex(name, "/")
dot := strings.IndexByte(name[slash+1:], '.') dot := strings.IndexByte(name[slash+1:], '.')
if dot < 0 { if dot < 0 {
@ -89,6 +92,15 @@ func packagePathFromSymbolName(name string) string {
return name[:slash+1+dot] return name[:slash+1+dot]
} }
// isCompilerGeneratedName reports whether a symbol name was synthesized by the compiler or linker rather
// than declared in Go source. These names use ':' or '..' (e.g. "type:.eq.*", "type..hash.*",
// "go:string.*") — byte sequences that never appear in a real Go import path or identifier — so they
// belong to no package and are dropped rather than mis-attributed (e.g. bucketed under a bogus "type"
// stdlib package).
func isCompilerGeneratedName(name string) bool {
return strings.Contains(name, ":") || strings.Contains(name, "..")
}
// funcNameTable returns every function name recorded in the pclntab's funcname table, including the // funcNameTable returns every function name recorded in the pclntab's funcname table, including the
// names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the // names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the
// Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so // Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so
@ -143,7 +155,7 @@ func funcNameTable(pclntab []byte) []string {
} }
var names []string var names []string
for _, raw := range bytes.Split(pclntab[start:end], []byte{0}) { for raw := range bytes.SplitSeq(pclntab[start:end], []byte{0}) {
if len(raw) == 0 { if len(raw) == 0 {
continue continue
} }
@ -264,9 +276,6 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
// "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like // "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like
// "github.com/foo/bar" whose leading element is a domain name. // "github.com/foo/bar" whose leading element is a domain name.
func isStandardImportPath(path string) bool { func isStandardImportPath(path string) bool {
first := path first, _, _ := strings.Cut(path, "/")
if i := strings.Index(path, "/"); i >= 0 {
first = path[:i]
}
return first != "" && !strings.Contains(first, ".") return first != "" && !strings.Contains(first, ".")
} }

View File

@ -1,6 +1,7 @@
package golang package golang
import ( import (
"debug/gosym"
"os" "os"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
@ -124,4 +125,65 @@ func Test_getSymbols(t *testing.T) {
} }
assert.True(t, foundRuntime, "expected to find runtime.main symbol") assert.True(t, foundRuntime, "expected to find runtime.main symbol")
assert.True(t, foundTesting, "expected to find testing.tRunner symbol") assert.True(t, foundTesting, "expected to find testing.tRunner symbol")
// the recovery loop relies on packagePathFromSymbolName, so confirm at least one recovered name is
// present that debug/gosym's table.Funcs does not surface directly (i.e. an inlined function).
require.True(t, hasInlinedOnlySymbol(t, exe, symbols), "expected to recover at least one inlined-only symbol")
}
// hasInlinedOnlySymbol reports whether syms contains a function name that is absent from the raw
// debug/gosym function table for the same binary — i.e. a name that could only have come from the
// funcname-table recovery path.
func hasInlinedOnlySymbol(t *testing.T, exe string, syms []binarySymbol) bool {
t.Helper()
f, err := os.Open(exe)
require.NoError(t, err)
defer f.Close()
pclntab, textStart, err := readPclntab(f)
require.NoError(t, err)
table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart))
require.NoError(t, err)
gosymNames := make(map[string]struct{}, len(table.Funcs))
for _, fn := range table.Funcs {
gosymNames[fn.Name] = struct{}{}
}
for _, sym := range syms {
if _, ok := gosymNames[sym.name]; !ok {
return true
}
}
return false
}
func Test_packagePathFromSymbolName(t *testing.T) {
tests := []struct {
name string
expected string
}{
{"path/filepath.IsLocal", "path/filepath"},
// pointer-receiver method
{"golang.org/x/net/html.(*Tokenizer).Next", "golang.org/x/net/html"},
// versioned (major-version-suffixed) module path
{"github.com/foo/bar/v2.Parse", "github.com/foo/bar/v2"},
{"github.com/foo/bar/v2.(*Client).Do", "github.com/foo/bar/v2"},
{"github.com/foo/bar.Parse.func1", "github.com/foo/bar"},
{"main.main", "main"},
{"runtime.gcBgMarkWorker", "runtime"},
// no package-qualifying dot
{"runtime", ""},
// compiler/linker-generated symbols belong to no package
{"type:.eq.[]string", ""},
{"type..hash.runtime._type", ""},
{"go:string.\"foo\"", ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, packagePathFromSymbolName(test.name))
})
}
} }