diff --git a/interpreter/dotnet/instance.go b/interpreter/dotnet/instance.go index a87771254..d14e7d309 100644 --- a/interpreter/dotnet/instance.go +++ b/interpreter/dotnet/instance.go @@ -9,8 +9,6 @@ package dotnet import ( "fmt" "hash/fnv" - "os" - "path" "slices" "strings" "sync/atomic" @@ -631,11 +629,8 @@ func (i *dotnetInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, info.simpleName, info.guid) if !info.reported { - open := func() (process.ReadAtCloser, error) { - return os.Open(m.Path) - } - symbolReporter.ExecutableMetadata(info.fileID, path.Base(m.Path), - info.guid, libpf.Dotnet, open) + symbolReporter.ExecutableMetadata(info.fileID, m.Path, + info.guid, libpf.Dotnet, pr) info.reported = true } diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 3a2273133..715381135 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 56999b921..58d713b40 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 52d71ab4f..5955b8b13 100644 --- a/libpf/pfelf/pfelf_test.go +++ b/libpf/pfelf/pfelf_test.go @@ -48,6 +48,31 @@ 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 TestGetGoBuildIDError(t *testing.T) { + elfFile := getELF("testdata/go-buildid-bazel", t) + defer elfFile.Close() + + buildID, err := pfelf.GetGoBuildID(elfFile) + if assert.ErrorIs(t, pfelf.ErrNoBuildID, err) { + assert.Equal(t, "", 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 000000000..6bb47ab47 Binary files /dev/null and b/libpf/pfelf/testdata/go-buildid differ diff --git a/libpf/pfelf/testdata/go-buildid-bazel b/libpf/pfelf/testdata/go-buildid-bazel new file mode 100755 index 000000000..cabf178ce Binary files /dev/null and b/libpf/pfelf/testdata/go-buildid-bazel differ diff --git a/process/coredump.go b/process/coredump.go index fcfbd820d..440ebe081 100644 --- a/process/coredump.go +++ b/process/coredump.go @@ -315,6 +315,11 @@ func (cd *CoredumpProcess) OpenELF(path string) (*pfelf.File, error) { return nil, fmt.Errorf("ELF file `%s` not found", path) } +// Open implements the FileOpener and Process interfaces +func (cd *CoredumpProcess) Open(path string) (ReadAtCloser, string, error) { + return nil, path, errors.New("coredump does not support opening files") +} + // getFile returns (creating if needed) a matching CoredumpFile for given file name func (cd *CoredumpProcess) getFile(name string) *CoredumpFile { if cf, ok := cd.files[name]; ok { diff --git a/process/process.go b/process/process.go index 9bf480651..64a3a7e3b 100644 --- a/process/process.go +++ b/process/process.go @@ -252,3 +252,36 @@ func (sp *systemProcess) OpenELF(file string) (*pfelf.File, error) { // Fall back to opening the file using the process specific root return pfelf.Open(fmt.Sprintf("/proc/%v/root/%s", sp.pid, file)) } + +type ReaderWithDummyClose struct { + io.ReaderAt +} + +func (r *ReaderWithDummyClose) Close() error { + return nil +} + +func (sp *systemProcess) Open(file string) (ReadAtCloser, string, error) { + // Always open via map_files as it can open deleted files if available. + // No fallback is attempted: + // - if the process exited, the fallback will error also (/proc/>PID> is gone) + // - if the error is due to ELF content, same error will occur in both cases + // - if the process unmapped the ELF, its data is no longer needed + if m, ok := sp.fileToMapping[file]; ok { + if m.IsVDSO() { + vdso, err := sp.extractMapping(m) + if err != nil { + return nil, "", fmt.Errorf("failed to extract VDSO: %v", err) + } + return &ReaderWithDummyClose{vdso}, "", nil + } + path := sp.getMappingFile(m) + f, err := os.Open(path) + return f, path, err + } + + // Fall back to opening the file using the process specific root + path := fmt.Sprintf("/proc/%v/root/%s", sp.pid, file) + f, err := os.Open(path) + return f, path, err +} diff --git a/process/types.go b/process/types.go index 52d6fe5d0..d493fa30d 100644 --- a/process/types.go +++ b/process/types.go @@ -92,6 +92,10 @@ type ReadAtCloser interface { io.Closer } +type FileOpener interface { + Open(string) (reader ReadAtCloser, actualPath string, err error) +} + // Process is the interface to inspect ELF coredump/process. // The current implementations do not allow concurrent access to this interface // from different goroutines. As an exception the ELFOpener and the returned @@ -125,4 +129,6 @@ type Process interface { io.Closer pfelf.ELFOpener + + FileOpener } diff --git a/processmanager/manager_test.go b/processmanager/manager_test.go index fa7acb259..ddd305a30 100644 --- a/processmanager/manager_test.go +++ b/processmanager/manager_test.go @@ -83,6 +83,11 @@ func (d *dummyProcess) Close() error { return nil } +func (d *dummyProcess) Open(file string) (process.ReadAtCloser, string, error) { + f, err := os.Open(file) + return f, file, err +} + func newTestProcess(pid libpf.PID) process.Process { return &dummyProcess{pid: pid} } @@ -255,7 +260,7 @@ type symbolReporterMockup struct{} func (s *symbolReporterMockup) ReportFallbackSymbol(_ libpf.FrameID, _ string) {} func (s *symbolReporterMockup) ExecutableMetadata(_ libpf.FileID, _, _ string, - _ libpf.InterpreterType, _ reporter.ExecutableOpener) { + _ libpf.InterpreterType, _ process.FileOpener) { } func (s *symbolReporterMockup) FrameMetadata(_ libpf.FileID, _ libpf.AddressOrLineno, diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index 1a936e641..7a72be2eb 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "os" - "path" "syscall" "time" @@ -291,21 +290,13 @@ func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mappin } pm.FileIDMapper.Set(hostFileID, fileID) - baseName := path.Base(mapping.Path) - if baseName == "/" { - // There are circumstances where there is no filename. - // E.g. kernel module 'bpfilter_umh' before Linux 5.9-rc1 uses - // fork_usermode_blob() and launches process with a blob without - // filename mapped in as the executable. - baseName = "" - } - buildID, _ := ef.GetBuildID() - mapping2 := *mapping // copy to avoid races if callee saves the closure - open := func() (process.ReadAtCloser, error) { - return pr.OpenMappingFile(&mapping2) + if buildID == "" { + // If the buildID is empty, try to get Go buildID. + buildID, _ = ef.GetGoBuildID() } - pm.reporter.ExecutableMetadata(fileID, baseName, buildID, libpf.Native, open) + + pm.reporter.ExecutableMetadata(fileID, mapping.Path, buildID, libpf.Native, pr) return info } diff --git a/reporter/iface.go b/reporter/iface.go index f9da855b3..8f26dcaab 100644 --- a/reporter/iface.go +++ b/reporter/iface.go @@ -53,8 +53,6 @@ type TraceReporter interface { SupportsReportTraceEvent() bool } -type ExecutableOpener = func() (process.ReadAtCloser, error) - type SymbolReporter interface { // ReportFallbackSymbol enqueues a fallback symbol for reporting, for a given frame. ReportFallbackSymbol(frameID libpf.FrameID, symbol string) @@ -67,7 +65,7 @@ type SymbolReporter interface { // wish to upload executables should NOT block this function to do so and instead just // open the file and then enqueue the upload in the background. ExecutableMetadata(fileID libpf.FileID, fileName, buildID string, - interp libpf.InterpreterType, open ExecutableOpener) + interp libpf.InterpreterType, opener process.FileOpener) // FrameMetadata accepts metadata associated with a frame and caches this information before // a periodic reporting to the backend. diff --git a/reporter/otlp_reporter.go b/reporter/otlp_reporter.go index 895e420b3..5db17603f 100644 --- a/reporter/otlp_reporter.go +++ b/reporter/otlp_reporter.go @@ -34,6 +34,7 @@ import ( "github.com/open-telemetry/opentelemetry-ebpf-profiler/libpf" "github.com/open-telemetry/opentelemetry-ebpf-profiler/libpf/xsync" + "github.com/open-telemetry/opentelemetry-ebpf-profiler/process" ) var ( @@ -206,7 +207,7 @@ func (r *OTLPReporter) ReportFallbackSymbol(frameID libpf.FrameID, symbol string // ExecutableMetadata accepts a fileID with the corresponding filename // and caches this information. func (r *OTLPReporter) ExecutableMetadata(fileID libpf.FileID, fileName, - buildID string, _ libpf.InterpreterType, _ ExecutableOpener) { + buildID string, _ libpf.InterpreterType, _ process.FileOpener) { r.executables.Add(fileID, execInfo{ fileName: fileName, buildID: buildID, diff --git a/tools/coredump/coredump.go b/tools/coredump/coredump.go index 2f79f4b65..4a1f329bb 100644 --- a/tools/coredump/coredump.go +++ b/tools/coredump/coredump.go @@ -23,7 +23,6 @@ import ( "github.com/open-telemetry/opentelemetry-ebpf-profiler/nativeunwind/elfunwindinfo" "github.com/open-telemetry/opentelemetry-ebpf-profiler/process" pm "github.com/open-telemetry/opentelemetry-ebpf-profiler/processmanager" - "github.com/open-telemetry/opentelemetry-ebpf-profiler/reporter" "github.com/open-telemetry/opentelemetry-ebpf-profiler/support" tracertypes "github.com/open-telemetry/opentelemetry-ebpf-profiler/tracer/types" ) @@ -65,7 +64,7 @@ func newSymbolizationCache() *symbolizationCache { } func (c *symbolizationCache) ExecutableMetadata(fileID libpf.FileID, - fileName, _ string, _ libpf.InterpreterType, _ reporter.ExecutableOpener) { + fileName, _ string, _ libpf.InterpreterType, _ process.FileOpener) { c.files[fileID] = fileName } diff --git a/tracer/ebpf_integration_test.go b/tracer/ebpf_integration_test.go index 0aa7869d4..d1063950b 100644 --- a/tracer/ebpf_integration_test.go +++ b/tracer/ebpf_integration_test.go @@ -22,7 +22,7 @@ import ( "github.com/open-telemetry/opentelemetry-ebpf-profiler/host" "github.com/open-telemetry/opentelemetry-ebpf-profiler/libpf" - "github.com/open-telemetry/opentelemetry-ebpf-profiler/reporter" + "github.com/open-telemetry/opentelemetry-ebpf-profiler/process" "github.com/open-telemetry/opentelemetry-ebpf-profiler/rlimit" "github.com/open-telemetry/opentelemetry-ebpf-profiler/support" tracertypes "github.com/open-telemetry/opentelemetry-ebpf-profiler/tracer/types" @@ -95,7 +95,7 @@ func (f mockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Seco type mockReporter struct{} func (f mockReporter) ExecutableMetadata(_ libpf.FileID, _, _ string, - _ libpf.InterpreterType, _ reporter.ExecutableOpener) { + _ libpf.InterpreterType, _ process.FileOpener) { } func (f mockReporter) ReportFallbackSymbol(_ libpf.FrameID, _ string) {} func (f mockReporter) FrameMetadata(_ libpf.FileID, _ libpf.AddressOrLineno, _ libpf.SourceLineno,