Skip to content
Open
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
5 changes: 5 additions & 0 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ func (c *Controller) Start(ctx context.Context) error {
// So if you change this log line update also the system test.
log.Info("Attached sched monitor")

if err := trc.AttachPrctlMonitor(); err != nil {
return fmt.Errorf("failed to attach prctl monitor: %w", err)
}
log.Info("Attached prctl monitor")

if err := c.startTraceHandling(ctx, trc); err != nil {
return fmt.Errorf("failed to start trace handling: %w", err)
}
Expand Down
5 changes: 4 additions & 1 deletion metrics/ids.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions metrics/metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -2096,5 +2096,12 @@
"name": "UnwindErrBadDTVRead",
"field": "bpf.errors.bad_dtv_read",
"id": 286
},
{
"description": "Number of prctl(PR_SET_VMA) calls naming an anonymous mapping OTEL_CTX",
"type": "counter",
"name": "NumPrctlSetVmaOtelCtx",
"field": "bpf.num_prctl_set_vma_otel_ctx",
"id": 287
}
]
11 changes: 11 additions & 0 deletions processcontext/integrationtests/processcontext_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,16 @@ func Test_ProcessContext(t *testing.T) {
tests := map[string]struct {
exeName string
args []string
env []string
}{
"glibc_exe": {exeName: "processctx_exe_glibc"},
// Publishes the process context after a delay, so the profiler discovers
// the PID before the publication and the prctl monitor must trigger a
// resync to pick up the OTEL_CTX mapping.
"glibc_exe_delayed_publish": {
exeName: "processctx_exe_glibc",
env: []string{"OTEL_PROCESS_CTX_PUBLISH_DELAY_MS=200"},
},
// "musl_exe": {exeName: "processctx_exe_musl"},
// "glibc_lib": {exeName: "processctx_lib_glibc"},
// "musl_lib": {exeName: "processctx_lib_musl"},
Expand Down Expand Up @@ -107,6 +115,9 @@ func Test_ProcessContext(t *testing.T) {

cmd := exec.CommandContext(ctx, filepath.Join(exeDir, tc.exeName), tc.args...)
cmd.Stderr = os.Stderr
if len(tc.env) > 0 {
cmd.Env = append(os.Environ(), tc.env...)
}
require.NoError(t, cmd.Start())

wg := sync.WaitGroup{}
Expand Down
14 changes: 14 additions & 0 deletions processcontext/integrationtests/testdata/processctx.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "processctx_lib.h"
Expand Down Expand Up @@ -82,6 +83,19 @@ int main(int argc, char *argv[]) {

signal(SIGTERM, handle_sigterm);

// OTEL_PROCESS_CTX_PUBLISH_DELAY_MS lets the test exercise the prctl monitor
// path: the profiler discovers the PID first, then the publish prctl fires
// and triggers a resync. We burn CPU rather than sleep so the process stays
// on-CPU and is sampled by the profiler's perf event, which drives process
// synchronization before the context is published.
const char *delay_str = getenv("OTEL_PROCESS_CTX_PUBLISH_DELAY_MS");
if (delay_str != NULL) {
int delay_ms = atoi(delay_str);
if (delay_ms > 0) {
burn(delay_ms);
}
}

if (init_process_context()) {
fprintf(stderr, "Failed to initialize process context\n");
return 1;
Expand Down
6 changes: 2 additions & 4 deletions processmanager/processinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,10 +545,8 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) {
numParseErrors, err := pr.IterateMappings(func(m process.RawMapping) bool {
if processcontext.IsContextMapping(m.IsExecutable(), m.Path) {
contextMappingAddr = m.Vaddr
// Even if process context is not found, it might be published in the future.
// For now, we rely on a new call to synchronizeMappings to pick it up.
// TODO: Add some kind of polling mechanism or a hook on prctl to be notified
// when the process context is published.
// The eBPF hook on prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME) will trigger a
// PID resynchronization when the process names its context mapping "OTEL_CTX".
}

// Executable mappings and VDSO, converted directly to libpf.FrameMapping
Expand Down
65 changes: 65 additions & 0 deletions support/ebpf/prctl_monitor.ebpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// This file contains the code for the tracepoint on prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ...)
// to detect when a process names an anonymous memory mapping "OTEL_CTX".

#include "bpfdefs.h"
#include "tracemgmt.h"

#include "types.h"

#ifndef TESTING_COREDUMP

// prctl constants from include/uapi/linux/prctl.h
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0

// See /sys/kernel/tracing/events/syscalls/sys_enter_prctl/format
struct sys_enter_prctl_ctx {
unsigned char skip[16]; // common fields (8) + __syscall_nr (4) + pad (4)
unsigned long option; // prctl option
unsigned long arg2; // sub-option (PR_SET_VMA_ANON_NAME for PR_SET_VMA)
unsigned long arg3; // addr
unsigned long arg4; // len
unsigned long arg5; // name (user-space pointer)
};

// tracepoint__sys_enter_prctl hooks prctl() calls to detect when a process
// names an anonymous VMA "OTEL_CTX". This triggers a PID resynchronization
// so the profiler can discover the newly published process context mapping.
SEC("tracepoint/syscalls/sys_enter_prctl")
int tracepoint__sys_enter_prctl(struct sys_enter_prctl_ctx *ctx)
{
if (ctx->option != PR_SET_VMA || ctx->arg2 != PR_SET_VMA_ANON_NAME) {
goto exit;
}

u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;

if (!bpf_map_lookup_elem(&reported_pids, &pid) && !pid_information_exists(pid)) {
// Only report PIDs that we explicitly track. This avoids sending kernel worker PIDs
// to userspace.
goto exit;
}

// Read the VMA name from user-space. We only need 9 bytes ("OTEL_CTX" + NUL).
__attribute__((aligned(8))) char name[9] = {};
if (bpf_probe_read_user(name, sizeof(name), (void *)ctx->arg5)) {
goto exit;
}

// Check for an exact "OTEL_CTX" match. We avoid bpf_strncmp (kernel 5.17+).
// Instead, compare as a u64 for the 8 characters plus a byte check for the
// NUL terminator.
if (*(u64 *)name != *(u64 *)"OTEL_CTX" || name[8] != '\0') {
goto exit;
}

if (report_pid(ctx, pid_tgid, RATELIMIT_ACTION_DEFAULT)) {
increment_metric(metricID_NumPrctlSetVmaOtelCtx);
}

exit:
return 0;
}

#endif
Binary file modified support/ebpf/tracer.ebpf.amd64
Binary file not shown.
Binary file modified support/ebpf/tracer.ebpf.arm64
Binary file not shown.
3 changes: 3 additions & 0 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ enum {
// number of failures to read TLS variables via the DTV
metricID_UnwindErrBadDTVRead,

// number of prctl(PR_SET_VMA) calls naming an anonymous mapping OTEL_CTX
metricID_NumPrctlSetVmaOtelCtx,

//
// Metric IDs above are for counters (cumulative values)
//
Expand Down
3 changes: 2 additions & 1 deletion support/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions support/types_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,5 @@ var MetricsTranslation = []metrics.MetricID{
C.metricID_UnwindRubyErrReadRbasicFlags: metrics.IDUnwindRubyErrReadRbasicFlags,
C.metricID_UnwindRubyErrCmeMaxEp: metrics.IDUnwindRubyErrCmeMaxEp,
C.metricID_UnwindErrBadDTVRead: metrics.IDUnwindErrBadDTVRead,
C.metricID_NumPrctlSetVmaOtelCtx: metrics.IDNumPrctlSetVmaOtelCtx,
}
12 changes: 12 additions & 0 deletions tracer/tracepoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,15 @@ func (t *Tracer) AttachSchedMonitor() error {
name := schedProcessFreeHookName(libpf.MapKeysToSet(t.ebpfProgs))
return t.attachToTracepoint("sched", "sched_process_free", t.ebpfProgs[name])
}

// AttachPrctlMonitor attaches a tracepoint on prctl() to detect when a process
// names an anonymous VMA "OTEL_CTX" via prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ...).
// This triggers a PID resynchronization so the profiler can discover newly published
// process context mappings.
func (t *Tracer) AttachPrctlMonitor() error {
prog, ok := t.ebpfProgs["tracepoint__sys_enter_prctl"]
if !ok {
return fmt.Errorf("eBPF program tracepoint__sys_enter_prctl not found")
}
return t.attachToTracepoint("syscalls", "sys_enter_prctl", prog)
}
7 changes: 6 additions & 1 deletion tracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ func loadPerfUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.P
LogLevel: cebpf.LogLevel(bpfVerifierLogLevel),
}

progs := make([]progLoaderHelper, len(tailCallProgs)+2)
progs := make([]progLoaderHelper, len(tailCallProgs)+3)
copy(progs, tailCallProgs)

schedProcessFree := schedProcessFreeHookName(libpf.MapKeysToSet(coll.Programs))
Expand All @@ -754,6 +754,11 @@ func loadPerfUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.P
noTailCallTarget: true,
enable: true,
},
progLoaderHelper{
name: "tracepoint__sys_enter_prctl",
noTailCallTarget: true,
enable: true,
},
progLoaderHelper{
name: "native_tracer_entry",
noTailCallTarget: true,
Expand Down