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
1 change: 1 addition & 0 deletions pkg/security/ebpf/c/include/constants/enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ enum erpc_op
USER_SESSION_CONTEXT_OP,
PRCTL_DISCARDER,
NOP_EVENT_OP,
REGISTER_OTEL_TLS_OP, // OTel Thread Local Context Record TLS registration (native applications)
};

enum selinux_source_event_t
Expand Down
17 changes: 17 additions & 0 deletions pkg/security/ebpf/c/include/constants/offsets/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,21 @@ u64 __attribute__((always_inline)) get_task_struct_pid_offset() {
return task_struct_pid_offset;
}

// OTel TLSDESC thread pointer access (x86_64 only for now).
// These two offsets are summed to compute the address of fsbase within a task_struct:
// fsbase_addr = (void *)task + thread_offset + fsbase_offset
// They are split because the BTF constant fetcher does not support dot-path
// traversal for named (non-anonymous) nested struct members.
u64 __attribute__((always_inline)) get_task_struct_thread_offset() {
u64 offset;
LOAD_CONSTANT("task_struct_thread_offset", offset);
return offset;
}

u64 __attribute__((always_inline)) get_thread_struct_fsbase_offset() {
u64 offset;
LOAD_CONSTANT("thread_struct_fsbase_offset", offset);
return offset;
}

