Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions internal/buildid/buildid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package buildid

import (
"bytes"
"debug/elf"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
)

const (
gnuBuildIDNoteName = "GNU\x00"
gnuBuildIDNoteType = 3
)

var ErrNotFound = errors.New("GNU build ID not found")

// Clean treats build IDs as C strings. This removes the trailing NUL byte that
// Go build IDs can carry when read directly from ELF notes.
func Clean(buildID string) string {
if idx := strings.IndexByte(buildID, 0); idx >= 0 {
return buildID[:idx]
}
return buildID
}

// Resolve prefers the executable's GNU build ID when the file is available,
// falling back to a cleaned build ID captured earlier by the caller.
func Resolve(executablePath, fallback string) string {
if executablePath != "" {
if buildID, err := GNU(executablePath); err == nil && buildID != "" {
return buildID
}
}
return Clean(fallback)
}

// GNU reads the GNU build ID from an ELF executable.
func GNU(executablePath string) (string, error) {
f, err := elf.Open(executablePath)
if err != nil {
return "", err
}
defer f.Close()

for _, section := range f.Sections {
if section.Type != elf.SHT_NOTE {
continue
}

data, err := section.Data()
if err != nil {
return "", fmt.Errorf("read note section %s: %w", section.Name, err)
}

buildID, ok, err := gnuFromNotes(data, f.ByteOrder, section.Addralign)
if err != nil {
return "", fmt.Errorf("parse note section %s: %w", section.Name, err)
}
if ok {
return buildID, nil
}
}

for _, prog := range f.Progs {
if prog.Type != elf.PT_NOTE {
continue
}

data, err := io.ReadAll(io.LimitReader(prog.Open(), int64(prog.Filesz)))
if err != nil {
return "", fmt.Errorf("read note program: %w", err)
}

buildID, ok, err := gnuFromNotes(data, f.ByteOrder, prog.Align)
if err != nil {
return "", fmt.Errorf("parse note program: %w", err)
}
if ok {
return buildID, nil
}
}

return "", ErrNotFound
}

func gnuFromNotes(data []byte, order binary.ByteOrder, alignment uint64) (string, bool, error) {
for len(data) >= 12 {
nameSize := order.Uint32(data[0:4])
descSize := order.Uint32(data[4:8])
noteType := order.Uint32(data[8:12])

nameStart := uint64(12)
nameEnd := nameStart + uint64(nameSize)
if nameEnd > uint64(len(data)) {
return "", false, io.ErrUnexpectedEOF
}

descStart := align(nameEnd, alignment)
descEnd := descStart + uint64(descSize)
if descEnd > uint64(len(data)) {
return "", false, io.ErrUnexpectedEOF
}

name := data[nameStart:nameEnd]
if noteType == gnuBuildIDNoteType && bytes.Equal(name, []byte(gnuBuildIDNoteName)) {
return hex.EncodeToString(data[descStart:descEnd]), true, nil
}

next := align(descEnd, alignment)
if next > uint64(len(data)) {
break
}
if next == 0 {
return "", false, errors.New("invalid ELF note alignment")
}
data = data[next:]
}

return "", false, nil
}

func align(offset, alignment uint64) uint64 {
if alignment == 0 {
alignment = 4
}
return (offset + alignment - 1) &^ (alignment - 1)
}
42 changes: 42 additions & 0 deletions internal/buildid/buildid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package buildid

import (
"bytes"
"encoding/binary"
"testing"

"github.com/stretchr/testify/require"
)

func TestClean(t *testing.T) {
t.Parallel()

require.Equal(t, "abc", Clean("abc"))
require.Equal(t, "abc", Clean("abc\x00"))
require.Equal(t, "abc", Clean("abc\x00ignored"))
}

func TestGNUFromNotes(t *testing.T) {
t.Parallel()

buildID := []byte{0xde, 0xad, 0xbe, 0xef}

var buf bytes.Buffer
require.NoError(t, binary.Write(&buf, binary.LittleEndian, uint32(len(gnuBuildIDNoteName))))
require.NoError(t, binary.Write(&buf, binary.LittleEndian, uint32(len(buildID))))
require.NoError(t, binary.Write(&buf, binary.LittleEndian, uint32(gnuBuildIDNoteType)))
buf.WriteString(gnuBuildIDNoteName)
buf.Write(buildID)

got, ok, err := gnuFromNotes(buf.Bytes(), binary.LittleEndian, 4)
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "deadbeef", got)
}

