Skip to content
Draft
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
1 change: 1 addition & 0 deletions pkg/ebpf/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ go_library(
"//pkg/ebpf/bytecode",
"//pkg/ebpf/maps",
"//pkg/ebpf/names",
"//pkg/ebpf/telemetry",
"//pkg/process/util",
"//pkg/remoteconfig/state",
"//pkg/system-probe/config",
Expand Down
7 changes: 7 additions & 0 deletions pkg/ebpf/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
manager "github.com/DataDog/ebpf-manager"

"github.com/DataDog/datadog-agent/pkg/ebpf/names"
ebpftelemetry "github.com/DataDog/datadog-agent/pkg/ebpf/telemetry"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

Expand Down Expand Up @@ -186,5 +187,11 @@ func (m *Manager) Start() error {
return err
}

// Hold the perf telemetry write lock so that concurrent Collect calls
// (which take RLock) don't race on PerfMap/RingBuffer telemetry fields
// that Manager.Start writes during initialization.
ebpftelemetry.LockForWrite()
defer ebpftelemetry.UnlockForWrite()
Comment thread
pgimalac marked this conversation as resolved.

return m.Manager.Start()
}
1 change: 1 addition & 0 deletions pkg/ebpf/telemetry/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dd_agent_go_test(
"errors_collector_linux_test.go",
"errors_telemetry_test.go",
"modifier_test.go",
"perf_metrics_test.go",
"types_linux_test.go",
],
data = glob(["testdata/**"]),
Expand Down
16 changes: 16 additions & 0 deletions pkg/ebpf/telemetry/perf_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,22 @@ func (p *perfUsageCollector) registerRingBufferChannel(rb *manager.RingBuffer, c
p.ringChannelLenFuncs[rb] = channelLenFunc
}

// LockForWrite acquires the perf telemetry write lock. Callers (e.g. ebpf.Manager.Start)
// must hold this lock while writing to PerfMap/RingBuffer telemetry fields so that
// concurrent Collect calls (which take RLock) don't race on those fields.
func LockForWrite() {
if perfCollector != nil {
perfCollector.mtx.Lock()
}
}

// UnlockForWrite releases the perf telemetry write lock.
func UnlockForWrite() {
if perfCollector != nil {
perfCollector.mtx.Unlock()
}
}

// UnregisterTelemetry unregisters the PerfMap and RingBuffers from telemetry
func UnregisterTelemetry(m *manager.Manager) {
if perfCollector != nil {
Expand Down
85 changes: 85 additions & 0 deletions pkg/ebpf/telemetry/perf_metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 2016-present Datadog, Inc.

//go:build linux

package telemetry

import (
"sync"
"testing"

"github.com/prometheus/client_golang/prometheus"
)

// TestLockForWriteBlocksCollect verifies that LockForWrite prevents
// concurrent Collect calls from reading perf telemetry fields. This is
// the synchronization mechanism that prevents the data race between
// perfUsageCollector.Collect (reader) and ebpf.Manager.Start (writer).
//
// The actual race requires eBPF support to trigger (Manager.Start writes
// to PerfMap/RingBuffer internal fields), so this test validates the lock
// mechanism itself rather than the full race.
func TestLockForWriteBlocksCollect(t *testing.T) {
c := NewPerfUsageCollector()
collector := c.(*perfUsageCollector)

// Take the write lock (simulating Manager.Start holding it)
LockForWrite()

collectStarted := make(chan struct{})
collectDone := make(chan struct{})
go func() {
close(collectStarted)
ch := make(chan prometheus.Metric, 1)
go func() {
for m := range ch {
_ = m
}
}()
collector.Collect(ch)
close(ch)
close(collectDone)
}()

// Wait for the goroutine to start, then confirm Collect is blocked
<-collectStarted
select {
case <-collectDone:
t.Fatal("Collect completed while write lock was held")
default:
// expected: Collect is blocked
}

// Release the write lock — Collect should now complete deterministically
UnlockForWrite()
<-collectDone
}

// TestCollectConcurrentSerializes verifies that multiple Collect calls
// serialize properly under the mutex without deadlocking.
func TestCollectConcurrentSerializes(t *testing.T) {
t.Parallel()
c := NewPerfUsageCollector()
collector := c.(*perfUsageCollector)

const goroutines = 4
var wg sync.WaitGroup

for range goroutines {
wg.Go(func() {
ch := make(chan prometheus.Metric, 10)
go func() {
for m := range ch {
_ = m
}
}()
collector.Collect(ch)
close(ch)
})
}

wg.Wait()
}
Loading