Skip to content

Commit 4660aeb

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

5 files changed

Lines changed: 251 additions & 16 deletions

File tree

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+
#define PID_NAMESPACE_MAX_DEPTH 4
94+
95+
// Resolve the PID of task as visible in the configured target namespace.
96+
static inline EBPF_INLINE bool get_pid_in_target_namespace(u64 task, u32 *result)
97+
{
98+
u64 pid_address = 0;
99+
if (
100+
bpf_probe_read_kernel(
101+
&pid_address, sizeof(pid_address), (void *)(task + task_thread_pid_offset)) ||
102+
pid_address == 0) {
103+
return false;
104+
}
105+
106+
u32 active_level = 0;
107+
if (bpf_probe_read_kernel(
108+
&active_level, sizeof(active_level), (void *)(pid_address + pid_level_offset))) {
109+
return false;
110+
}
111+
112+
#pragma unroll
113+
for (u32 depth = 0; depth < PID_NAMESPACE_MAX_DEPTH; 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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"context"
1010
"math"
1111
"os"
12+
"os/exec"
1213
"runtime"
1314
"slices"
1415
"sync"
16+
"syscall"
1517
"testing"
1618
"time"
1719

@@ -288,3 +290,77 @@ func TestAllTracers(t *testing.T) {
288290
})
289291
}
290292
}
293+
294+
func TestPIDNamespaceTranslationFromDescendant(t *testing.T) {
295+
if os.Getenv("OTEL_EBPF_PROFILER_PIDNS_CHILD") == "1" {
296+
require.Equal(t, 1, os.Getpid())
297+
deadline := time.Now().Add(2 * time.Second)
298+
for time.Now().Before(deadline) {
299+
runtime.Gosched()
300+
}
301+
return
302+
}
303+
304+
tr, err := tracer.NewTracer(t.Context(), &tracer.Config{
305+
Intervals: &mockIntervals{},
306+
InterpretersConfig: interpreterconfig.AllInterpreters(),
307+
SamplesPerSecond: 20,
308+
ProbabilisticInterval: 100,
309+
ProbabilisticThreshold: 100,
310+
PIDNamespaceTranslation: true,
311+
})
312+
require.NoError(t, err)
313+
defer tr.Close()
314+
315+
traceChan := make(chan *libpf.EbpfTrace, 1024)
316+
require.NoError(t, tr.StartMapMonitors(t.Context(), traceChan))
317+
318+
coll, err := support.LoadCollectionSpec()
319+
require.NoError(t, err)
320+
require.NoError(t, tracer.RewriteMaps(coll, tr.GetEbpfMaps()))
321+
322+
restoreRlimit, err := rlimit.MaximizeMemlock()
323+
require.NoError(t, err)
324+
defer restoreRlimit()
325+
326+
prog, err := cebpf.NewProgram(coll.Programs["tracepoint_integration__sched_switch"])
327+
require.NoError(t, err)
328+
defer prog.Close()
329+
330+
event, err := link.Tracepoint("sched", "sched_switch", prog, nil)
331+
require.NoError(t, err)
332+
defer event.Close()
333+
334+
cmd := exec.Command(os.Args[0], "-test.run=^TestPIDNamespaceTranslationFromDescendant$")
335+
cmd.Env = append(os.Environ(), "OTEL_EBPF_PROFILER_PIDNS_CHILD=1")
336+
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWPID}
337+
require.NoError(t, cmd.Start())
338+
t.Cleanup(func() {
339+
if cmd.ProcessState == nil {
340+
_ = cmd.Process.Kill()
341+
_ = cmd.Wait()
342+
}
343+
})
344+
345+
targetPID := libpf.PID(cmd.Process.Pid)
346+
timer := time.NewTimer(5 * time.Second)
347+
defer timer.Stop()
348+
349+
for {
350+
select {
351+
case <-timer.C:
352+
t.Fatalf("no trace received for descendant namespace PID %d", targetPID)
353+
case <-tr.Done():
354+
t.Fatal("tracer encountered an unrecoverable error")
355+
case trace := <-traceChan:
356+
comm := trace.Comm.String()
357+
if len(comm) < 3 || comm[:3] != "\xAA\xBB\xCC" || trace.PID != targetPID {
358+
continue
359+
}
360+
require.Equal(t, targetPID, trace.TID)
361+
require.NoError(t, event.Close())
362+
require.NoError(t, cmd.Wait())
363+
return
364+
}
365+
}
366+
}