func TestResolveFallsBackToCleanBuildID(t *testing.T) {
t.Parallel()

require.Equal(t, "go-build-id", Resolve("/does/not/exist", "go-build-id\x00"))
require.Equal(t, "", Resolve("", "\x00"))
}
8 changes: 8 additions & 0 deletions oom/oomprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
profilestorepb "buf.build/gen/go/parca-dev/parca/protocolbuffers/go/parca/profilestore/v1alpha1"

"github.com/parca-dev/oomprof/oomprof"
"github.com/parca-dev/parca-agent/internal/buildid"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
Expand Down Expand Up @@ -55,6 +56,13 @@ func handleOOMProfData(ctx context.Context, profileCh <-chan oomprof.ProfileData

// sendOOMProfileToParca sends an OOM profile directly to Parca using the gRPC client
func sendOOMProfile(ctx context.Context, client profilestoregrpc.ProfileStoreServiceClient, profileData oomprof.ProfileData, nodeName string, externalLabels map[string]string) error {
for _, mapping := range profileData.Profile.Mapping {
if mapping == nil {
continue
}
mapping.BuildID = buildid.Resolve(mapping.File, mapping.BuildID)
}

// Convert profile to raw bytes
var buf bytes.Buffer
err := profileData.Profile.Write(&buf)
Expand Down
23 changes: 19 additions & 4 deletions reporter/parca_reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"go.opentelemetry.io/ebpf-profiler/reporter/samples"
"go.opentelemetry.io/ebpf-profiler/support"

"github.com/parca-dev/parca-agent/internal/buildid"
"github.com/parca-dev/parca-agent/metrics"
"github.com/parca-dev/parca-agent/reporter/metadata"
)
Expand Down Expand Up @@ -503,7 +504,11 @@ func (r *ParcaReporter) appendLocationV2(frame libpf.Frame) uint32 {
case oomprofMemoryFrame:
b.locFrameType.AppendString(libpf.NativeFrame.String())
b.locMappingFile.AppendString(frame.SourceFile.String())
b.locMappingID.AppendString(frame.FunctionName.String())
if frame.FunctionName.String() == "" {
b.locMappingID.AppendNull()
} else {
b.locMappingID.AppendString(frame.FunctionName.String())
}
// No lines for oomprof frames

default:
Expand Down Expand Up @@ -712,6 +717,12 @@ func (r *ParcaReporter) SampleEvents(oomprofSamples []oomprof.Sample, meta oompr
TID: libpf.PID(meta.PID), // For oomprof, TID is same as PID
}

mappingFile := meta.ExecutablePath
if mappingFile == "" {
mappingFile = "UNKNOWN"
}
mappingBuildID := buildid.Resolve(meta.ExecutablePath, meta.BuildID)

for _, sample := range oomprofSamples {
// Create a trace from the oomprof sample
t := &libpf.Trace{}
Expand All @@ -721,8 +732,8 @@ func (r *ParcaReporter) SampleEvents(oomprofSamples []oomprof.Sample, meta oompr
t.Frames.Append(&libpf.Frame{
Type: oomprofMemoryFrame,
AddressOrLineno: libpf.AddressOrLineno(addr),
FunctionName: libpf.Intern(meta.BuildID), // Stash the BuildID here
SourceFile: libpf.Intern(meta.ExecutablePath), // MappingFile
FunctionName: libpf.Intern(mappingBuildID), // Stash the BuildID here
SourceFile: libpf.Intern(mappingFile), // MappingFile
})
}

Expand Down Expand Up @@ -1669,7 +1680,11 @@ func (r *ParcaReporter) buildStacktraceRecord(ctx context.Context, stacktraceIDs
// This is a special frame that is used to report OOMProf samples.
w.FrameType.AppendString(libpf.NativeFrame.String())
w.MappingFile.AppendString(frame.SourceFile.String())
w.MappingBuildID.AppendString(frame.FunctionName.String())
if frame.FunctionName.String() == "" {
w.MappingBuildID.AppendNull()
} else {
w.MappingBuildID.AppendString(frame.FunctionName.String())
}
w.Lines.Append(false)
isComplete = false
default:
Expand Down