diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 3a227313..71538113 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -469,6 +469,23 @@ func (f *File) EHFrame() (*Prog, error) { return nil, errors.New("no PT_LOAD segment for PT_GNU_EH_FRAME found") } +// GetGoBuildID returns the Go BuildID if present +func (f *File) GetGoBuildID() (string, error) { + s := f.Section(".note.go.buildid") + if s == nil { + s = f.Section(".notes") + } + if s == nil { + return "", ErrNoBuildID + } + data, err := s.Data(maxBytesSmallSection) + if err != nil { + return "", err + } + + return getGoBuildIDFromNotes(data) +} + // GetBuildID returns the ELF BuildID if present func (f *File) GetBuildID() (string, error) { s := f.Section(".note.gnu.build-id") diff --git a/libpf/pfelf/pfelf.go b/libpf/pfelf/pfelf.go index 56999b92..58d713b4 100644 --- a/libpf/pfelf/pfelf.go +++ b/libpf/pfelf/pfelf.go @@ -221,6 +221,38 @@ func GetBuildID(elfFile *elf.File) (string, error) { return getBuildIDFromNotes(sectionData) } +// GetBuildID extracts the Go build ID from the provided ELF file. This is read from +// the .note.go.buildid or .notes section of the ELF, and may not exist. If no build ID is present +// an ErrNoBuildID is returned. +func GetGoBuildID(elfFile *elf.File) (string, error) { + sectionData, err := getSectionData(elfFile, ".note.go.buildid") + if err != nil { + sectionData, err = getSectionData(elfFile, ".notes") + if err != nil { + return "", ErrNoBuildID + } + } + + return getGoBuildIDFromNotes(sectionData) +} + +// getGoBuildIDFromNotes returns the Go build ID from an ELF notes section data. +func getGoBuildIDFromNotes(notes []byte) (string, error) { + // 0x4 is the "Go Build ID" type. Not sure where this is standardized. + buildID, found, err := getNoteString(notes, "Go", 0x4) + if err != nil { + return "", fmt.Errorf("could not determine BuildID: %v", err) + } + //nolint:lll + // When building Go binaries, Bazel explicitly sets their build ID to "redacted" + // see https://github.com/bazelbuild/rules_go/blob/199d8e4827f87d382a85febd0148c1b42fa949cc/go/private/actions/link.bzl#L174. + // In that case, we don't want to associate the build ID with the binary in the mapping. + if !found || buildID == "redacted" { + return "", ErrNoBuildID + } + return buildID, nil +} + // GetBuildIDFromNotesFile returns the build ID contained in a file with the format of an ELF notes // section. func GetBuildIDFromNotesFile(filePath string) (string, error) { @@ -261,10 +293,10 @@ func GetSectionAddress(e *elf.File, sectionName string) ( return section.Addr, true, nil } -// getNoteHexString returns the hex string contents of an ELF note from a note section, as described +// getNoteDescBytes returns the bytes contents of an ELF note from a note section, as described // in the ELF standard in Figure 2-3. -func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( - noteHexString string, found bool, err error) { +func getNoteDescBytes(sectionBytes []byte, name string, noteType uint32) ( + noteBytes []byte, found bool, err error) { // The data stored inside ELF notes is made of one or multiple structs, containing the // following fields: // - namesz // 32-bit, size of "name" @@ -284,10 +316,10 @@ func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( // Try to find the note in the section idx := bytes.Index(sectionBytes, noteHeader) if idx == -1 { - return "", false, nil + return nil, false, nil } if idx < 4 { // there needs to be room for descsz - return "", false, errors.New("could not read note data size") + return nil, false, errors.New("could not read note data size") } idxDataStart := idx + len(noteHeader) @@ -297,13 +329,39 @@ func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( dataSize := binary.LittleEndian.Uint32(sectionBytes[idx-4 : idx]) idxDataEnd := uint64(idxDataStart) + uint64(dataSize) - // Check sanity (64 is totally arbitrary, as we only use it for Linux ID and Build ID) - if idxDataEnd > uint64(len(sectionBytes)) || dataSize > 64 { - return "", false, fmt.Errorf( + // Check sanity (84 is totally arbitrary, as we only use it for Linux ID and (Go) Build ID) + if idxDataEnd > uint64(len(sectionBytes)) || dataSize > 84 { + return nil, false, fmt.Errorf( "non-sensical note: %d start index: %d, %v end index %d, size %d, section size %d", idx, idxDataStart, noteHeader, idxDataEnd, dataSize, len(sectionBytes)) } - return hex.EncodeToString(sectionBytes[idxDataStart:idxDataEnd]), true, nil + return sectionBytes[idxDataStart:idxDataEnd], true, nil +} + +// getNoteHexString returns the hex string contents of an ELF note from a note section, as described +// in the ELF standard in Figure 2-3. +func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( + noteHexString string, found bool, err error) { + noteBytes, found, err := getNoteDescBytes(sectionBytes, name, noteType) + if err != nil { + return "", false, err + } + if !found { + return "", false, nil + } + return hex.EncodeToString(noteBytes), true, nil +} + +func getNoteString(sectionBytes []byte, name string, noteType uint32) ( + noteString string, found bool, err error) { + noteBytes, found, err := getNoteDescBytes(sectionBytes, name, noteType) + if err != nil { + return "", false, err + } + if !found { + return "", false, nil + } + return string(noteBytes), true, nil } // GetLinuxBuildSalt extracts the linux kernel build salt from the provided ELF path. diff --git a/libpf/pfelf/pfelf_test.go b/libpf/pfelf/pfelf_test.go index 52d71ab4..b5fb7325 100644 --- a/libpf/pfelf/pfelf_test.go +++ b/libpf/pfelf/pfelf_test.go @@ -48,6 +48,21 @@ func TestGetBuildID(t *testing.T) { assert.Equal(t, "6920fd217a8416131f4377ef018a2c932f311b6d", buildID) } +func TestGetGoBuildID(t *testing.T) { + elfFile := getELF("testdata/go-buildid", t) + defer elfFile.Close() + + buildID, err := pfelf.GetGoBuildID(elfFile) + if err != nil { + t.Fatalf("GetGoBuildID failed with error: %s", err) + } + + if buildID != + "tUhrGOwxi48kXlLhYlY3/WlmPekR2qonrFvofssLt/8beXJbt0rDaHhn3I6x8D/IA6Zd8Qc8Rsh_bFKoPVn" { + t.Fatalf("Invalid build-id: %s", buildID) + } +} + func TestGetDebugLink(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable1() require.NoError(t, err) diff --git a/libpf/pfelf/testdata/go-buildid b/libpf/pfelf/testdata/go-buildid new file mode 100755 index 00000000..6bb47ab4 Binary files /dev/null and b/libpf/pfelf/testdata/go-buildid differ diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index 1a936e64..cc460eac 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -301,6 +301,11 @@ func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mappin } buildID, _ := ef.GetBuildID() + if buildID == "" { + // If the buildID is empty, try to get Go buildID. + buildID, _ = ef.GetGoBuildID() + } + mapping2 := *mapping // copy to avoid races if callee saves the closure open := func() (process.ReadAtCloser, error) { return pr.OpenMappingFile(&mapping2)