diff --git a/docs/cloud-workload-security/backend_linux.md b/docs/cloud-workload-security/backend_linux.md index 1b9507d49c7c..097c64d3fb6d 100644 --- a/docs/cloud-workload-security/backend_linux.md +++ b/docs/cloud-workload-security/backend_linux.md @@ -339,6 +339,13 @@ Workload Protection events for Linux systems have the following JSON schema: "trace_id": { "type": "string", "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Attributes contains custom OTel thread-local attributes from the span context" } }, "additionalProperties": false, @@ -2279,6 +2286,12 @@ Workload Protection events for Linux systems have the following JSON schema: }, "logs_collected": { "type": "boolean" + }, + "threadlocal_attribute_keys": { + "items": { + "type": "string" + }, + "type": "array" } }, "additionalProperties": false, @@ -3072,6 +3085,13 @@ Workload Protection events for Linux systems have the following JSON schema: "trace_id": { "type": "string", "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Attributes contains custom OTel thread-local attributes from the span context" } }, "additionalProperties": false, @@ -3085,6 +3105,7 @@ Workload Protection events for Linux systems have the following JSON schema: | ----- | ----------- | | `span_id` | Span ID used for APM correlation | | `trace_id` | Trace ID used for APM correlation | +| `attributes` | Attributes contains custom OTel thread-local attributes from the span context | ## `DNSEvent` @@ -5892,6 +5913,12 @@ Workload Protection events for Linux systems have the following JSON schema: }, "logs_collected": { "type": "boolean" + }, + "threadlocal_attribute_keys": { + "items": { + "type": "string" + }, + "type": "array" } }, "additionalProperties": false, diff --git a/docs/cloud-workload-security/backend_linux.schema.json b/docs/cloud-workload-security/backend_linux.schema.json index f51544f3fd6c..67e9c771bf81 100644 --- a/docs/cloud-workload-security/backend_linux.schema.json +++ b/docs/cloud-workload-security/backend_linux.schema.json @@ -328,6 +328,13 @@ "trace_id": { "type": "string", "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Attributes contains custom OTel thread-local attributes from the span context" } }, "additionalProperties": false, @@ -2268,6 +2275,12 @@ }, "logs_collected": { "type": "boolean" + }, + "threadlocal_attribute_keys": { + "items": { + "type": "string" + }, + "type": "array" } }, "additionalProperties": false, diff --git a/pkg/discovery/tracermetadata/model/tracer_metadata.go b/pkg/discovery/tracermetadata/model/tracer_metadata.go index c8433433f8b8..f5b0c7598047 100644 --- a/pkg/discovery/tracermetadata/model/tracer_metadata.go +++ b/pkg/discovery/tracermetadata/model/tracer_metadata.go @@ -28,6 +28,27 @@ type TracerMetadata struct { ProcessTags string `json:"process_tags,omitempty"` ContainerID string `json:"container_id,omitempty"` LogsCollected bool `json:"logs_collected,omitempty"` + // ThreadlocalAttributeKeys is the ordered list of attribute key names for OTel + // Thread Local Context Records (per OTel spec PR #4947). Key indices in a thread's + // attrs_data section index into this list to resolve the full attribute name. + // The first entry is implicitly "datadog.local_root_span_id" (index 0). + ThreadlocalAttributeKeys []string `json:"threadlocal_attribute_keys,omitempty"` +} + +// IsZero returns true if the TracerMetadata is empty (zero value). +func (t TracerMetadata) IsZero() bool { + return t.SchemaVersion == 0 && + t.RuntimeID == "" && + t.TracerLanguage == "" && + t.TracerVersion == "" && + t.Hostname == "" && + t.ServiceName == "" && + t.ServiceEnv == "" && + t.ServiceVersion == "" && + t.ProcessTags == "" && + t.ContainerID == "" && + !t.LogsCollected && + len(t.ThreadlocalAttributeKeys) == 0 } // ShouldSkipServiceTagKV checks if a tracer service tag key-value pair should be diff --git a/pkg/discovery/tracermetadata/model/tracer_metadata_gen.go b/pkg/discovery/tracermetadata/model/tracer_metadata_gen.go index 83cd294db43b..0eb3737a79e2 100644 --- a/pkg/discovery/tracermetadata/model/tracer_metadata_gen.go +++ b/pkg/discovery/tracermetadata/model/tracer_metadata_gen.go @@ -90,6 +90,25 @@ func (z *TracerMetadata) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "LogsCollected") return } + case "threadlocal_attribute_keys": + var zb0002 uint32 + zb0002, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys") + return + } + if cap(z.ThreadlocalAttributeKeys) >= int(zb0002) { + z.ThreadlocalAttributeKeys = (z.ThreadlocalAttributeKeys)[:zb0002] + } else { + z.ThreadlocalAttributeKeys = make([]string, zb0002) + } + for za0001 := range z.ThreadlocalAttributeKeys { + z.ThreadlocalAttributeKeys[za0001], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys", za0001) + return + } + } default: err = dc.Skip() if err != nil { @@ -104,8 +123,8 @@ func (z *TracerMetadata) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *TracerMetadata) EncodeMsg(en *msgp.Writer) (err error) { // check for omitted fields - zb0001Len := uint32(11) - var zb0001Mask uint16 /* 11 bits */ + zb0001Len := uint32(12) + var zb0001Mask uint16 /* 12 bits */ _ = zb0001Mask if z.RuntimeID == "" { zb0001Len-- @@ -135,6 +154,10 @@ func (z *TracerMetadata) EncodeMsg(en *msgp.Writer) (err error) { zb0001Len-- zb0001Mask |= 0x400 } + if z.ThreadlocalAttributeKeys == nil { + zb0001Len-- + zb0001Mask |= 0x800 + } // variable map header, size zb0001Len err = en.Append(0x80 | uint8(zb0001Len)) if err != nil { @@ -267,6 +290,25 @@ func (z *TracerMetadata) EncodeMsg(en *msgp.Writer) (err error) { return } } + if (zb0001Mask & 0x800) == 0 { // if not omitted + // write "threadlocal_attribute_keys" + err = en.Append(0xba, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73) + if err != nil { + return + } + err = en.WriteArrayHeader(uint32(len(z.ThreadlocalAttributeKeys))) + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys") + return + } + for za0001 := range z.ThreadlocalAttributeKeys { + err = en.WriteString(z.ThreadlocalAttributeKeys[za0001]) + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys", za0001) + return + } + } + } } return } @@ -275,8 +317,8 @@ func (z *TracerMetadata) EncodeMsg(en *msgp.Writer) (err error) { func (z *TracerMetadata) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // check for omitted fields - zb0001Len := uint32(11) - var zb0001Mask uint16 /* 11 bits */ + zb0001Len := uint32(12) + var zb0001Mask uint16 /* 12 bits */ _ = zb0001Mask if z.RuntimeID == "" { zb0001Len-- @@ -306,6 +348,10 @@ func (z *TracerMetadata) MarshalMsg(b []byte) (o []byte, err error) { zb0001Len-- zb0001Mask |= 0x400 } + if z.ThreadlocalAttributeKeys == nil { + zb0001Len-- + zb0001Mask |= 0x800 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) @@ -358,6 +404,14 @@ func (z *TracerMetadata) MarshalMsg(b []byte) (o []byte, err error) { o = append(o, 0xae, 0x6c, 0x6f, 0x67, 0x73, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64) o = msgp.AppendBool(o, z.LogsCollected) } + if (zb0001Mask & 0x800) == 0 { // if not omitted + // string "threadlocal_attribute_keys" + o = append(o, 0xba, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73) + o = msgp.AppendArrayHeader(o, uint32(len(z.ThreadlocalAttributeKeys))) + for za0001 := range z.ThreadlocalAttributeKeys { + o = msgp.AppendString(o, z.ThreadlocalAttributeKeys[za0001]) + } + } } return } @@ -446,6 +500,25 @@ func (z *TracerMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "LogsCollected") return } + case "threadlocal_attribute_keys": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys") + return + } + if cap(z.ThreadlocalAttributeKeys) >= int(zb0002) { + z.ThreadlocalAttributeKeys = (z.ThreadlocalAttributeKeys)[:zb0002] + } else { + z.ThreadlocalAttributeKeys = make([]string, zb0002) + } + for za0001 := range z.ThreadlocalAttributeKeys { + z.ThreadlocalAttributeKeys[za0001], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ThreadlocalAttributeKeys", za0001) + return + } + } default: bts, err = msgp.Skip(bts) if err != nil { @@ -460,6 +533,9 @@ func (z *TracerMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *TracerMetadata) Msgsize() (s int) { - s = 1 + 15 + msgp.Uint8Size + 11 + msgp.StringPrefixSize + len(z.RuntimeID) + 16 + msgp.StringPrefixSize + len(z.TracerLanguage) + 15 + msgp.StringPrefixSize + len(z.TracerVersion) + 9 + msgp.StringPrefixSize + len(z.Hostname) + 13 + msgp.StringPrefixSize + len(z.ServiceName) + 12 + msgp.StringPrefixSize + len(z.ServiceEnv) + 16 + msgp.StringPrefixSize + len(z.ServiceVersion) + 13 + msgp.StringPrefixSize + len(z.ProcessTags) + 13 + msgp.StringPrefixSize + len(z.ContainerID) + 15 + msgp.BoolSize + s = 1 + 15 + msgp.Uint8Size + 11 + msgp.StringPrefixSize + len(z.RuntimeID) + 16 + msgp.StringPrefixSize + len(z.TracerLanguage) + 15 + msgp.StringPrefixSize + len(z.TracerVersion) + 9 + msgp.StringPrefixSize + len(z.Hostname) + 13 + msgp.StringPrefixSize + len(z.ServiceName) + 12 + msgp.StringPrefixSize + len(z.ServiceEnv) + 16 + msgp.StringPrefixSize + len(z.ServiceVersion) + 13 + msgp.StringPrefixSize + len(z.ProcessTags) + 13 + msgp.StringPrefixSize + len(z.ContainerID) + 15 + msgp.BoolSize + 27 + msgp.ArrayHeaderSize + for za0001 := range z.ThreadlocalAttributeKeys { + s += msgp.StringPrefixSize + len(z.ThreadlocalAttributeKeys[za0001]) + } return } diff --git a/pkg/discovery/tracermetadata/testdata/tracer_cpp_with_attrs.data b/pkg/discovery/tracermetadata/testdata/tracer_cpp_with_attrs.data new file mode 100644 index 000000000000..bd9802f1a2f0 --- /dev/null +++ b/pkg/discovery/tracermetadata/testdata/tracer_cpp_with_attrs.data @@ -0,0 +1 @@ +†®schema_version̯tracer_language£cpp®tracer_version¦v1.0.0¨hostname©test-host¬service_name±otel-test-serviceºthreadlocal_attribute_keys“«http.method«http.target©http.user \ No newline at end of file diff --git a/pkg/discovery/tracermetadata/tracer_memfd_test.go b/pkg/discovery/tracermetadata/tracer_memfd_test.go index 32d135e266e8..a6baf97bbdef 100644 --- a/pkg/discovery/tracermetadata/tracer_memfd_test.go +++ b/pkg/discovery/tracermetadata/tracer_memfd_test.go @@ -84,6 +84,23 @@ func TestGetTracerMetadata(t *testing.T) { }, tags) }) + t.Run("cpp_with_threadlocal_attribute_keys", func(t *testing.T) { + loadTracerMetadata(t, "testdata/tracer_cpp_with_attrs.data") + trm, err := GetTracerMetadata(pid, procfs) + require.NoError(t, err) + require.Equal(t, "otel-test-service", trm.ServiceName) + require.Equal(t, "cpp", trm.TracerLanguage) + require.Equal(t, "v1.0.0", trm.TracerVersion) + require.Equal(t, "test-host", trm.Hostname) + require.Equal(t, uint8(2), trm.SchemaVersion) + + // Verify threadlocal_attribute_keys are parsed + require.Len(t, trm.ThreadlocalAttributeKeys, 3, "should have 3 threadlocal attribute keys") + assert.Equal(t, "http.method", trm.ThreadlocalAttributeKeys[0]) + assert.Equal(t, "http.target", trm.ThreadlocalAttributeKeys[1]) + assert.Equal(t, "http.user", trm.ThreadlocalAttributeKeys[2]) + }) + t.Run("invalid data", func(t *testing.T) { createTracerMemfd(t, []byte("invalid data")) trm, err := GetTracerMetadata(pid, procfs) diff --git a/pkg/network/events/monitor.go b/pkg/network/events/monitor.go index 1efb9511ce16..c62c9c0d3d98 100644 --- a/pkg/network/events/monitor.go +++ b/pkg/network/events/monitor.go @@ -156,7 +156,7 @@ func (h *eventConsumerWrapper) Copy(ev *model.Event) any { } } - if tmeta := ev.GetProcessTracerMetadata(); (tmeta != tracermetadata.TracerMetadata{}) { + if tmeta := ev.GetProcessTracerMetadata(); !tmeta.IsZero() { for key, value := range tmeta.Tags() { if tracermetadata.ShouldSkipServiceTagKV(key, value, tagsFound["DD_SERVICE"], diff --git a/pkg/security/ebpf/c/include/constants/enums.h b/pkg/security/ebpf/c/include/constants/enums.h index cf49c639b206..f4d9774a06d6 100644 --- a/pkg/security/ebpf/c/include/constants/enums.h +++ b/pkg/security/ebpf/c/include/constants/enums.h @@ -147,6 +147,12 @@ enum tls_format DEFAULT_TLS_FORMAT }; +enum otel_runtime_language +{ + OTEL_RUNTIME_NATIVE = 0, + OTEL_RUNTIME_GOLANG = 1, +}; + enum bpf_cmd_def { BPF_MAP_CREATE_CMD, @@ -232,6 +238,7 @@ enum erpc_op PRCTL_DISCARDER, AUID_DISCARDER, NOP_EVENT_OP, + REGISTER_OTEL_TLS_OP_DEPRECATED, // DEPRECATED: replaced by dynsym-based discovery }; 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 f94eb022610c..15fc7cd50c94 100644 --- a/pkg/security/ebpf/c/include/constants/offsets/process.h +++ b/pkg/security/ebpf/c/include/constants/offsets/process.h @@ -51,4 +51,32 @@ u64 __attribute__((always_inline)) get_task_struct_pid_offset() { return task_struct_pid_offset; } +// OTel TLSDESC thread pointer access. +// Two offsets are summed to compute the address of the thread pointer within a task_struct: +// x86_64: fsbase_addr = (void *)task + thread_offset + fsbase_offset +// ARM64: tp_value_addr = (void *)task + thread_offset + uw_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; +} + +#if defined(__x86_64__) +u64 __attribute__((always_inline)) get_thread_struct_fsbase_offset() { + u64 offset; + LOAD_CONSTANT("thread_struct_fsbase_offset", offset); + return offset; +} +#elif defined(__aarch64__) +// thread_struct.uw.tp_value: tp_value is the first member of uw (offset 0), +// so the offset of uw within thread_struct gives us the tp_value address. +u64 __attribute__((always_inline)) get_thread_struct_uw_offset() { + u64 offset; + LOAD_CONSTANT("thread_struct_uw_offset", offset); + return offset; +} +#endif + #endif diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 01420f6ee162..1c15963b5a79 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,19 @@ int __attribute__((always_inline)) unregister_span_memory() { return 0; } +// --- OTel Thread Local Context Record helpers (separate file) --- +#include "span_otel.h" + +// --- Go pprof labels helpers (separate file) --- +#include "span_go.h" + +// --- 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,17 +53,32 @@ 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; + } + + // Fall back to Go pprof labels (dd-trace-go sets "span id" / "local root span id"). + if (fill_span_context_go(span)) { + return; + } + + // No span context available. + span->span_id = 0; + span->trace_id[0] = span->trace_id[1] = 0; + span->has_extra_attrs = 0; } void __attribute__((always_inline)) reset_span_context(struct span_context_t *span) { span->span_id = 0; span->trace_id[0] = 0; span->trace_id[1] = 0; + span->has_extra_attrs = 0; } void __attribute__((always_inline)) copy_span_context(struct span_context_t *src, struct span_context_t *dst) { diff --git a/pkg/security/ebpf/c/include/helpers/span_go.h b/pkg/security/ebpf/c/include/helpers/span_go.h new file mode 100644 index 000000000000..562eac748c39 --- /dev/null +++ b/pkg/security/ebpf/c/include/helpers/span_go.h @@ -0,0 +1,251 @@ +#ifndef _HELPERS_SPAN_GO_H_ +#define _HELPERS_SPAN_GO_H_ + +#include "maps.h" +#include "process.h" +#include "span_otel.h" // for read_thread_pointer() + +// --- Go pprof labels reader (for dd-trace-go) --- +// dd-trace-go sets goroutine-level pprof labels: +// "span id" -> decimal string of span ID +// "local root span id" -> decimal string of local root span ID +// +// The chain from eBPF is: +// thread_pointer + tls_offset -> G (runtime.g) +// G + m_offset -> M (runtime.m) +// M + curg -> curg (current user goroutine) +// curg + labels -> labels pointer (map or slice) +// +// The fill_span_context_go function is __noinline to give it its own 512-byte +// stack frame, avoiding overflow when inlined into hooks that already have +// large event structs on the stack. + +#define GO_LABEL_MAX_KEY_LEN 24 +#define GO_LABEL_MAX_VAL_LEN 24 +#define GO_MAX_LABELS 10 + +// Per-CPU scratch buffer for Go label parsing. +// ALL large allocations live here to stay under the 512-byte eBPF stack limit. +struct go_labels_scratch_t { + char key_buf[GO_LABEL_MAX_KEY_LEN]; + char val_buf[GO_LABEL_MAX_VAL_LEN]; + struct go_string_t pairs[GO_MAX_LABELS * 2]; + struct go_map_bucket_t bucket; + struct go_slice_t slice; +}; + +BPF_PERCPU_ARRAY_MAP(go_labels_scratch_gen, struct go_labels_scratch_t, 1) + +// Parse the decimal string in s->val_buf to u64. +// Uses explicit array indexing on the struct field so the verifier can prove +// all accesses stay within the map value bounds. +// The loop uses a running flag instead of break to allow full unrolling. +static u64 __attribute__((always_inline)) parse_decimal_val(struct go_labels_scratch_t *s, u64 len) { + u64 val = 0; + int done = 0; + if (len > 20) len = 20; + #pragma unroll + for (int i = 0; i < 20; i++) { + if (!done && i < (int)len) { + char c = s->val_buf[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + } else { + done = 1; + } + } + } + return val; +} + +static void __attribute__((always_inline)) process_go_label( + struct span_context_t *span, + struct go_labels_scratch_t *s, + u64 key_len, u64 val_len) +{ + if (key_len == 7 && + s->key_buf[0] == 's' && s->key_buf[1] == 'p' && s->key_buf[2] == 'a' && + s->key_buf[3] == 'n' && s->key_buf[4] == ' ' && s->key_buf[5] == 'i' && + s->key_buf[6] == 'd') { + span->span_id = parse_decimal_val(s, val_len); + return; + } + // "local root span id" = 18 chars: l(0)o(1)c(2)a(3)l(4) (5)r(6)o(7)o(8)t(9) (10)s(11)p(12)a(13)n(14) (15)i(16)d(17) + if (key_len == 18 && + s->key_buf[0] == 'l' && s->key_buf[1] == 'o' && s->key_buf[2] == 'c' && + s->key_buf[3] == 'a' && s->key_buf[4] == 'l' && s->key_buf[5] == ' ' && + s->key_buf[6] == 'r' && s->key_buf[7] == 'o' && s->key_buf[8] == 'o' && + s->key_buf[9] == 't' && s->key_buf[10] == ' ' && s->key_buf[11] == 's' && + s->key_buf[12] == 'p' && s->key_buf[13] == 'a' && s->key_buf[14] == 'n' && + s->key_buf[15] == ' ' && s->key_buf[16] == 'i' && s->key_buf[17] == 'd') { + span->trace_id[0] = parse_decimal_val(s, val_len); + return; + } +} + +static int __attribute__((always_inline)) read_and_process_label( + struct span_context_t *span, + struct go_labels_scratch_t *s, + struct go_string_t *key_hdr, + struct go_string_t *val_hdr) +{ + if (key_hdr->str == NULL || key_hdr->len == 0) { + return 0; + } + + __builtin_memset(s->key_buf, 0, GO_LABEL_MAX_KEY_LEN); + __builtin_memset(s->val_buf, 0, GO_LABEL_MAX_VAL_LEN); + + u64 klen = key_hdr->len; + if (klen > GO_LABEL_MAX_KEY_LEN) klen = GO_LABEL_MAX_KEY_LEN; + if (bpf_probe_read_user(s->key_buf, klen & 0x1f, key_hdr->str) < 0) { + return -1; + } + + u64 vlen = val_hdr->len; + if (vlen > GO_LABEL_MAX_VAL_LEN) vlen = GO_LABEL_MAX_VAL_LEN; + if (vlen > 0 && val_hdr->str != NULL) { + if (bpf_probe_read_user(s->val_buf, vlen & 0x1f, val_hdr->str) < 0) { + return -1; + } + } + + process_go_label(span, s, key_hdr->len, val_hdr->len); + return 0; +} + +// Try to fill span context from Go pprof labels. +// Returns 1 on success, 0 otherwise. +// +// __noinline: this function gets its own 512-byte stack frame so it doesn't +// add to the calling hook's stack usage. BPF subprograms are supported on +// kernel 5.10+ which is within our target range (5.15+). +int __attribute__((__noinline__)) fill_span_context_go(struct span_context_t *span) { + if (!span) { + return 0; + } + + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tgid = pid_tgid >> 32; + + struct go_labels_offsets_t *offs = bpf_map_lookup_elem(&go_labels_procs, &tgid); + if (!offs) { + return 0; + } + + u32 zero = 0; + struct go_labels_scratch_t *scratch = bpf_map_lookup_elem(&go_labels_scratch_gen, &zero); + if (!scratch) { + return 0; + } + + u64 tp = read_thread_pointer(); + if (tp == 0) { + return 0; + } + + // TLS -> G + u64 g_addr = 0; + if (bpf_probe_read_user(&g_addr, sizeof(g_addr), + (void *)((s64)tp + offs->tls_offset)) < 0 || g_addr == 0) { + return 0; + } + + // G -> M + void *m_ptr = NULL; + if (bpf_probe_read_user(&m_ptr, sizeof(m_ptr), + (void *)(g_addr + offs->m_offset)) < 0 || m_ptr == NULL) { + return 0; + } + + // M -> curg + u64 curg_addr = 0; + if (bpf_probe_read_user(&curg_addr, sizeof(curg_addr), + (void *)((u64)m_ptr + offs->curg)) < 0 || curg_addr == 0) { + return 0; + } + + // curg -> labels + void *labels_ptr = NULL; + if (bpf_probe_read_user(&labels_ptr, sizeof(labels_ptr), + (void *)(curg_addr + offs->labels)) < 0 || labels_ptr == NULL) { + return 0; + } + + // Go >=1.24: slice format (hmap_buckets == 0) + if (offs->hmap_buckets == 0) { + if (bpf_probe_read_user(&scratch->slice, sizeof(scratch->slice), labels_ptr) < 0) { + return 0; + } + if (scratch->slice.len == 0 || scratch->slice.array == NULL) { + return 0; + } + u64 num_pairs = scratch->slice.len; + if (num_pairs > GO_MAX_LABELS) num_pairs = GO_MAX_LABELS; + + if (bpf_probe_read_user(scratch->pairs, + sizeof(struct go_string_t) * 2 * num_pairs, + scratch->slice.array) < 0) { + return 0; + } + for (int i = 0; i < GO_MAX_LABELS; i++) { + if (i >= (int)num_pairs) break; + read_and_process_label(span, scratch, + &scratch->pairs[i * 2], + &scratch->pairs[i * 2 + 1]); + } + return (span->span_id != 0) ? 1 : 0; + } + + // Go <1.24: map format + void *labels_map_ptr = NULL; + if (bpf_probe_read_user(&labels_map_ptr, sizeof(labels_map_ptr), labels_ptr) < 0 || labels_map_ptr == NULL) { + return 0; + } + + u64 labels_count = 0; + if (bpf_probe_read_user(&labels_count, sizeof(labels_count), + labels_map_ptr + offs->hmap_count) < 0 || labels_count == 0) { + return 0; + } + + unsigned char log_2_bucket_count = 0; + if (bpf_probe_read_user(&log_2_bucket_count, sizeof(log_2_bucket_count), + labels_map_ptr + offs->hmap_log2_bucket_count) < 0) { + return 0; + } + + void *label_buckets = NULL; + if (bpf_probe_read_user(&label_buckets, sizeof(label_buckets), + labels_map_ptr + offs->hmap_buckets) < 0 || label_buckets == NULL) { + return 0; + } + + u8 bucket_count = 1 << log_2_bucket_count; + if (bucket_count > 4) bucket_count = 4; + + for (int b = 0; b < 4; b++) { + if (b >= bucket_count) break; + if (bpf_probe_read_user(&scratch->bucket, sizeof(struct go_map_bucket_t), + label_buckets + (b * sizeof(struct go_map_bucket_t))) < 0) { + return 0; + } + for (int i = 0; i < GO_MAP_BUCKET_SIZE; i++) { + if (scratch->bucket.tophash[i] == 0) continue; + read_and_process_label(span, scratch, + &scratch->bucket.keys[i], + &scratch->bucket.values[i]); + } + } + + return (span->span_id != 0) ? 1 : 0; +} + +int __attribute__((always_inline)) unregister_go_labels() { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tgid = pid_tgid >> 32; + bpf_map_delete_elem(&go_labels_procs, &tgid); + return 0; +} + +#endif diff --git a/pkg/security/ebpf/c/include/helpers/span_otel.h b/pkg/security/ebpf/c/include/helpers/span_otel.h new file mode 100644 index 000000000000..42fa0aea217b --- /dev/null +++ b/pkg/security/ebpf/c/include/helpers/span_otel.h @@ -0,0 +1,140 @@ +#ifndef _HELPERS_SPAN_OTEL_H_ +#define _HELPERS_SPAN_OTEL_H_ + +#include "maps.h" +#include "process.h" + +// --- OTel Thread Local Context Record (per OTel spec PR #4947) --- +// Targets native applications using ELF TLSDESC (C, C++, Rust, Java/JNI, etc.). +// Supported architectures: x86_64 (fsbase), ARM64 (tpidr_el0 / uw.tp_value). +// The otel_tls BPF map is populated by user-space after parsing the ELF dynsym table +// for the `otel_thread_ctx_v1` TLS symbol. No eRPC registration is used. +// Go runtime support uses pprof labels instead (see span_go.h). + +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]); +} + +// Read the thread pointer (TLS base) from the current task_struct. +// x86_64: task_struct->thread.fsbase +// ARM64: task_struct->thread.uw.tp_value (tp_value at offset 0 within uw) +static u64 __attribute__((always_inline)) read_thread_pointer() { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + u64 thread_offset = get_task_struct_thread_offset(); + +#if defined(__x86_64__) + u64 tp_field_offset = get_thread_struct_fsbase_offset(); +#elif defined(__aarch64__) + u64 tp_field_offset = get_thread_struct_uw_offset(); +#else + return 0; +#endif + + u64 tp = 0; + int ret = bpf_probe_read_kernel(&tp, sizeof(tp), + (void *)task + thread_offset + tp_field_offset); + if (ret < 0) { + return 0; + } + return tp; +} + +// Per-CPU scratch buffer for OTel span attributes. +// Avoids placing the 258-byte otel_span_attrs_t on the stack. +BPF_PERCPU_ARRAY_MAP(otel_span_attrs_gen, struct otel_span_attrs_t, 1) + +// Try to fill span context from an OTel Thread Local Context Record. +// Returns 1 on success, 0 otherwise. +// Only attempts TLS resolution for native runtimes (not Go). +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; + } + + // Only resolve TLS-based context for native runtimes. + // Go uses pprof labels instead (see span_go.h / fill_span_context_go). + if (otls->runtime != OTEL_RUNTIME_NATIVE) { + return 0; + } + + // Read the thread pointer from the kernel task_struct. + u64 tp = read_thread_pointer(); + if (tp == 0) { + return 0; + } + + // The TLSDESC TLS variable is a pointer to the active Thread Local Context Record. + // Read the pointer at [thread_pointer + tls_offset]. + void *record_ptr = NULL; + int ret = bpf_probe_read_user(&record_ptr, sizeof(record_ptr), + (void *)(tp + 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); + + // If the record has custom attributes, read them and store in the otel_span_attrs map. + if (record.attrs_data_size > 0) { + u32 zero = 0; + struct otel_span_attrs_t *attrs_val = bpf_map_lookup_elem(&otel_span_attrs_gen, &zero); + if (attrs_val) { + u16 attrs_size = record.attrs_data_size; + if (attrs_size > OTEL_ATTRS_MAX_SIZE) { + attrs_size = OTEL_ATTRS_MAX_SIZE; + } + + __builtin_memset(attrs_val, 0, sizeof(*attrs_val)); + attrs_val->size = attrs_size; + + // Read attrs_data from right after the 28-byte fixed header. + ret = bpf_probe_read_user(attrs_val->data, attrs_size & 0xff, + record_ptr + sizeof(struct otel_thread_ctx_record_t)); + if (ret >= 0) { + struct otel_span_attrs_key_t attrs_key = { + .span_id = span->span_id, + .trace_id = { span->trace_id[0], span->trace_id[1] }, + }; + bpf_map_update_elem(&otel_span_attrs, &attrs_key, attrs_val, BPF_ANY); + span->has_extra_attrs = 1; + } + } + } + + return 1; +} + +#endif diff --git a/pkg/security/ebpf/c/include/hooks/exec.h b/pkg/security/ebpf/c/include/hooks/exec.h index 879eea5ba3be..1fa346bdfe95 100644 --- a/pkg/security/ebpf/c/include/hooks/exec.h +++ b/pkg/security/ebpf/c/include/hooks/exec.h @@ -370,6 +370,8 @@ int __attribute__((always_inline)) handle_do_exit(ctx_t *ctx) { send_event(ctx, EVENT_EXIT, event); unregister_span_memory(); + unregister_otel_tls(); + unregister_go_labels(); // [activity_dump] cleanup tracing state for this pid cleanup_traced_state(tgid); @@ -869,6 +871,8 @@ 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(); + unregister_go_labels(); return 0; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index aeb7b1121446..8d52f926af1a 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -78,6 +78,9 @@ 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(go_labels_procs, u32, struct go_labels_offsets_t, 1) // max entries will be overridden at runtime +BPF_LRU_MAP(otel_span_attrs, struct otel_span_attrs_key_t, struct otel_span_attrs_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(auid_discarders, u32, struct auid_discarder_params_t, 1024) diff --git a/pkg/security/ebpf/c/include/structs/events_context.h b/pkg/security/ebpf/c/include/structs/events_context.h index f4a39354d5ab..3100e86c95b1 100644 --- a/pkg/security/ebpf/c/include/structs/events_context.h +++ b/pkg/security/ebpf/c/include/structs/events_context.h @@ -22,6 +22,8 @@ struct syscall_context_t { struct span_context_t { u64 span_id; u64 trace_id[2]; + u8 has_extra_attrs; // 1 if extra OTel attributes are available in the otel_span_attrs map + u8 _pad[7]; }; struct process_context_t { diff --git a/pkg/security/ebpf/c/include/structs/process.h b/pkg/security/ebpf/c/include/structs/process.h index 1339f80ef503..79daeb444550 100644 --- a/pkg/security/ebpf/c/include/structs/process.h +++ b/pkg/security/ebpf/c/include/structs/process.h @@ -79,4 +79,85 @@ 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. +// Targets native applications (C, C++, Rust, Java/JNI, .NET/FFI, etc.) on x86_64 and ARM64. +// Go runtime support uses pprof labels instead (see span_go.h). +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 following this header +}; + +// Maximum size of OTel custom attributes data stored in the otel_span_attrs map. +// The RFC allows up to 65535 bytes (u16), but typical records are <=64 bytes. +// 256 bytes is generous while keeping BPF map value size reasonable. +#define OTEL_ATTRS_MAX_SIZE 256 + +// Key for the otel_span_attrs map: uniquely identifies a span. +struct otel_span_attrs_key_t { + u64 span_id; + u64 trace_id[2]; +}; + +// Value for the otel_span_attrs map: raw attrs_data bytes from the OTel record. +// Format per RFC: repeated [key(u8) + length(u8) + val(u8[length])]. +struct otel_span_attrs_t { + u16 size; // actual size of attrs_data + u8 data[OTEL_ATTRS_MAX_SIZE]; // raw attribute bytes +}; + +// OTel TLSDESC-based TLS registration for a process. +// The tls_offset is discovered by user-space by parsing the ELF dynsym table for +// the `otel_thread_ctx_v1` TLS symbol, then pushed to the otel_tls BPF map. +// x86_64: reads fsbase from task_struct->thread.fsbase +// ARM64: reads tp_value from task_struct->thread.uw.tp_value +struct otel_tls_t { + s64 tls_offset; // signed offset from thread pointer to the TLS variable + u32 runtime; // enum otel_runtime_language + u32 _pad; +}; + +// --- Go pprof labels support --- +// dd-trace-go sets pprof labels on goroutines with keys "span id" and +// "local root span id" (decimal string values). The eBPF code traverses +// TLS → runtime.g → runtime.m → curg → labels to read them. + +// Go runtime string header: {pointer, length}. +struct go_string_t { + char *str; + u64 len; +}; + +// Go runtime slice header: {array pointer, length, capacity}. +struct go_slice_t { + void *array; + u64 len; + s64 cap; +}; + +// Go runtime map bucket (runtime.bmap) for map[string]string. +// Each bucket holds up to 8 key-value pairs. +#define GO_MAP_BUCKET_SIZE 8 +struct go_map_bucket_t { + char tophash[GO_MAP_BUCKET_SIZE]; + struct go_string_t keys[GO_MAP_BUCKET_SIZE]; + struct go_string_t values[GO_MAP_BUCKET_SIZE]; + void *overflow; +}; + +// Per-process offsets for reading Go pprof labels from eBPF. +// Populated by user-space after detecting a Go binary via tracer metadata. +struct go_labels_offsets_t { + u32 m_offset; // offset of 'm' field in runtime.g + u32 curg; // offset of 'curg' field in runtime.m + u32 labels; // offset of 'labels' field in runtime.g + u32 hmap_count; // offset of 'count' in runtime.hmap (0 for Go >=1.24) + u32 hmap_log2_bucket_count; // offset of 'B' in runtime.hmap + u32 hmap_buckets; // offset of 'buckets' in runtime.hmap (0 = slice format) + s32 tls_offset; // TLS offset to G pointer (from thread pointer) +}; + #endif diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index e0f0f8638871..2ca1f3af3a78 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -279,6 +279,18 @@ 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, + }, + "go_labels_procs": { + MaxEntries: uint32(opts.SpanTrackMaxCount), + EditorFlag: manager.EditMaxEntries, + }, + "otel_span_attrs": { + 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 756aab4ac4cf..264c6e084490 100644 --- a/pkg/security/probe/constantfetch/constant_names.go +++ b/pkg/security/probe/constantfetch/constant_names.go @@ -122,6 +122,14 @@ const ( OffsetNameFlowI6StructProto = "flowi6_proto_offset" OffsetNameRtnlLinkOpsKind = "rtnl_link_ops_kind_offset" + // OTel TLSDESC thread pointer offsets. + // Used to read the thread pointer from task_struct for OTel Thread Local Context Record support. + // x86_64: task_struct->thread.fsbase + // ARM64: task_struct->thread.uw.tp_value (tp_value is first member of uw, offset within uw = 0) + OffsetNameTaskStructThread = "task_struct_thread_offset" + OffsetNameThreadStructFsbase = "thread_struct_fsbase_offset" + OffsetNameThreadStructUw = "thread_struct_uw_offset" + // Interpreter constants OffsetNameLinuxBinprmStructFile = "binprm_file_offset" diff --git a/pkg/security/probe/constantfetch/fallback.go b/pkg/security/probe/constantfetch/fallback.go index ea524aa88431..bc45753cf161 100644 --- a/pkg/security/probe/constantfetch/fallback.go +++ b/pkg/security/probe/constantfetch/fallback.go @@ -133,6 +133,9 @@ func computeCallbacksTable() map[string]func(*kernel.Version) uint64 { OffsetNameMountMountpoint: getMountMountpointOffset, OffsetNameTaskStructRealParent: getTaskStructRealParentOffset, OffsetNameTaskStructTGID: getTaskStructTGIDOffset, + OffsetNameTaskStructThread: getTaskStructThreadOffset, + OffsetNameThreadStructFsbase: getThreadStructFsbaseOffset, + OffsetNameThreadStructUw: getThreadStructUwOffset, } } @@ -1081,6 +1084,31 @@ 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 getThreadStructUwOffset(_ *kernel.Version) uint64 { + // thread_struct.uw offset on ARM64. Varies with kernel config. + // BTF is strongly preferred for this offset. + 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 b5213fd23a90..0e4b0a48cb1d 100644 --- a/pkg/security/probe/erpc/erpc.go +++ b/pkg/security/probe/erpc/erpc.go @@ -53,6 +53,8 @@ const ( DiscardAuidOp // NopEventOp is used to nop an event NopEventOp + // registerOTelTLSOpDeprecated is deprecated: OTel TLS is now discovered via ELF dynsym parsing. + _ //nolint:revive ) // ERPC defines a krpc object diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 258f4470d995..cc4678175818 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -10,14 +10,17 @@ package probe import ( "context" + "encoding/binary" "errors" "fmt" "math" "net/netip" "os" "path/filepath" + "runtime" "slices" "sort" + "strconv" "strings" "sync" "time" @@ -156,8 +159,11 @@ type EBPFProbe struct { discarderPushedCallbacksLock sync.RWMutex discarderRateLimiter *rate.Limiter + // OTel span attributes + otelSpanAttrsMap *lib.Map + // kill action - killListMap *lib.Map + killListMap *lib.Map supportsBPFSendSignal bool processKiller *ProcessKiller @@ -580,6 +586,9 @@ func (p *EBPFProbe) Init() error { p.eventStream.SetMonitor(p.monitors.eventStreamMonitor) + // otel_span_attrs map is optional — non-fatal if not found. + p.otelSpanAttrsMap, _ = managerhelper.Map(p.Manager, "otel_span_attrs") + p.killListMap, err = managerhelper.Map(p.Manager, "kill_list") if err != nil { return err @@ -1036,6 +1045,66 @@ func (p *EBPFProbe) unmarshalContexts(data []byte, event *model.Event, cgroupCon return read, nil } +// resolveOTelSpanAttrs looks up OTel custom attributes from the otel_span_attrs BPF map +// and parses them using the ThreadlocalAttributeKeys from the process's TracerMetadata. +func (p *EBPFProbe) resolveOTelSpanAttrs(event *model.Event) { + // Build the map key: span_id + trace_id[2] + key := make([]byte, 24) + binary.NativeEndian.PutUint64(key[0:8], event.SpanContext.SpanID) + binary.NativeEndian.PutUint64(key[8:16], event.SpanContext.TraceID.Lo) + binary.NativeEndian.PutUint64(key[16:24], event.SpanContext.TraceID.Hi) + + data, err := p.otelSpanAttrsMap.LookupBytes(key) + if err != nil || len(data) < 2 { + return + } + + // Delete the entry after reading (one-shot consumption). + _ = p.otelSpanAttrsMap.Delete(key) + + // Parse the value: u16 size + data[OTEL_ATTRS_MAX_SIZE] + size := binary.NativeEndian.Uint16(data[0:2]) + if size == 0 || int(size)+2 > len(data) { + return + } + attrsData := data[2 : 2+size] + + // Get the ThreadlocalAttributeKeys from the process's TracerMetadata. + // ProcessContext may not be resolved yet at unmarshal time, so guard against nil. + var keyNames []string + if event.ProcessContext != nil { + keyNames = event.ProcessContext.Process.TracerMetadata.ThreadlocalAttributeKeys + } + + // Parse attrs_data: repeated [key(u8) + length(u8) + val(u8[length])] + attrs := make(map[string]string) + off := 0 + for off+2 <= len(attrsData) { + keyIdx := attrsData[off] + valLen := int(attrsData[off+1]) + off += 2 + + if off+valLen > len(attrsData) { + break + } + val := string(attrsData[off : off+valLen]) + off += valLen + + // Map key index to attribute name. + var keyName string + if int(keyIdx) < len(keyNames) { + keyName = keyNames[keyIdx] + } else { + keyName = strconv.Itoa(int(keyIdx)) + } + attrs[keyName] = val + } + + if len(attrs) > 0 { + event.SpanContext.Attributes = attrs + } +} + func eventWithNoProcessContext(eventType model.EventType) bool { switch eventType { case model.ShortDNSResponseEventType, @@ -1254,6 +1323,11 @@ func (p *EBPFProbe) handleEvent(CPU int, data []byte) { return } + // Resolve OTel custom attributes now that process context (and TracerMetadata) is available. + if event.SpanContext.HasExtraAttrs && p.otelSpanAttrsMap != nil { + p.resolveOTelSpanAttrs(event) + } + // handle regular events if !p.handleRegularEvent(event, offset, dataLen, data, newEntryCb) { return @@ -3353,6 +3427,17 @@ 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: reads task_struct->thread.fsbase + // ARM64: reads task_struct->thread.uw.tp_value (tp_value is at offset 0 within uw) + appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameTaskStructThread, "struct task_struct", "thread") + if runtime.GOARCH == "amd64" { + appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameThreadStructFsbase, "struct thread_struct", "fsbase") + } else if runtime.GOARCH == "arm64" { + appendOffsetofRequest(constantFetcher, constantfetch.OffsetNameThreadStructUw, "struct thread_struct", "uw") + } + // 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 7dd61074aee7..9f4fc0bbbb07 100644 --- a/pkg/security/ptracer/erpc.go +++ b/pkg/security/ptracer/erpc.go @@ -17,6 +17,9 @@ const ( RPCCmd uint64 = 0xdeadc001 // RegisterSpanTLSOp defines the span TLS register op code RegisterSpanTLSOp uint8 = 6 + // registerOTelTLSOpDeprecated is deprecated: OTel TLS is now discovered via ELF dynsym parsing. + // Kept as a comment to document the value was 15; do not reuse. + // registerOTelTLSOpDeprecated uint8 = 15 ) func registerERPCHandlers(handlers map[int]syscallHandler) []string { diff --git a/pkg/security/ptracer/span.go b/pkg/security/ptracer/span.go index 576e89570d5e..fc16194335a1 100644 --- a/pkg/security/ptracer/span.go +++ b/pkg/security/ptracer/span.go @@ -53,3 +53,4 @@ func fillSpanContext(tracer *Tracer, pid int, tid int, span *SpanTLS) *ebpfless. }, } } + diff --git a/pkg/security/resolvers/process/go_labels.go b/pkg/security/resolvers/process/go_labels.go new file mode 100644 index 000000000000..86a2b30b04d5 --- /dev/null +++ b/pkg/security/resolvers/process/go_labels.go @@ -0,0 +1,193 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +package process + +import ( + "encoding/binary" + "fmt" + "runtime" + "strconv" + + "github.com/DataDog/datadog-agent/pkg/network/go/binversion" + "github.com/DataDog/datadog-agent/pkg/util/kernel" + "github.com/DataDog/datadog-agent/pkg/util/safeelf" + "github.com/go-delve/delve/pkg/goversion" +) + +// goLabelsOffsetsValueSize is the serialized size of go_labels_offsets_t: +// 6 * u32 + 1 * s32 = 28 bytes. +const goLabelsOffsetsValueSize = 28 + +// getGoLabelsOffsets returns the Go runtime struct offsets for pprof label reading, +// based on the Go version. Ported from the OTel eBPF profiler's runtime_data.go. +// +// References: +// - runtime.g: https://github.com/golang/go/blob/master/src/runtime/runtime2.go +// - runtime.m: https://github.com/golang/go/blob/master/src/runtime/runtime2.go +// - runtime.hmap: https://github.com/golang/go/blob/master/src/runtime/map.go +func getGoLabelsOffsets(goVer goversion.GoVersion) (mOffset, curg, labels, hmapCount, hmapLog2BucketCount, hmapBuckets uint32) { + // m_offset: offset of 'm' field in runtime.g — stable across versions. + mOffset = 48 + + // curg: offset of 'curg' field in runtime.m. + if goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 25}) { + curg = 184 + } else { + curg = 192 + } + + // labels: offset of 'labels' field in runtime.g. + switch { + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 26}): + labels = 352 + // Go 1.24+ changed labels from map to slice — signal with hmap_buckets=0. + return + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 25}): + labels = 344 + return + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 24}): + labels = 352 + return + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 23}): + labels = 352 + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 21}): + labels = 344 + case goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 17}): + labels = 360 + default: + labels = 344 + } + + // Go <1.24: labels is a map, need hmap offsets. + hmapLog2BucketCount = 9 + hmapBuckets = 16 + return +} + +// getGoTLSGOffset computes the TLS offset for the G pointer in a Go binary. +// This is the offset from the thread pointer (fsbase on x86_64, tpidr_el0 on ARM64) +// to the runtime.g pointer. +// +// Based on github.com/go-delve/delve/pkg/proc.(*BinaryInfo).setGStructOffsetElf. +func getGoTLSGOffset(elfFile *safeelf.File) (int32, error) { + var tls *safeelf.Prog + for _, prog := range elfFile.Progs { + if prog.Type == safeelf.PT_TLS { + tls = prog + break + } + } + + switch runtime.GOARCH { + case "amd64": + // Look for runtime.tlsg symbol. + syms, err := elfFile.Symbols() + if err != nil { + // Pure Go binary without symbol table: G is at fs-8. + return -8, nil + } + var tlsg *safeelf.Symbol + for i := range syms { + if syms[i].Name == "runtime.tlsg" { + tlsg = &syms[i] + break + } + } + if tlsg == nil || tls == nil { + return -8, nil // Pure Go, no cgo: G at fs-8. + } + + // Linker padding formula (from LLVM lld): + memsz := tls.Memsz + (-tls.Vaddr-tls.Memsz)&(tls.Align-1) + // TLS register points to end of TLS block; tlsg is offset from start. + offset := int64(tlsg.Value) - int64(memsz) + return int32(offset), nil + + case "arm64": + syms, err := elfFile.Symbols() + if err != nil { + return 16, nil // Default: 2 * pointer_size = 16. + } + var tlsg *safeelf.Symbol + for i := range syms { + if syms[i].Name == "runtime.tls_g" { + tlsg = &syms[i] + break + } + } + if tlsg == nil || tls == nil { + return 16, nil + } + offset := int64(tlsg.Value) + 16 + int64((tls.Vaddr-16)&(tls.Align-1)) + return int32(offset), nil + + default: + return 0, fmt.Errorf("unsupported architecture: %s", runtime.GOARCH) + } +} + +// resolveGoLabels discovers the Go runtime offsets for pprof label reading +// and pushes them to the go_labels_procs BPF map. +func (p *EBPFResolver) resolveGoLabels(pid uint32) error { + if p.goLabelsMap == nil { + return fmt.Errorf("go_labels_procs map not available") + } + + pidStr := strconv.FormatUint(uint64(pid), 10) + exePath := kernel.HostProc(pidStr, "exe") + + elfFile, err := safeelf.Open(exePath) + if err != nil { + return fmt.Errorf("failed to open ELF: %w", err) + } + defer elfFile.Close() + + // Detect Go version from the binary. + goVersionStr, err := binversion.ReadElfBuildInfo(elfFile) + if err != nil { + return fmt.Errorf("not a Go binary or failed to read build info: %w", err) + } + + goVer, ok := goversion.Parse(goVersionStr) + if !ok { + return fmt.Errorf("failed to parse Go version: %s", goVersionStr) + } + + // Minimum supported Go version: 1.13. + if !goVer.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 13}) { + return fmt.Errorf("unsupported Go version: %s (need >= 1.13)", goVersionStr) + } + + // Get struct offsets from version table. + mOffset, curgOffset, labelsOffset, hmapCount, hmapLog2BC, hmapBuckets := getGoLabelsOffsets(goVer) + + // Get TLS G offset from ELF analysis. + tlsOffset, err := getGoTLSGOffset(elfFile) + if err != nil { + return fmt.Errorf("failed to compute TLS G offset: %w", err) + } + + // Serialize and push to BPF map. + value := serializeGoLabelsOffsets(mOffset, curgOffset, labelsOffset, + hmapCount, hmapLog2BC, hmapBuckets, tlsOffset) + + return p.goLabelsMap.Put(pid, value) +} + +// serializeGoLabelsOffsets serializes the go_labels_offsets_t struct for the BPF map. +func serializeGoLabelsOffsets(mOffset, curg, labels, hmapCount, hmapLog2BC, hmapBuckets uint32, tlsOffset int32) []byte { + buf := make([]byte, goLabelsOffsetsValueSize) + binary.NativeEndian.PutUint32(buf[0:4], mOffset) + binary.NativeEndian.PutUint32(buf[4:8], curg) + binary.NativeEndian.PutUint32(buf[8:12], labels) + binary.NativeEndian.PutUint32(buf[12:16], hmapCount) + binary.NativeEndian.PutUint32(buf[16:20], hmapLog2BC) + binary.NativeEndian.PutUint32(buf[20:24], hmapBuckets) + binary.NativeEndian.PutUint32(buf[24:28], uint32(tlsOffset)) + return buf +} diff --git a/pkg/security/resolvers/process/otel_tls.go b/pkg/security/resolvers/process/otel_tls.go new file mode 100644 index 000000000000..1b8c7c1d90f1 --- /dev/null +++ b/pkg/security/resolvers/process/otel_tls.go @@ -0,0 +1,177 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +package process + +import ( + "encoding/binary" + "errors" + "fmt" + "runtime" + "strconv" + + "github.com/DataDog/datadog-agent/pkg/security/probe/procfs" + "github.com/DataDog/datadog-agent/pkg/util/kernel" + "github.com/DataDog/datadog-agent/pkg/util/safeelf" +) + +const ( + // otelTLSSymbolName is the TLS symbol name defined by OTel spec PR #4947. + otelTLSSymbolName = "otel_thread_ctx_v1" + + // otelRuntimeNative represents a native runtime (C, C++, Rust, Java/JNI, etc.) + // that uses ELF TLSDESC for thread-local storage. + otelRuntimeNative uint32 = 0 + + // otelRuntimeGolang represents the Go runtime, which uses a different mechanism + // (pprof labels) for thread-level context. Not yet supported. + otelRuntimeGolang uint32 = 1 + + // otelTLSValueSize is the serialized size of otel_tls_t: s64 + u32 + u32. + otelTLSValueSize = 16 +) + +// mapTracerLanguageToRuntime maps the tracer language string from TracerMetadata +// to the otel_runtime_language enum used in the BPF map. +func mapTracerLanguageToRuntime(tracerLanguage string) uint32 { + switch tracerLanguage { + case "go": + return otelRuntimeGolang + default: + return otelRuntimeNative + } +} + +// findOTelTLSSymbol searches an ELF file for the otel_thread_ctx_v1 TLS symbol. +// It first tries DynamicSymbols (.dynsym), then falls back to Symbols (.symtab). +func findOTelTLSSymbol(elfFile *safeelf.File) (*safeelf.Symbol, error) { + // Try dynamic symbols first (always present in shared libraries). + syms, err := elfFile.DynamicSymbols() + if err == nil { + for i := range syms { + if syms[i].Name == otelTLSSymbolName && safeelf.ST_TYPE(syms[i].Info) == safeelf.STT_TLS { + return &syms[i], nil + } + } + } + + // Fall back to static symbol table (present in unstripped binaries). + syms, err = elfFile.Symbols() + if err == nil { + for i := range syms { + if syms[i].Name == otelTLSSymbolName && safeelf.ST_TYPE(syms[i].Info) == safeelf.STT_TLS { + return &syms[i], nil + } + } + } + + return nil, fmt.Errorf("TLS symbol %q not found", otelTLSSymbolName) +} + +// alignUp rounds v up to the nearest multiple of align. +func alignUp(v, align uint64) uint64 { + return (v + align - 1) &^ (align - 1) +} + +// computeStaticTLSOffset computes the static TLS offset for a symbol, given the +// ELF file that contains it. The offset is relative to the thread pointer. +// +// x86_64: TLS block is below the thread pointer → negative offset. +// ARM64: TLS block is above the thread pointer with a 16-byte TCB gap → positive offset. +func computeStaticTLSOffset(sym *safeelf.Symbol, elfFile *safeelf.File) (int64, error) { + // Find the PT_TLS program header. + var tlsSeg *safeelf.Prog + for _, prog := range elfFile.Progs { + if prog.Type == safeelf.PT_TLS { + tlsSeg = prog + break + } + } + if tlsSeg == nil { + return 0, errors.New("no PT_TLS segment found") + } + + switch runtime.GOARCH { + case "amd64": + // x86_64 variant II TLS: TLS block placed below the thread pointer. + // offset = sym.Value - alignUp(memsz, align) + memsz := alignUp(tlsSeg.Memsz, tlsSeg.Align) + return int64(sym.Value) - int64(memsz), nil + case "arm64": + // ARM64 variant I TLS: TLS block placed above the thread pointer with 16-byte TCB. + return int64(sym.Value) + 16, nil + default: + return 0, fmt.Errorf("unsupported architecture: %s", runtime.GOARCH) + } +} + +// resolveOTelTLS discovers the OTel TLS symbol for a process and computes the +// static TLS offset. It searches the main executable first, then loaded shared +// libraries via /proc//maps. +func resolveOTelTLS(pid uint32, tracerLanguage string) (int64, uint32, error) { + runtimeLang := mapTracerLanguageToRuntime(tracerLanguage) + + // For Go runtimes, we still register the entry (with the language tag) so that + // the eBPF side can differentiate, but we don't need to resolve the TLS symbol + // since Go doesn't use ELF TLSDESC. + if runtimeLang == otelRuntimeGolang { + return 0, runtimeLang, nil + } + + pidStr := strconv.FormatUint(uint64(pid), 10) + + // Try the main executable first. + exePath := kernel.HostProc(pidStr, "exe") + offset, err := findTLSOffsetInFile(exePath) + if err == nil { + return offset, runtimeLang, nil + } + + // Fall back to searching loaded shared libraries via /proc//maps. + mappedFiles, mapErr := procfs.GetMappedFiles(int32(pid), 0, procfs.FilterExecutableRegularFiles) + if mapErr != nil { + return 0, 0, fmt.Errorf("symbol not in executable (%w), and failed to read maps: %w", err, mapErr) + } + + for _, libPath := range mappedFiles { + // Access the library through the process's root filesystem. + hostLibPath := kernel.HostProc(pidStr, "root", libPath) + offset, libErr := findTLSOffsetInFile(hostLibPath) + if libErr == nil { + return offset, runtimeLang, nil + } + } + + return 0, 0, fmt.Errorf("TLS symbol %q not found in executable or any loaded library", otelTLSSymbolName) +} + +// findTLSOffsetInFile opens an ELF file, searches for the OTel TLS symbol, and +// computes the static TLS offset. +func findTLSOffsetInFile(path string) (int64, error) { + elfFile, err := safeelf.Open(path) + if err != nil { + return 0, err + } + defer elfFile.Close() + + sym, err := findOTelTLSSymbol(elfFile) + if err != nil { + return 0, err + } + + return computeStaticTLSOffset(sym, elfFile) +} + +// serializeOTelTLSValue serializes the otel_tls_t struct for the BPF map. +// Layout: s64 tls_offset (8 bytes) + u32 runtime (4 bytes) + u32 _pad (4 bytes) +func serializeOTelTLSValue(offset int64, runtimeLang uint32) []byte { + buf := make([]byte, otelTLSValueSize) + binary.NativeEndian.PutUint64(buf[0:8], uint64(offset)) + binary.NativeEndian.PutUint32(buf[8:12], runtimeLang) + // buf[12:16] is padding, already zero + return buf +} diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index 4c1529a06ba7..c2c4ef1273ca 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -85,6 +85,8 @@ type EBPFResolver struct { procCacheMap ebpf.Map pidCacheMap ebpf.Map pathIDMap ebpf.Map + otelTLSMap ebpf.Map + goLabelsMap ebpf.Map opts ResolverOpts // stats @@ -1417,7 +1419,9 @@ func (p *EBPFResolver) UpdateLoginUID(pid uint32, e *model.Event) { } } -// AddTracerMetadata reads tracer metadata from a memfd and adds it to the process cache entry +// AddTracerMetadata reads tracer metadata from a memfd and adds it to the process cache entry. +// If the metadata is successfully parsed, it also attempts to resolve the OTel TLS symbol +// from the process's ELF binary and populate the otel_tls BPF map. func (p *EBPFResolver) AddTracerMetadata(pid uint32, event *model.Event) error { fd := event.TracerMemfdSeal.Fd fdPath := kernel.HostProc(strconv.Itoa(int(pid)), "fd", strconv.Itoa(int(fd))) @@ -1428,16 +1432,43 @@ func (p *EBPFResolver) AddTracerMetadata(pid uint32, event *model.Event) error { } p.Lock() - defer p.Unlock() - entry := p.entryCache[pid] if entry != nil { entry.TracerMetadata = tmeta } + p.Unlock() + + // Attempt span context resolution based on the tracer language. + // Done outside the lock to avoid holding it during ELF I/O. + if tmeta.TracerLanguage == "go" { + // Go: resolve pprof label offsets for goroutine-level span context. + if err := p.resolveGoLabels(pid); err != nil { + seclog.Debugf("Go labels resolution for pid %d: %s", pid, err) + } + } else { + // Native: resolve OTel TLS symbol for TLSDESC-based span context. + if p.otelTLSMap != nil { + if err := p.resolveAndUpdateOTelTLS(pid, tmeta.TracerLanguage); err != nil { + seclog.Debugf("OTel TLS resolution for pid %d: %s", pid, err) + } + } + } return nil } +// resolveAndUpdateOTelTLS resolves the OTel TLS symbol from the process's ELF +// binary and writes the offset + runtime to the otel_tls BPF map. +func (p *EBPFResolver) resolveAndUpdateOTelTLS(pid uint32, tracerLanguage string) error { + offset, runtimeLang, err := resolveOTelTLS(pid, tracerLanguage) + if err != nil { + return err + } + + value := serializeOTelTLSValue(offset, runtimeLang) + return p.otelTLSMap.Put(pid, value) +} + // UpdateAWSSecurityCredentials updates the list of AWS Security Credentials func (p *EBPFResolver) UpdateAWSSecurityCredentials(pid uint32, e *model.Event) { if len(e.IMDS.AWS.SecurityCredentials.AccessKeyID) == 0 { @@ -1504,6 +1535,10 @@ func (p *EBPFResolver) Start(ctx context.Context) error { return err } + // otel_tls and go_labels_procs maps are optional — non-fatal if not found. + p.otelTLSMap, _ = managerhelper.Map(p.manager, "otel_tls") + p.goLabelsMap, _ = managerhelper.Map(p.manager, "go_labels_procs") + go p.cacheFlush(ctx) return nil diff --git a/pkg/security/secl/model/consts_map_names_linux.go b/pkg/security/secl/model/consts_map_names_linux.go index 4d59bd1f0614..dc52296610ed 100644 --- a/pkg/security/secl/model/consts_map_names_linux.go +++ b/pkg/security/secl/model/consts_map_names_linux.go @@ -57,6 +57,7 @@ var bpfMapNames = []string{ "filtered_dns_rc", "flow_pid", "global_rate_lim", + "go_labels_procs", "imds_event", "in_upper_layer_", "inet_bind_args", @@ -81,6 +82,8 @@ var bpfMapNames = []string{ "open_flags_appr", "open_flags_rdon", "open_samples", + "otel_span_attrs", + "otel_tls", "packets", "path_id_high", "path_id_low", diff --git a/pkg/security/secl/model/event_deep_copy_unix.go b/pkg/security/secl/model/event_deep_copy_unix.go index ba1606a48793..331bbe4cb423 100644 --- a/pkg/security/secl/model/event_deep_copy_unix.go +++ b/pkg/security/secl/model/event_deep_copy_unix.go @@ -415,6 +415,7 @@ func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermet copied.ServiceEnv = fieldToCopy.ServiceEnv copied.ServiceName = fieldToCopy.ServiceName copied.ServiceVersion = fieldToCopy.ServiceVersion + copied.ThreadlocalAttributeKeys = deepCopystringArr(fieldToCopy.ThreadlocalAttributeKeys) copied.TracerLanguage = fieldToCopy.TracerLanguage copied.TracerVersion = fieldToCopy.TracerVersion return copied @@ -1122,6 +1123,8 @@ func deepCopySignalEvent(fieldToCopy SignalEvent) SignalEvent { } func deepCopySpanContext(fieldToCopy SpanContext) SpanContext { copied := SpanContext{} + copied.Attributes = deepCopystringMap(fieldToCopy.Attributes) + copied.HasExtraAttrs = fieldToCopy.HasExtraAttrs copied.SpanID = fieldToCopy.SpanID copied.TraceID = deepCopyTraceID(fieldToCopy.TraceID) return copied diff --git a/pkg/security/secl/model/event_deep_copy_windows.go b/pkg/security/secl/model/event_deep_copy_windows.go index fcf404f59b5c..ad9559cc4bf1 100644 --- a/pkg/security/secl/model/event_deep_copy_windows.go +++ b/pkg/security/secl/model/event_deep_copy_windows.go @@ -165,6 +165,7 @@ func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermet copied.ServiceEnv = fieldToCopy.ServiceEnv copied.ServiceName = fieldToCopy.ServiceName copied.ServiceVersion = fieldToCopy.ServiceVersion + copied.ThreadlocalAttributeKeys = deepCopystringArr(fieldToCopy.ThreadlocalAttributeKeys) copied.TracerLanguage = fieldToCopy.TracerLanguage copied.TracerVersion = fieldToCopy.TracerVersion return copied diff --git a/pkg/security/secl/model/model.go b/pkg/security/secl/model/model.go index 3672723a8d29..95e03bd66592 100644 --- a/pkg/security/secl/model/model.go +++ b/pkg/security/secl/model/model.go @@ -195,8 +195,10 @@ func (nc *NetworkContext) IsZero() bool { // SpanContext describes a span context type SpanContext struct { - SpanID uint64 `field:"-"` - TraceID utils.TraceID `field:"-"` + SpanID uint64 `field:"-"` + TraceID utils.TraceID `field:"-"` + HasExtraAttrs bool `field:"-"` + Attributes map[string]string `field:"-"` } // RuleContext defines a rule context diff --git a/pkg/security/secl/model/unmarshallers_linux.go b/pkg/security/secl/model/unmarshallers_linux.go index 9de4dace6852..5ffa0032c09f 100644 --- a/pkg/security/secl/model/unmarshallers_linux.go +++ b/pkg/security/secl/model/unmarshallers_linux.go @@ -532,14 +532,16 @@ func (e *OpenEvent) UnmarshalBinary(data []byte) (int, error) { // UnmarshalBinary unmarshalls a binary representation of itself func (s *SpanContext) UnmarshalBinary(data []byte) (int, error) { - if len(data) < 24 { + if len(data) < 32 { return 0, ErrNotEnoughData } s.SpanID = binary.NativeEndian.Uint64(data[0:8]) s.TraceID.Lo = binary.NativeEndian.Uint64(data[8:16]) s.TraceID.Hi = binary.NativeEndian.Uint64(data[16:24]) - return 24, nil + s.HasExtraAttrs = data[24] != 0 + // bytes 25-31 are padding + return 32, nil } // UnmarshalBinary unmarshalls a binary representation of itself diff --git a/pkg/security/seclwin/model/model.go b/pkg/security/seclwin/model/model.go index 3672723a8d29..95e03bd66592 100644 --- a/pkg/security/seclwin/model/model.go +++ b/pkg/security/seclwin/model/model.go @@ -195,8 +195,10 @@ func (nc *NetworkContext) IsZero() bool { // SpanContext describes a span context type SpanContext struct { - SpanID uint64 `field:"-"` - TraceID utils.TraceID `field:"-"` + SpanID uint64 `field:"-"` + TraceID utils.TraceID `field:"-"` + HasExtraAttrs bool `field:"-"` + Attributes map[string]string `field:"-"` } // RuleContext defines a rule context diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 6dfc16866896..2f88d8562a60 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -1670,6 +1670,33 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadat } else { out.LogsCollected = bool(in.Bool()) } + case "threadlocal_attribute_keys": + if in.IsNull() { + in.Skip() + out.ThreadlocalAttributeKeys = nil + } else { + in.Delim('[') + if out.ThreadlocalAttributeKeys == nil { + if !in.IsDelim(']') { + out.ThreadlocalAttributeKeys = make([]string, 0, 4) + } else { + out.ThreadlocalAttributeKeys = []string{} + } + } else { + out.ThreadlocalAttributeKeys = (out.ThreadlocalAttributeKeys)[:0] + } + for !in.IsDelim(']') { + var v32 string + if in.IsNull() { + in.Skip() + } else { + v32 = string(in.String()) + } + out.ThreadlocalAttributeKeys = append(out.ThreadlocalAttributeKeys, v32) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -1739,6 +1766,20 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadat out.RawString(prefix) out.Bool(bool(in.LogsCollected)) } + if len(in.ThreadlocalAttributeKeys) != 0 { + const prefix string = ",\"threadlocal_attribute_keys\":" + out.RawString(prefix) + { + out.RawByte('[') + for v33, v34 := range in.ThreadlocalAttributeKeys { + if v33 > 0 { + out.RawByte(',') + } + out.String(string(v34)) + } + out.RawByte(']') + } + } out.RawByte('}') } func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(in *jlexer.Lexer, out *SyscallSerializer) { @@ -1905,21 +1946,21 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(i out.Flows = (out.Flows)[:0] } for !in.IsDelim(']') { - var v32 *FlowSerializer + var v35 *FlowSerializer if in.IsNull() { in.Skip() - v32 = nil + v35 = nil } else { - if v32 == nil { - v32 = new(FlowSerializer) + if v35 == nil { + v35 = new(FlowSerializer) } if in.IsNull() { in.Skip() } else { - (*v32).UnmarshalEasyJSON(in) + (*v35).UnmarshalEasyJSON(in) } } - out.Flows = append(out.Flows, v32) + out.Flows = append(out.Flows, v35) in.WantComma() } in.Delim(']') @@ -1954,14 +1995,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(o } { out.RawByte('[') - for v33, v34 := range in.Flows { - if v33 > 0 { + for v36, v37 := range in.Flows { + if v36 > 0 { out.RawByte(',') } - if v34 == nil { + if v37 == nil { out.RawString("null") } else { - (*v34).MarshalEasyJSON(out) + (*v37).MarshalEasyJSON(out) } } out.RawByte(']') @@ -2247,13 +2288,13 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v35 string + var v38 string if in.IsNull() { in.Skip() } else { - v35 = string(in.String()) + v38 = string(in.String()) } - out.Tags = append(out.Tags, v35) + out.Tags = append(out.Tags, v38) in.WantComma() } in.Delim(']') @@ -2310,11 +2351,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( } { out.RawByte('[') - for v36, v37 := range in.Tags { - if v36 > 0 { + for v39, v40 := range in.Tags { + if v39 > 0 { out.RawByte(',') } - out.String(string(v37)) + out.String(string(v40)) } out.RawByte(']') } @@ -2849,13 +2890,13 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.MatchedRules = (out.MatchedRules)[:0] } for !in.IsDelim(']') { - var v38 MatchedRuleSerializer + var v41 MatchedRuleSerializer if in.IsNull() { in.Skip() } else { - (v38).UnmarshalEasyJSON(in) + (v41).UnmarshalEasyJSON(in) } - out.MatchedRules = append(out.MatchedRules, v38) + out.MatchedRules = append(out.MatchedRules, v41) in.WantComma() } in.Delim(']') @@ -2938,11 +2979,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( } { out.RawByte('[') - for v39, v40 := range in.MatchedRules { - if v39 > 0 { + for v42, v43 := range in.MatchedRules { + if v42 > 0 { out.RawByte(',') } - (v40).MarshalEasyJSON(out) + (v43).MarshalEasyJSON(out) } out.RawByte(']') } diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 5ba23c534e77..4e37bfdb3572 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -1006,7 +1006,7 @@ func newProcessSerializer(ps *model.Process, e *model.Event) *ProcessSerializer } } - if (ps.TracerMetadata != tracermetadata.TracerMetadata{}) { + if !ps.TracerMetadata.IsZero() { tmetaCopy := ps.TracerMetadata psSerializer.Tracer = &tmetaCopy } @@ -1446,6 +1446,8 @@ type DDContextSerializer struct { SpanID string `json:"span_id,omitempty"` // Trace ID used for APM correlation TraceID string `json:"trace_id,omitempty"` + // Attributes contains custom OTel thread-local attributes from the span context + Attributes map[string]string `json:"attributes,omitempty"` } func newDDContextSerializer(e *model.Event) *DDContextSerializer { @@ -1453,6 +1455,7 @@ func newDDContextSerializer(e *model.Event) *DDContextSerializer { if e.SpanContext.SpanID != 0 && (e.SpanContext.TraceID.Hi != 0 || e.SpanContext.TraceID.Lo != 0) { s.SpanID = strconv.FormatUint(e.SpanContext.SpanID, 10) s.TraceID = fmt.Sprintf("%x%x", e.SpanContext.TraceID.Hi, e.SpanContext.TraceID.Lo) + s.Attributes = e.SpanContext.Attributes return s } diff --git a/pkg/security/serializers/serializers_linux_easyjson.go b/pkg/security/serializers/serializers_linux_easyjson.go index 6ff5da69e6a6..ae960a86b094 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.go +++ b/pkg/security/serializers/serializers_linux_easyjson.go @@ -2968,6 +2968,33 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadat } else { out.LogsCollected = bool(in.Bool()) } + case "threadlocal_attribute_keys": + if in.IsNull() { + in.Skip() + out.ThreadlocalAttributeKeys = nil + } else { + in.Delim('[') + if out.ThreadlocalAttributeKeys == nil { + if !in.IsDelim(']') { + out.ThreadlocalAttributeKeys = make([]string, 0, 4) + } else { + out.ThreadlocalAttributeKeys = []string{} + } + } else { + out.ThreadlocalAttributeKeys = (out.ThreadlocalAttributeKeys)[:0] + } + for !in.IsDelim(']') { + var v30 string + if in.IsNull() { + in.Skip() + } else { + v30 = string(in.String()) + } + out.ThreadlocalAttributeKeys = append(out.ThreadlocalAttributeKeys, v30) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -3037,6 +3064,20 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadat out.RawString(prefix) out.Bool(bool(in.LogsCollected)) } + if len(in.ThreadlocalAttributeKeys) != 0 { + const prefix string = ",\"threadlocal_attribute_keys\":" + out.RawString(prefix) + { + out.RawByte('[') + for v31, v32 := range in.ThreadlocalAttributeKeys { + if v31 > 0 { + out.RawByte(',') + } + out.String(string(v32)) + } + out.RawByte(']') + } + } out.RawByte('}') } func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(in *jlexer.Lexer, out *SyscallSerializer) { @@ -3208,13 +3249,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v30 string + var v33 string if in.IsNull() { in.Skip() } else { - v30 = string(in.String()) + v33 = string(in.String()) } - out.CapEffective = append(out.CapEffective, v30) + out.CapEffective = append(out.CapEffective, v33) in.WantComma() } in.Delim(']') @@ -3235,13 +3276,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v31 string + var v34 string if in.IsNull() { in.Skip() } else { - v31 = string(in.String()) + v34 = string(in.String()) } - out.CapPermitted = append(out.CapPermitted, v31) + out.CapPermitted = append(out.CapPermitted, v34) in.WantComma() } in.Delim(']') @@ -3349,11 +3390,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.RawString("null") } else { out.RawByte('[') - for v32, v33 := range in.CapEffective { - if v32 > 0 { + for v35, v36 := range in.CapEffective { + if v35 > 0 { out.RawByte(',') } - out.String(string(v33)) + out.String(string(v36)) } out.RawByte(']') } @@ -3365,11 +3406,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.RawString("null") } else { out.RawByte('[') - for v34, v35 := range in.CapPermitted { - if v34 > 0 { + for v37, v38 := range in.CapPermitted { + if v37 > 0 { out.RawByte(',') } - out.String(string(v35)) + out.String(string(v38)) } out.RawByte(']') } @@ -3873,13 +3914,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.Argv = (out.Argv)[:0] } for !in.IsDelim(']') { - var v36 string + var v39 string if in.IsNull() { in.Skip() } else { - v36 = string(in.String()) + v39 = string(in.String()) } - out.Argv = append(out.Argv, v36) + out.Argv = append(out.Argv, v39) in.WantComma() } in.Delim(']') @@ -3927,11 +3968,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.RawString(prefix) { out.RawByte('[') - for v37, v38 := range in.Argv { - if v37 > 0 { + for v40, v41 := range in.Argv { + if v40 > 0 { out.RawByte(',') } - out.String(string(v38)) + out.String(string(v41)) } out.RawByte(']') } @@ -4180,13 +4221,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( out.K8SGroups = (out.K8SGroups)[:0] } for !in.IsDelim(']') { - var v39 string + var v42 string if in.IsNull() { in.Skip() } else { - v39 = string(in.String()) + v42 = string(in.String()) } - out.K8SGroups = append(out.K8SGroups, v39) + out.K8SGroups = append(out.K8SGroups, v42) in.WantComma() } in.Delim(']') @@ -4204,34 +4245,34 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v40 []string + var v43 []string if in.IsNull() { in.Skip() - v40 = nil + v43 = nil } else { in.Delim('[') - if v40 == nil { + if v43 == nil { if !in.IsDelim(']') { - v40 = make([]string, 0, 4) + v43 = make([]string, 0, 4) } else { - v40 = []string{} + v43 = []string{} } } else { - v40 = (v40)[:0] + v43 = (v43)[:0] } for !in.IsDelim(']') { - var v41 string + var v44 string if in.IsNull() { in.Skip() } else { - v41 = string(in.String()) + v44 = string(in.String()) } - v40 = append(v40, v41) + v43 = append(v43, v44) in.WantComma() } in.Delim(']') } - (out.K8SExtra)[key] = v40 + (out.K8SExtra)[key] = v43 in.WantComma() } in.Delim('}') @@ -4286,11 +4327,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( } { out.RawByte('[') - for v42, v43 := range in.K8SGroups { - if v42 > 0 { + for v45, v46 := range in.K8SGroups { + if v45 > 0 { out.RawByte(',') } - out.String(string(v43)) + out.String(string(v46)) } out.RawByte(']') } @@ -4305,24 +4346,24 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( } { out.RawByte('{') - v44First := true - for v44Name, v44Value := range in.K8SExtra { - if v44First { - v44First = false + v47First := true + for v47Name, v47Value := range in.K8SExtra { + if v47First { + v47First = false } else { out.RawByte(',') } - out.String(string(v44Name)) + out.String(string(v47Name)) out.RawByte(':') - if v44Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + if v47Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { out.RawString("null") } else { out.RawByte('[') - for v45, v46 := range v44Value { - if v45 > 0 { + for v48, v49 := range v47Value { + if v48 > 0 { out.RawByte(',') } - out.String(string(v46)) + out.String(string(v49)) } out.RawByte(']') } @@ -4494,13 +4535,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v47 string + var v50 string if in.IsNull() { in.Skip() } else { - v47 = string(in.String()) + v50 = string(in.String()) } - out.Flags = append(out.Flags, v47) + out.Flags = append(out.Flags, v50) in.WantComma() } in.Delim(']') @@ -4611,13 +4652,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v48 string + var v51 string if in.IsNull() { in.Skip() } else { - v48 = string(in.String()) + v51 = string(in.String()) } - out.Hashes = append(out.Hashes, v48) + out.Hashes = append(out.Hashes, v51) in.WantComma() } in.Delim(']') @@ -4828,11 +4869,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( out.RawString(prefix) { out.RawByte('[') - for v49, v50 := range in.Flags { - if v49 > 0 { + for v52, v53 := range in.Flags { + if v52 > 0 { out.RawByte(',') } - out.String(string(v50)) + out.String(string(v53)) } out.RawByte(']') } @@ -4892,11 +4933,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( out.RawString(prefix) { out.RawByte('[') - for v51, v52 := range in.Hashes { - if v51 > 0 { + for v54, v55 := range in.Hashes { + if v54 > 0 { out.RawByte(',') } - out.String(string(v52)) + out.String(string(v55)) } out.RawByte(']') } @@ -5296,13 +5337,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v53 string + var v56 string if in.IsNull() { in.Skip() } else { - v53 = string(in.String()) + v56 = string(in.String()) } - out.Flags = append(out.Flags, v53) + out.Flags = append(out.Flags, v56) in.WantComma() } in.Delim(']') @@ -5413,13 +5454,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v54 string + var v57 string if in.IsNull() { in.Skip() } else { - v54 = string(in.String()) + v57 = string(in.String()) } - out.Hashes = append(out.Hashes, v54) + out.Hashes = append(out.Hashes, v57) in.WantComma() } in.Delim(']') @@ -5670,11 +5711,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( out.RawString(prefix) { out.RawByte('[') - for v55, v56 := range in.Flags { - if v55 > 0 { + for v58, v59 := range in.Flags { + if v58 > 0 { out.RawByte(',') } - out.String(string(v56)) + out.String(string(v59)) } out.RawByte(']') } @@ -5734,11 +5775,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( out.RawString(prefix) { out.RawByte('[') - for v57, v58 := range in.Hashes { - if v57 > 0 { + for v60, v61 := range in.Hashes { + if v60 > 0 { out.RawByte(',') } - out.String(string(v58)) + out.String(string(v61)) } out.RawByte(']') } @@ -6115,9 +6156,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( *out.SyscallsEventSerializer = (*out.SyscallsEventSerializer)[:0] } for !in.IsDelim(']') { - var v59 SyscallSerializer - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(in, &v59) - *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v59) + var v62 SyscallSerializer + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(in, &v62) + *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v62) in.WantComma() } in.Delim(']') @@ -6545,11 +6586,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( out.RawString("null") } else { out.RawByte('[') - for v60, v61 := range *in.SyscallsEventSerializer { - if v60 > 0 { + for v63, v64 := range *in.SyscallsEventSerializer { + if v63 > 0 { out.RawByte(',') } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(out, v61) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(out, v64) } out.RawByte(']') } @@ -6752,6 +6793,30 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( } else { out.TraceID = string(in.String()) } + case "attributes": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Attributes = make(map[string]string) + } else { + out.Attributes = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v65 string + if in.IsNull() { + in.Skip() + } else { + v65 = string(in.String()) + } + (out.Attributes)[key] = v65 + in.WantComma() + } + in.Delim('}') + } default: in.SkipRecursive() } @@ -6782,6 +6847,30 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( } out.String(string(in.TraceID)) } + if len(in.Attributes) != 0 { + const prefix string = ",\"attributes\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('{') + v66First := true + for v66Name, v66Value := range in.Attributes { + if v66First { + v66First = false + } else { + out.RawByte(',') + } + out.String(string(v66Name)) + out.RawByte(':') + out.String(string(v66Value)) + } + out.RawByte('}') + } + } out.RawByte('}') } @@ -6902,13 +6991,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v62 string + var v67 string if in.IsNull() { in.Skip() } else { - v62 = string(in.String()) + v67 = string(in.String()) } - out.CapEffective = append(out.CapEffective, v62) + out.CapEffective = append(out.CapEffective, v67) in.WantComma() } in.Delim(']') @@ -6929,13 +7018,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v63 string + var v68 string if in.IsNull() { in.Skip() } else { - v63 = string(in.String()) + v68 = string(in.String()) } - out.CapPermitted = append(out.CapPermitted, v63) + out.CapPermitted = append(out.CapPermitted, v68) in.WantComma() } in.Delim(']') @@ -7026,11 +7115,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( out.RawString("null") } else { out.RawByte('[') - for v64, v65 := range in.CapEffective { - if v64 > 0 { + for v69, v70 := range in.CapEffective { + if v69 > 0 { out.RawByte(',') } - out.String(string(v65)) + out.String(string(v70)) } out.RawByte(']') } @@ -7042,11 +7131,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( out.RawString("null") } else { out.RawByte('[') - for v66, v67 := range in.CapPermitted { - if v66 > 0 { + for v71, v72 := range in.CapPermitted { + if v71 > 0 { out.RawByte(',') } - out.String(string(v67)) + out.String(string(v72)) } out.RawByte(']') } @@ -7099,13 +7188,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( out.Hostnames = (out.Hostnames)[:0] } for !in.IsDelim(']') { - var v68 string + var v73 string if in.IsNull() { in.Skip() } else { - v68 = string(in.String()) + v73 = string(in.String()) } - out.Hostnames = append(out.Hostnames, v68) + out.Hostnames = append(out.Hostnames, v73) in.WantComma() } in.Delim(']') @@ -7142,11 +7231,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( out.RawString("null") } else { out.RawByte('[') - for v69, v70 := range in.Hostnames { - if v69 > 0 { + for v74, v75 := range in.Hostnames { + if v74 > 0 { out.RawByte(',') } - out.String(string(v70)) + out.String(string(v75)) } out.RawByte(']') } @@ -7198,13 +7287,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v71 string + var v76 string if in.IsNull() { in.Skip() } else { - v71 = string(in.String()) + v76 = string(in.String()) } - out.CapEffective = append(out.CapEffective, v71) + out.CapEffective = append(out.CapEffective, v76) in.WantComma() } in.Delim(']') @@ -7225,13 +7314,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v72 string + var v77 string if in.IsNull() { in.Skip() } else { - v72 = string(in.String()) + v77 = string(in.String()) } - out.CapPermitted = append(out.CapPermitted, v72) + out.CapPermitted = append(out.CapPermitted, v77) in.WantComma() } in.Delim(']') @@ -7257,11 +7346,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( out.RawString("null") } else { out.RawByte('[') - for v73, v74 := range in.CapEffective { - if v73 > 0 { + for v78, v79 := range in.CapEffective { + if v78 > 0 { out.RawByte(',') } - out.String(string(v74)) + out.String(string(v79)) } out.RawByte(']') } @@ -7273,11 +7362,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( out.RawString("null") } else { out.RawByte('[') - for v75, v76 := range in.CapPermitted { - if v75 > 0 { + for v80, v81 := range in.CapPermitted { + if v80 > 0 { out.RawByte(',') } - out.String(string(v76)) + out.String(string(v81)) } out.RawByte(']') } @@ -7324,13 +7413,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( out.CapsAttempted = (out.CapsAttempted)[:0] } for !in.IsDelim(']') { - var v77 string + var v82 string if in.IsNull() { in.Skip() } else { - v77 = string(in.String()) + v82 = string(in.String()) } - out.CapsAttempted = append(out.CapsAttempted, v77) + out.CapsAttempted = append(out.CapsAttempted, v82) in.WantComma() } in.Delim(']') @@ -7351,13 +7440,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( out.CapsUsed = (out.CapsUsed)[:0] } for !in.IsDelim(']') { - var v78 string + var v83 string if in.IsNull() { in.Skip() } else { - v78 = string(in.String()) + v83 = string(in.String()) } - out.CapsUsed = append(out.CapsUsed, v78) + out.CapsUsed = append(out.CapsUsed, v83) in.WantComma() } in.Delim(']') @@ -7382,11 +7471,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( out.RawString(prefix[1:]) { out.RawByte('[') - for v79, v80 := range in.CapsAttempted { - if v79 > 0 { + for v84, v85 := range in.CapsAttempted { + if v84 > 0 { out.RawByte(',') } - out.String(string(v80)) + out.String(string(v85)) } out.RawByte(']') } @@ -7401,11 +7490,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( } { out.RawByte('[') - for v81, v82 := range in.CapsUsed { - if v81 > 0 { + for v86, v87 := range in.CapsUsed { + if v86 > 0 { out.RawByte(',') } - out.String(string(v82)) + out.String(string(v87)) } out.RawByte(']') } @@ -7614,13 +7703,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38( out.Helpers = (out.Helpers)[:0] } for !in.IsDelim(']') { - var v83 string + var v88 string if in.IsNull() { in.Skip() } else { - v83 = string(in.String()) + v88 = string(in.String()) } - out.Helpers = append(out.Helpers, v83) + out.Helpers = append(out.Helpers, v88) in.WantComma() } in.Delim(']') @@ -7685,11 +7774,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38( } { out.RawByte('[') - for v84, v85 := range in.Helpers { - if v84 > 0 { + for v89, v90 := range in.Helpers { + if v89 > 0 { out.RawByte(',') } - out.String(string(v85)) + out.String(string(v90)) } out.RawByte(']') } @@ -7950,13 +8039,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42( out.Hostnames = (out.Hostnames)[:0] } for !in.IsDelim(']') { - var v86 string + var v91 string if in.IsNull() { in.Skip() } else { - v86 = string(in.String()) + v91 = string(in.String()) } - out.Hostnames = append(out.Hostnames, v86) + out.Hostnames = append(out.Hostnames, v91) in.WantComma() } in.Delim(']') @@ -7987,11 +8076,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42( out.RawString("null") } else { out.RawByte('[') - for v87, v88 := range in.Hostnames { - if v87 > 0 { + for v92, v93 := range in.Hostnames { + if v92 > 0 { out.RawByte(',') } - out.String(string(v88)) + out.String(string(v93)) } out.RawByte(']') } diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 089c6de42e91..32ca7b38fa5b 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -12,7 +12,9 @@ import ( "fmt" "os" "os/exec" + "runtime" "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -111,3 +113,284 @@ 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" && runtime.GOARCH != "arm64" { + t.Skip("OTel TLSDESC span test only supported on amd64 and arm64") + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_otel_span_rule_open", + Expression: `open.file.path == "{{.Root}}/test-otel-span"`, + }, + { + ID: "test_otel_span_rule_open_invalid", + Expression: `open.file.path == "{{.Root}}/test-otel-span-invalid"`, + }, + { + ID: "test_otel_span_rule_open_null_ptr", + Expression: `open.file.path == "{{.Root}}/test-otel-span-null-ptr"`, + }, + } + + 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" + + t.Run("valid_record", func(t *testing.T) { + 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()) + + // Verify custom OTel attributes were parsed from attrs_data. + assert.NotNil(t, event.SpanContext.Attributes, "attributes should be non-nil") + assert.Equal(t, "GET", event.SpanContext.Attributes["http.method"], + "http.method attribute should be GET") + assert.Equal(t, "/test", event.SpanContext.Attributes["http.target"], + "http.target attribute should be /test") + assert.Equal(t, "will@datadoghq.com", event.SpanContext.Attributes["http.user"], + "http.user attribute should be will@datadoghq.com") + }, "test_otel_span_rule_open") + }) + }) + + t.Run("invalid_record", func(t *testing.T) { + // Tests that the eBPF reader rejects a record with valid=0 and returns zero span context. + 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-invalid") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{"otel-span-open-invalid", 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_invalid") + + // The record has valid=0, so span context must be zero. + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_open_invalid") + }) + }) + + t.Run("null_pointer", func(t *testing.T) { + // Tests that the eBPF reader handles a NULL TLS pointer gracefully (zero span context). + 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-null-ptr") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{"otel-span-open-null-ptr", 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_null_ptr") + + // The TLS pointer is NULL, so span context must be zero. + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_open_null_ptr") + }) + }) +} + +// TestGoSpan tests Go pprof label-based span context collection. +// dd-trace-go sets goroutine labels "span id" and "local root span id" as decimal strings. +// The eBPF code traverses TLS -> runtime.g -> runtime.m -> curg -> labels to read them. +func TestGoSpan(t *testing.T) { + SkipIfNotAvailable(t) + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_go_span_rule_open", + Expression: `open.file.path == "{{.Root}}/test-go-span"`, + }, + } + + test, err := newTestModule(t, nil, ruleDefs) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + goSyscallTester, err := loadSyscallTester(t, test, "syscall_go_tester") + if err != nil { + t.Fatal(err) + } + + t.Run("valid_span", func(t *testing.T) { + test.RunMultiMode(t, "open", func(t *testing.T, _ wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-go-span") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-go-span-test", + "-go-span-span-id", "987654321", + "-go-span-local-root-span-id", "123456789", + "-go-span-file-path", testFile, + } + envs := []string{} + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, 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_go_span_rule_open") + + assert.Equal(t, uint64(987654321), event.SpanContext.SpanID, + "span ID should match the pprof label value") + assert.Equal(t, uint64(123456789), event.SpanContext.TraceID.Lo, + "trace ID lo should match the local root span ID label value") + }, "test_go_span_rule_open") + }) + }) +} + +// TestDDTraceGoSpan tests the full dd-trace-go integration: dd-trace-go creates +// a real span which internally sets pprof labels ("span id", "local root span id"), +// and the eBPF Go labels reader extracts them from the goroutine's label storage. +func TestDDTraceGoSpan(t *testing.T) { + SkipIfNotAvailable(t) + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_ddtrace_span_rule_open", + Expression: `open.file.path == "{{.Root}}/test-ddtrace-span"`, + }, + } + + test, err := newTestModule(t, nil, ruleDefs) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + goSyscallTester, err := loadSyscallTester(t, test, "syscall_go_tester") + if err != nil { + t.Fatal(err) + } + + t.Run("ddtrace_span", func(t *testing.T) { + test.RunMultiMode(t, "open", func(t *testing.T, _ wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-ddtrace-span") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-ddtrace-span-test", + "-ddtrace-span-file-path", testFile, + } + envs := []string{} + + // Capture the tester's stdout to extract the span IDs + // that dd-trace-go generated at runtime. + var expectedSpanID, expectedLocalRootSpanID uint64 + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, envs) + out, err := cmd.CombinedOutput() + + if err != nil { + return fmt.Errorf("%s: %w", out, err) + } + + // Parse the span IDs from the tester's output. + for _, line := range strings.Split(string(out), "\n") { + if strings.HasPrefix(line, "ddtrace_span_id=") { + val := strings.TrimPrefix(line, "ddtrace_span_id=") + expectedSpanID, _ = strconv.ParseUint(strings.TrimSpace(val), 10, 64) + } + if strings.HasPrefix(line, "ddtrace_local_root_span_id=") { + val := strings.TrimPrefix(line, "ddtrace_local_root_span_id=") + expectedLocalRootSpanID, _ = strconv.ParseUint(strings.TrimSpace(val), 10, 64) + } + } + + if expectedSpanID == 0 { + return fmt.Errorf("failed to parse ddtrace_span_id from output: %s", out) + } + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_ddtrace_span_rule_open") + + assert.Equal(t, expectedSpanID, event.SpanContext.SpanID, + "span ID should match the dd-trace-go generated value") + assert.Equal(t, expectedLocalRootSpanID, event.SpanContext.TraceID.Lo, + "trace ID lo should match the dd-trace-go local root span ID") + }, "test_ddtrace_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 cb459501b595..9eefc40d8c05 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -179,6 +179,325 @@ 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. +// The agent discovers this TLS symbol via ELF dynsym parsing, triggered when the +// process seals its datadog-tracer-info memfd. + +// 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 +}; + +// Thread-local pointer to the active OTel context record. +// This is the standard symbol name from the OTel spec. It must NOT be static +// so that it appears in the symbol table for the agent's dynsym resolver. +__thread struct otel_thread_ctx_record *otel_thread_ctx_v1 = 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); +} + +// Create and seal a tracer-info memfd with native (cpp) tracer metadata. +// This triggers the agent's memfd seal event, which in turn triggers ELF dynsym +// resolution for the otel_thread_ctx_v1 TLS symbol. +// Includes threadlocal_attribute_keys so the agent can parse attrs_data. +// Returns the fd (kept open so the agent can read via /proc/pid/fd/). +static int create_tracer_memfd() { + // Msgpack-encoded TracerMetadata with tracer_language="cpp" and + // threadlocal_attribute_keys=["http.method", "http.target", "http.user"]. + const char tracer_data[] = + "\x86" // fixmap with 6 entries + "\xae" "schema_version" "\x02" // "schema_version": 2 + "\xaf" "tracer_language" "\xa3" "cpp" // "tracer_language": "cpp" + "\xae" "tracer_version" "\xa5" "0.0.1" // "tracer_version": "0.0.1" + "\xa8" "hostname" "\xa4" "test" // "hostname": "test" + "\xac" "service_name" "\xa8" "oteltest" // "service_name": "oteltest" + "\xba" "threadlocal_attribute_keys" // key (26 chars = 0xa0 | 26 = 0xba) + "\x93" // fixarray with 3 elements + "\xab" "http.method" // str (11 chars) + "\xab" "http.target" // str (11 chars) + "\xa9" "http.user"; // str (9 chars) + + int fd = memfd_create("datadog-tracer-info-oteltest", MFD_ALLOW_SEALING); + if (fd < 0) { + fprintf(stderr, "memfd_create failed\n"); + return -1; + } + + ssize_t written = write(fd, tracer_data, sizeof(tracer_data) - 1); + if (written != (ssize_t)(sizeof(tracer_data) - 1)) { + fprintf(stderr, "memfd write failed\n"); + close(fd); + return -1; + } + + if (fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW) < 0) { + fprintf(stderr, "memfd seal failed\n"); + close(fd); + return -1; + } + + return fd; +} + +// OTel Thread Local Context Record with inline attrs_data buffer. +// Used for testing: 28-byte header + up to 64 bytes of attrs. +struct otel_record_with_attrs { + struct otel_thread_ctx_record header; + uint8_t attrs_data[64]; +}; + +struct otel_thread_opts { + char **argv; + int memfd; // fd returned by create_tracer_memfd(), kept open for agent to read +}; + +static void *thread_otel_open(void *data) { + struct otel_thread_opts *opts = (struct otel_thread_opts *)data; + +#if defined(__x86_64__) || defined(__aarch64__) + // Create and seal a tracer-info memfd to trigger the agent's dynsym resolver. + opts->memfd = create_tracer_memfd(); + if (opts->memfd < 0) { + fprintf(stderr, "Failed to create tracer memfd\n"); + return NULL; + } + + // Wait for the agent to process the memfd seal event and populate the BPF map. + usleep(500000); + + // RFC 4-step writer protocol: + // Step 1: Ensure pointer is NULL so readers see no record during construction. + otel_thread_ctx_v1 = NULL; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + // Step 2: Build the record with custom attributes in a separate buffer. + __int128_t trace_id = atouint128(opts->argv[1]); + uint64_t span_id = (uint64_t)atol(opts->argv[2]); + + struct otel_record_with_attrs full_record; + memset(&full_record, 0, sizeof(full_record)); + + uint64_t trace_hi = (uint64_t)(trace_id >> 64); + uint64_t trace_lo = (uint64_t)(trace_id); + u64_to_be_bytes(trace_hi, &full_record.header.trace_id[0]); + u64_to_be_bytes(trace_lo, &full_record.header.trace_id[8]); + u64_to_be_bytes(span_id, full_record.header.span_id); + + // Build attrs_data: repeated [key(u8) + length(u8) + val(u8[length])] + // Attr 0: key=0 ("http.method"), len=3, val="GET" + // Attr 1: key=1 ("http.target"), len=5, val="/test" + // Attr 2: key=2 ("http.user"), len=18, val="will@datadoghq.com" + uint8_t *p = full_record.attrs_data; + int off = 0; + + p[off++] = 0; p[off++] = 3; + memcpy(&p[off], "GET", 3); off += 3; + + p[off++] = 1; p[off++] = 5; + memcpy(&p[off], "/test", 5); off += 5; + + p[off++] = 2; p[off++] = 18; + memcpy(&p[off], "will@datadoghq.com", 18); off += 18; + + full_record.header.attrs_data_size = off; + + // Step 3: Mark the record as valid. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + full_record.header.valid = 1; + + // Step 4: Publish the pointer to the record header. + // The eBPF code reads the 28-byte header first, then attrs_data from header+28. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &full_record.header; + __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_thread_ctx_v1 = NULL; + return NULL; + } + close(fd); + unlink(opts->argv[3]); + + // Detach context: set TLS pointer to NULL. + otel_thread_ctx_v1 = NULL; +#else + fprintf(stderr, "OTel TLS test not supported on this architecture\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; + } + +#if !defined(__x86_64__) && !defined(__aarch64__) + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); + return EXIT_FAILURE; +#endif + + struct otel_thread_opts opts = { .argv = argv, .memfd = -1 }; + + pthread_t thread; + if (pthread_create(&thread, NULL, thread_otel_open, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + if (opts.memfd >= 0) close(opts.memfd); + return EXIT_SUCCESS; +} + +// Test: OTel TLS discovered but the record has valid=0 (invalid). +// The eBPF reader should reject this record and return zero span context. +static void *thread_otel_open_invalid(void *data) { + struct otel_thread_opts *opts = (struct otel_thread_opts *)data; + +#if defined(__x86_64__) || defined(__aarch64__) + opts->memfd = create_tracer_memfd(); + if (opts->memfd < 0) { + fprintf(stderr, "Failed to create tracer memfd\n"); + return NULL; + } + usleep(500000); + + otel_thread_ctx_v1 = NULL; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + __int128_t trace_id = atouint128(opts->argv[1]); + uint64_t span_id = (uint64_t)atol(opts->argv[2]); + + struct otel_thread_ctx_record record; + memset(&record, 0, sizeof(record)); + + 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]); + u64_to_be_bytes(span_id, record.span_id); + record.attrs_data_size = 0; + + // Deliberately leave valid=0 to simulate an incomplete/invalid record. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + record.valid = 0; + + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &record; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + int fd = open(opts->argv[3], O_CREAT); + if (fd < 0) { + fprintf(stderr, "Unable to create file `%s`\n", opts->argv[3]); + otel_thread_ctx_v1 = NULL; + return NULL; + } + close(fd); + unlink(opts->argv[3]); + + otel_thread_ctx_v1 = NULL; +#else + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); +#endif + + return NULL; +} + +int otel_span_open_invalid(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: otel-span-open-invalid \n"); + return EXIT_FAILURE; + } + +#if !defined(__x86_64__) && !defined(__aarch64__) + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); + return EXIT_FAILURE; +#endif + + struct otel_thread_opts opts = { .argv = argv, .memfd = -1 }; + + pthread_t thread; + if (pthread_create(&thread, NULL, thread_otel_open_invalid, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + if (opts.memfd >= 0) close(opts.memfd); + return EXIT_SUCCESS; +} + +// Test: OTel TLS discovered but the pointer is never set (remains NULL). +// The eBPF reader should see NULL and return zero span context. +static void *thread_otel_open_null_ptr(void *data) { + struct otel_thread_opts *opts = (struct otel_thread_opts *)data; + +#if defined(__x86_64__) || defined(__aarch64__) + opts->memfd = create_tracer_memfd(); + if (opts->memfd < 0) { + fprintf(stderr, "Failed to create tracer memfd\n"); + return NULL; + } + usleep(500000); + + // Do NOT set otel_thread_ctx_v1 — leave it as NULL. + // The eBPF reader should read NULL from [thread_pointer + tls_offset] and bail out. + + int fd = open(opts->argv[1], O_CREAT); + if (fd < 0) { + fprintf(stderr, "Unable to create file `%s`\n", opts->argv[1]); + return NULL; + } + close(fd); + unlink(opts->argv[1]); +#else + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); +#endif + + return NULL; +} + +int otel_span_open_null_ptr(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: otel-span-open-null-ptr \n"); + return EXIT_FAILURE; + } + +#if !defined(__x86_64__) && !defined(__aarch64__) + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); + return EXIT_FAILURE; +#endif + + struct otel_thread_opts opts = { .argv = argv, .memfd = -1 }; + + pthread_t thread; + if (pthread_create(&thread, NULL, thread_otel_open_null_ptr, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + if (opts.memfd >= 0) close(opts.memfd); + + return EXIT_SUCCESS; +} + int ptrace_traceme() { int child = fork(); if (child == 0) { @@ -1234,6 +1553,48 @@ int test_tracer_memfd(int argc, char **argv) { return EXIT_SUCCESS; } +int test_tracer_memfd_with_keys(int argc, char **argv) { + // TracerMetadata with threadlocal_attribute_keys=["http.method", "http.target", "http.user"] + const char tracer_data[] = + "\x89" // fixmap with 9 entries + "\xae" "schema_version" "\x02" // "schema_version": 2 + "\xaf" "tracer_language" "\xa3" "cpp" // "tracer_language": "cpp" + "\xae" "tracer_version" "\xa5" "0.0.1" // "tracer_version": "0.0.1" + "\xa8" "hostname" "\xa4" "test" // "hostname": "test" + "\xac" "service_name" + "\xac" "test-service" + "\xab" "service_env" + "\xa8" "test-env" + "\xaf" "service_version" + "\xa5" "1.0.0" + "\xac" "process_tags" + "\xb0" "custom.tag:value" + "\xba" "threadlocal_attribute_keys" // key (26 chars) + "\x93" // fixarray with 3 elements + "\xab" "http.method" // str (11 chars) + "\xab" "http.target" // str (11 chars) + "\xa9" "http.user"; // str (9 chars) + + int fd = memfd_create("datadog-tracer-info-keytest0", MFD_ALLOW_SEALING); + if (fd < 0) { + err(1, "%s failed", "memfd_create"); + } + + ssize_t written = write(fd, tracer_data, sizeof(tracer_data) - 1); + if (written != (ssize_t)(sizeof(tracer_data) - 1)) { + err(1, "%s failed: wrote %zd bytes, expected %lu", "write", written, sizeof(tracer_data) - 1); + } + + if (fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW) < 0) { + err(1, "%s failed", "fcntl F_ADD_SEALS"); + } + + sleep(3); + + close(fd); + return EXIT_SUCCESS; +} + int test_new_netns_exec(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "Please specify at least an executable path\n"); @@ -2040,6 +2401,12 @@ 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, "otel-span-open-invalid") == 0) { + exit_code = otel_span_open_invalid(sub_argc, sub_argv); + } else if (strcmp(cmd, "otel-span-open-null-ptr") == 0) { + exit_code = otel_span_open_null_ptr(sub_argc, sub_argv); } else if (strcmp(cmd, "pipe-chown") == 0) { exit_code = test_pipe_chown(); } else if (strcmp(cmd, "signal") == 0) { @@ -2084,6 +2451,8 @@ int main(int argc, char **argv) { exit_code = test_memfd_create(sub_argc, sub_argv); } else if (strcmp(cmd, "tracer-memfd") == 0) { exit_code = test_tracer_memfd(sub_argc, sub_argv); + } else if (strcmp(cmd, "tracer-memfd-with-keys") == 0) { + exit_code = test_tracer_memfd_with_keys(sub_argc, sub_argv); } else if (strcmp(cmd, "new_netns_exec") == 0) { exit_code = test_new_netns_exec(sub_argc, sub_argv); } else if (strcmp(cmd, "slow-cat") == 0) { diff --git a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go index f2ae8b7c8dd7..0d282c8980f1 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -10,21 +10,27 @@ package main import ( "bytes" + "context" _ "embed" "flag" "fmt" "net/http" "os" "os/exec" + "runtime" + "runtime/pprof" "strconv" "syscall" "time" "unsafe" + "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" manager "github.com/DataDog/ebpf-manager" "github.com/syndtr/gocapability/capability" "github.com/vishvananda/netlink" + "github.com/vmihailenco/msgpack/v5" authenticationv1 "k8s.io/api/authentication/v1" + "golang.org/x/sys/unix" "github.com/DataDog/datadog-agent/cmd/cws-instrumentation/subcommands/injectcmd" "github.com/DataDog/datadog-agent/pkg/security/tests/testutils" @@ -47,6 +53,12 @@ var ( loginUIDPath string loginUIDEventType string loginUIDValue int + goSpanTest bool + goSpanSpanID string + goSpanLocalRootSpanID string + goSpanFilePath string + ddtraceSpanTest bool + ddtraceSpanFilePath string ) //go:embed ebpf_probe.o @@ -269,6 +281,152 @@ func RunLoginUIDTest() error { return nil } +// RunGoSpanTest creates a tracer-info memfd (triggering Go label offset resolution), +// sets pprof labels simulating what dd-trace-go does, then opens a file. +// The eBPF reader should extract the span context from the goroutine's pprof labels. +func RunGoSpanTest(spanID, localRootSpanID, filePath string) error { + // Create and seal a tracer-info memfd with tracer_language="go". + // This triggers the agent's AddTracerMetadata → resolveGoLabels flow. + type TracerMeta struct { + SchemaVersion uint8 `msgpack:"schema_version"` + TracerLanguage string `msgpack:"tracer_language"` + TracerVersion string `msgpack:"tracer_version"` + Hostname string `msgpack:"hostname"` + ServiceName string `msgpack:"service_name"` + } + meta := TracerMeta{ + SchemaVersion: 2, + TracerLanguage: "go", + TracerVersion: "0.0.1-test", + Hostname: "test", + ServiceName: "go-span-test", + } + data, err := msgpack.Marshal(&meta) + if err != nil { + return fmt.Errorf("msgpack marshal: %w", err) + } + + fd, err := unix.MemfdCreate("datadog-tracer-info-gotest01", unix.MFD_ALLOW_SEALING) + if err != nil { + return fmt.Errorf("memfd_create: %w", err) + } + defer unix.Close(fd) + + if _, err := unix.Write(fd, data); err != nil { + return fmt.Errorf("memfd write: %w", err) + } + const fAddSeals = 1033 // F_ADD_SEALS + const fSealWrite = 0x0008 + const fSealShrink = 0x0002 + const fSealGrow = 0x0004 + if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), fAddSeals, fSealWrite|fSealShrink|fSealGrow); errno != 0 { + return fmt.Errorf("memfd seal: %w", errno) + } + + // Wait for the agent to process the memfd seal event and populate the go_labels_procs BPF map. + time.Sleep(500 * time.Millisecond) + + // Set pprof labels exactly like dd-trace-go does. + // Keys: "span id" and "local root span id", values: decimal strings. + labels := pprof.Labels("span id", spanID, "local root span id", localRootSpanID) + ctx := pprof.WithLabels(context.Background(), labels) + pprof.SetGoroutineLabels(ctx) + defer pprof.SetGoroutineLabels(context.Background()) + + // Trigger the file open that the CWS rule is watching. + f, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("create file: %w", err) + } + f.Close() + os.Remove(filePath) + + return nil +} + +// RunDDTraceSpanTest uses dd-trace-go to create a real span, which sets pprof +// labels automatically via the profiler code hotspots integration. This tests +// the full dd-trace-go → pprof labels → eBPF Go labels reader pipeline. +func RunDDTraceSpanTest(filePath string) error { + // Create and seal a tracer-info memfd with tracer_language="go". + type TracerMeta struct { + SchemaVersion uint8 `msgpack:"schema_version"` + TracerLanguage string `msgpack:"tracer_language"` + TracerVersion string `msgpack:"tracer_version"` + Hostname string `msgpack:"hostname"` + ServiceName string `msgpack:"service_name"` + } + meta := TracerMeta{ + SchemaVersion: 2, + TracerLanguage: "go", + TracerVersion: "0.0.1-test", + Hostname: "test", + ServiceName: "ddtrace-test", + } + data, err := msgpack.Marshal(&meta) + if err != nil { + return fmt.Errorf("msgpack marshal: %w", err) + } + + fd, err := unix.MemfdCreate("datadog-tracer-info-ddtrace0", unix.MFD_ALLOW_SEALING) + if err != nil { + return fmt.Errorf("memfd_create: %w", err) + } + defer unix.Close(fd) + + if _, err := unix.Write(fd, data); err != nil { + return fmt.Errorf("memfd write: %w", err) + } + const fAddSeals = 1033 + const fSealWrite = 0x0008 + const fSealShrink = 0x0002 + const fSealGrow = 0x0004 + if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), fAddSeals, fSealWrite|fSealShrink|fSealGrow); errno != 0 { + return fmt.Errorf("memfd seal: %w", errno) + } + + // Wait for the agent to process the memfd seal event and populate the BPF map. + time.Sleep(500 * time.Millisecond) + + // Start dd-trace-go with: + // - WithTestDefaults: uses a dummy transport (no real agent needed) + // - WithProfilerCodeHotspots: enables "span id" and "local root span id" pprof labels + // - WithService: set a service name + tracer.Start( + tracer.WithTestDefaults(nil), + tracer.WithProfilerCodeHotspots(true), + tracer.WithService("ddtrace-test"), + tracer.WithLogStartup(false), + ) + defer tracer.Stop() + + // Create a span. dd-trace-go will automatically set pprof labels + // "span id" and "local root span id" on the current goroutine. + span, ctx := tracer.StartSpanFromContext(context.Background(), "test.operation") + + // Print the span ID and local root span ID so the test can parse and verify them. + spanID := span.Context().SpanID() + localRootSpanID := span.Root().Context().SpanID() + fmt.Printf("ddtrace_span_id=%d\n", spanID) + fmt.Printf("ddtrace_local_root_span_id=%d\n", localRootSpanID) + + _ = ctx + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Trigger the file open that the CWS rule is watching. + f, err := os.Create(filePath) + if err != nil { + span.Finish() + return fmt.Errorf("create file: %w", err) + } + f.Close() + os.Remove(filePath) + + span.Finish() + return nil +} + func main() { flag.BoolVar(&bpfLoad, "load-bpf", false, "load the eBPF programs") flag.BoolVar(&bpfClone, "clone-bpf", false, "clone maps") @@ -285,6 +443,12 @@ func main() { flag.StringVar(&loginUIDPath, "login-uid-path", "", "file used for the login_uid open test") flag.StringVar(&loginUIDEventType, "login-uid-event-type", "", "event type used for the login_uid open test") flag.IntVar(&loginUIDValue, "login-uid-value", 0, "uid used for the login_uid open test") + flag.BoolVar(&goSpanTest, "go-span-test", false, "when set, runs the Go pprof labels span test") + flag.StringVar(&goSpanSpanID, "go-span-span-id", "", "span ID for the Go span test (decimal string)") + flag.StringVar(&goSpanLocalRootSpanID, "go-span-local-root-span-id", "", "local root span ID for the Go span test (decimal string)") + flag.StringVar(&goSpanFilePath, "go-span-file-path", "", "file path to open for the Go span test") + flag.BoolVar(&ddtraceSpanTest, "ddtrace-span-test", false, "when set, runs the dd-trace-go span test") + flag.StringVar(&ddtraceSpanFilePath, "ddtrace-span-file-path", "", "file path to open for the dd-trace-go span test") flag.Parse() @@ -345,4 +509,16 @@ func main() { panic(err) } } + + if goSpanTest { + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath); err != nil { + panic(err) + } + } + + if ddtraceSpanTest { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath); err != nil { + panic(err) + } + } } diff --git a/pkg/security/tests/tracer_memfd_test.go b/pkg/security/tests/tracer_memfd_test.go index 9cfcc79f39cb..38432669ddad 100644 --- a/pkg/security/tests/tracer_memfd_test.go +++ b/pkg/security/tests/tracer_memfd_test.go @@ -161,6 +161,31 @@ func TestTracerMemfd(t *testing.T) { assert.Contains(t, tmeta.ProcessTags, "custom.tag:value", "ProcessTags should contain custom.tag") }) + test.RunMultiMode(t, "validate-threadlocal-attribute-keys", func(t *testing.T, _ wrapperType, cmd func(bin string, args []string, envs []string) *exec.Cmd) { + consumer.eventReceived.Store(false) + consumer.capturedPid.Store(0) + consumer.capturedFd.Store(0) + + cmdExec := cmd(syscallTester, []string{"tracer-memfd-with-keys"}, nil) + _ = cmdExec.Run() + + require.Eventually(t, consumer.eventReceived.Load, 2*time.Second, 200*time.Millisecond, "tracer-memfd event should be received") + + consumer.capturedMutex.Lock() + tmeta := consumer.capturedMetadata + consumer.capturedMutex.Unlock() + + require.NotEmpty(t, tmeta.ServiceName, "ServiceName should not be empty") + assert.Equal(t, "test-service", tmeta.ServiceName, "ServiceName mismatch") + assert.Equal(t, "cpp", tmeta.TracerLanguage, "TracerLanguage mismatch") + + // Verify threadlocal_attribute_keys are parsed from the memfd + require.Len(t, tmeta.ThreadlocalAttributeKeys, 3, "should have 3 threadlocal attribute keys") + assert.Equal(t, "http.method", tmeta.ThreadlocalAttributeKeys[0]) + assert.Equal(t, "http.target", tmeta.ThreadlocalAttributeKeys[1]) + assert.Equal(t, "http.user", tmeta.ThreadlocalAttributeKeys[2]) + }) + test.RunMultiMode(t, "validate-tracer-serialization", func(t *testing.T, _ wrapperType, cmd func(bin string, args []string, envs []string) *exec.Cmd) { consumer.eventReceived.Store(false) consumer.capturedPid.Store(0) diff --git a/pkg/util/safeelf/types.go b/pkg/util/safeelf/types.go index 80d5547e9310..907be79ae8ce 100644 --- a/pkg/util/safeelf/types.go +++ b/pkg/util/safeelf/types.go @@ -60,6 +60,7 @@ const STB_WEAK = elf.STB_WEAK const STT_OBJECT = elf.STT_OBJECT const STT_FUNC = elf.STT_FUNC const STT_FILE = elf.STT_FILE +const STT_TLS = elf.STT_TLS const SHN_UNDEF = elf.SHN_UNDEF const SHF_WRITE = elf.SHF_WRITE const SHT_NOBITS = elf.SHT_NOBITS