#endif
2 changes: 2 additions & 0 deletions pkg/security/ebpf/c/include/helpers/erpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ int __attribute__((always_inline)) handle_erpc_request(ctx_t *ctx) {
return handle_register_user_session(data);
case REGISTER_SPAN_TLS_OP:
return handle_register_span_memory(data);
case REGISTER_OTEL_TLS_OP:
return handle_register_otel_tls(data);
case EXPIRE_INODE_DISCARDER_OP:
return handle_expire_inode_discarder(data);
case BUMP_DISCARDERS_REVISION:
Expand Down
109 changes: 106 additions & 3 deletions pkg/security/ebpf/c/include/helpers/span.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "process.h"

// --- Datadog proprietary span TLS (existing mechanism) ---

int __attribute__((always_inline)) handle_register_span_memory(void *data) {
struct span_tls_t tls = {};
bpf_probe_read(&tls, sizeof(tls), data);
Expand All @@ -26,10 +28,103 @@ int __attribute__((always_inline)) unregister_span_memory() {
return 0;
}

// --- OTel Thread Local Context Record (per OTel spec PR #4947) ---
// Targets native applications using ELF TLSDESC (C, C++, Rust, Java/JNI, etc.).
// Support for additional runtimes (e.g., Go via pprof labels) will be added later.

int __attribute__((always_inline)) handle_register_otel_tls(void *data) {
struct otel_tls_t tls = {};
bpf_probe_read(&tls, sizeof(tls), data);

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

bpf_map_update_elem(&otel_tls, &tgid, &tls, BPF_NOEXIST);

return 0;
}

int __attribute__((always_inline)) unregister_otel_tls() {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tgid = pid_tgid >> 32;

bpf_map_delete_elem(&otel_tls, &tgid);

return 0;
}

// Convert 8 bytes in W3C (big-endian / network byte order) to a native-endian u64.
static u64 __attribute__((always_inline)) otel_bytes_to_u64(const u8 *bytes) {
return ((u64)bytes[0] << 56) | ((u64)bytes[1] << 48) |
((u64)bytes[2] << 40) | ((u64)bytes[3] << 32) |
((u64)bytes[4] << 24) | ((u64)bytes[5] << 16) |
((u64)bytes[6] << 8) | ((u64)bytes[7]);
}

// Try to fill span context from an OTel Thread Local Context Record.
// Returns 1 on success, 0 otherwise.
// Architecture: x86_64 only (reads fsbase from task_struct->thread.fsbase).
// ARM64 support (tpidr_el0) will be added later.
static int __attribute__((always_inline)) fill_span_context_otel(struct span_context_t *span) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tgid = pid_tgid >> 32;

struct otel_tls_t *otls = bpf_map_lookup_elem(&otel_tls, &tgid);
if (!otls) {
return 0;
}

// Read fsbase (thread pointer) from task_struct->thread.fsbase.
// The two offsets are summed because "thread" is a named (non-anonymous)
// member of task_struct, so we need separate BTF lookups.
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
u64 thread_offset = get_task_struct_thread_offset();
u64 fsbase_offset = get_thread_struct_fsbase_offset();
Comment on lines +81 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard OTel fsbase reads behind resolved offsets

These offsets are consumed unconditionally, but the commit only requests them on amd64 (probe_ebpf.go) and the new fallback getters return ErrorSentinel (constantfetch/fallback.go), which is later materialized as 0 in constant editors. If a process sends REGISTER_OTEL_TLS_OP on an unsupported/missing-offset environment (e.g., arm64 or x86_64 kernels where BTF lookup fails), this path reads task + 0 instead of task->thread.fsbase, so OTel context extraction becomes incorrect and can mis-associate or drop span correlation. The OTel path should be disabled unless both offsets are successfully resolved.

Useful? React with 👍 / 👎.


u64 fsbase = 0;
int ret = bpf_probe_read_kernel(&fsbase, sizeof(fsbase),
(void *)task + thread_offset + fsbase_offset);
if (ret < 0 || fsbase == 0) {
return 0;
}

// The TLSDESC TLS variable is a pointer to the active Thread Local Context Record.
// Read the pointer at [fsbase + tls_offset].
void *record_ptr = NULL;
ret = bpf_probe_read_user(&record_ptr, sizeof(record_ptr),
(void *)(fsbase + otls->tls_offset));
if (ret < 0 || record_ptr == NULL) {
return 0;
}

// Read the OTel Thread Local Context Record (28-byte fixed header).
struct otel_thread_ctx_record_t record = {};
ret = bpf_probe_read_user(&record, sizeof(record), record_ptr);
if (ret < 0) {
return 0;
}

// The record is only valid when the valid field is exactly 1.
if (record.valid != 1) {
return 0;
}

// Convert W3C byte order (big-endian) to native-endian span_context_t.
// OTel trace-id: bytes[0..7] = high 64 bits, bytes[8..15] = low 64 bits.
span->trace_id[1] = otel_bytes_to_u64(&record.trace_id[0]); // Hi
span->trace_id[0] = otel_bytes_to_u64(&record.trace_id[8]); // Lo
span->span_id = otel_bytes_to_u64(record.span_id);

return 1;
}

// --- Unified span context fill ---

void __attribute__((always_inline)) fill_span_context(struct span_context_t *span) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tgid = pid_tgid >> 32;

// Try Datadog proprietary TLS first (existing behavior).
struct span_tls_t *tls = bpf_map_lookup_elem(&span_tls, &tgid);
if (tls) {
u32 tid = pid_tgid;
Expand All @@ -42,11 +137,19 @@ void __attribute__((always_inline)) fill_span_context(struct span_context_t *spa

int offset = (tid % tls->max_threads) * sizeof(struct span_context_t);
int ret = bpf_probe_read_user(span, sizeof(struct span_context_t), tls->base + offset);
if (ret < 0) {
span->span_id = 0;
span->trace_id[0] = span->trace_id[1] = 0;
if (ret >= 0 && (span->span_id != 0 || span->trace_id[0] != 0 || span->trace_id[1] != 0)) {
return;
}
}

// Fall back to OTel Thread Local Context Record (native applications only).
if (fill_span_context_otel(span)) {
return;
}

// No span context available.
span->span_id = 0;
span->trace_id[0] = span->trace_id[1] = 0;
}

void __attribute__((always_inline)) reset_span_context(struct span_context_t *span) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/security/ebpf/c/include/hooks/exec.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ int __attribute__((always_inline)) handle_do_exit(ctx_t *ctx) {
send_event(ctx, EVENT_EXIT, event);

unregister_span_memory();
unregister_otel_tls();

// [activity_dump] cleanup tracing state for this pid
cleanup_traced_state(tgid);
Expand Down Expand Up @@ -863,6 +864,7 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) {

// as previously registered memory will become unreachable, we'll have to unregister the TLS
unregister_span_memory();
unregister_otel_tls();

return 0;
}
Expand Down
1 change: 1 addition & 0 deletions pkg/security/ebpf/c/include/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ BPF_LRU_MAP(exec_pid_transfer, u32, u64, 512)
BPF_LRU_MAP(netns_cache, u32, u32, 40960)
BPF_LRU_MAP(mntns_cache, u32, u32, 40960)
BPF_LRU_MAP(span_tls, u32, struct span_tls_t, 1) // max entries will be overridden at runtime
BPF_LRU_MAP(otel_tls, u32, struct otel_tls_t, 1) // max entries will be overridden at runtime
BPF_LRU_MAP(inode_discarders, struct inode_discarder_t, struct inode_discarder_params_t, 4096)
BPF_LRU_MAP(prctl_discarders, char[MAX_PRCTL_NAME_LEN], int, 1024)
BPF_LRU_MAP(flow_pid, struct pid_route_t, struct pid_route_entry_t, 10240)
Expand Down
21 changes: 21 additions & 0 deletions pkg/security/ebpf/c/include/structs/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,25 @@ struct span_tls_t {
void *base;
};

// OTel Thread Local Context Record (per OTel spec PR #4947).
// This is the fixed 28-byte header that OTel SDKs publish via ELF TLSDESC.
// Currently targets native applications (C, C++, Rust, Java/JNI, .NET/FFI, etc.).
// Support for additional runtimes (e.g., Go via pprof labels) will be added later.
struct otel_thread_ctx_record_t {
u8 trace_id[16]; // W3C Trace Context byte order (big-endian)
u8 span_id[8]; // W3C Trace Context byte order (big-endian)
u8 valid; // must be 1 for the record to be considered valid
u8 _reserved; // padding for alignment
u16 attrs_data_size; // size of custom attributes data (not read)
};

// OTel TLSDESC-based TLS registration for a process.
// The tls_offset is discovered by user-space (by parsing the dynsym table for
// the `custom_labels_current_set_v2` TLS symbol) and communicated to eBPF via eRPC.
// Currently x86_64 only (reads fsbase from task_struct->thread.fsbase).
// ARM64 support (tpidr_el0) will be added later.
struct otel_tls_t {
s64 tls_offset; // signed offset from thread pointer to the TLS variable
};

#endif
4 changes: 4 additions & 0 deletions pkg/security/ebpf/probes/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ func AllMapSpecEditors(numCPU int, opts MapSpecEditorOpts, kv *kernel.Version) m
MaxEntries: uint32(opts.SpanTrackMaxCount),
EditorFlag: manager.EditMaxEntries,
},
"otel_tls": {
MaxEntries: uint32(opts.SpanTrackMaxCount),
EditorFlag: manager.EditMaxEntries,
},
"capabilities_usage": {
MaxEntries: capabilitiesUsageMaxEntries,
EditorFlag: manager.EditMaxEntries,
Expand Down
6 changes: 6 additions & 0 deletions pkg/security/probe/constantfetch/constant_names.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ const (
OffsetNameFlowI6StructProto = "flowi6_proto_offset"
OffsetNameRtnlLinkOpsKind = "rtnl_link_ops_kind_offset"

// OTel TLSDESC thread pointer offsets (x86_64).
// Used to read task_struct->thread.fsbase for OTel Thread Local Context Record support.
// Native applications only; additional runtimes will be added later.
OffsetNameTaskStructThread = "task_struct_thread_offset"
OffsetNameThreadStructFsbase = "thread_struct_fsbase_offset"

// Interpreter constants
OffsetNameLinuxBinprmStructFile = "binprm_file_offset"

Expand Down
21 changes: 21 additions & 0 deletions pkg/security/probe/constantfetch/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ func computeCallbacksTable() map[string]func(*kernel.Version) uint64 {
OffsetNameMountMountpoint: getMountMountpointOffset,
OffsetNameTaskStructRealParent: getTaskStructRealParentOffset,
OffsetNameTaskStructTGID: getTaskStructTGIDOffset,
OffsetNameTaskStructThread: getTaskStructThreadOffset,
OffsetNameThreadStructFsbase: getThreadStructFsbaseOffset,
}
}

Expand Down Expand Up @@ -1081,6 +1083,25 @@ func getTaskStructRealParentOffset(kv *kernel.Version) uint64 {
}
}

// OTel TLSDESC thread pointer offsets (x86_64 only).
// These offsets are used to read task_struct->thread.fsbase for OTel Thread Local
// Context Record support in native applications.
// BTF is the primary source for these offsets; fallbacks are minimal since the
// task_struct.thread offset varies significantly with kernel config.

func getTaskStructThreadOffset(_ *kernel.Version) uint64 {
// The offset of 'thread' within task_struct depends heavily on kernel config
// (debug options, KASAN, etc.). BTF is strongly preferred for this offset.
return ErrorSentinel
}

func getThreadStructFsbaseOffset(_ *kernel.Version) uint64 {
// thread_struct.fsbase is at offset 40 on x86_64 for kernels >= 4.15.
// Before 4.15, the field was named 'fsbase' but at a different offset or
// accessed via usergs_base. BTF is preferred for accuracy.
return ErrorSentinel
}

func getTaskStructTGIDOffset(kv *kernel.Version) uint64 {
switch {
case kv.IsRH7Kernel() && kv.Code < kernel.Kernel4_9:
Expand Down
2 changes: 2 additions & 0 deletions pkg/security/probe/erpc/erpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
DiscardPrctlOp
// NopEventOp is used to nop an event
NopEventOp
// RegisterOTelTLSOp is used for OTel Thread Local Context Record TLS registration (native applications)
RegisterOTelTLSOp
)

// ERPC defines a krpc object
Expand Down
8 changes: 8 additions & 0 deletions pkg/security/probe/probe_ebpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"net/netip"
"os"
"path/filepath"
"runtime"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -3313,6 +3314,13 @@ func AppendProbeRequestsToFetcher(constantFetcher constantfetch.ConstantFetcher,
appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameTaskStructRealParent, "struct task_struct", "real_parent")
appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameTaskStructTGID, "struct task_struct", "tgid")

// OTel TLSDESC thread pointer offsets for reading OTel Thread Local Context Records
// from native applications. x86_64 only for now; ARM64 (tpidr_el0) will be added later.
if runtime.GOARCH == "amd64" {
appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameTaskStructThread, "struct task_struct", "thread")
appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameThreadStructFsbase, "struct thread_struct", "fsbase")
}

