diff --git a/pkg/network/go/bininspect/pclntab.go b/pkg/network/go/bininspect/pclntab.go new file mode 100644 index 00000000000000..57ce2764537656 --- /dev/null +++ b/pkg/network/go/bininspect/pclntab.go @@ -0,0 +1,282 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package bininspect + +import ( + "debug/elf" + "encoding/binary" + "errors" + "fmt" +) + +const ( + pclntabSectionName = ".gopclntab" + + go116magic = 0xfffffffa + go118magic = 0xfffffff0 + go120magic = 0xfffffff1 +) + +// version of the pclntab +type version int + +const ( + verUnknown version = iota + ver11 + ver12 + ver116 + ver118 + ver120 +) + +var ( + // ErrMissingPCLNTABSection is returned when the pclntab section is missing. + ErrMissingPCLNTABSection = errors.New("failed to find pclntab section") + + // ErrUnsupportedPCLNTABVersion is returned when the pclntab version is not supported. + ErrUnsupportedPCLNTABVersion = errors.New("unsupported pclntab version") + + // ErrFailedToFindAllSymbols is returned when not all symbols were found. + ErrFailedToFindAllSymbols = errors.New("failed to find all symbols") +) + +// sectionAccess is a wrapper around elf.Section to provide ReadAt functionality. +// This is used to lazy read from the pclntab section, as the pclntab is large and we don't want to read it all at once, +// or store it in memory. +type sectionAccess struct { + section *elf.Section + baseOffset int64 +} + +// ReadAt reads len(p) bytes from the section starting at the given offset. +func (s *sectionAccess) ReadAt(outBuffer []byte, offset int64) (int, error) { + return s.section.ReadAt(outBuffer, s.baseOffset+offset) +} + +// pclntanSymbolParser is a parser for pclntab symbols. +// Similar to LineTable struct in https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L43 +type pclntanSymbolParser struct { + // section is the pclntab section. + section *elf.Section + // symbolFilter is the filter for the symbols. + symbolFilter symbolFilter + + // byteOrderParser is the binary.ByteOrder for the pclntab. + byteOrderParser binary.ByteOrder + // cachedVersion is the version of the pclntab. + cachedVersion version + // funcNameTable is the sectionAccess for the function name table. + funcNameTable sectionAccess + // funcData is the sectionAccess for the function data. + funcData sectionAccess + // funcTable is the sectionAccess for the function table. + funcTable sectionAccess + // funcTableSize is the size of the function table. + funcTableSize uint32 + // ptrSize is the size of a pointer in the architecture of the binary. + ptrSize uint32 + // ptrBufferSizeHelper is a buffer for reading pointers of the size ptrSize. + ptrBufferSizeHelper []byte + // funcNameHelper is a buffer for reading function names. Of the maximum size of the symbol names. + funcNameHelper []byte + // funcTableFieldSize is the size of a field in the function table. + funcTableFieldSize int + // funcTableBuffer is a buffer for reading fields in the function table. + funcTableBuffer []byte +} + +// GetPCLNTABSymbolParser returns the matching symbols from the pclntab section. +func GetPCLNTABSymbolParser(f *elf.File, symbolFilter symbolFilter) (map[string]*elf.Symbol, error) { + section := f.Section(pclntabSectionName) + if section == nil { + return nil, ErrMissingPCLNTABSection + } + + parser := &pclntanSymbolParser{section: section, symbolFilter: symbolFilter} + + if err := parser.parsePclntab(); err != nil { + return nil, err + } + // Late initialization, to prevent allocation if the binary is not supported. + _, maxSymbolsSize := symbolFilter.getMinMaxLength() + parser.funcNameHelper = make([]byte, maxSymbolsSize) + parser.funcTableFieldSize = getFuncTableFieldSize(parser.cachedVersion, int(parser.ptrSize)) + // Allocate the buffer for reading the function table. + // TODO: Do we need 2*funcTableFieldSize? + parser.funcTableBuffer = make([]byte, 2*parser.funcTableFieldSize) + return parser.getSymbols() +} + +// parsePclntab parses the pclntab, setting the version and verifying the header. +// Based on parsePclnTab in https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L194 +func (p *pclntanSymbolParser) parsePclntab() error { + p.cachedVersion = ver11 + + pclntabHeader := make([]byte, 8) + if n, err := p.section.ReadAt(pclntabHeader, 0); err != nil || n != len(pclntabHeader) { + return fmt.Errorf("failed to read pclntab header: %w", err) + } + // Matching the condition https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L216-L220 + // Check header: 4-byte magic, two zeros, pc quantum, pointer size. + if p.section.Size < 16 || pclntabHeader[4] != 0 || pclntabHeader[5] != 0 || + (pclntabHeader[6] != 1 && pclntabHeader[6] != 2 && pclntabHeader[6] != 4) || // pc quantum + (pclntabHeader[7] != 4 && pclntabHeader[7] != 8) { // pointer size + // TODO: add explicit error message + return errors.New("invalid pclntab header") + } + + leMagic := binary.LittleEndian.Uint32(pclntabHeader) + beMagic := binary.BigEndian.Uint32(pclntabHeader) + switch { + case leMagic == go116magic: + p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver116 + case beMagic == go116magic: + p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver116 + case leMagic == go118magic: + p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver118 + case beMagic == go118magic: + p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver118 + case leMagic == go120magic: + p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver120 + case beMagic == go120magic: + p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver120 + default: + return ErrUnsupportedPCLNTABVersion + } + + p.ptrSize = uint32(pclntabHeader[7]) + p.ptrBufferSizeHelper = make([]byte, p.ptrSize) + + // offset is based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L252 + offset := func(word uint32) uint64 { + off := 8 + word*p.ptrSize + if n, err := p.section.ReadAt(p.ptrBufferSizeHelper, int64(off)); err != nil || n != int(p.ptrSize) { + return 0 + } + return p.uintptr(p.ptrBufferSizeHelper) + } + + switch p.cachedVersion { + case ver118, ver120: + p.funcTableSize = uint32(offset(0)) + p.funcNameTable = sectionAccess{ + section: p.section, + baseOffset: int64(offset(3)), + } + p.funcData = sectionAccess{ + section: p.section, + baseOffset: int64(offset(7)), + } + p.funcTable = sectionAccess{ + section: p.section, + baseOffset: int64(offset(7)), + } + case ver116: + p.funcTableSize = uint32(offset(0)) + p.funcNameTable = sectionAccess{ + section: p.section, + baseOffset: int64(offset(2)), + } + p.funcData = sectionAccess{ + section: p.section, + baseOffset: int64(offset(6)), + } + p.funcTable = sectionAccess{ + section: p.section, + baseOffset: int64(offset(6)), + } + } + + return nil +} + +// uintptr returns the pointer-sized value encoded at b. +// The pointer size is dictated by the table being read. +// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L186. +func (p *pclntanSymbolParser) uintptr(b []byte) uint64 { + if p.ptrSize == 4 { + return uint64(p.byteOrderParser.Uint32(b)) + } + return p.byteOrderParser.Uint64(b) +} + +// getFuncTableFieldSize returns the size of a field in the function table. +// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L388-L392 +func getFuncTableFieldSize(version version, ptrSize int) int { + if version >= ver118 { + return 4 + } + return ptrSize +} + +// getSymbols returns the symbols from the pclntab section that match the symbol filter. +// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L300-L329 +func (p *pclntanSymbolParser) getSymbols() (map[string]*elf.Symbol, error) { + symbols := make(map[string]*elf.Symbol, p.symbolFilter.getNumWanted()) + data := sectionAccess{section: p.section} + for currentIdx := uint32(0); currentIdx < p.funcTableSize; currentIdx++ { + // based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L315 + _, err := p.funcTable.ReadAt(p.funcTableBuffer, int64((2*currentIdx+1)*uint32(p.funcTableFieldSize))) + if err != nil { + continue + } + + // based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L321 + data.baseOffset = int64(p.uint(p.funcTableBuffer)) + p.funcData.baseOffset + nameOffset := field(p.ptrSize, p.cachedVersion, p.byteOrderParser, data, p.ptrBufferSizeHelper) + funcName := p.funcName(nameOffset) + + if funcName == "" { + continue + } + symbols[funcName] = &elf.Symbol{ + Name: funcName, + } + if len(symbols) == p.symbolFilter.getNumWanted() { + break + } + } + if len(symbols) < p.symbolFilter.getNumWanted() { + return symbols, ErrFailedToFindAllSymbols + } + return symbols, nil +} + +// funcName returns the name of the function found at off. +func (p *pclntanSymbolParser) funcName(off uint32) string { + n, err := p.funcNameTable.ReadAt(p.funcNameHelper, int64(off)) + if err != nil { + return "" + } + if p.symbolFilter.want(string(p.funcNameHelper[:n])) { + return string(p.funcNameHelper[:n]) + } + return "" +} + +// uint returns the uint stored at b. +// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L427-L432 +func (p *pclntanSymbolParser) uint(b []byte) uint64 { + if p.funcTableFieldSize == 4 { + return uint64(p.byteOrderParser.Uint32(b)) + } + return p.byteOrderParser.Uint64(b) +} + +// field returns the uint32 field at off. +// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L472-L485 +// We can only for the usage of this function for getting the name of the function (https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L463) +// So we explicitly set `n = 1` in the original implementation. +func field(ptrSize uint32, version version, binary binary.ByteOrder, data sectionAccess, helper []byte) uint32 { + off := ptrSize + if version >= ver118 { + off = 4 + } + if n, err := data.ReadAt(helper, int64(off)); err != nil || n != int(ptrSize) { + return 0 + } + return binary.Uint32(helper) +} diff --git a/pkg/network/go/bininspect/symbols.go b/pkg/network/go/bininspect/symbols.go index 90dd50c6932d10..910bc37d3ff30d 100644 --- a/pkg/network/go/bininspect/symbols.go +++ b/pkg/network/go/bininspect/symbols.go @@ -281,3 +281,19 @@ func GetAnySymbolWithPrefix(elfFile *elf.File, prefix string, maxLength int) (*e // Shouldn't happen return nil, errors.New("empty symbols map") } + +// GetAnySymbolWithPrefixPCLNTAB returns any one symbol with the given prefix and the +// specified maximum length from the pclntab section in ELF file. +func GetAnySymbolWithPrefixPCLNTAB(elfFile *elf.File, prefix string, maxLength int) (*elf.Symbol, error) { + symbols, err := GetPCLNTABSymbolParser(elfFile, newPrefixSymbolFilter(prefix, maxLength)) + if err != nil { + return nil, err + } + + for key := range symbols { + return symbols[key], nil + } + + // Shouldn't happen + return nil, errors.New("empty symbols map") +}