11package golang
22
33import (
4+ "bytes"
45 "debug/elf"
56 "debug/gosym"
67 "debug/macho"
8+ "encoding/binary"
79 "fmt"
810 "io"
911 "runtime/debug"
@@ -40,19 +42,113 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
4042 return nil , fmt .Errorf ("unable to parse pclntab: %w" , err )
4143 }
4244
45+ seen := make (map [string ]struct {})
4346 for _ , fn := range table .Funcs {
4447 if fn .Sym == nil {
4548 continue
4649 }
50+ seen [fn .Name ] = struct {}{}
4751 syms = append (syms , binarySymbol {
4852 packagePath : fn .PackageName (),
4953 name : fn .Name ,
5054 })
5155 }
5256
57+ // debug/gosym only exposes top-level functions; functions that the compiler inlined into their
58+ // callers are absent from table.Funcs even though their names are recorded in the pclntab funcname
59+ // table (used to reconstruct inlined frames in tracebacks). Recover those names so that a
60+ // vulnerable-but-inlined function (e.g. a small stdlib wrapper) is still reported as present.
61+ for _ , name := range funcNameTable (pclntab ) {
62+ if _ , ok := seen [name ]; ok {
63+ continue
64+ }
65+ pkgPath := packagePathFromSymbolName (name )
66+ if pkgPath == "" {
67+ continue
68+ }
69+ seen [name ] = struct {}{}
70+ syms = append (syms , binarySymbol {packagePath : pkgPath , name : name })
71+ }
72+
5373 return syms , nil
5474}
5575
76+ // packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name.
77+ // The package path is everything up to the first "." that follows the final "/" — e.g.
78+ // "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" ->
79+ // "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot.
80+ func packagePathFromSymbolName (name string ) string {
81+ slash := strings .LastIndex (name , "/" )
82+ dot := strings .IndexByte (name [slash + 1 :], '.' )
83+ if dot < 0 {
84+ return ""
85+ }
86+ return name [:slash + 1 + dot ]
87+ }
88+
89+ // funcNameTable returns every function name recorded in the pclntab's funcname table, including the
90+ // names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the
91+ // Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so
92+ // callers fall back to the debug/gosym function set. See the runtime's pcHeader / moduledata layout.
93+ func funcNameTable (pclntab []byte ) []string {
94+ if len (pclntab ) < 8 {
95+ return nil
96+ }
97+
98+ magic := binary .LittleEndian .Uint32 (pclntab [0 :4 ])
99+ // the field before funcnameOffset is textStart, which exists in the 1.18+ headers but not 1.16/1.17
100+ var hasTextStart bool
101+ switch magic {
102+ case 0xfffffff1 , 0xfffffff0 : // go1.20+, go1.18/1.19
103+ hasTextStart = true
104+ case 0xfffffffa : // go1.16/1.17
105+ hasTextStart = false
106+ default :
107+ return nil
108+ }
109+
110+ ptrSize := int (pclntab [7 ])
111+ if ptrSize != 4 && ptrSize != 8 {
112+ return nil
113+ }
114+
115+ readWord := func (idx int ) (uint64 , bool ) {
116+ off := 8 + idx * ptrSize
117+ if off + ptrSize > len (pclntab ) {
118+ return 0 , false
119+ }
120+ if ptrSize == 8 {
121+ return binary .LittleEndian .Uint64 (pclntab [off : off + 8 ]), true
122+ }
123+ return uint64 (binary .LittleEndian .Uint32 (pclntab [off : off + 4 ])), true
124+ }
125+
126+ // header words after (nfunc, nfiles): [textStart,] funcnameOffset, cuOffset, ...
127+ funcnameIdx := 2
128+ if hasTextStart {
129+ funcnameIdx = 3
130+ }
131+ funcnameOffset , ok1 := readWord (funcnameIdx )
132+ cuOffset , ok2 := readWord (funcnameIdx + 1 )
133+ if ! ok1 || ! ok2 {
134+ return nil
135+ }
136+
137+ start , end := int (funcnameOffset ), int (cuOffset )
138+ if start < 0 || end > len (pclntab ) || start >= end {
139+ return nil
140+ }
141+
142+ var names []string
143+ for _ , raw := range bytes .Split (pclntab [start :end ], []byte {0 }) {
144+ if len (raw ) == 0 {
145+ continue
146+ }
147+ names = append (names , string (raw ))
148+ }
149+ return names
150+ }
151+
56152// readPclntab locates the pclntab and the start address of the text segment within the binary.
57153func readPclntab (r io.ReaderAt ) (pclntab []byte , textStart uint64 , err error ) {
58154 ident := make ([]byte , 16 )
@@ -106,11 +202,13 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
106202
107203// moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix
108204// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path.
109- // Symbols from the "main" package are attributed to the main module. Stdlib and runtime symbols are not
110- // attributed to any module.
111- func moduleSymbols (symbols []binarySymbol , main * debug.Module , deps []* debug.Module ) map [string ][]string {
205+ // Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong
206+ // to no module) are collected separately and returned as the second value so they can be attached to the
207+ // synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
208+ // recognizable stdlib import path are dropped.
209+ func moduleSymbols (symbols []binarySymbol , main * debug.Module , deps []* debug.Module ) (byModule map [string ][]string , stdlib []string ) {
112210 if len (symbols ) == 0 {
113- return nil
211+ return nil , nil
114212 }
115213
116214 var modulePaths []string
@@ -138,6 +236,9 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
138236 }
139237 }
140238 if best == "" {
239+ if pkgPath != "main" && isStandardImportPath (pkgPath ) {
240+ stdlib = append (stdlib , sym .name )
241+ }
141242 continue
142243 }
143244 results [best ] = append (results [best ], sym .name )
@@ -147,6 +248,22 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
147248 slices .Sort (names )
148249 results [modPath ] = slices .Compact (names )
149250 }
251+ if len (stdlib ) > 0 {
252+ slices .Sort (stdlib )
253+ stdlib = slices .Compact (stdlib )
254+ }
150255
151- return results
256+ return results , stdlib
257+ }
258+
259+ // isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule
260+ // the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g.
261+ // "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like
262+ // "github.com/foo/bar" whose leading element is a domain name.
263+ func isStandardImportPath (path string ) bool {
264+ first := path
265+ if i := strings .Index (path , "/" ); i >= 0 {
266+ first = path [:i ]
267+ }
268+ return first != "" && ! strings .Contains (first , "." )
152269}
0 commit comments