Skip to content

Commit f44ab59

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

7 files changed

Lines changed: 324 additions & 30 deletions

File tree

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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ BPF_RODATA_VAR(u64, target_pid_ns_inode, 0)
6262
// Required by the bpf_get_ns_current_pid_tgid helper to uniquely
6363
// identify the namespace filesystem (nsfs) instance.
6464
BPF_RODATA_VAR(u64, target_pid_ns_dev, 0)
65+
66+
// Kernel BTF-derived layout used to translate tasks in descendant PID
67+
// namespaces into target_pid_ns_inode. bpf_get_ns_current_pid_tgid only
68+
// handles tasks whose active PID namespace exactly matches the target.
69+
BPF_RODATA_VAR(u32, task_thread_pid_offset, 0)
70+
BPF_RODATA_VAR(u32, pid_level_offset, 0)
71+
BPF_RODATA_VAR(u32, pid_numbers_offset, 0)
72+
BPF_RODATA_VAR(u32, upid_size, 0)
73+
BPF_RODATA_VAR(u32, upid_nr_offset, 0)
74+
BPF_RODATA_VAR(u32, upid_ns_offset, 0)
75+
BPF_RODATA_VAR(u32, pid_namespace_inum_offset, 0)
6576
// origin_id_sampling is set during load time.
6677
BPF_RODATA_VAR(u16, origin_id_sampling, 0)
6778

support/ebpf/tracemgmt.h

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ extern u64 target_pid_ns_inode;
7474
// target_pid_ns_dev is declared in native_stack_trace.ebpf.c
7575
extern u64 target_pid_ns_dev;
7676

77+
extern u32 task_thread_pid_offset;
78+
extern u32 pid_level_offset;
79+
extern u32 pid_numbers_offset;
80+
extern u32 upid_size;
81+
extern u32 upid_nr_offset;
82+
extern u32 upid_ns_offset;
83+
extern u32 pid_namespace_inum_offset;
84+
7785
// Mirrors the kernel's struct bpf_pidns_info for use with bpf_get_ns_current_pid_tgid().
7886
// pid: thread PID as seen within the target PID namespace.
7987
// tgid: thread group ID (= process PID in userspace) within the target PID namespace.
@@ -82,6 +90,67 @@ struct bpf_pidns_info {
8290
u32 tgid;
8391
};
8492

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

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

tracer/ebpf_integration_test.go

Lines changed: 93 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,93 @@ 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+
tr, err := tracer.NewTracer(t.Context(), &tracer.Config{
320+
Intervals: &mockIntervals{},
321+
InterpretersConfig: interpreterconfig.AllInterpreters(),
322+
SamplesPerSecond: 20,
323+
ProbabilisticInterval: 100,
324+
ProbabilisticThreshold: 100,
325+
PIDNamespaceTranslation: true,
326+
})
327+
require.NoError(t, err)
328+
defer tr.Close()
329+
330+
traceChan := make(chan *libpf.EbpfTrace, 1024)
331+
require.NoError(t, tr.StartMapMonitors(t.Context(), traceChan))
332+
333+
coll, err := support.LoadCollectionSpec()
334+
require.NoError(t, err)
335+
require.NoError(t, tracer.RewriteMaps(coll, tr.GetEbpfMaps()))
336+
337+
restoreRlimit, err := rlimit.MaximizeMemlock()
338+
require.NoError(t, err)
339+
defer restoreRlimit()
340+
341+
prog, err := cebpf.NewProgram(coll.Programs["tracepoint_integration__sched_switch"])
342+
require.NoError(t, err)
343+
defer prog.Close()
344+
345+
event, err := link.Tracepoint("sched", "sched_switch", prog, nil)
346+
require.NoError(t, err)
347+
defer event.Close()
348+
349+
cmd := exec.Command(os.Args[0], "-test.run=^TestPIDNamespaceTranslationFromDescendant$")
350+
cmd.Env = append(os.Environ(), "OTEL_EBPF_PROFILER_PIDNS_CHILD=1")
351+
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWPID}
352+
require.NoError(t, cmd.Start())
353+
t.Cleanup(func() {
354+
if cmd.ProcessState == nil {
355+
_ = cmd.Process.Kill()
356+
_ = cmd.Wait()
357+
}
358+
})
359+
360+
targetPID := libpf.PID(cmd.Process.Pid)
361+
timer := time.NewTimer(5 * time.Second)
362+
defer timer.Stop()
363+
364+
for {
365+
select {
366+
case <-timer.C:
367+
t.Fatalf("no trace received for descendant namespace PID %d", targetPID)
368+
case <-tr.Done():
369+
t.Fatal("tracer encountered an unrecoverable error")
370+
case trace := <-traceChan:
371+
comm := trace.Comm.String()
372+
if len(comm) < 3 || comm[:3] != "\xAA\xBB\xCC" ||
373+
trace.PID != targetPID || trace.TID == targetPID {
374+
continue
375+
}
376+
_, err := os.Stat(fmt.Sprintf("/proc/%d/task/%d", trace.PID, trace.TID))
377+
require.NoError(t, err, "translated TID does not belong to the target process")
378+
require.NoError(t, event.Close())
379+
require.NoError(t, cmd.Wait())
380+
return
381+
}
382+
}
383+
}

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)