tracer/systemconfig.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type SysConfigVars struct {
4141
vma_vm_flags_offset uint32
4242
task_group_leader_offset uint32
4343
task_start_time_offset uint32
44+
pidNamespaceOffsets map[string]uint32
4445
}
4546

4647
var (
@@ -139,8 +140,52 @@ func parseVMAOffsets(spec *btf.Spec, vars *SysConfigVars) {
139140
vars.vma_vm_flags_offset = uint32(flagsOffset)
140141
}
141142

143+
func parsePIDNamespaceOffsets(spec *btf.Spec, vars *SysConfigVars) error {
144+
fields := []struct {
145+
typeName string
146+
field string
147+
variable string
148+
}{
149+
{typeName: "task_struct", field: "thread_pid", variable: "task_thread_pid_offset"},
150+
{typeName: "pid", field: "level", variable: "pid_level_offset"},
151+
{typeName: "pid", field: "numbers", variable: "pid_numbers_offset"},
152+
{typeName: "upid", field: "nr", variable: "upid_nr_offset"},
153+
{typeName: "upid", field: "ns", variable: "upid_ns_offset"},
154+
{typeName: "pid_namespace", field: "ns.inum", variable: "pid_namespace_inum_offset"},
155+
}
156+
157+
vars.pidNamespaceOffsets = make(map[string]uint32, len(fields)+1)
158+
for _, field := range fields {
159+
typ, err := spec.AnyTypeByName(field.typeName)
160+
if err != nil {
161+
return err
162+
}
163+
offset, err := calculateFieldOffset(typ, field.field)
164+
if err != nil {
165+
return err
166+
}
167+
vars.pidNamespaceOffsets[field.variable] = uint32(offset)
168+
}
169+
170+
upid, err := spec.AnyTypeByName("upid")
171+
if err != nil {
172+
return err
173+
}
174+
upidSize, err := btf.Sizeof(upid)
175+
if err != nil {
176+
return err
177+
}
178+
if upidSize <= 0 {
179+
return fmt.Errorf("invalid kernel BTF upid size %d", upidSize)
180+
}
181+
vars.pidNamespaceOffsets["upid_size"] = uint32(upidSize)
182+
return nil
183+
}
184+
142185
// parseBTF resolves the SystemConfig data from kernel BTF
143-
func parseBTF(vars *SysConfigVars, needTPBase, needProcessStartTime bool) error {
186+
func parseBTF(vars *SysConfigVars, needTPBase, needProcessStartTime,
187+
needPIDNamespaceTranslation bool,
188+
) error {
144189
fh, err := os.Open("/sys/kernel/btf/vmlinux")
145190
if err != nil {
146191
return err
@@ -171,19 +216,27 @@ func parseBTF(vars *SysConfigVars, needTPBase, needProcessStartTime bool) error
171216
}
172217
}
173218

174-
if needProcessStartTime {
219+
if needProcessStartTime || needPIDNamespaceTranslation {
175220
groupLeaderOffset, err := calculateFieldOffset(taskStruct, "group_leader")
176221
if err != nil {
177222
return err
178223
}
179224
vars.task_group_leader_offset = uint32(groupLeaderOffset)
225+
}
180226

227+
if needProcessStartTime {
181228
startTimeOffset, err := calculateFieldOffset(taskStruct, "start_time")
182229
if err != nil {
183230
return err
184231
}
185232
vars.task_start_time_offset = uint32(startTimeOffset)
186233
}
234+
235+
if needPIDNamespaceTranslation {
236+
if err := parsePIDNamespaceOffsets(spec, vars); err != nil {
237+
return err
238+
}
239+
}
187240
parseVMAOffsets(spec, vars)
188241

189242
return nil
@@ -380,13 +433,16 @@ func getCurrentNS(filename string) (dev, ino uint64, err error) {
380433

381434
func determineSysConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map,
382435
kmod *kallsyms.Module, interpretersConfig interpreterconfig.Config, needProcessStartTime bool,
383-
vars *SysConfigVars,
436+
pidNamespaceTranslation bool, vars *SysConfigVars,
384437
) error {
385438
needTPBase := !interpretersConfig.Perl.IsDisabled() ||
386439
!interpretersConfig.Python.IsDisabled() ||
387440
!interpretersConfig.Ruby.IsDisabled() ||
388441
!interpretersConfig.Go.IsLabelsDisabled()
389-
if err := parseBTF(vars, needTPBase, needProcessStartTime); err != nil {
442+
if err := parseBTF(vars, needTPBase, needProcessStartTime, pidNamespaceTranslation); err != nil {
443+
if pidNamespaceTranslation {
444+
return fmt.Errorf("PID namespace translation requires readable kernel BTF with task and PID namespace layout: %w", err)
445+
}
390446
if needProcessStartTime {
391447
return fmt.Errorf("process age filter requires kernel BTF to resolve task_struct offsets: %w", err)
392448
}
@@ -573,6 +629,9 @@ func loadRodataVars(coll *cebpf.CollectionSpec, kmod *kallsyms.Module, cfg *Conf
573629
if err != nil {
574630
return fmt.Errorf("failed to read PID namespace info: %v", err)
575631
}
632+
if dev == 0 || ino == 0 {
633+
return fmt.Errorf("invalid PID namespace identity: device=%d, inode=%d", dev, ino)
634+
}
576635
if err := coll.Variables["pid_ns_translation_enabled"].Set(uint8(1)); err != nil {
577636
return fmt.Errorf("failed to set pid_ns_translation_enabled: %v", err)
578637
}
@@ -582,7 +641,7 @@ func loadRodataVars(coll *cebpf.CollectionSpec, kmod *kallsyms.Module, cfg *Conf
582641
if err := coll.Variables["target_pid_ns_inode"].Set(ino); err != nil {
583642
return fmt.Errorf("failed to set target_pid_ns_inode: %v", err)
584643
}
585-
log.Infof("PID namespace translation enabled (dev=%d, ino=%d), only processes traces within the profiler namespace will be collected", dev, ino)
644+
log.Infof("PID namespace translation enabled (dev=%d, ino=%d), only process traces visible in the profiler namespace will be collected", dev, ino)
586645
}
587646

588647
// The Python/native hybrid unwinder's per program loop count defaults to 10
@@ -644,10 +703,17 @@ func loadRodataVars(coll *cebpf.CollectionSpec, kmod *kallsyms.Module, cfg *Conf
644703

645704
if err := determineSysConfig(
646705
systemAnalysisColl, maps, kmod, cfg.InterpretersConfig, cfg.FilterMinProcessAge > 0,
647-
&rodataVars,
706+
cfg.PIDNamespaceTranslation, &rodataVars,
648707
); err != nil {
649708
return fmt.Errorf("failed to determine system configs: %v", err)
650709
}
710+
if cfg.PIDNamespaceTranslation {
711+
for name, offset := range rodataVars.pidNamespaceOffsets {
712+
if err := coll.Variables[name].Set(offset); err != nil {
713+
return fmt.Errorf("failed to set %s: %v", name, err)
714+
}
715+
}
716+
}
651717
if err := coll.Variables["tpbase_offset"].Set(rodataVars.tpbase_offset); err != nil {
652718
return fmt.Errorf("failed to set tpbase_offset: %v", err)
653719
}

tracer/tracer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,8 @@ type Config struct {
220220
// process discovery time. Multiple enrichers are called in order.
221221
ProcessMetaEnrichers []process.MetaEnricher
222222
// PIDNamespaceTranslation toggles translation of host-level PIDs/TGIDs into
223-
// their container-namespace equivalents. Useful for sidecar deployments where
224-
// the profiler and the target application share a PID namespace but not host PIDs.
223+
// the profiler's PID namespace. Tasks in descendant PID namespaces are translated
224+
// into the IDs visible to the profiler.
225225
PIDNamespaceTranslation bool
226226
}
227227

0 commit comments

Comments
 (0)