// splice event
constantFetcher.AppendSizeofRequest(constantfetch.SizeOfPipeBuffer, "struct pipe_buffer")
appendOffsetofRequest(constantFetcher, constantfetch.OffsetNamePipeInodeInfoStructBufs, "struct pipe_inode_info", "bufs")
Expand Down
2 changes: 2 additions & 0 deletions pkg/security/ptracer/erpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
RPCCmd uint64 = 0xdeadc001
// RegisterSpanTLSOp defines the span TLS register op code
RegisterSpanTLSOp uint8 = 6
// RegisterOTelTLSOp defines the OTel Thread Local Context Record TLS register op code (native applications)
RegisterOTelTLSOp uint8 = 14
)

func registerERPCHandlers(handlers map[int]syscallHandler) []string {
Expand Down
20 changes: 20 additions & 0 deletions pkg/security/ptracer/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,23 @@ func fillSpanContext(tracer *Tracer, pid int, tid int, span *SpanTLS) *ebpfless.
},
}
}

// OTelTLS holds information needed to read OTel Thread Local Context Records
// from native applications using ELF TLSDESC.
// Support for additional runtimes (e.g., Go via pprof labels) will be added later.
type OTelTLS struct {
tlsOffset int64 // signed TLS offset from thread pointer (discovered via dynsym)
}

func isOTelTLSRegisterRequest(req []byte) bool {
return req[0] == RegisterOTelTLSOp
}

func handleOTelTLSRegister(req []byte) *OTelTLS {
if len(req) < 9 {
return nil
}
return &OTelTLS{
tlsOffset: int64(binary.NativeEndian.Uint64(req[1:9])),
}
}
Loading
Loading