Skip to content

Commit 4830c9f

Browse files
committed
tracer: support descendant PID namespaces
1 parent 8cb5715 commit 4830c9f

9 files changed

Lines changed: 349 additions & 30 deletions

support/ebpf/integration_test.ebpf.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#include "tracemgmt.h"
77
#include "types.h"
88

9-
static EBPF_INLINE void send_sample_traces(void *ctx, u64 pid)
9+
static EBPF_INLINE void send_sample_traces(void *ctx, u64 pid, u64 tid)
1010
{
1111
// Use the per CPU record for trace storage: it's too big for stack.
1212
PerCPURecord *record = get_pristine_per_cpu_record();
@@ -26,7 +26,7 @@ static EBPF_INLINE void send_sample_traces(void *ctx, u64 pid)
2626

2727
trace->comm[3] = 1;
2828
trace->pid = pid;
29-
trace->tid = pid;
29+
trace->tid = tid;
3030

3131
u64 *data = push_frame(&record->state, trace, FRAME_MARKER_NATIVE, 0, 21, 1);
3232
if (data) {
@@ -60,7 +60,7 @@ int tracepoint_integration__sched_switch(void *ctx)
6060

6161
printt("pid %d in integration test", pid);
6262

63-
send_sample_traces(ctx, pid);
63+
send_sample_traces(ctx, pid, tid);
6464

6565
return 0;
6666
}

support/ebpf/native_stack_trace.ebpf.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ BPF_RODATA_VAR(u32, stack_ptregs_offset, 0)
5454
// internal view (e.g., reporting PID 1 instead of the host PID).
5555
BPF_RODATA_VAR(bool, pid_ns_translation_enabled, false)
5656

57+
// If enabled, tasks in descendant PID namespaces are translated by walking
58+
// their PID namespace hierarchy when the kernel helper cannot resolve them.
59+
BPF_RODATA_VAR(bool, translate_descendant_pids, false)
60+
5761
// The inode number of the target PID namespace.
5862
// Obtained by calling stat() on /proc/self/ns/pid.
5963
BPF_RODATA_VAR(u64, target_pid_ns_inode, 0)
@@ -62,6 +66,17 @@ BPF_RODATA_VAR(u64, target_pid_ns_inode, 0)
6266
// Required by the bpf_get_ns_current_pid_tgid helper to uniquely
6367
// identify the namespace filesystem (nsfs) instance.
6468
BPF_RODATA_VAR(u64, target_pid_ns_dev, 0)
69+
70+
// Kernel BTF-derived layout used to translate tasks in descendant PID
71+
// namespaces into target_pid_ns_inode. bpf_get_ns_current_pid_tgid only
72+
// handles tasks whose active PID namespace exactly matches the target.
73+
BPF_RODATA_VAR(u32, task_thread_pid_offset, 0)
74+
BPF_RODATA_VAR(u32, pid_level_offset, 0)
75+
BPF_RODATA_VAR(u32, pid_numbers_offset, 0)
76+
BPF_RODATA_VAR(u32, upid_size, 0)
77+
BPF_RODATA_VAR(u32, upid_nr_offset, 0)
78+
BPF_RODATA_VAR(u32, upid_ns_offset, 0)
79+
BPF_RODATA_VAR(u32, pid_namespace_inum_offset, 0)
6580
// origin_id_sampling is set during load time.
6681
BPF_RODATA_VAR(u16, origin_id_sampling, 0)
6782

support/ebpf/tracemgmt.h

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,22 @@ extern u16 origin_id_sampling;
6868
// pid_ns_translation_enabled is declared in native_stack_trace.ebpf.c
6969
extern bool pid_ns_translation_enabled;
7070

71+
extern bool translate_descendant_pids;
72+
7173
// target_pid_ns_inode is declared in native_stack_trace.ebpf.c
7274
extern u64 target_pid_ns_inode;
7375

7476
// target_pid_ns_dev is declared in native_stack_trace.ebpf.c
7577
extern u64 target_pid_ns_dev;
7678

79+
extern u32 task_thread_pid_offset;
80+
extern u32 pid_level_offset;
81+
extern u32 pid_numbers_offset;
82+
extern u32 upid_size;
83+
extern u32 upid_nr_offset;
84+
extern u32 upid_ns_offset;
85+
extern u32 pid_namespace_inum_offset;
86+
7787
// Mirrors the kernel's struct bpf_pidns_info for use with bpf_get_ns_current_pid_tgid().
7888
// pid: thread PID as seen within the target PID namespace.
7989
// tgid: thread group ID (= process PID in userspace) within the target PID namespace.
@@ -82,6 +92,67 @@ struct bpf_pidns_info {
8292
u32 tgid;
8393
};
8494

95+
// Linux permits levels 0 through 32, including the root PID namespace.
96+
#define PID_NAMESPACE_MAX_LEVELS 33
97+
98+
// Resolve the PID of task as visible in the configured target namespace.
99+
static inline EBPF_INLINE bool get_pid_in_target_namespace(u64 task, u32 *result)
100+
{
101+
u64 pid_address = 0;
102+
if (
103+
bpf_probe_read_kernel(
104+
&pid_address, sizeof(pid_address), (void *)(task + task_thread_pid_offset)) ||
105+
pid_address == 0) {
106+
return false;
107+
}
108+
109+
u32 active_level = 0;
110+
if (bpf_probe_read_kernel(
111+
&active_level, sizeof(active_level), (void *)(pid_address + pid_level_offset))) {
112+
return false;
113+
}
114+
115+
for (u32 depth = 0; depth < PID_NAMESPACE_MAX_LEVELS; depth++) {
116+
if (depth > active_level) {
117+
break;
118+
}
119+
120+
u32 level = active_level - depth;
121+
u64 upid_address = pid_address + pid_numbers_offset + ((u64)level * upid_size);
122+
123+
u64 namespace_address = 0;
124+
if (
125+
bpf_probe_read_kernel(
126+
&namespace_address, sizeof(namespace_address), (void *)(upid_address + upid_ns_offset)) ||
127+
namespace_address == 0) {
128+
continue;
129+
}
130+
131+
u32 namespace_inode = 0;
132+
if (
133+
bpf_probe_read_kernel(
134+
&namespace_inode,
135+
sizeof(namespace_inode),
136+
(void *)(namespace_address + pid_namespace_inum_offset)) ||
137+
namespace_inode != (u32)target_pid_ns_inode) {
138+
continue;
139+
}
140+
141+
u32 translated_pid = 0;
142+
if (
143+
bpf_probe_read_kernel(
144+
&translated_pid, sizeof(translated_pid), (void *)(upid_address + upid_nr_offset)) ||
145+
translated_pid == 0) {
146+
return false;
147+
}
148+
149+
*result = translated_pid;
150+
return true;
151+
}
152+
153+
return false;
154+
}
155+
85156
// get_pid_tgid resolves the current task's PID and TGID, translating them into the
86157
// configured target PID namespace if pid_ns_translation_enabled is set. Returns false if
87158
// the task could not be resolved (e.g. it is not part of the target namespace), in which
@@ -92,16 +163,33 @@ static inline EBPF_INLINE bool get_pid_tgid(u32 *pid, u32 *tid)
92163
struct bpf_pidns_info ns_info = {0};
93164
long ret = bpf_get_ns_current_pid_tgid(
94165
target_pid_ns_dev, target_pid_ns_inode, &ns_info, sizeof(ns_info));
95-
if (ret < 0) {
96-
// Task is not in the target namespace, signal caller to skip it.
166+
if (ret == 0) {
167+
// ns_info.tgid is the thread group ID (= process PID in userspace) in the namespace.
168+
// ns_info.pid is the thread PID in the namespace.
169+
// Match the convention of the non-namespace path where pid holds the TGID.
170+
*pid = ns_info.tgid;
171+
*tid = ns_info.pid;
172+
return true;
173+
}
174+
175+
if (!translate_descendant_pids) {
97176
return false;
98177
}
99-
// ns_info.tgid is the thread group ID (= process PID in userspace) in the namespace.
100-
// ns_info.pid is the thread PID in the namespace.
101-
// Match the convention of the non-namespace path where pid holds the TGID.
102-
*pid = ns_info.tgid;
103-
*tid = ns_info.pid;
104-
return true;
178+
179+
u64 task = bpf_get_current_task();
180+
u64 group_leader = 0;
181+
if (
182+
task == 0 ||
183+
bpf_probe_read_kernel(
184+
&group_leader, sizeof(group_leader), (void *)(task + task_group_leader_offset)) ||
185+
group_leader == 0) {
186+
return false;
187+
}
188+
189+
// A helper miss can mean either a descendant namespace or an unrelated
190+
// namespace. Both translations validate the target namespace inode, so
191+
// untranslated host PIDs are never returned from this path.
192+
return get_pid_in_target_namespace(group_leader, pid) && get_pid_in_target_namespace(task, tid);
105193
}
106194

107195
// bpf_get_current_pid_tgid returns (tgid << 32 | pid).

support/ebpf/tracer.ebpf.amd64

54.4 KB
Binary file not shown.

support/ebpf/tracer.ebpf.arm64

54.4 KB
Binary file not shown.

tracer/ebpf_integration_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ package tracer_test
77

88
import (
99
"context"
10+
"fmt"
1011
"math"
1112
"os"
13+
"os/exec"
1214
"runtime"
1315
"slices"
1416
"sync"
17+
"syscall"
1518
"testing"
1619
"time"
1720

@@ -288,3 +291,100 @@ func TestAllTracers(t *testing.T) {
288291
})
289292
}
290293
}
294+
295+
func TestPIDNamespaceTranslationFromDescendant(t *testing.T) {
296+
if os.Getenv("OTEL_EBPF_PROFILER_PIDNS_CHILD") == "1" {
297+
require.Equal(t, 1, os.Getpid())
298+
runtime.GOMAXPROCS(2)
299+
deadline := time.Now().Add(3 * time.Second)
300+
ready := make(chan struct{})
301+
done := make(chan struct{})
302+
go func() {
303+
runtime.LockOSThread()
304+
close(ready)
305+
for time.Now().Before(deadline) {
306+
runtime.Gosched()
307+
}
308+
close(done)
309+
}()
310+
<-ready
311+
runtime.LockOSThread()
312+
for time.Now().Before(deadline) {
313+
runtime.Gosched()
314+
}
315+
<-done
316+
return
317+
}
318+
319+
_, btfErr := os.Stat("/sys/kernel/btf/vmlinux")
320+
tr, err := tracer.NewTracer(t.Context(), &tracer.Config{
321+
Intervals: &mockIntervals{},
322+
InterpretersConfig: interpreterconfig.AllInterpreters(),
323+
SamplesPerSecond: 20,
324+
ProbabilisticInterval: 100,
325+
ProbabilisticThreshold: 100,
326+
PIDNamespaceTranslation: true,
327+
TranslateDescendantPIDs: true,
328+
})
329+
if os.IsNotExist(btfErr) {
330+
require.ErrorContains(t, err, "PID translation from descendant namespaces requires readable kernel BTF")
331+
return
332+
}
333+
require.NoError(t, btfErr)
334+
require.NoError(t, err)
335+
defer tr.Close()
336+
337+
traceChan := make(chan *libpf.EbpfTrace, 1024)
338+
require.NoError(t, tr.StartMapMonitors(t.Context(), traceChan))
339+
340+
coll, err := support.LoadCollectionSpec()
341+
require.NoError(t, err)
342+
require.NoError(t, tracer.RewriteMaps(coll, tr.GetEbpfMaps()))
343+
344+
restoreRlimit, err := rlimit.MaximizeMemlock()
345+
require.NoError(t, err)
346+
defer restoreRlimit()
347+
348+
prog, err := cebpf.NewProgram(coll.Programs["tracepoint_integration__sched_switch"])
349+
require.NoError(t, err)
350+
defer prog.Close()
351+
352+
event, err := link.Tracepoint("sched", "sched_switch", prog, nil)
353+
require.NoError(t, err)
354+
defer event.Close()
355+
356+
cmd := exec.Command(os.Args[0], "-test.run=^TestPIDNamespaceTranslationFromDescendant$")
357+
cmd.Env = append(os.Environ(), "OTEL_EBPF_PROFILER_PIDNS_CHILD=1")
358+
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWPID}
359+
require.NoError(t, cmd.Start())
360+
t.Cleanup(func() {
361+
if cmd.ProcessState == nil {
362+
_ = cmd.Process.Kill()
363+
_ = cmd.Wait()
364+
}
365+
})
366+
367+
targetPID := libpf.PID(cmd.Process.Pid)
368+
timer := time.NewTimer(5 * time.Second)
369+
defer timer.Stop()
370+
371+
for {
372+
select {
373+
case <-timer.C:
374+
t.Fatalf("no trace received for descendant namespace PID %d", targetPID)
375+
case <-tr.Done():
376+
t.Fatal("tracer encountered an unrecoverable error")
377+
case trace := <-traceChan:
378+
comm := trace.Comm.String()
379+
if len(comm) < 3 || comm[:3] != "\xAA\xBB\xCC" ||
380+
trace.PID != targetPID || trace.TID == targetPID {
381+
continue
382+
}
383+
_, err := os.Stat(fmt.Sprintf("/proc/%d/task/%d", trace.PID, trace.TID))
384+
require.NoError(t, err, "translated TID does not belong to the target process")
385+
require.NoError(t, event.Close())
386+
require.NoError(t, cmd.Wait())
387+
return
388+
}
389+
}
390+
}

tracer/probes.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ type sysVar struct {
109109
// both the include list in CollectionSpecWith and the apply pass in applySystemVars.
110110
func (c *ProbeContext) sysVarSetters() []sysVar {
111111
sv := c.sysVars
112-
return []sysVar{
112+
vars := []sysVar{
113113
{"inverse_pac_mask", sv.inverse_pac_mask},
114114
{"tpbase_offset", sv.tpbase_offset},
115115
{"task_stack_offset", sv.task_stack_offset},
@@ -120,6 +120,7 @@ func (c *ProbeContext) sysVarSetters() []sysVar {
120120
{"task_group_leader_offset", sv.task_group_leader_offset},
121121
{"task_start_time_offset", sv.task_start_time_offset},
122122
}
123+
return append(vars, sv.pidNamespaceVars()...)
123124
}
124125

125126
// applySystemVars writes the system configuration values determined at tracer startup into

0 commit comments

Comments
 (0)