From 760c7334993c8a7e7aa957cf0bc2400416e57094 Mon Sep 17 00:00:00 2001 From: Tommy Reilly Date: Wed, 1 Apr 2026 15:12:12 -0400 Subject: [PATCH 1/3] golabels: tolerate duplicate detach when Go plugins are loaded When a Go process loads shared libraries via plugin.Open(), the profiler creates separate golabels interpreter instances for each Go ELF file in the process. Since they all share the same PID-keyed eBPF map entry, the second Detach fails with "key does not exist". Fix by wrapping the error in DeleteProcData with %w so callers can inspect it, and ignoring ErrKeyNotExist in golabels Detach. Fixes open-telemetry/opentelemetry-ebpf-profiler#1301 --- interpreter/golabels/golabels.go | 11 ++++++++++- processmanager/ebpf/ebpf.go | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/interpreter/golabels/golabels.go b/interpreter/golabels/golabels.go index 71e00e284..a90a71b9a 100644 --- a/interpreter/golabels/golabels.go +++ b/interpreter/golabels/golabels.go @@ -9,6 +9,8 @@ import ( "go/version" "unsafe" + cebpf "github.com/cilium/ebpf" + "go.opentelemetry.io/ebpf-profiler/internal/log" "go.opentelemetry.io/ebpf-profiler/interpreter" @@ -40,7 +42,14 @@ func (d *data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, } func (d *data) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { - return ebpf.DeleteProcData(libpf.GoLabels, pid) + // Go plugins share the runtime with the main binary, so multiple Go ELF + // files in the same process produce duplicate golabels instances that all + // write/delete the same eBPF map entry. Tolerate the key already being + // removed by another instance. + if err := ebpf.DeleteProcData(libpf.GoLabels, pid); err != nil && !errors.Is(err, cebpf.ErrKeyNotExist) { + return err + } + return nil } func (d *data) Unload(_ interpreter.EbpfHandler) {} diff --git a/processmanager/ebpf/ebpf.go b/processmanager/ebpf/ebpf.go index 256812cce..0302f9d4d 100644 --- a/processmanager/ebpf/ebpf.go +++ b/processmanager/ebpf/ebpf.go @@ -216,7 +216,7 @@ func (impl *ebpfMapsImpl) DeleteProcData(typ libpf.InterpreterType, pid libpf.PI pid32 := uint32(pid) if err := ebpfMap.Delete(unsafe.Pointer(&pid32)); err != nil { - return fmt.Errorf("failed to remove info: %v", err) + return fmt.Errorf("failed to remove info: %w", err) } return nil } From b00ef1f39f9bb109144303293e50d2ebcb32b6b4 Mon Sep 17 00:00:00 2001 From: Tommy Reilly Date: Thu, 2 Apr 2026 04:19:03 -0400 Subject: [PATCH 2/3] Add logging --- interpreter/golabels/golabels.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/interpreter/golabels/golabels.go b/interpreter/golabels/golabels.go index a90a71b9a..29c4a62c3 100644 --- a/interpreter/golabels/golabels.go +++ b/interpreter/golabels/golabels.go @@ -46,10 +46,12 @@ func (d *data) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { // files in the same process produce duplicate golabels instances that all // write/delete the same eBPF map entry. Tolerate the key already being // removed by another instance. - if err := ebpf.DeleteProcData(libpf.GoLabels, pid); err != nil && !errors.Is(err, cebpf.ErrKeyNotExist) { - return err + err := ebpf.DeleteProcData(libpf.GoLabels, pid) + if errors.Is(err, cebpf.ErrKeyNotExist) { + log.Debugf("golabels entry for %d already removed", pid) + return nil } - return nil + return err } func (d *data) Unload(_ interpreter.EbpfHandler) {} From 07d594b9ff2cf1eac4f04cd6aeb99f0355df9662 Mon Sep 17 00:00:00 2001 From: Tommy Reilly Date: Fri, 3 Apr 2026 07:27:38 -0400 Subject: [PATCH 3/3] Ignore Go plugins Go plugins can't change go labels version/offset information nor the TLS information so ignore them by detecting when a go exe is a shared object. --- interpreter/golabels/golabels.go | 33 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/interpreter/golabels/golabels.go b/interpreter/golabels/golabels.go index 29c4a62c3..1e733d0a6 100644 --- a/interpreter/golabels/golabels.go +++ b/interpreter/golabels/golabels.go @@ -4,13 +4,12 @@ package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" import ( + "debug/elf" "errors" "fmt" "go/version" "unsafe" - cebpf "github.com/cilium/ebpf" - "go.opentelemetry.io/ebpf-profiler/internal/log" "go.opentelemetry.io/ebpf-profiler/interpreter" @@ -42,16 +41,7 @@ func (d *data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, } func (d *data) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { - // Go plugins share the runtime with the main binary, so multiple Go ELF - // files in the same process produce duplicate golabels instances that all - // write/delete the same eBPF map entry. Tolerate the key already being - // removed by another instance. - err := ebpf.DeleteProcData(libpf.GoLabels, pid) - if errors.Is(err, cebpf.ErrKeyNotExist) { - log.Debugf("golabels entry for %d already removed", pid) - return nil - } - return err + return ebpf.DeleteProcData(libpf.GoLabels, pid) } func (d *data) Unload(_ interpreter.EbpfHandler) {} @@ -70,6 +60,25 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete return nil, nil } + // Go plugins are shared objects that share the runtime with the main + // binary. The offsets we need are determined by the main binary so + // there is no reason to create a duplicate golabels instance for + // a plugin. A shared library is ET_DYN without a PT_INTERP segment + // (PIE executables are also ET_DYN but have PT_INTERP). + if file.Type == elf.ET_DYN { + hasInterp := false + for i := range file.Progs { + if file.Progs[i].Type == elf.PT_INTERP { + hasInterp = true + break + } + } + if !hasInterp { + log.Debugf("file %s is a Go shared library, skipping golabels", info.FileName()) + return nil, nil + } + } + if version.Compare(goVersion, "go1.27") >= 0 { return nil, fmt.Errorf("unsupported Go version %s (need >= 1.13 and <= 1.26)", goVersion) }