diff --git a/pkg/security/ebpf/c/include/constants/enums.h b/pkg/security/ebpf/c/include/constants/enums.h index bbe120429714b5..60148936518c1a 100644 --- a/pkg/security/ebpf/c/include/constants/enums.h +++ b/pkg/security/ebpf/c/include/constants/enums.h @@ -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 diff --git a/pkg/security/ebpf/c/include/constants/offsets/process.h b/pkg/security/ebpf/c/include/constants/offsets/process.h index f94eb022610c06..bfb32f4791df06 100644 --- a/pkg/security/ebpf/c/include/constants/offsets/process.h +++ b/pkg/security/ebpf/c/include/constants/offsets/process.h @@ -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 diff --git a/pkg/security/ebpf/c/include/helpers/erpc.h b/pkg/security/ebpf/c/include/helpers/erpc.h index 6d7438192d51ba..217573f0accedd 100644 --- a/pkg/security/ebpf/c/include/helpers/erpc.h +++ b/pkg/security/ebpf/c/include/helpers/erpc.h @@ -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: diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 01420f6ee16206..52837eae57d4c7 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -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); @@ -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(); + + 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; @@ -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) { diff --git a/pkg/security/ebpf/c/include/hooks/exec.h b/pkg/security/ebpf/c/include/hooks/exec.h index 57b232fd76fad2..36151df161562f 100644 --- a/pkg/security/ebpf/c/include/hooks/exec.h +++ b/pkg/security/ebpf/c/include/hooks/exec.h @@ -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); @@ -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; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index b5a97113d84b41..cb2e5354958a0c 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -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) diff --git a/pkg/security/ebpf/c/include/structs/process.h b/pkg/security/ebpf/c/include/structs/process.h index 1339f80ef5038a..bb4d4fdb919bb8 100644 --- a/pkg/security/ebpf/c/include/structs/process.h +++ b/pkg/security/ebpf/c/include/structs/process.h @@ -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 diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index 4273ee9bbec07a..87992bff70dc49 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -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, diff --git a/pkg/security/probe/constantfetch/constant_names.go b/pkg/security/probe/constantfetch/constant_names.go index 756aab4ac4cfe2..42f646f3cec90f 100644 --- a/pkg/security/probe/constantfetch/constant_names.go +++ b/pkg/security/probe/constantfetch/constant_names.go @@ -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" diff --git a/pkg/security/probe/constantfetch/fallback.go b/pkg/security/probe/constantfetch/fallback.go index ea524aa884314f..10ecd9e400b1b8 100644 --- a/pkg/security/probe/constantfetch/fallback.go +++ b/pkg/security/probe/constantfetch/fallback.go @@ -133,6 +133,8 @@ func computeCallbacksTable() map[string]func(*kernel.Version) uint64 { OffsetNameMountMountpoint: getMountMountpointOffset, OffsetNameTaskStructRealParent: getTaskStructRealParentOffset, OffsetNameTaskStructTGID: getTaskStructTGIDOffset, + OffsetNameTaskStructThread: getTaskStructThreadOffset, + OffsetNameThreadStructFsbase: getThreadStructFsbaseOffset, } } @@ -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: diff --git a/pkg/security/probe/erpc/erpc.go b/pkg/security/probe/erpc/erpc.go index 9366834bdea59a..dfc0a2b521c9f3 100644 --- a/pkg/security/probe/erpc/erpc.go +++ b/pkg/security/probe/erpc/erpc.go @@ -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 diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 387e49d603b7ef..81e5e94a67580b 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -17,6 +17,7 @@ import ( "net/netip" "os" "path/filepath" + "runtime" "slices" "sort" "strings" @@ -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") diff --git a/pkg/security/ptracer/erpc.go b/pkg/security/ptracer/erpc.go index 7dd61074aee771..207844ab92f85b 100644 --- a/pkg/security/ptracer/erpc.go +++ b/pkg/security/ptracer/erpc.go @@ -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 { diff --git a/pkg/security/ptracer/span.go b/pkg/security/ptracer/span.go index 576e89570d5edb..7989fe0a09685a 100644 --- a/pkg/security/ptracer/span.go +++ b/pkg/security/ptracer/span.go @@ -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])), + } +} diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 089c6de42e9107..e62b542f5445d3 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -12,6 +12,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "strconv" "testing" @@ -111,3 +112,63 @@ func TestSpan(t *testing.T) { }, "test_span_rule_exec") }) } + +// TestOTelSpan tests OTel Thread Local Context Record based span context collection. +// This tests the native application TLSDESC path (per OTel spec PR #4947). +// Only supported on x86_64 (reads fsbase from task_struct->thread.fsbase). +func TestOTelSpan(t *testing.T) { + SkipIfNotAvailable(t) + + if runtime.GOARCH != "amd64" { + t.Skip("OTel TLSDESC span test only supported on amd64") + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_otel_span_rule_open", + Expression: `open.file.path == "{{.Root}}/test-otel-span"`, + }, + } + + test, err := newTestModule(t, nil, ruleDefs) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + syscallTester, err := loadSyscallTester(t, test, "syscall_tester") + if err != nil { + t.Fatal(err) + } + + fakeTraceID128b := "136272290892501783905308705057321818530" + + test.RunMultiMode(t, "open", func(t *testing.T, _ wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-otel-span") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{"otel-span-open", fakeTraceID128b, "204", testFile} + envs := []string{} + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, envs) + out, err := cmd.CombinedOutput() + + if err != nil { + return fmt.Errorf("%s: %w", out, err) + } + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_otel_span_rule_open") + + test.validateSpanSchema(t, event) + + assert.Equal(t, "204", strconv.FormatUint(event.SpanContext.SpanID, 10)) + assert.Equal(t, fakeTraceID128b, event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_open") + }) +} diff --git a/pkg/security/tests/syscall_tester/c/syscall_tester.c b/pkg/security/tests/syscall_tester/c/syscall_tester.c index cb459501b59514..a42ca4dd79f300 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -29,8 +29,13 @@ #include #include +#ifdef __x86_64__ +#include +#endif + #define RPC_CMD 0xdeadc001 #define REGISTER_SPAN_TLS_OP 6 +#define REGISTER_OTEL_TLS_OP 14 #ifndef SYS_gettid #error "SYS_gettid unavailable on this system" @@ -179,6 +184,147 @@ int span_open(int argc, char **argv) { return EXIT_SUCCESS; } +// --- OTel Thread Local Context Record (per OTel spec PR #4947) --- +// Native application implementation using ELF TLSDESC. + +// OTel Thread Local Context Record layout (28-byte fixed header). +struct otel_thread_ctx_record { + uint8_t trace_id[16]; // W3C big-endian byte order + uint8_t span_id[8]; // W3C big-endian byte order + uint8_t valid; // must be 1 + uint8_t _reserved; + uint16_t attrs_data_size; // 0 for no custom attributes +}; + +struct otel_tls_t { + int64_t tls_offset; +}; + +// Thread-local pointer to the active OTel context record. +// This simulates what an OTel SDK would expose via TLSDESC. +static __thread struct otel_thread_ctx_record *otel_ctx_ptr = NULL; + +// Convert a native uint64 to big-endian (W3C) bytes. +static void u64_to_be_bytes(uint64_t val, uint8_t *out) { + out[0] = (uint8_t)(val >> 56); + out[1] = (uint8_t)(val >> 48); + out[2] = (uint8_t)(val >> 40); + out[3] = (uint8_t)(val >> 32); + out[4] = (uint8_t)(val >> 24); + out[5] = (uint8_t)(val >> 16); + out[6] = (uint8_t)(val >> 8); + out[7] = (uint8_t)(val); +} + +#ifdef __x86_64__ +static int register_otel_tls() { + // Get the current thread's fsbase (thread pointer) via arch_prctl. + unsigned long fsbase = 0; + if (syscall(SYS_arch_prctl, ARCH_GET_FS, &fsbase) != 0) { + fprintf(stderr, "arch_prctl(ARCH_GET_FS) failed\n"); + return -1; + } + + // Compute the TLS offset: address of otel_ctx_ptr relative to fsbase. + int64_t tls_offset = (int64_t)((uintptr_t)&otel_ctx_ptr - fsbase); + + // Register via eRPC. + uint8_t request[257]; + memset(request, 0, sizeof(request)); + request[0] = REGISTER_OTEL_TLS_OP; + + struct otel_tls_t tls = { .tls_offset = tls_offset }; + memcpy(&request[1], &tls, sizeof(tls)); + + ioctl(0, RPC_CMD, &request); + return 0; +} +#endif + +struct otel_thread_opts { + char **argv; +}; + +static void *thread_otel_open(void *data) { + struct otel_thread_opts *opts = (struct otel_thread_opts *)data; + +#ifdef __x86_64__ + // Register OTel TLS for this process (from the worker thread). + if (register_otel_tls() != 0) { + fprintf(stderr, "Failed to register OTel TLS\n"); + return NULL; + } + + // Parse trace-id (128-bit) and span-id. + __int128_t trace_id = atouint128(opts->argv[1]); + uint64_t span_id = (uint64_t)atol(opts->argv[2]); + + // Build the OTel record. + struct otel_thread_ctx_record record; + memset(&record, 0, sizeof(record)); + + // Convert trace-id to W3C byte order (big-endian). + // Hi 64 bits go to bytes[0..7], Lo 64 bits go to bytes[8..15]. + uint64_t trace_hi = (uint64_t)(trace_id >> 64); + uint64_t trace_lo = (uint64_t)(trace_id); + u64_to_be_bytes(trace_hi, &record.trace_id[0]); + u64_to_be_bytes(trace_lo, &record.trace_id[8]); + + // Convert span-id to W3C byte order. + u64_to_be_bytes(span_id, record.span_id); + + record.valid = 1; + record.attrs_data_size = 0; + + // Publish: set the TLS pointer to the record. + // Use a compiler fence to ensure the record is fully written before the pointer is visible. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_ctx_ptr = &record; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + // Trigger the syscall that the test is waiting for. + int fd = open(opts->argv[3], O_CREAT); + if (fd < 0) { + fprintf(stderr, "Unable to create file `%s`\n", opts->argv[3]); + otel_ctx_ptr = NULL; + return NULL; + } + close(fd); + unlink(opts->argv[3]); + + // Detach context. + otel_ctx_ptr = NULL; +#else + fprintf(stderr, "OTel TLS test only supported on x86_64\n"); +#endif + + return NULL; +} + +int otel_span_open(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: otel-span-open \n"); + return EXIT_FAILURE; + } + +#ifndef __x86_64__ + fprintf(stderr, "OTel TLS test only supported on x86_64\n"); + return EXIT_FAILURE; +#endif + + struct otel_thread_opts opts = { + .argv = argv, + }; + + pthread_t thread; + if (pthread_create(&thread, NULL, thread_otel_open, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + return EXIT_SUCCESS; +} + int ptrace_traceme() { int child = fork(); if (child == 0) { @@ -2040,6 +2186,8 @@ int main(int argc, char **argv) { exit_code = setrlimit_core(); } else if (strcmp(cmd, "span-open") == 0) { exit_code = span_open(sub_argc, sub_argv); + } else if (strcmp(cmd, "otel-span-open") == 0) { + exit_code = otel_span_open(sub_argc, sub_argv); } else if (strcmp(cmd, "pipe-chown") == 0) { exit_code = test_pipe_chown(); } else if (strcmp(cmd, "signal") == 0) {