From 930fbe9ff78791963aa2718812c2e836053258ac Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 10 Apr 2026 17:56:29 +0200 Subject: [PATCH 01/14] [WP] Implementation of Otel 4947 thread context --- .../tracermetadata/model/tracer_metadata.go | 21 ++ .../model/tracer_metadata_gen.go | 86 ++++- pkg/network/events/monitor.go | 2 +- pkg/security/ebpf/c/include/constants/enums.h | 7 + .../c/include/constants/offsets/process.h | 28 ++ pkg/security/ebpf/c/include/helpers/span.h | 22 +- .../ebpf/c/include/helpers/span_otel.h | 109 +++++++ pkg/security/ebpf/c/include/hooks/exec.h | 2 + pkg/security/ebpf/c/include/maps.h | 1 + pkg/security/ebpf/c/include/structs/process.h | 23 ++ pkg/security/ebpf/probes/all.go | 4 + .../probe/constantfetch/constant_names.go | 8 + pkg/security/probe/constantfetch/fallback.go | 28 ++ pkg/security/probe/erpc/erpc.go | 2 + pkg/security/probe/probe_ebpf.go | 12 + pkg/security/ptracer/erpc.go | 3 + pkg/security/ptracer/span.go | 1 + pkg/security/resolvers/process/otel_tls.go | 177 +++++++++++ .../resolvers/process/resolver_ebpf.go | 33 +- pkg/security/serializers/serializers_linux.go | 2 +- pkg/security/tests/span_test.go | 142 +++++++++ .../tests/syscall_tester/c/syscall_tester.c | 293 ++++++++++++++++++ pkg/util/safeelf/types.go | 1 + 23 files changed, 990 insertions(+), 17 deletions(-) create mode 100644 pkg/security/ebpf/c/include/helpers/span_otel.h create mode 100644 pkg/security/resolvers/process/otel_tls.go 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/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 5a78e0bfc894..061d301dbd7e 100644 --- a/pkg/security/ebpf/c/include/constants/enums.h +++ b/pkg/security/ebpf/c/include/constants/enums.h @@ -160,6 +160,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, @@ -245,6 +251,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..8ccb8336f170 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,16 @@ int __attribute__((always_inline)) unregister_span_memory() { return 0; } +// --- OTel Thread Local Context Record helpers (separate file) --- +#include "span_otel.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,11 +50,19 @@ void __attribute__((always_inline)) fill_span_context(struct span_context_t *spa int offset = (tid % tls->max_threads) * sizeof(struct span_context_t); int ret = bpf_probe_read_user(span, sizeof(struct span_context_t), tls->base + offset); - if (ret < 0) { - span->span_id = 0; - span->trace_id[0] = span->trace_id[1] = 0; + if (ret >= 0 && (span->span_id != 0 || span->trace_id[0] != 0 || span->trace_id[1] != 0)) { + return; } } + + // Fall back to OTel Thread Local Context Record (native applications only). + if (fill_span_context_otel(span)) { + return; + } + + // No span context available. + span->span_id = 0; + span->trace_id[0] = span->trace_id[1] = 0; } void __attribute__((always_inline)) reset_span_context(struct span_context_t *span) { diff --git a/pkg/security/ebpf/c/include/helpers/span_otel.h b/pkg/security/ebpf/c/include/helpers/span_otel.h new file mode 100644 index 000000000000..aa00f9150f4b --- /dev/null +++ b/pkg/security/ebpf/c/include/helpers/span_otel.h @@ -0,0 +1,109 @@ +#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. +// Support for additional runtimes (e.g., Go via pprof labels) will be added later. + +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; +} + +// 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 a different mechanism (pprof labels) that will be added later. + 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); + + return 1; +} + +#endif diff --git a/pkg/security/ebpf/c/include/hooks/exec.h b/pkg/security/ebpf/c/include/hooks/exec.h index 295246f1eded..777939ec68e5 100644 --- a/pkg/security/ebpf/c/include/hooks/exec.h +++ b/pkg/security/ebpf/c/include/hooks/exec.h @@ -374,6 +374,7 @@ int __attribute__((always_inline)) handle_do_exit(ctx_t *ctx) { send_event(ctx, EVENT_EXIT, event); unregister_span_memory(); + unregister_otel_tls(); // [activity_dump] cleanup tracing state for this pid cleanup_traced_state(tgid); @@ -871,6 +872,7 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { // as previously registered memory will become unreachable, we'll have to unregister the TLS unregister_span_memory(); + unregister_otel_tls(); return 0; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index 45a24d1a1e75..448997396266 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -79,6 +79,7 @@ BPF_LRU_MAP(exec_pid_transfer, u32, u64, 512) BPF_LRU_MAP(netns_cache, u32, u32, 40960) BPF_LRU_MAP(mntns_cache, u32, u32, 40960) BPF_LRU_MAP(span_tls, u32, struct span_tls_t, 1) // max entries will be overridden at runtime +BPF_LRU_MAP(otel_tls, u32, struct otel_tls_t, 1) // max entries will be overridden at runtime BPF_LRU_MAP(inode_discarders, struct inode_discarder_t, struct inode_discarder_params_t, 4096) BPF_LRU_MAP(prctl_discarders, char[MAX_PRCTL_NAME_LEN], int, 1024) BPF_LRU_MAP(auid_discarders, u32, struct auid_discarder_params_t, 1024) diff --git a/pkg/security/ebpf/c/include/structs/process.h b/pkg/security/ebpf/c/include/structs/process.h index 758d231ad5e6..9b1d24eba2b5 100644 --- a/pkg/security/ebpf/c/include/structs/process.h +++ b/pkg/security/ebpf/c/include/structs/process.h @@ -81,4 +81,27 @@ 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. +// Support for additional runtimes (e.g., Go via pprof labels) will be added later. +struct otel_thread_ctx_record_t { + u8 trace_id[16]; // W3C Trace Context byte order (big-endian) + u8 span_id[8]; // W3C Trace Context byte order (big-endian) + u8 valid; // must be 1 for the record to be considered valid + u8 _reserved; // padding for alignment + u16 attrs_data_size; // size of custom attributes data (not read) +}; + +// OTel TLSDESC-based TLS registration for a process. +// The tls_offset is discovered by user-space by parsing the 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; +}; + #endif diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index 228851630d5b..3c01c3813273 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -277,6 +277,10 @@ func AllMapSpecEditors(numCPU int, opts MapSpecEditorOpts, kv *kernel.Version) m MaxEntries: uint32(opts.SpanTrackMaxCount), EditorFlag: manager.EditMaxEntries, }, + "otel_tls": { + MaxEntries: uint32(opts.SpanTrackMaxCount), + EditorFlag: manager.EditMaxEntries, + }, "capabilities_usage": { MaxEntries: capabilitiesUsageMaxEntries, EditorFlag: manager.EditMaxEntries, diff --git a/pkg/security/probe/constantfetch/constant_names.go b/pkg/security/probe/constantfetch/constant_names.go index 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 7acd9d2b0d6d..8665f0643311 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -16,6 +16,7 @@ import ( "net/netip" "os" "path/filepath" + "runtime" "slices" "sort" "strings" @@ -3505,6 +3506,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/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 97d337531d8c..18a6c150431d 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -86,6 +86,7 @@ type EBPFResolver struct { pidCacheMap ebpf.Map pathIDMap ebpf.Map kernelThreadPidsMap ebpf.Map + otelTLSMap ebpf.Map opts ResolverOpts // stats @@ -1465,7 +1466,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))) @@ -1476,16 +1479,36 @@ 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 OTel TLS resolution. Done outside the lock to avoid holding it + // during ELF I/O. Non-fatal: the symbol may not be present if the application + // doesn't use OTel thread-local 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 { @@ -1552,10 +1575,6 @@ func (p *EBPFResolver) Start(ctx context.Context) error { return err } - if p.kernelThreadPidsMap, err = managerhelper.Map(p.manager, "kernel_thread_pids"); err != nil { - return err - } - go p.cacheFlush(ctx) return nil diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 615d6127d3e9..409ebdd03a99 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -1021,7 +1021,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 } diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 089c6de42e91..91148267ac2c 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -12,6 +12,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "strconv" "testing" @@ -111,3 +112,144 @@ 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") + }) + }) +} diff --git a/pkg/security/tests/syscall_tester/c/syscall_tester.c b/pkg/security/tests/syscall_tester/c/syscall_tester.c index 44ed6fb0d56f..3fa4825ceba1 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -180,6 +180,293 @@ 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. +// 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". + const char tracer_data[] = + "\x85" // fixmap with 5 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" + + 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; +} + +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 in a separate buffer. + __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; + + // Step 3: Mark the record as valid. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + record.valid = 1; + + // Step 4: Publish the pointer to the record. + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &record; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + // Trigger the syscall that the test is waiting for. + int fd = open(opts->argv[3], O_CREAT); + if (fd < 0) { + fprintf(stderr, "Unable to create file `%s`\n", opts->argv[3]); + otel_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) { @@ -2242,6 +2529,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) { 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 From 95b731d1871230573e8f0d80241da96dc77faeae Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 10 Apr 2026 19:45:56 +0200 Subject: [PATCH 02/14] [WP] Implementation of Otel 4947 thread context: golang pprof label --- pkg/security/ebpf/c/include/helpers/span.h | 8 + pkg/security/ebpf/c/include/helpers/span_go.h | 251 ++++++++++++++++++ .../ebpf/c/include/helpers/span_otel.h | 4 +- pkg/security/ebpf/c/include/hooks/exec.h | 2 + pkg/security/ebpf/c/include/maps.h | 1 + pkg/security/ebpf/c/include/structs/process.h | 42 ++- pkg/security/ebpf/probes/all.go | 4 + pkg/security/resolvers/process/go_labels.go | 193 ++++++++++++++ .../resolvers/process/resolver_ebpf.go | 24 +- pkg/security/tests/span_test.go | 61 +++++ .../syscall_tester/go/syscall_go_tester.go | 81 ++++++ 11 files changed, 662 insertions(+), 9 deletions(-) create mode 100644 pkg/security/ebpf/c/include/helpers/span_go.h create mode 100644 pkg/security/resolvers/process/go_labels.go diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 8ccb8336f170..a6c4f171e20e 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -31,6 +31,9 @@ int __attribute__((always_inline)) unregister_span_memory() { // --- 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) { @@ -60,6 +63,11 @@ void __attribute__((always_inline)) fill_span_context(struct span_context_t *spa 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; 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 index aa00f9150f4b..8fbdba37ebc6 100644 --- a/pkg/security/ebpf/c/include/helpers/span_otel.h +++ b/pkg/security/ebpf/c/include/helpers/span_otel.h @@ -9,7 +9,7 @@ // 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. -// Support for additional runtimes (e.g., Go via pprof labels) will be added later. +// 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(); @@ -65,7 +65,7 @@ static int __attribute__((always_inline)) fill_span_context_otel(struct span_con } // Only resolve TLS-based context for native runtimes. - // Go uses a different mechanism (pprof labels) that will be added later. + // Go uses pprof labels instead (see span_go.h / fill_span_context_go). if (otls->runtime != OTEL_RUNTIME_NATIVE) { return 0; } diff --git a/pkg/security/ebpf/c/include/hooks/exec.h b/pkg/security/ebpf/c/include/hooks/exec.h index 777939ec68e5..743d638912d2 100644 --- a/pkg/security/ebpf/c/include/hooks/exec.h +++ b/pkg/security/ebpf/c/include/hooks/exec.h @@ -375,6 +375,7 @@ int __attribute__((always_inline)) handle_do_exit(ctx_t *ctx) { unregister_span_memory(); unregister_otel_tls(); + unregister_go_labels(); // [activity_dump] cleanup tracing state for this pid cleanup_traced_state(tgid); @@ -873,6 +874,7 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { // as previously registered memory will become unreachable, we'll have to unregister the TLS unregister_span_memory(); unregister_otel_tls(); + unregister_go_labels(); return 0; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index 448997396266..adf7a83e165e 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -80,6 +80,7 @@ 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(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/process.h b/pkg/security/ebpf/c/include/structs/process.h index 9b1d24eba2b5..05c16d1e2ad3 100644 --- a/pkg/security/ebpf/c/include/structs/process.h +++ b/pkg/security/ebpf/c/include/structs/process.h @@ -84,7 +84,7 @@ struct span_tls_t { // 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. -// Support for additional runtimes (e.g., Go via pprof labels) will be added later. +// 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) @@ -104,4 +104,44 @@ struct otel_tls_t { 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 3c01c3813273..891a4660b973 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -281,6 +281,10 @@ func AllMapSpecEditors(numCPU int, opts MapSpecEditorOpts, kv *kernel.Version) m MaxEntries: uint32(opts.SpanTrackMaxCount), EditorFlag: manager.EditMaxEntries, }, + "go_labels_procs": { + MaxEntries: uint32(opts.SpanTrackMaxCount), + EditorFlag: manager.EditMaxEntries, + }, "capabilities_usage": { MaxEntries: capabilitiesUsageMaxEntries, EditorFlag: manager.EditMaxEntries, 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/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index 18a6c150431d..e47b218ba765 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -87,6 +87,7 @@ type EBPFResolver struct { pathIDMap ebpf.Map kernelThreadPidsMap ebpf.Map otelTLSMap ebpf.Map + goLabelsMap ebpf.Map opts ResolverOpts // stats @@ -1485,12 +1486,19 @@ func (p *EBPFResolver) AddTracerMetadata(pid uint32, event *model.Event) error { } p.Unlock() - // Attempt OTel TLS resolution. Done outside the lock to avoid holding it - // during ELF I/O. Non-fatal: the symbol may not be present if the application - // doesn't use OTel thread-local 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) + // 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) + } } } @@ -1575,6 +1583,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/tests/span_test.go b/pkg/security/tests/span_test.go index 91148267ac2c..a9cdd6411e09 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -253,3 +253,64 @@ func TestOTelSpan(t *testing.T) { }) }) } + +// 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") + }) + }) +} 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..fe7f180598b2 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -10,12 +10,14 @@ package main import ( "bytes" + "context" _ "embed" "flag" "fmt" "net/http" "os" "os/exec" + "runtime/pprof" "strconv" "syscall" "time" @@ -24,7 +26,9 @@ import ( 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 +51,10 @@ var ( loginUIDPath string loginUIDEventType string loginUIDValue int + goSpanTest bool + goSpanSpanID string + goSpanLocalRootSpanID string + goSpanFilePath string ) //go:embed ebpf_probe.o @@ -269,6 +277,69 @@ 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 +} + func main() { flag.BoolVar(&bpfLoad, "load-bpf", false, "load the eBPF programs") flag.BoolVar(&bpfClone, "clone-bpf", false, "clone maps") @@ -285,6 +356,10 @@ 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.Parse() @@ -345,4 +420,10 @@ func main() { panic(err) } } + + if goSpanTest { + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath); err != nil { + panic(err) + } + } } From 5c95dd27cbd02616a685ee9d06de4e93163f13c7 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 10 Apr 2026 19:46:18 +0200 Subject: [PATCH 03/14] [WP] Implementation of Otel 4947 thread context: dd-trace-go test --- pkg/security/tests/span_test.go | 80 ++++++++++++++++ .../syscall_tester/go/syscall_go_tester.go | 95 +++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index a9cdd6411e09..32ca7b38fa5b 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "runtime" "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -314,3 +315,82 @@ func TestGoSpan(t *testing.T) { }) }) } + +// 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/go/syscall_go_tester.go b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go index fe7f180598b2..0d282c8980f1 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -17,12 +17,14 @@ import ( "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" @@ -55,6 +57,8 @@ var ( goSpanSpanID string goSpanLocalRootSpanID string goSpanFilePath string + ddtraceSpanTest bool + ddtraceSpanFilePath string ) //go:embed ebpf_probe.o @@ -340,6 +344,89 @@ func RunGoSpanTest(spanID, localRootSpanID, filePath string) error { 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") @@ -360,6 +447,8 @@ func main() { 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() @@ -426,4 +515,10 @@ func main() { panic(err) } } + + if ddtraceSpanTest { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath); err != nil { + panic(err) + } + } } From 85e056c486857702344881dbf6d0d6f4877e1b9d Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 10 Apr 2026 23:31:29 +0200 Subject: [PATCH 04/14] [WP] Add support for attrs data --- docs/cloud-workload-security/backend_linux.md | 27 +++++ .../backend_linux.schema.json | 13 +++ .../testdata/tracer_cpp_with_attrs.data | 1 + .../tracermetadata/tracer_memfd_test.go | 17 +++ pkg/security/ebpf/c/include/helpers/span.h | 2 + .../ebpf/c/include/helpers/span_otel.h | 31 +++++ pkg/security/ebpf/c/include/maps.h | 1 + .../ebpf/c/include/structs/events_context.h | 2 + pkg/security/ebpf/c/include/structs/process.h | 20 +++- pkg/security/ebpf/probes/all.go | 4 + pkg/security/probe/probe_ebpf.go | 75 +++++++++++- .../secl/model/consts_map_names_linux.go | 3 + .../secl/model/event_deep_copy_unix.go | 3 + .../secl/model/event_deep_copy_windows.go | 1 + pkg/security/secl/model/model.go | 6 +- .../secl/model/unmarshallers_linux.go | 6 +- pkg/security/seclwin/model/model.go | 6 +- pkg/security/serializers/serializers_linux.go | 3 + .../tests/syscall_tester/c/syscall_tester.c | 110 +++++++++++++++--- pkg/security/tests/tracer_memfd_test.go | 25 ++++ 20 files changed, 331 insertions(+), 25 deletions(-) create mode 100644 pkg/discovery/tracermetadata/testdata/tracer_cpp_with_attrs.data diff --git a/docs/cloud-workload-security/backend_linux.md b/docs/cloud-workload-security/backend_linux.md index 5353a3324411..2886871ed920 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, @@ -2313,6 +2320,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, @@ -3110,6 +3123,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, @@ -3123,6 +3143,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` @@ -5980,6 +6001,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 879bb5ad3c92..9e84fb9acc80 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, @@ -2302,6 +2309,12 @@ }, "logs_collected": { "type": "boolean" + }, + "threadlocal_attribute_keys": { + "items": { + "type": "string" + }, + "type": "array" } }, "additionalProperties": false, 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/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index a6c4f171e20e..1c15963b5a79 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -71,12 +71,14 @@ void __attribute__((always_inline)) fill_span_context(struct span_context_t *spa // 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_otel.h b/pkg/security/ebpf/c/include/helpers/span_otel.h index 8fbdba37ebc6..42fa0aea217b 100644 --- a/pkg/security/ebpf/c/include/helpers/span_otel.h +++ b/pkg/security/ebpf/c/include/helpers/span_otel.h @@ -52,6 +52,10 @@ static u64 __attribute__((always_inline)) read_thread_pointer() { 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). @@ -103,6 +107,33 @@ static int __attribute__((always_inline)) fill_span_context_otel(struct span_con 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; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index adf7a83e165e..e0a18ee5741d 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -81,6 +81,7 @@ 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 0a41a37aec58..8f7f4f32a969 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 05c16d1e2ad3..75facaaf8bf0 100644 --- a/pkg/security/ebpf/c/include/structs/process.h +++ b/pkg/security/ebpf/c/include/structs/process.h @@ -90,7 +90,25 @@ struct otel_thread_ctx_record_t { u8 span_id[8]; // W3C Trace Context byte order (big-endian) u8 valid; // must be 1 for the record to be considered valid u8 _reserved; // padding for alignment - u16 attrs_data_size; // size of custom attributes data (not read) + 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. diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index 891a4660b973..950216d8838b 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -285,6 +285,10 @@ func AllMapSpecEditors(numCPU int, opts MapSpecEditorOpts, kv *kernel.Version) m 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/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 8665f0643311..00f900096a59 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -10,6 +10,7 @@ package probe import ( "context" + "encoding/binary" "errors" "fmt" "math" @@ -19,6 +20,7 @@ import ( "runtime" "slices" "sort" + "strconv" "strings" "sync" "time" @@ -157,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 @@ -589,6 +594,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 @@ -1066,6 +1074,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, @@ -1334,6 +1402,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) { return diff --git a/pkg/security/secl/model/consts_map_names_linux.go b/pkg/security/secl/model/consts_map_names_linux.go index d1d817fae5ad..7bd00b40cf62 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", @@ -82,6 +83,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 c5bb3d2870f9..dacbdf7d89d8 100644 --- a/pkg/security/secl/model/event_deep_copy_unix.go +++ b/pkg/security/secl/model/event_deep_copy_unix.go @@ -417,6 +417,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 @@ -1127,6 +1128,8 @@ func deepCopySocketEvent(fieldToCopy SocketEvent) SocketEvent { } 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 e0b6339f082a..d5bdaade1414 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 9493466503b7..2dedfafdf095 100644 --- a/pkg/security/secl/model/unmarshallers_linux.go +++ b/pkg/security/secl/model/unmarshallers_linux.go @@ -534,14 +534,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 e0b6339f082a..d5bdaade1414 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_linux.go b/pkg/security/serializers/serializers_linux.go index 409ebdd03a99..4e90bb6dd037 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -1462,6 +1462,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 { @@ -1469,6 +1471,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/tests/syscall_tester/c/syscall_tester.c b/pkg/security/tests/syscall_tester/c/syscall_tester.c index 3fa4825ceba1..21a5ae93131f 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -214,16 +214,23 @@ static void u64_to_be_bytes(uint64_t val, uint8_t *out) { // 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". + // Msgpack-encoded TracerMetadata with tracer_language="cpp" and + // threadlocal_attribute_keys=["http.method", "http.target", "http.user"]. const char tracer_data[] = - "\x85" // fixmap with 5 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" + "\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) { @@ -247,6 +254,13 @@ static int create_tracer_memfd() { 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 @@ -271,27 +285,45 @@ static void *thread_otel_open(void *data) { otel_thread_ctx_v1 = NULL; __atomic_signal_fence(__ATOMIC_SEQ_CST); - // Step 2: Build the record in a separate buffer. + // 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_thread_ctx_record record; - memset(&record, 0, sizeof(record)); + 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, &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; + 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); - record.valid = 1; + full_record.header.valid = 1; - // Step 4: Publish the pointer to the record. + // 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 = &record; + otel_thread_ctx_v1 = &full_record.header; __atomic_signal_fence(__ATOMIC_SEQ_CST); // Trigger the syscall that the test is waiting for. @@ -1577,6 +1609,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"); @@ -2581,6 +2655,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/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) From ef1a2d59c1bc124f97ec0399f22a786363ecf6ea Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Tue, 26 May 2026 16:12:16 +0200 Subject: [PATCH 05/14] [WP] Regenerate post rebase --- pkg/security/ebpf/c/include/helpers/span.h | 40 +- .../resolvers/process/resolver_ebpf.go | 4 + .../serializers_base_linux_easyjson.go | 85 +++-- .../serializers/serializers_linux_easyjson.go | 343 +++++++++++------- 4 files changed, 303 insertions(+), 169 deletions(-) diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 1c15963b5a79..4221130aa94e 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -37,26 +37,26 @@ int __attribute__((always_inline)) unregister_span_memory() { // --- 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; - - struct task_struct *current_ptr = (struct task_struct *)bpf_get_current_task(); - u32 pid = get_namespace_nr_from_task_struct(current_ptr); - if (pid) { - tid = pid; - } - - 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] != 0 || span->trace_id[1] != 0)) { - return; - } - } +// 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; +// +// struct task_struct *current_ptr = (struct task_struct *)bpf_get_current_task(); +// u32 pid = get_namespace_nr_from_task_struct(current_ptr); +// if (pid) { +// tid = pid; +// } +// +// 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] != 0 || span->trace_id[1] != 0)) { +// return; +// } +// } // Fall back to OTel Thread Local Context Record (native applications only). if (fill_span_context_otel(span)) { diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index e47b218ba765..414e72eee300 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -1583,6 +1583,10 @@ func (p *EBPFResolver) Start(ctx context.Context) error { return err } + if p.kernelThreadPidsMap, err = managerhelper.Map(p.manager, "kernel_thread_pids"); err != nil { + 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") diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 70b96b9e7655..20ee392dfe3d 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -1676,6 +1676,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() } @@ -1745,6 +1772,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) { @@ -1911,21 +1952,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(']') @@ -1960,14 +2001,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(']') @@ -2253,13 +2294,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(']') @@ -2316,11 +2357,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(']') } @@ -2855,13 +2896,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(']') @@ -2944,11 +2985,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_easyjson.go b/pkg/security/serializers/serializers_linux_easyjson.go index c3ed7b5fc16a..5cdc17c79d8d 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.go +++ b/pkg/security/serializers/serializers_linux_easyjson.go @@ -3047,6 +3047,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() } @@ -3116,6 +3143,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 easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in *jlexer.Lexer, out *SyscallSerializer) { @@ -3287,13 +3328,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( 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(']') @@ -3314,13 +3355,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( 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(']') @@ -3428,11 +3469,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( 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(']') } @@ -3444,11 +3485,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( 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(']') } @@ -3952,13 +3993,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( 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(']') @@ -4006,11 +4047,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( 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(']') } @@ -4259,13 +4300,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( 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(']') @@ -4283,34 +4324,34 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( 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('}') @@ -4365,11 +4406,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( } { 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(']') } @@ -4384,24 +4425,24 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( } { 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(']') } @@ -4573,13 +4614,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( 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(']') @@ -4690,13 +4731,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( 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(']') @@ -4907,11 +4948,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( 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(']') } @@ -4971,11 +5012,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( 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(']') } @@ -5375,13 +5416,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( 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(']') @@ -5492,13 +5533,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( 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(']') @@ -5749,11 +5790,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( 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(']') } @@ -5813,11 +5854,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( 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(']') } @@ -6195,9 +6236,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( *out.SyscallsEventSerializer = (*out.SyscallsEventSerializer)[:0] } for !in.IsDelim(']') { - var v59 SyscallSerializer - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in, &v59) - *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v59) + var v62 SyscallSerializer + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in, &v62) + *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v62) in.WantComma() } in.Delim(']') @@ -6639,11 +6680,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( 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(',') } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out, v61) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out, v64) } out.RawByte(']') } @@ -6856,6 +6897,30 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( } 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() } @@ -6886,6 +6951,30 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32( } 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('}') } @@ -7006,13 +7095,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( 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(']') @@ -7033,13 +7122,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( 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(']') @@ -7130,11 +7219,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( 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(']') } @@ -7146,11 +7235,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( 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(']') } @@ -7203,13 +7292,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( 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(']') @@ -7246,11 +7335,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( 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(']') } @@ -7302,13 +7391,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( 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(']') @@ -7329,13 +7418,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( 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(']') @@ -7361,11 +7450,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( 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(']') } @@ -7377,11 +7466,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( 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(']') } @@ -7428,13 +7517,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( 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(']') @@ -7455,13 +7544,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( 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(']') @@ -7486,11 +7575,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( 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(']') } @@ -7505,11 +7594,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( } { 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(']') } @@ -7718,13 +7807,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39( 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(']') @@ -7789,11 +7878,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39( } { 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(']') } @@ -8054,13 +8143,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43( 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(']') @@ -8091,11 +8180,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43( 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(']') } From f7511a0f9baff2b7bbc612a03610c34f612a8d31 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Tue, 26 May 2026 17:32:15 +0200 Subject: [PATCH 06/14] [WP] Add configuration parameter for legacy apm correlation --- pkg/config/setup/system_probe_cws.go | 6 ++ pkg/security/config/config.go | 8 ++ pkg/security/ebpf/c/include/helpers/span.h | 87 +++++++++++++++++----- pkg/security/probe/probe_ebpf.go | 4 + pkg/security/tests/module_tester.go | 1 + pkg/security/tests/module_tester_linux.go | 2 + pkg/security/tests/span_test.go | 8 +- pkg/security/tests/testopts.go | 1 + 8 files changed, 96 insertions(+), 21 deletions(-) diff --git a/pkg/config/setup/system_probe_cws.go b/pkg/config/setup/system_probe_cws.go index 6de38ad89469..68637bb8ec98 100644 --- a/pkg/config/setup/system_probe_cws.go +++ b/pkg/config/setup/system_probe_cws.go @@ -172,6 +172,12 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Setup) { // return as handled (constant patched at probe load). Defaults to false. cfg.BindEnvAndSetDefault("runtime_security_config.syscalls.capture_all_errors.enabled", false) + // CWS - APM correlation + // When enabled, the eBPF probe also consults the legacy Datadog proprietary + // TLS span context (populated via eRPC) when building a span context. Disabled + // by default; OTel and Go pprof-label paths are always enabled. + cfg.BindEnvAndSetDefault("runtime_security_config.apm_correlation.legacy_enabled", false) + // CWS -eBPF Less cfg.BindEnvAndSetDefault("runtime_security_config.ebpfless.enabled", false) cfg.BindEnvAndSetDefault("runtime_security_config.ebpfless.socket", constants.DefaultEBPFLessProbeAddr) diff --git a/pkg/security/config/config.go b/pkg/security/config/config.go index c4dda6b6248b..08df3ccecd06 100644 --- a/pkg/security/config/config.go +++ b/pkg/security/config/config.go @@ -407,6 +407,11 @@ type RuntimeSecurityConfig struct { // IS_UNHANDLED_ERROR treats every negative syscall return as handled. CaptureAllSyscallErrorsEnabled bool + // LegacyAPMCorrelationEnabled, when true, sets the eBPF load-time constant + // so fill_span_context also consults the legacy Datadog proprietary TLS map + // (span_tls) populated via eRPC RegisterSpanTLSOP. + LegacyAPMCorrelationEnabled bool + // EBPFLessEnabled enables the ebpfless probe EBPFLessEnabled bool // EBPFLessSocket defines the socket used for the communication between system-probe and the ebpfless source @@ -682,6 +687,9 @@ func NewRuntimeSecurityConfig() (*RuntimeSecurityConfig, error) { // Capture all syscall errors CaptureAllSyscallErrorsEnabled: pkgconfigsetup.SystemProbe().GetBool("runtime_security_config.syscalls.capture_all_errors.enabled"), + // APM correlation + LegacyAPMCorrelationEnabled: pkgconfigsetup.SystemProbe().GetBool("runtime_security_config.apm_correlation.legacy_enabled"), + // ebpf less EBPFLessEnabled: IsEBPFLessModeEnabled(), EBPFLessSocket: pkgconfigsetup.SystemProbe().GetString("runtime_security_config.ebpfless.socket"), diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 4221130aa94e..547f0b25e446 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -5,6 +5,18 @@ #include "process.h" +#include "constants/macros.h" + +// Load-time gate for the legacy Datadog proprietary TLS APM-correlation path. +// The manager patches this constant at probe load based on +// runtime_security_config.apm_correlation.legacy_enabled (default: false). +// When the constant is 0, the verifier sees the gated block as dead code. +static __attribute__((always_inline)) int legacy_apm_correlation_enabled(void) { + u64 enabled; + LOAD_CONSTANT("legacy_apm_correlation_enabled", enabled); + return enabled != 0; +} + // --- Datadog proprietary span TLS (existing mechanism) --- int __attribute__((always_inline)) handle_register_span_memory(void *data) { @@ -34,29 +46,64 @@ int __attribute__((always_inline)) unregister_span_memory() { // --- Go pprof labels helpers (separate file) --- #include "span_go.h" +// --- Legacy Datadog proprietary TLS lookup --- +// +// Marked __noinline__ so it gets its own 512-byte BPF stack frame and does not +// contribute to the caller's stack budget. This mirrors the technique used for +// fill_span_context_go in span_go.h (see comment there), and is the only way +// to keep module_load → trace_init_module_ret under the 512-byte verifier +// limit once init_module_event_t (event + name + args buffers) is on the +// caller's stack. +// +// All locals in this body — the pointer returned by bpf_map_lookup_elem, the +// resolved namespace PID, and the inlined helper's scratch — live in this +// subprog's own frame and disappear after the call returns. +// +// Returns 1 when the proprietary TLS lookup produced a non-zero span context +// (caller should stop and return), 0 otherwise. + +// Wire-protocol slot size for the legacy proprietary TLS array, fixed at +// 24 bytes: span_id (u64) + trace_id (__int128). This is independent of +// sizeof(struct span_context_t), which grew to 32 bytes when has_extra_attrs +// + _pad[7] were appended for the OTel path. User-space registrants +// (dd-trace, syscall_tester) still write 24-byte slots, so the eBPF reader +// must use 24 for both the per-thread offset and the read length. +#define LEGACY_SPAN_TLS_SLOT_SIZE 24 + +int __attribute__((__noinline__)) fill_span_context_legacy(struct span_context_t *span) { + if (!span) { + return 0; + } + + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tgid = pid_tgid >> 32; + + struct span_tls_t *tls = bpf_map_lookup_elem(&span_tls, &tgid); + if (!tls) { + return 0; + } + + u32 tid = get_namespace_nr_from_task_struct((struct task_struct *)bpf_get_current_task()); + if (!tid) { + tid = (u32)pid_tgid; + } + + if (bpf_probe_read_user(span, LEGACY_SPAN_TLS_SLOT_SIZE, + tls->base + (tid % tls->max_threads) * LEGACY_SPAN_TLS_SLOT_SIZE) < 0) { + return 0; + } + return span->span_id != 0 || span->trace_id[0] != 0 || span->trace_id[1] != 0; +} + // --- 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; -// -// struct task_struct *current_ptr = (struct task_struct *)bpf_get_current_task(); -// u32 pid = get_namespace_nr_from_task_struct(current_ptr); -// if (pid) { -// tid = pid; -// } -// -// 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] != 0 || span->trace_id[1] != 0)) { -// return; -// } -// } + // Legacy Datadog proprietary TLS — opt-in via load-time constant. The + // helper is __noinline__ so its locals never contribute to this caller's + // stack frame, regardless of whether the gate is on or off. + if (legacy_apm_correlation_enabled() && fill_span_context_legacy(span)) { + return; + } // Fall back to OTel Thread Local Context Record (native applications only). if (fill_span_context_otel(span)) { diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 00f900096a59..8ff230ab47fb 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -2918,6 +2918,10 @@ func (p *EBPFProbe) initManagerOptionsConstants() { Name: "capture_all_errors_enabled", Value: utils.BoolTouint64(p.config.RuntimeSecurity.CaptureAllSyscallErrorsEnabled), }, + manager.ConstantEditor{ + Name: "legacy_apm_correlation_enabled", + Value: utils.BoolTouint64(p.config.RuntimeSecurity.LegacyAPMCorrelationEnabled), + }, manager.ConstantEditor{ Name: "imds_ip", Value: uint64(p.config.RuntimeSecurity.IMDSIPv4), diff --git a/pkg/security/tests/module_tester.go b/pkg/security/tests/module_tester.go index 19c3bb1358c2..5fb64bb2ab2d 100644 --- a/pkg/security/tests/module_tester.go +++ b/pkg/security/tests/module_tester.go @@ -851,6 +851,7 @@ func genTestConfigs(t testing.TB, cfgDir string, opts testOpts) (*emconfig.Confi "NetworkFlowMonitorEnabled": opts.networkFlowMonitorEnabled, "CapabilitiesMonitoringEnabled": opts.capabilitiesMonitoringEnabled, "CaptureAllSyscallErrorsEnabled": opts.captureAllSyscallErrorsEnabled, + "LegacyAPMCorrelationEnabled": opts.legacyAPMCorrelationEnabled, }); err != nil { return nil, nil, err } diff --git a/pkg/security/tests/module_tester_linux.go b/pkg/security/tests/module_tester_linux.go index 7022e26181b5..cf882afced02 100644 --- a/pkg/security/tests/module_tester_linux.go +++ b/pkg/security/tests/module_tester_linux.go @@ -218,6 +218,8 @@ runtime_security_config: syscalls: capture_all_errors: enabled: {{ .CaptureAllSyscallErrorsEnabled }} + apm_correlation: + legacy_enabled: {{ .LegacyAPMCorrelationEnabled }} ` const ( diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 32ca7b38fa5b..9a6f5b02d699 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -39,7 +39,13 @@ func TestSpan(t *testing.T) { }, } - test, err := newTestModule(t, nil, ruleDefs) + // TestSpan exercises the legacy Datadog proprietary TLS path (eRPC + // RegisterSpanTLSOP populates span_tls; the eBPF probe reads back the + // trace_id/span_id via that map). That path is gated off by default; opt in + // here so the runtime constant patches the eBPF program to consult it. + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{ + legacyAPMCorrelationEnabled: true, + })) if err != nil { t.Fatal(err) } diff --git a/pkg/security/tests/testopts.go b/pkg/security/tests/testopts.go index f62d73ef1ab0..9ca7c9192872 100644 --- a/pkg/security/tests/testopts.go +++ b/pkg/security/tests/testopts.go @@ -75,6 +75,7 @@ type testOpts struct { traceSystemdCgroups bool capabilitiesMonitoringEnabled bool captureAllSyscallErrorsEnabled bool + legacyAPMCorrelationEnabled bool } type dynamicTestOpts struct { From 0aed84879d24ece95a54bad16e0b97b83656abd6 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Tue, 26 May 2026 18:54:20 +0200 Subject: [PATCH 07/14] [WP] Improve test coverage --- pkg/security/tests/span_test.go | 376 +++++++++++++++++- .../tests/syscall_tester/c/syscall_tester.c | 200 ++++++++++ .../syscall_tester/go/syscall_go_tester.go | 234 ++++++----- 3 files changed, 695 insertions(+), 115 deletions(-) diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 9a6f5b02d699..794de7ea73f7 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -130,6 +130,8 @@ func TestOTelSpan(t *testing.T) { t.Skip("OTel TLSDESC span test only supported on amd64 and arm64") } + executable := which(t, "touch") + ruleDefs := []*rules.RuleDefinition{ { ID: "test_otel_span_rule_open", @@ -143,6 +145,12 @@ func TestOTelSpan(t *testing.T) { ID: "test_otel_span_rule_open_null_ptr", Expression: `open.file.path == "{{.Root}}/test-otel-span-null-ptr"`, }, + { + // Shared exec rule for all OTel exec sub-tests. Each sub-test runs + // sequentially and waits for its own match. + ID: "test_otel_span_rule_exec", + Expression: fmt.Sprintf(`exec.file.path in [ "/usr/bin/touch", "%s" ] && exec.args_flags == "reference"`, executable), + }, } test, err := newTestModule(t, nil, ruleDefs) @@ -158,6 +166,17 @@ func TestOTelSpan(t *testing.T) { fakeTraceID128b := "136272290892501783905308705057321818530" + // otelExecArgs returns the touch invocation that the exec rule matches. + // The std and docker modes use a different touch binary path; the rule + // covers both via `exec.file.path in [ "/usr/bin/touch", "" ]`. + otelExecArgs := func(kind wrapperType, testFile string) []string { + touchPath := "/usr/bin/touch" + if kind == stdWrapperType { + touchPath = executable + } + return []string{touchPath, "--reference", "/etc/passwd", testFile} + } + 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") @@ -259,6 +278,88 @@ func TestOTelSpan(t *testing.T) { }, "test_otel_span_rule_open_null_ptr") }) }) + + t.Run("valid_record_exec", func(t *testing.T) { + // Exec path: the TLS record is set before execv. fill_exec_context → + // fill_span_context runs at prepare_binprm, before the new image + // takes over, so the TLS read still succeeds. + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-otel-span-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := append([]string{"otel-span-exec", fakeTraceID128b, "204"}, otelExecArgs(kind, testFile)...) + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + test.validateSpanSchema(t, event) + + assert.Equal(t, "204", strconv.FormatUint(event.SpanContext.SpanID, 10)) + assert.Equal(t, fakeTraceID128b, event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_exec") + }) + }) + + t.Run("invalid_record_exec", func(t *testing.T) { + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-otel-span-exec-invalid") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := append([]string{"otel-span-exec-invalid", fakeTraceID128b, "204"}, otelExecArgs(kind, testFile)...) + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + // valid=0 → no span context. + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_exec") + }) + }) + + t.Run("null_pointer_exec", func(t *testing.T) { + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-otel-span-exec-null-ptr") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := append([]string{"otel-span-exec-null-ptr"}, otelExecArgs(kind, testFile)...) + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + // NULL TLS pointer → no span context. + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_otel_span_rule_exec") + }) + }) } // TestGoSpan tests Go pprof label-based span context collection. @@ -267,11 +368,21 @@ func TestOTelSpan(t *testing.T) { func TestGoSpan(t *testing.T) { SkipIfNotAvailable(t) + executable := which(t, "touch") + ruleDefs := []*rules.RuleDefinition{ { ID: "test_go_span_rule_open", Expression: `open.file.path == "{{.Root}}/test-go-span"`, }, + { + ID: "test_go_span_rule_open_no_labels", + Expression: `open.file.path == "{{.Root}}/test-go-span-no-labels"`, + }, + { + ID: "test_go_span_rule_exec", + Expression: fmt.Sprintf(`exec.file.path in [ "/usr/bin/touch", "%s" ] && exec.args_flags == "reference"`, executable), + }, } test, err := newTestModule(t, nil, ruleDefs) @@ -285,6 +396,15 @@ func TestGoSpan(t *testing.T) { t.Fatal(err) } + // touchPathFor picks the touch binary path the wrapper-mode expects so the + // exec rule's `in [ "/usr/bin/touch", "" ]` clause matches. + touchPathFor := func(kind wrapperType) string { + if kind == stdWrapperType { + return executable + } + return "/usr/bin/touch" + } + 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") @@ -313,6 +433,8 @@ func TestGoSpan(t *testing.T) { }, func(event *model.Event, rule *rules.Rule) { assertTriggeredRule(t, rule, "test_go_span_rule_open") + test.validateSpanSchema(t, event) + 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, @@ -320,6 +442,104 @@ func TestGoSpan(t *testing.T) { }, "test_go_span_rule_open") }) }) + + t.Run("valid_span_exec", func(t *testing.T) { + // Set pprof labels then execv touch. fill_span_context_go runs at + // prepare_binprm — before the image switch — so the goroutine's + // labels are still readable. + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-go-span-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-go-span-exec-test", + "-go-span-span-id", "987654321", + "-go-span-local-root-span-id", "123456789", + "-go-span-file-path", testFile, + "-go-span-exec-target", touchPathFor(kind), + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + test.validateSpanSchema(t, event) + + 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_exec") + }) + }) + + t.Run("no_labels", func(t *testing.T) { + // Memfd is registered (so the agent resolves Go label offsets) but + // pprof labels are never set. The eBPF reader should yield an empty + // 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-go-span-no-labels") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-go-span-no-labels-test", + "-go-span-file-path", testFile, + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_no_labels") + + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_go_span_rule_open_no_labels") + }) + }) + + t.Run("no_labels_exec", func(t *testing.T) { + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-go-span-no-labels-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-go-span-no-labels-exec-test", + "-go-span-file-path", testFile, + "-go-span-exec-target", touchPathFor(kind), + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_go_span_rule_exec") + }) + }) } // TestDDTraceGoSpan tests the full dd-trace-go integration: dd-trace-go creates @@ -328,11 +548,21 @@ func TestGoSpan(t *testing.T) { func TestDDTraceGoSpan(t *testing.T) { SkipIfNotAvailable(t) + executable := which(t, "touch") + ruleDefs := []*rules.RuleDefinition{ { ID: "test_ddtrace_span_rule_open", Expression: `open.file.path == "{{.Root}}/test-ddtrace-span"`, }, + { + ID: "test_ddtrace_span_rule_open_no_span", + Expression: `open.file.path == "{{.Root}}/test-ddtrace-span-no-span"`, + }, + { + ID: "test_ddtrace_span_rule_exec", + Expression: fmt.Sprintf(`exec.file.path in [ "/usr/bin/touch", "%s" ] && exec.args_flags == "reference"`, executable), + }, } test, err := newTestModule(t, nil, ruleDefs) @@ -346,6 +576,30 @@ func TestDDTraceGoSpan(t *testing.T) { t.Fatal(err) } + touchPathFor := func(kind wrapperType) string { + if kind == stdWrapperType { + return executable + } + return "/usr/bin/touch" + } + + // parseDDTraceIDs scans the tester's stdout for the span/local-root-span + // IDs that dd-trace-go generated at runtime. Returns (0, 0) when not + // found (used by the no-span negative path). + parseDDTraceIDs := func(out []byte) (spanID, lrsID uint64) { + for _, line := range strings.Split(string(out), "\n") { + if strings.HasPrefix(line, "ddtrace_span_id=") { + val := strings.TrimPrefix(line, "ddtrace_span_id=") + spanID, _ = 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=") + lrsID, _ = strconv.ParseUint(strings.TrimSpace(val), 10, 64) + } + } + return spanID, lrsID + } + 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") @@ -358,40 +612,25 @@ func TestDDTraceGoSpan(t *testing.T) { "-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) + cmd := cmdFunc(goSyscallTester, args, []string{}) 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) - } - } - + expectedSpanID, expectedLocalRootSpanID = parseDDTraceIDs(out) 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") + test.validateSpanSchema(t, event) + 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, @@ -399,4 +638,103 @@ func TestDDTraceGoSpan(t *testing.T) { }, "test_ddtrace_span_rule_open") }) }) + + t.Run("ddtrace_span_exec", func(t *testing.T) { + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-ddtrace-span-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-ddtrace-span-exec-test", + "-ddtrace-span-file-path", testFile, + "-ddtrace-span-exec-target", touchPathFor(kind), + } + + var expectedSpanID, expectedLocalRootSpanID uint64 + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", out, err) + } + expectedSpanID, expectedLocalRootSpanID = parseDDTraceIDs(out) + 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_exec") + + test.validateSpanSchema(t, event) + + 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_exec") + }) + }) + + t.Run("no_span", func(t *testing.T) { + // dd-trace-go is started but no active span is created. The eBPF + // reader should yield an empty 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-ddtrace-span-no-span") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-ddtrace-no-span-test", + "-ddtrace-span-file-path", testFile, + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_ddtrace_span_rule_open_no_span") + + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_ddtrace_span_rule_open_no_span") + }) + }) + + t.Run("no_span_exec", func(t *testing.T) { + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-ddtrace-span-no-span-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-ddtrace-no-span-exec-test", + "-ddtrace-span-file-path", testFile, + "-ddtrace-span-exec-target", touchPathFor(kind), + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_ddtrace_span_rule_exec") + + assert.Equal(t, uint64(0), event.SpanContext.SpanID) + assert.Equal(t, "0", event.SpanContext.TraceID.String()) + }, "test_ddtrace_span_rule_exec") + }) + }) } diff --git a/pkg/security/tests/syscall_tester/c/syscall_tester.c b/pkg/security/tests/syscall_tester/c/syscall_tester.c index 21a5ae93131f..60f9b3c3ac14 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -499,6 +499,200 @@ int otel_span_open_null_ptr(int argc, char **argv) { return EXIT_SUCCESS; } +// --- OTel exec variants --- +// The exec hook fires at prepare_binprm (before image replacement), so the +// calling thread's TLS is still intact when the eBPF probe reads it. +// argv layout (matches the legacy span-exec command): argv[1]=trace_id, +// argv[2]=span_id, argv[3..]=exec target + its args. + +static void *thread_otel_exec(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_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); + + 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; + + __atomic_signal_fence(__ATOMIC_SEQ_CST); + full_record.header.valid = 1; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &full_record.header; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + // execv replaces the thread's image; TLS is read by the eBPF probe at + // prepare_binprm, before the new image takes over. + execv(opts->argv[3], opts->argv + 3); + fprintf(stderr, "execv failed for %s\n", 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_exec(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: otel-span-exec [args...]\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_exec, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + if (opts.memfd >= 0) close(opts.memfd); + return EXIT_SUCCESS; +} + +// Exec variant of the invalid-record (valid=0) negative test. +static void *thread_otel_exec_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; + record.valid = 0; // intentionally invalid + + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &record; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + execv(opts->argv[3], opts->argv + 3); + fprintf(stderr, "execv failed for %s\n", 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_exec_invalid(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: otel-span-exec-invalid [args...]\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_exec_invalid, &opts) < 0) { + return EXIT_FAILURE; + } + pthread_join(thread, NULL); + + if (opts.memfd >= 0) close(opts.memfd); + return EXIT_SUCCESS; +} + +// Exec variant of the null-pointer negative test. +static void *thread_otel_exec_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); + + // Leave otel_thread_ctx_v1 NULL. + execv(opts->argv[1], opts->argv + 1); + fprintf(stderr, "execv failed for %s\n", opts->argv[1]); +#else + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); +#endif + + return NULL; +} + +int otel_span_exec_null_ptr(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: otel-span-exec-null-ptr [args...]\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_exec_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) { @@ -2609,6 +2803,12 @@ int main(int argc, char **argv) { 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, "otel-span-exec") == 0) { + exit_code = otel_span_exec(sub_argc, sub_argv); + } else if (strcmp(cmd, "otel-span-exec-invalid") == 0) { + exit_code = otel_span_exec_invalid(sub_argc, sub_argv); + } else if (strcmp(cmd, "otel-span-exec-null-ptr") == 0) { + exit_code = otel_span_exec_null_ptr(sub_argc, sub_argv); } else if (strcmp(cmd, "pipe-chown") == 0) { exit_code = test_pipe_chown(); } else if (strcmp(cmd, "signal") == 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 0d282c8980f1..5e466cd654eb 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -53,12 +53,20 @@ var ( loginUIDPath string loginUIDEventType string loginUIDValue int - goSpanTest bool - goSpanSpanID string - goSpanLocalRootSpanID string - goSpanFilePath string - ddtraceSpanTest bool - ddtraceSpanFilePath string + goSpanTest bool + goSpanExecTest bool + goSpanNoLabelsTest bool + goSpanNoLabelsExecTest bool + goSpanSpanID string + goSpanLocalRootSpanID string + goSpanFilePath string + goSpanExecTarget string + ddtraceSpanTest bool + ddtraceSpanExecTest bool + ddtraceNoSpanTest bool + ddtraceNoSpanExecTest bool + ddtraceSpanFilePath string + ddtraceSpanExecTarget string ) //go:embed ebpf_probe.o @@ -281,12 +289,10 @@ 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. +// setupGoTracerMemfd creates and seals the tracer-info memfd that drives the +// agent's resolveGoLabels flow. Shared by all Go-span test modes (with/without +// pprof labels, open or exec). +func setupGoTracerMemfd(serviceName, memfdName string) (int, error) { type TracerMeta struct { SchemaVersion uint8 `msgpack:"schema_version"` TracerLanguage string `msgpack:"tracer_language"` @@ -294,99 +300,102 @@ func RunGoSpanTest(spanID, localRootSpanID, filePath string) error { Hostname string `msgpack:"hostname"` ServiceName string `msgpack:"service_name"` } - meta := TracerMeta{ + data, err := msgpack.Marshal(&TracerMeta{ SchemaVersion: 2, TracerLanguage: "go", TracerVersion: "0.0.1-test", Hostname: "test", - ServiceName: "go-span-test", - } - data, err := msgpack.Marshal(&meta) + ServiceName: serviceName, + }) if err != nil { - return fmt.Errorf("msgpack marshal: %w", err) + return -1, fmt.Errorf("msgpack marshal: %w", err) } - fd, err := unix.MemfdCreate("datadog-tracer-info-gotest01", unix.MFD_ALLOW_SEALING) + fd, err := unix.MemfdCreate(memfdName, unix.MFD_ALLOW_SEALING) if err != nil { - return fmt.Errorf("memfd_create: %w", err) + return -1, 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) + unix.Close(fd) + return -1, 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) + unix.Close(fd) + return -1, fmt.Errorf("memfd seal: %w", errno) } - // Wait for the agent to process the memfd seal event and populate the go_labels_procs BPF map. + // Wait for the agent to process the memfd seal event and populate the + // go_labels_procs BPF map. time.Sleep(500 * time.Millisecond) + return fd, nil +} - // 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. +// triggerOpen creates filePath, closes, and unlinks — the CWS rule fires on +// the open hook before unlink. +func triggerOpen(filePath string) error { 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) +// triggerExec execs the target binary with `--reference /etc/passwd ` +// so the existing exec rule (exec.args_flags == "reference") matches. The +// current process image is replaced; the eBPF probe captures the span context +// at prepare_binprm, before the replacement. +func triggerExec(target, filePath string) error { + if target == "" { + return fmt.Errorf("exec target is required") } + argv := []string{target, "--reference", "/etc/passwd", filePath} + return syscall.Exec(target, argv, os.Environ()) +} - fd, err := unix.MemfdCreate("datadog-tracer-info-ddtrace0", unix.MFD_ALLOW_SEALING) +// RunGoSpanTest creates a tracer-info memfd (triggering Go label offset +// resolution), optionally sets pprof labels (skipped for negative-path +// scenarios), and then either opens a file or execs a target. The eBPF reader +// should extract the span context from the goroutine's pprof labels when the +// labels are set, or yield an empty span context when they are not. +func RunGoSpanTest(spanID, localRootSpanID, filePath, execTarget string, setLabels bool) error { + fd, err := setupGoTracerMemfd("go-span-test", "datadog-tracer-info-gotest01") if err != nil { - return fmt.Errorf("memfd_create: %w", err) + return err } defer unix.Close(fd) - if _, err := unix.Write(fd, data); err != nil { - return fmt.Errorf("memfd write: %w", err) + if setLabels { + // 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()) } - 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) + + if execTarget != "" { + return triggerExec(execTarget, filePath) } + return triggerOpen(filePath) +} - // Wait for the agent to process the memfd seal event and populate the BPF map. - time.Sleep(500 * time.Millisecond) +// RunDDTraceSpanTest uses dd-trace-go to create a real span (which sets pprof +// labels via the profiler code-hotspots integration) and then triggers either +// an open or an exec depending on execTarget. If startSpan is false, the +// tracer is started but no active span is created — the eBPF reader should +// yield an empty span context (negative path). +func RunDDTraceSpanTest(filePath, execTarget string, startSpan bool) error { + fd, err := setupGoTracerMemfd("ddtrace-test", "datadog-tracer-info-ddtrace0") + if err != nil { + return err + } + defer unix.Close(fd) // Start dd-trace-go with: // - WithTestDefaults: uses a dummy transport (no real agent needed) @@ -400,31 +409,30 @@ func RunDDTraceSpanTest(filePath string) error { ) 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) + var span *tracer.Span + if startSpan { + // dd-trace-go will automatically set pprof labels "span id" and + // "local root span id" on the current goroutine. + var ctx context.Context + span, ctx = tracer.StartSpanFromContext(context.Background(), "test.operation") + _ = ctx - _ = ctx - runtime.LockOSThread() - defer runtime.UnlockOSThread() + // 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) - // 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) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer span.Finish() } - f.Close() - os.Remove(filePath) - span.Finish() - return nil + if execTarget != "" { + return triggerExec(execTarget, filePath) + } + return triggerOpen(filePath) } func main() { @@ -443,12 +451,20 @@ 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.BoolVar(&goSpanTest, "go-span-test", false, "when set, runs the Go pprof labels span test (open, labels set)") + flag.BoolVar(&goSpanExecTest, "go-span-exec-test", false, "when set, runs the Go pprof labels span exec test (exec, labels set)") + flag.BoolVar(&goSpanNoLabelsTest, "go-span-no-labels-test", false, "when set, runs the Go span open test WITHOUT setting pprof labels (negative path)") + flag.BoolVar(&goSpanNoLabelsExecTest, "go-span-no-labels-exec-test", false, "when set, runs the Go span exec test WITHOUT setting pprof labels (negative path)") 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.StringVar(&goSpanFilePath, "go-span-file-path", "", "file path to open / touch for the Go span test") + flag.StringVar(&goSpanExecTarget, "go-span-exec-target", "", "executable to exec for the Go span exec test (e.g. /usr/bin/touch)") + flag.BoolVar(&ddtraceSpanTest, "ddtrace-span-test", false, "when set, runs the dd-trace-go span test (open, active span)") + flag.BoolVar(&ddtraceSpanExecTest, "ddtrace-span-exec-test", false, "when set, runs the dd-trace-go span exec test (exec, active span)") + flag.BoolVar(&ddtraceNoSpanTest, "ddtrace-no-span-test", false, "when set, runs the dd-trace-go open test WITHOUT an active span (negative path)") + flag.BoolVar(&ddtraceNoSpanExecTest, "ddtrace-no-span-exec-test", false, "when set, runs the dd-trace-go exec test WITHOUT an active span (negative path)") + flag.StringVar(&ddtraceSpanFilePath, "ddtrace-span-file-path", "", "file path to open / touch for the dd-trace-go span test") + flag.StringVar(&ddtraceSpanExecTarget, "ddtrace-span-exec-target", "", "executable to exec for the dd-trace-go span exec test (e.g. /usr/bin/touch)") flag.Parse() @@ -510,14 +526,40 @@ func main() { } } - if goSpanTest { - if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath); err != nil { + switch { + case goSpanTest: + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, "", true); err != nil { + panic(err) + } + case goSpanExecTest: + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, goSpanExecTarget, true); err != nil { + panic(err) + } + case goSpanNoLabelsTest: + if err := RunGoSpanTest("", "", goSpanFilePath, "", false); err != nil { + panic(err) + } + case goSpanNoLabelsExecTest: + if err := RunGoSpanTest("", "", goSpanFilePath, goSpanExecTarget, false); err != nil { panic(err) } } - if ddtraceSpanTest { - if err := RunDDTraceSpanTest(ddtraceSpanFilePath); err != nil { + switch { + case ddtraceSpanTest: + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", true); err != nil { + panic(err) + } + case ddtraceSpanExecTest: + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, true); err != nil { + panic(err) + } + case ddtraceNoSpanTest: + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", false); err != nil { + panic(err) + } + case ddtraceNoSpanExecTest: + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, false); err != nil { panic(err) } } From c5068fbcecd5af77ec0fdfbe3a4d9727821b5550 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Wed, 27 May 2026 11:14:39 +0200 Subject: [PATCH 08/14] [WP] Add fork exec test case --- pkg/security/tests/span_test.go | 372 ++++++++++++++++++ .../tests/syscall_tester/c/syscall_tester.c | 121 ++++++ .../syscall_tester/go/syscall_go_tester.go | 108 +++-- 3 files changed, 570 insertions(+), 31 deletions(-) diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 794de7ea73f7..2f2283360192 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -9,7 +9,9 @@ package tests import ( + "encoding/json" "fmt" + "math/big" "os" "os/exec" "runtime" @@ -23,6 +25,19 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/secl/rules" ) +// splitTraceID parses a decimal 128-bit trace id into (hi, lo) the same way +// secl utils.TraceID stores them. +func splitTraceID(decimalTraceID string) (hi, lo uint64, ok bool) { + v, parseOK := new(big.Int).SetString(decimalTraceID, 10) + if !parseOK { + return 0, 0, false + } + mask := new(big.Int).SetUint64(^uint64(0)) + lo = new(big.Int).And(v, mask).Uint64() + hi = new(big.Int).Rsh(v, 64).Uint64() + return hi, lo, true +} + func TestSpan(t *testing.T) { SkipIfNotAvailable(t) @@ -118,6 +133,87 @@ func TestSpan(t *testing.T) { assert.Equal(t, fakeTraceID128b, event.SpanContext.TraceID.String()) }, "test_span_rule_exec") }) + + t.Run("fork_exec_propagates_via_ancestor", func(t *testing.T) { + // Fork+exec with the legacy proprietary TLS path: the parent + // registers a span_tls entry (via eRPC), writes span_id/trace_id to + // its slot, then fork()s. At sched_process_fork the eBPF probe runs + // in the parent's context, fill_span_context_legacy reads + // span_tls[parent_tgid] + the parent's TLS slot, and the captured + // span/trace are saved on the child's ProcessCacheEntry via + // AddForkEntry → SetSpan. The child execs touch — its own exec + // event has an empty SpanContext (new tgid, no span_tls entry), but + // newDDContextSerializer surfaces the parent's span by walking the + // ancestor chain. This sub-test pins all three points. + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-span-fork-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + var args []string + if kind == dockerWrapperType { + args = []string{"span-fork-exec", fakeTraceID128b, "204", "/usr/bin/touch", "--reference", "/etc/passwd", testFile} + } else { + args = []string{"span-fork-exec", fakeTraceID128b, "204", executable, "--reference", "/etc/passwd", testFile} + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_span_rule_exec") + + // (1) The exec'd program (touch) has no tracer, so the raw + // exec event SpanContext is empty by design. + assert.Equal(t, uint64(0), event.SpanContext.SpanID, + "exec event should not carry a span context: touch has no tracer") + assert.Equal(t, "0", event.SpanContext.TraceID.String(), + "exec event should not carry a trace id: touch has no tracer") + + // (2) The fork-parent ancestor should carry the legacy TLS + // span captured by fill_span_context_legacy at fork time. + var foundAncestor *model.ProcessCacheEntry + for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { + if pce.SpanID != 0 { + foundAncestor = pce + break + } + } + if assert.NotNil(t, foundAncestor, + "an ancestor should carry the parent's legacy TLS span captured at fork time") { + assert.Equal(t, uint64(204), foundAncestor.SpanID, + "fork-parent ancestor SpanID should equal the legacy-TLS span_id") + assert.Equal(t, fakeTraceID128b, foundAncestor.TraceID.String(), + "fork-parent ancestor TraceID should equal the legacy-TLS trace_id") + } + + // (3) Serialized dd field should carry the propagated values. + expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) + if !assert.True(t, ok, "splitTraceID") { + return + } + jsonStr, err := test.marshalEvent(event) + assert.NoError(t, err, "marshalEvent") + var parsed struct { + DD struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + } `json:"dd"` + } + if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { + assert.Equal(t, "204", parsed.DD.SpanID, + "serialized dd.span_id should equal the legacy-TLS span_id") + assert.Equal(t, fmt.Sprintf("%x%x", expectedHi, expectedLo), parsed.DD.TraceID, + "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the legacy-TLS trace_id") + } + }, "test_span_rule_exec") + }) + }) } // TestOTelSpan tests OTel Thread Local Context Record based span context collection. @@ -360,6 +456,84 @@ func TestOTelSpan(t *testing.T) { }, "test_otel_span_rule_exec") }) }) + + t.Run("fork_exec_propagates_via_ancestor", func(t *testing.T) { + // Fork+exec with the OTel Thread Local Context Record path: the + // parent's tracer-info memfd triggers ELF dynsym resolution so the + // agent populates otel_tls[parent_tgid] with the TLS offset, the + // parent publishes a valid record via the otel_thread_ctx_v1 TLS + // pointer, then fork()s. At sched_process_fork the eBPF probe runs + // in the parent's context, fill_span_context_otel reads the + // parent's thread.fsbase + tls_offset to find the record, and the + // captured span/trace are saved on the child's ProcessCacheEntry. + // The child execs touch — its own exec event has an empty + // SpanContext (new tgid has no otel_tls entry), but + // newDDContextSerializer surfaces the parent's span by walking the + // ancestor chain. This sub-test pins all three points. + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-otel-span-fork-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := append([]string{"otel-span-fork-exec", fakeTraceID128b, "204"}, otelExecArgs(kind, testFile)...) + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(syscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + // (1) The exec'd program (touch) has no tracer, so the raw + // exec event SpanContext is empty by design. + assert.Equal(t, uint64(0), event.SpanContext.SpanID, + "exec event should not carry a span context: touch has no tracer") + assert.Equal(t, "0", event.SpanContext.TraceID.String(), + "exec event should not carry a trace id: touch has no tracer") + + // (2) The fork-parent ancestor should carry the OTel TLS + // span captured by fill_span_context_otel at fork time. + var foundAncestor *model.ProcessCacheEntry + for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { + if pce.SpanID != 0 { + foundAncestor = pce + break + } + } + if assert.NotNil(t, foundAncestor, + "an ancestor should carry the parent's OTel TLS span captured at fork time") { + assert.Equal(t, uint64(204), foundAncestor.SpanID, + "fork-parent ancestor SpanID should equal the OTel record span_id") + assert.Equal(t, fakeTraceID128b, foundAncestor.TraceID.String(), + "fork-parent ancestor TraceID should equal the OTel record trace_id") + } + + // (3) Serialized dd field should carry the propagated values. + expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) + if !assert.True(t, ok, "splitTraceID") { + return + } + jsonStr, err := test.marshalEvent(event) + assert.NoError(t, err, "marshalEvent") + var parsed struct { + DD struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + } `json:"dd"` + } + if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { + assert.Equal(t, "204", parsed.DD.SpanID, + "serialized dd.span_id should equal the OTel record span_id") + assert.Equal(t, fmt.Sprintf("%x%x", expectedHi, expectedLo), parsed.DD.TraceID, + "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the OTel record trace_id") + } + }, "test_otel_span_rule_exec") + }) + }) } // TestGoSpan tests Go pprof label-based span context collection. @@ -540,6 +714,103 @@ func TestGoSpan(t *testing.T) { }, "test_go_span_rule_exec") }) }) + + t.Run("fork_exec_propagates_via_ancestor", func(t *testing.T) { + // Fork+exec is correct-by-design here, not a bug: the exec'd program + // (touch) has no tracer, so the exec event's own SpanContext must be + // empty. What carries the parent's span is the fork event: + // sched_process_fork fires in the PARENT's context, so + // fill_span_context_go reads the parent's pprof labels and the + // captured SpanID/TraceID are persisted on the child's + // ProcessCacheEntry via AddForkEntry → SetSpan. + // + // At serialization time, newDDContextSerializer + // (serializers_linux.go:1457) prefers event.SpanContext, but when + // that is zero it walks event.ProcessContext.Ancestor and surfaces + // the first non-zero SpanID/TraceID it finds — which is the fork + // parent's. So the JSON "dd" field carries the parent's span values + // even though the raw exec event does not. + // + // This sub-test pins all three points of that wiring. + const parentSpanID uint64 = 987654321 + const parentLocalRootSpanID uint64 = 123456789 + + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-go-span-fork-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-go-span-fork-exec-test", + "-go-span-span-id", strconv.FormatUint(parentSpanID, 10), + "-go-span-local-root-span-id", strconv.FormatUint(parentLocalRootSpanID, 10), + "-go-span-file-path", testFile, + "-go-span-exec-target", touchPathFor(kind), + } + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + if out, err := cmd.CombinedOutput(); 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_exec") + + // (1) The exec'd program (touch) has no tracer, so the raw + // exec event SpanContext is empty by design. + assert.Equal(t, uint64(0), event.SpanContext.SpanID, + "exec event should not carry a span context: touch has no tracer") + assert.Equal(t, "0", event.SpanContext.TraceID.String(), + "exec event should not carry a trace id: touch has no tracer") + + // (2) The immediate fork-parent in the ancestor lineage + // should carry the parent's pprof-label span. Walk the + // ancestor chain like newDDContextSerializer does and + // confirm we find a PCE with the expected SpanID/TraceID. + var foundSpan bool + var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 + for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { + if pce.SpanID != 0 { + foundSpan = true + ancestorSpanID = pce.SpanID + ancestorTraceIDLo = pce.TraceID.Lo + ancestorTraceIDHi = pce.TraceID.Hi + break + } + } + assert.True(t, foundSpan, + "an ancestor should carry the parent's pprof-label span captured at fork time") + assert.Equal(t, parentSpanID, ancestorSpanID, + "fork-parent ancestor SpanID should equal the parent's pprof span_id") + assert.Equal(t, parentLocalRootSpanID, ancestorTraceIDLo, + "fork-parent ancestor TraceID.Lo should equal the parent's pprof local_root_span_id") + assert.Equal(t, uint64(0), ancestorTraceIDHi, + "Go pprof labels only populate the low 64 bits of trace_id") + + // (3) The serialized event's "dd" field should carry those + // same propagated values (newDDContextSerializer's ancestor + // fallback path). + jsonStr, err := test.marshalEvent(event) + assert.NoError(t, err, "marshalEvent") + var parsed struct { + DD struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + } `json:"dd"` + } + if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { + assert.Equal(t, strconv.FormatUint(parentSpanID, 10), parsed.DD.SpanID, + "serialized dd.span_id should equal the parent's pprof span_id") + expectedTraceID := fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID) + assert.Equal(t, expectedTraceID, parsed.DD.TraceID, + "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the parent's local_root_span_id") + } + }, "test_go_span_rule_exec") + }) + }) } // TestDDTraceGoSpan tests the full dd-trace-go integration: dd-trace-go creates @@ -737,4 +1008,105 @@ func TestDDTraceGoSpan(t *testing.T) { }, "test_ddtrace_span_rule_exec") }) }) + + t.Run("fork_exec_propagates_via_ancestor", func(t *testing.T) { + // Fork+exec with a real dd-trace-go span in the parent is + // correct-by-design, not a bug: the exec'd program (touch) has no + // tracer, so the exec event's own SpanContext is intentionally + // empty. The parent's span travels with the fork: sched_process_fork + // runs in the parent's context, so fill_span_context_go reads the + // parent's pprof labels and the captured SpanID/TraceID are saved + // on the child's ProcessCacheEntry via AddForkEntry → SetSpan. + // + // newDDContextSerializer (serializers_linux.go:1457) walks + // event.ProcessContext.Ancestor when event.SpanContext is zero and + // surfaces the first non-zero SpanID/TraceID it finds — i.e. the + // fork-parent's. So the serialized "dd" field is populated with the + // parent's span values. + // + // This sub-test pins all three points of that wiring with a real + // dd-trace-go span. + test.RunMultiMode(t, "exec", func(t *testing.T, kind wrapperType, cmdFunc func(cmd string, args []string, envs []string) *exec.Cmd) { + testFile, _, err := test.Path("test-ddtrace-span-fork-exec") + if err != nil { + t.Fatal(err) + } + defer os.Remove(testFile) + + args := []string{ + "-ddtrace-span-fork-exec-test", + "-ddtrace-span-file-path", testFile, + "-ddtrace-span-exec-target", touchPathFor(kind), + } + + // Read the tester's stdout for the span IDs dd-trace-go + // generated at runtime; they're the ground truth for what the + // fork-parent ancestor + serialized dd field should carry. + var parentSpanID, parentLocalRootSpanID uint64 + + test.WaitSignalFromRule(t, func() error { + cmd := cmdFunc(goSyscallTester, args, []string{}) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", out, err) + } + parentSpanID, parentLocalRootSpanID = parseDDTraceIDs(out) + if parentSpanID == 0 { + return fmt.Errorf("parent dd-trace-go span never produced a non-zero span_id: %s", out) + } + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_ddtrace_span_rule_exec") + + // (1) The exec'd program (touch) has no tracer, so the raw + // exec event SpanContext is empty by design. + assert.Equal(t, uint64(0), event.SpanContext.SpanID, + "exec event should not carry a span context: touch has no tracer") + assert.Equal(t, "0", event.SpanContext.TraceID.String(), + "exec event should not carry a trace id: touch has no tracer") + + // (2) The immediate fork-parent in the ancestor lineage + // should carry dd-trace-go's parent span. Walk the chain + // the same way newDDContextSerializer does. + var foundSpan bool + var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 + for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { + if pce.SpanID != 0 { + foundSpan = true + ancestorSpanID = pce.SpanID + ancestorTraceIDLo = pce.TraceID.Lo + ancestorTraceIDHi = pce.TraceID.Hi + break + } + } + assert.True(t, foundSpan, + "an ancestor should carry the dd-trace-go parent span captured at fork time") + assert.Equal(t, parentSpanID, ancestorSpanID, + "fork-parent ancestor SpanID should equal dd-trace-go's parent span_id") + assert.Equal(t, parentLocalRootSpanID, ancestorTraceIDLo, + "fork-parent ancestor TraceID.Lo should equal dd-trace-go's local_root_span_id") + assert.Equal(t, uint64(0), ancestorTraceIDHi, + "dd-trace-go pprof labels only populate the low 64 bits of trace_id") + + // (3) The serialized event's "dd" field should carry the + // same propagated values (the ancestor-fallback path in + // newDDContextSerializer). + jsonStr, err := test.marshalEvent(event) + assert.NoError(t, err, "marshalEvent") + var parsed struct { + DD struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + } `json:"dd"` + } + if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { + assert.Equal(t, strconv.FormatUint(parentSpanID, 10), parsed.DD.SpanID, + "serialized dd.span_id should equal dd-trace-go's parent span_id") + expectedTraceID := fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID) + assert.Equal(t, expectedTraceID, parsed.DD.TraceID, + "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of dd-trace-go's local_root_span_id") + } + }, "test_ddtrace_span_rule_exec") + }) + }) } diff --git a/pkg/security/tests/syscall_tester/c/syscall_tester.c b/pkg/security/tests/syscall_tester/c/syscall_tester.c index 60f9b3c3ac14..73f915c3d272 100644 --- a/pkg/security/tests/syscall_tester/c/syscall_tester.c +++ b/pkg/security/tests/syscall_tester/c/syscall_tester.c @@ -180,6 +180,48 @@ int span_open(int argc, char **argv) { return EXIT_SUCCESS; } +// span_fork_exec exercises the legacy fork+exec propagation path. The parent +// registers a legacy span TLS, writes span_id/trace_id into the slot for its +// own gettid(), then fork()s. At sched_process_fork the eBPF probe runs in +// the parent's context, fill_span_context_legacy reads span_tls[parent_tgid] +// + parent's TLS slot, and the captured span/trace land on the child's PCE +// via AddForkEntry. The child then execv()s the target — its own exec event +// has an empty SpanContext, but newDDContextSerializer surfaces the parent's +// span by walking the ancestor chain. +int span_fork_exec(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: span-fork-exec [args...]\n"); + return EXIT_FAILURE; + } + + struct span_tls_t *tls = register_tls(); + if (!tls) { + fprintf(stderr, "Failed to register TLS\n"); + return EXIT_FAILURE; + } + + __int128_t trace_id = atouint128(argv[1]); + unsigned span_id = atoi(argv[2]); + register_span(tls, trace_id, span_id); + + pid_t child = fork(); + if (child < 0) { + fprintf(stderr, "fork failed\n"); + return EXIT_FAILURE; + } + if (child == 0) { + // Child: brand-new tgid, no span_tls entry. The exec event will have + // an empty SpanContext; ancestor lineage carries the parent's span. + execv(argv[3], argv + 3); + fprintf(stderr, "execv failed in child: %s\n", argv[3]); + _exit(EXIT_FAILURE); + } + + int status; + waitpid(child, &status, 0); + 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 @@ -693,6 +735,81 @@ int otel_span_exec_null_ptr(int argc, char **argv) { return EXIT_SUCCESS; } +// otel_span_fork_exec exercises the OTel fork+exec propagation path. The +// parent sets up its tracer-info memfd (so the agent resolves the OTel TLS +// symbol and populates otel_tls[parent_tgid]), publishes a valid record via +// the otel_thread_ctx_v1 TLS pointer, and then fork()s. At sched_process_fork +// the eBPF probe runs in the parent's context, fill_span_context_otel walks +// the parent's task_struct → thread.fsbase + tls_offset to read the record, +// and the captured span/trace land on the child's PCE via AddForkEntry. +// The child execv()s the target; its own exec event has an empty SpanContext +// (new tgid has no otel_tls entry) but the ancestor lineage carries the span. +int otel_span_fork_exec(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: otel-span-fork-exec [args...]\n"); + return EXIT_FAILURE; + } + +#if !defined(__x86_64__) && !defined(__aarch64__) + fprintf(stderr, "OTel TLS test not supported on this architecture\n"); + return EXIT_FAILURE; +#else + int memfd = create_tracer_memfd(); + if (memfd < 0) { + fprintf(stderr, "Failed to create tracer memfd\n"); + return EXIT_FAILURE; + } + // Give the agent time to resolve the OTel TLS symbol and populate + // otel_tls[parent_tgid] before we fork. + usleep(500000); + + otel_thread_ctx_v1 = NULL; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + __int128_t trace_id = atouint128(argv[1]); + uint64_t span_id = (uint64_t)atol(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); + full_record.header.attrs_data_size = 0; + + __atomic_signal_fence(__ATOMIC_SEQ_CST); + full_record.header.valid = 1; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + otel_thread_ctx_v1 = &full_record.header; + __atomic_signal_fence(__ATOMIC_SEQ_CST); + + pid_t child = fork(); + if (child < 0) { + fprintf(stderr, "fork failed\n"); + otel_thread_ctx_v1 = NULL; + close(memfd); + return EXIT_FAILURE; + } + if (child == 0) { + // Child: brand-new tgid, no otel_tls entry. The exec event will + // carry an empty SpanContext; the ancestor lineage holds the + // parent's captured span/trace from the fork event. + execv(argv[3], argv + 3); + fprintf(stderr, "execv failed in child: %s\n", argv[3]); + _exit(EXIT_FAILURE); + } + + int status; + waitpid(child, &status, 0); + + otel_thread_ctx_v1 = NULL; + close(memfd); + return EXIT_SUCCESS; +#endif +} + int ptrace_traceme() { int child = fork(); if (child == 0) { @@ -2783,6 +2900,8 @@ int main(int argc, char **argv) { exit_code = EXIT_SUCCESS; } else if (strcmp(cmd, "span-exec") == 0) { exit_code = span_exec(sub_argc, sub_argv); + } else if (strcmp(cmd, "span-fork-exec") == 0) { + exit_code = span_fork_exec(sub_argc, sub_argv); } else if (strcmp(cmd, "ptrace-traceme") == 0) { exit_code = ptrace_traceme(); } else if (strcmp(cmd, "ptrace-attach") == 0) { @@ -2809,6 +2928,8 @@ int main(int argc, char **argv) { exit_code = otel_span_exec_invalid(sub_argc, sub_argv); } else if (strcmp(cmd, "otel-span-exec-null-ptr") == 0) { exit_code = otel_span_exec_null_ptr(sub_argc, sub_argv); + } else if (strcmp(cmd, "otel-span-fork-exec") == 0) { + exit_code = otel_span_fork_exec(sub_argc, sub_argv); } else if (strcmp(cmd, "pipe-chown") == 0) { exit_code = test_pipe_chown(); } else if (strcmp(cmd, "signal") == 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 5e466cd654eb..7033ebcacbd0 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -53,20 +53,22 @@ var ( loginUIDPath string loginUIDEventType string loginUIDValue int - goSpanTest bool - goSpanExecTest bool - goSpanNoLabelsTest bool - goSpanNoLabelsExecTest bool - goSpanSpanID string - goSpanLocalRootSpanID string - goSpanFilePath string - goSpanExecTarget string - ddtraceSpanTest bool - ddtraceSpanExecTest bool - ddtraceNoSpanTest bool - ddtraceNoSpanExecTest bool - ddtraceSpanFilePath string - ddtraceSpanExecTarget string + goSpanTest bool + goSpanExecTest bool + goSpanNoLabelsTest bool + goSpanNoLabelsExecTest bool + goSpanForkExecTest bool + goSpanSpanID string + goSpanLocalRootSpanID string + goSpanFilePath string + goSpanExecTarget string + ddtraceSpanTest bool + ddtraceSpanExecTest bool + ddtraceNoSpanTest bool + ddtraceNoSpanExecTest bool + ddtraceSpanForkExecTest bool + ddtraceSpanFilePath string + ddtraceSpanExecTarget string ) //go:embed ebpf_probe.o @@ -358,12 +360,36 @@ func triggerExec(target, filePath string) error { return syscall.Exec(target, argv, os.Environ()) } +// triggerForkExec fork+execs the target binary via os/exec.Cmd.Run — i.e. the +// child runs in a brand-new tgid. This is the canonical "Go program shells out +// to a subprocess" pattern (the one os/exec exposes), and is the scenario in +// which the agent currently loses the parent's APM correlation: the eBPF fork +// hook does not propagate span_tls / otel_tls / go_labels_procs from the +// parent's tgid to the child's, so fill_span_context in the child's exec hook +// returns an empty span context. +// +// The test that drives this asserts the empty result, pinning the current +// behaviour; when fork-time inheritance is later added, that assertion will +// need to flip. +func triggerForkExec(target, filePath string) error { + if target == "" { + return fmt.Errorf("exec target is required") + } + cmd := exec.Command(target, "--reference", "/etc/passwd", filePath) + return cmd.Run() +} + // RunGoSpanTest creates a tracer-info memfd (triggering Go label offset // resolution), optionally sets pprof labels (skipped for negative-path -// scenarios), and then either opens a file or execs a target. The eBPF reader -// should extract the span context from the goroutine's pprof labels when the -// labels are set, or yield an empty span context when they are not. -func RunGoSpanTest(spanID, localRootSpanID, filePath, execTarget string, setLabels bool) error { +// scenarios), and then triggers the syscall the rule watches. The trigger is: +// - open of filePath when execTarget == "" +// - in-process execve of execTarget when forkExec == false +// - fork+execve (os/exec.Cmd.Run) of execTarget when forkExec == true +// +// The fork+execve mode is used by the "fork_exec_no_inheritance" regression +// test, which pins the current behaviour where the child's brand-new tgid has +// no entry in go_labels_procs. +func RunGoSpanTest(spanID, localRootSpanID, filePath, execTarget string, setLabels, forkExec bool) error { fd, err := setupGoTracerMemfd("go-span-test", "datadog-tracer-info-gotest01") if err != nil { return err @@ -379,6 +405,9 @@ func RunGoSpanTest(spanID, localRootSpanID, filePath, execTarget string, setLabe defer pprof.SetGoroutineLabels(context.Background()) } + if forkExec { + return triggerForkExec(execTarget, filePath) + } if execTarget != "" { return triggerExec(execTarget, filePath) } @@ -386,11 +415,15 @@ func RunGoSpanTest(spanID, localRootSpanID, filePath, execTarget string, setLabe } // RunDDTraceSpanTest uses dd-trace-go to create a real span (which sets pprof -// labels via the profiler code-hotspots integration) and then triggers either -// an open or an exec depending on execTarget. If startSpan is false, the -// tracer is started but no active span is created — the eBPF reader should -// yield an empty span context (negative path). -func RunDDTraceSpanTest(filePath, execTarget string, startSpan bool) error { +// labels via the profiler code-hotspots integration) and then triggers the +// syscall the rule watches. Trigger selection mirrors RunGoSpanTest: +// - open of filePath when execTarget == "" +// - in-process execve when forkExec == false +// - fork+execve when forkExec == true +// +// If startSpan is false, the tracer is started but no active span is created — +// the eBPF reader should yield an empty span context (negative path). +func RunDDTraceSpanTest(filePath, execTarget string, startSpan, forkExec bool) error { fd, err := setupGoTracerMemfd("ddtrace-test", "datadog-tracer-info-ddtrace0") if err != nil { return err @@ -429,6 +462,9 @@ func RunDDTraceSpanTest(filePath, execTarget string, startSpan bool) error { defer span.Finish() } + if forkExec { + return triggerForkExec(execTarget, filePath) + } if execTarget != "" { return triggerExec(execTarget, filePath) } @@ -455,6 +491,7 @@ func main() { flag.BoolVar(&goSpanExecTest, "go-span-exec-test", false, "when set, runs the Go pprof labels span exec test (exec, labels set)") flag.BoolVar(&goSpanNoLabelsTest, "go-span-no-labels-test", false, "when set, runs the Go span open test WITHOUT setting pprof labels (negative path)") flag.BoolVar(&goSpanNoLabelsExecTest, "go-span-no-labels-exec-test", false, "when set, runs the Go span exec test WITHOUT setting pprof labels (negative path)") + flag.BoolVar(&goSpanForkExecTest, "go-span-fork-exec-test", false, "when set, sets pprof labels then fork+execs the target via os/exec (parent's labels are not inherited by the child's new tgid — pins the current fork+exec gap)") 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 / touch for the Go span test") @@ -463,6 +500,7 @@ func main() { flag.BoolVar(&ddtraceSpanExecTest, "ddtrace-span-exec-test", false, "when set, runs the dd-trace-go span exec test (exec, active span)") flag.BoolVar(&ddtraceNoSpanTest, "ddtrace-no-span-test", false, "when set, runs the dd-trace-go open test WITHOUT an active span (negative path)") flag.BoolVar(&ddtraceNoSpanExecTest, "ddtrace-no-span-exec-test", false, "when set, runs the dd-trace-go exec test WITHOUT an active span (negative path)") + flag.BoolVar(&ddtraceSpanForkExecTest, "ddtrace-span-fork-exec-test", false, "when set, starts an active dd-trace-go span and fork+execs the target via os/exec (pins the current fork+exec gap)") flag.StringVar(&ddtraceSpanFilePath, "ddtrace-span-file-path", "", "file path to open / touch for the dd-trace-go span test") flag.StringVar(&ddtraceSpanExecTarget, "ddtrace-span-exec-target", "", "executable to exec for the dd-trace-go span exec test (e.g. /usr/bin/touch)") @@ -528,38 +566,46 @@ func main() { switch { case goSpanTest: - if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, "", true); err != nil { + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, "", true, false); err != nil { panic(err) } case goSpanExecTest: - if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, goSpanExecTarget, true); err != nil { + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, goSpanExecTarget, true, false); err != nil { panic(err) } case goSpanNoLabelsTest: - if err := RunGoSpanTest("", "", goSpanFilePath, "", false); err != nil { + if err := RunGoSpanTest("", "", goSpanFilePath, "", false, false); err != nil { panic(err) } case goSpanNoLabelsExecTest: - if err := RunGoSpanTest("", "", goSpanFilePath, goSpanExecTarget, false); err != nil { + if err := RunGoSpanTest("", "", goSpanFilePath, goSpanExecTarget, false, false); err != nil { + panic(err) + } + case goSpanForkExecTest: + if err := RunGoSpanTest(goSpanSpanID, goSpanLocalRootSpanID, goSpanFilePath, goSpanExecTarget, true, true); err != nil { panic(err) } } switch { case ddtraceSpanTest: - if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", true); err != nil { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", true, false); err != nil { panic(err) } case ddtraceSpanExecTest: - if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, true); err != nil { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, true, false); err != nil { panic(err) } case ddtraceNoSpanTest: - if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", false); err != nil { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, "", false, false); err != nil { panic(err) } case ddtraceNoSpanExecTest: - if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, false); err != nil { + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, false, false); err != nil { + panic(err) + } + case ddtraceSpanForkExecTest: + if err := RunDDTraceSpanTest(ddtraceSpanFilePath, ddtraceSpanExecTarget, true, true); err != nil { panic(err) } } From ccc674d28cd2bbc0275a346051751e58dc619fd3 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Wed, 27 May 2026 15:35:01 +0200 Subject: [PATCH 09/14] [WP] Add SpanContext to PCE and tests --- docs/cloud-workload-security/backend_linux.md | 30 ++ .../backend_linux.schema.json | 8 + pkg/security/ebpf/c/include/helpers/span.h | 7 + pkg/security/probe/probe_ebpf.go | 36 ++- .../resolvers/process/resolver_ebpf.go | 10 +- .../secl/model/event_deep_copy_unix.go | 42 ++- pkg/security/secl/model/model_helpers_unix.go | 19 +- pkg/security/secl/model/model_unix.go | 4 +- .../serializers_base_linux_easyjson.go | 19 ++ pkg/security/serializers/serializers_linux.go | 22 +- .../serializers/serializers_linux_easyjson.go | 19 ++ .../serializers_linux_easyjson.mock | 8 + pkg/security/tests/span_test.go | 271 +++++++++++++----- 13 files changed, 386 insertions(+), 109 deletions(-) diff --git a/docs/cloud-workload-security/backend_linux.md b/docs/cloud-workload-security/backend_linux.md index 2886871ed920..36f819c7c8ea 100644 --- a/docs/cloud-workload-security/backend_linux.md +++ b/docs/cloud-workload-security/backend_linux.md @@ -1508,6 +1508,10 @@ Workload Protection events for Linux systems have the following JSON schema: "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" @@ -1691,6 +1695,10 @@ Workload Protection events for Linux systems have the following JSON schema: "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" @@ -4771,6 +4779,10 @@ Workload Protection events for Linux systems have the following JSON schema: "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" @@ -4828,6 +4840,12 @@ Workload Protection events for Linux systems have the following JSON schema: | `syscalls` | List of syscalls captured to generate the event | | `aws_security_credentials` | List of AWS Security Credentials that the process had access to | | `tracer` | Metadata from APM tracer instrumentation | +| `span_context` | APM span context captured for this process. For a process that +fork+exec'd a subprocess this carries the parent's span (the one +captured by fill_span_context at sched_process_fork). The top-level +event "dd" field is built by newDDContextSerializer which walks the +ancestor lineage; this per-process field exposes the same data at +each level of the ancestor chain. | | `variables` | Variable values | | References | @@ -4839,6 +4857,7 @@ Workload Protection events for Linux systems have the following JSON schema: | [ContainerContext](#containercontext) | | [SyscallsEvent](#syscallsevent) | | [TracerMetadata](#tracermetadata) | +| [DDContext](#ddcontext) | | [Variables](#variables) | ## `ProcessContext` @@ -5013,6 +5032,10 @@ Workload Protection events for Linux systems have the following JSON schema: "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" @@ -5085,6 +5108,12 @@ Workload Protection events for Linux systems have the following JSON schema: | `syscalls` | List of syscalls captured to generate the event | | `aws_security_credentials` | List of AWS Security Credentials that the process had access to | | `tracer` | Metadata from APM tracer instrumentation | +| `span_context` | APM span context captured for this process. For a process that +fork+exec'd a subprocess this carries the parent's span (the one +captured by fill_span_context at sched_process_fork). The top-level +event "dd" field is built by newDDContextSerializer which walks the +ancestor lineage; this per-process field exposes the same data at +each level of the ancestor chain. | | `variables` | Variable values | | `parent` | Parent process | | `ancestors` | Ancestor processes | @@ -5099,6 +5128,7 @@ Workload Protection events for Linux systems have the following JSON schema: | [ContainerContext](#containercontext) | | [SyscallsEvent](#syscallsevent) | | [TracerMetadata](#tracermetadata) | +| [DDContext](#ddcontext) | | [Variables](#variables) | | [Process](#process) | diff --git a/docs/cloud-workload-security/backend_linux.schema.json b/docs/cloud-workload-security/backend_linux.schema.json index 9e84fb9acc80..83e6efdc55e5 100644 --- a/docs/cloud-workload-security/backend_linux.schema.json +++ b/docs/cloud-workload-security/backend_linux.schema.json @@ -1497,6 +1497,10 @@ "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" @@ -1680,6 +1684,10 @@ "$ref": "#/$defs/TracerMetadata", "description": "Metadata from APM tracer instrumentation" }, + "span_context": { + "$ref": "#/$defs/DDContext", + "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + }, "variables": { "$ref": "#/$defs/Variables", "description": "Variable values" diff --git a/pkg/security/ebpf/c/include/helpers/span.h b/pkg/security/ebpf/c/include/helpers/span.h index 547f0b25e446..af34ca33d597 100644 --- a/pkg/security/ebpf/c/include/helpers/span.h +++ b/pkg/security/ebpf/c/include/helpers/span.h @@ -132,6 +132,13 @@ void __attribute__((always_inline)) copy_span_context(struct span_context_t *src dst->span_id = src->span_id; dst->trace_id[0] = src->trace_id[0]; dst->trace_id[1] = src->trace_id[1]; + // has_extra_attrs must be copied too: for exec events, fill_span_context + // runs against syscall->exec.span_context at prepare_binprm, and the + // event-side span_context only gets populated via this helper at + // send_exec_event. Without copying this flag, the userspace + // resolveOTelSpanAttrs path never fires for execs and Attributes never + // makes it onto the PCE. + dst->has_extra_attrs = src->has_extra_attrs; } #endif diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 8ff230ab47fb..8a41585feef8 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -1076,7 +1076,14 @@ func (p *EBPFProbe) unmarshalContexts(data []byte, event *model.Event, cgroupCon // 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) { +// +// For fork/exec events, when attributes are successfully resolved, they are +// also pushed onto the cached PCE via SetSpanContextAttributes — at this +// point AddForkEntry / AddExecEntry already stamped the IDs onto the PCE, so +// we only need to add the now-resolved Attributes. For other event types the +// PCE is the existing cached entry shared with future events and must not be +// mutated with the transient event's attributes. +func (p *EBPFProbe) resolveOTelSpanAttrs(event *model.Event, eventType model.EventType) { // Build the map key: span_id + trace_id[2] key := make([]byte, 24) binary.NativeEndian.PutUint64(key[0:8], event.SpanContext.SpanID) @@ -1100,9 +1107,21 @@ func (p *EBPFProbe) resolveOTelSpanAttrs(event *model.Event) { // Get the ThreadlocalAttributeKeys from the process's TracerMetadata. // ProcessContext may not be resolved yet at unmarshal time, so guard against nil. + // For exec events the new PCE has no TracerMetadata (touch never sealed a + // tracer-info memfd); the metadata lives on the pre-exec PCE in the ancestor + // lineage. Walk ancestors until we find one with non-empty + // ThreadlocalAttributeKeys so we can map index → name correctly. var keyNames []string if event.ProcessContext != nil { keyNames = event.ProcessContext.Process.TracerMetadata.ThreadlocalAttributeKeys + if len(keyNames) == 0 { + for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { + if len(pce.Process.TracerMetadata.ThreadlocalAttributeKeys) > 0 { + keyNames = pce.Process.TracerMetadata.ThreadlocalAttributeKeys + break + } + } + } } // Parse attrs_data: repeated [key(u8) + length(u8) + val(u8[length])] @@ -1131,6 +1150,14 @@ func (p *EBPFProbe) resolveOTelSpanAttrs(event *model.Event) { if len(attrs) > 0 { event.SpanContext.Attributes = attrs + + // For fork/exec events, push the resolved attributes onto the cached + // PCE so the per-process span_context serializer can surface them. + // AddForkEntry / AddExecEntry already stamped SpanID/TraceID earlier; + // only the Attributes field still needs to be set here. + if event.ProcessCacheEntry != nil && (eventType == model.ForkEventType || eventType == model.ExecEventType) { + event.ProcessCacheEntry.SetSpanContextAttributes(attrs) + } } } @@ -1402,9 +1429,12 @@ func (p *EBPFProbe) handleEvent(CPU int, data []byte) { return } - // Resolve OTel custom attributes now that process context (and TracerMetadata) is available. + // Resolve OTel custom attributes now that process context (and + // TracerMetadata) is available. resolveOTelSpanAttrs also handles + // pushing the resolved attributes onto the cached PCE for fork/exec + // events via SetSpanContextAttributes. if event.SpanContext.HasExtraAttrs && p.otelSpanAttrsMap != nil { - p.resolveOTelSpanAttrs(event) + p.resolveOTelSpanAttrs(event, eventType) } // handle regular events diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index 414e72eee300..9cca6d7cdfcf 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -562,7 +562,7 @@ func (p *EBPFResolver) UpdateArgsEnvs(event *model.ArgsEnvsEvent) { // AddForkEntry adds an entry to the local cache and returns the newly created entry func (p *EBPFResolver) AddForkEntry(event *model.Event, cgroupContext model.CGroupContext, newEntryCb func(*model.ProcessCacheEntry, error)) error { p.ApplyBootTime(event.ProcessCacheEntry) - event.ProcessCacheEntry.SetSpan(event.SpanContext.SpanID, event.SpanContext.TraceID) + event.ProcessCacheEntry.SetSpanContext(event.SpanContext) if event.ProcessCacheEntry.Pid == 0 { return errors.New("no pid") @@ -582,6 +582,14 @@ func (p *EBPFResolver) AddExecEntry(event *model.Event, cgroupContext model.CGro p.Lock() defer p.Unlock() + // Mirror AddForkEntry: if fill_span_context captured a span at + // prepare_binprm (e.g. the parent had a legacy/OTel/Go tracer active when + // it execve'd), persist it on the new PCE so the process serializer can + // surface it as process.span_context. This is a no-op for the fork+exec + // case where the child's tgid has no correlation entry — event.SpanContext + // is zero there and ancestor lineage still carries the parent's span. + event.ProcessCacheEntry.SetSpanContext(event.SpanContext) + var err error if err := p.resolveNewProcessCacheEntry(event.ProcessCacheEntry); err != nil { var errResolution *spath.ErrPathResolution diff --git a/pkg/security/secl/model/event_deep_copy_unix.go b/pkg/security/secl/model/event_deep_copy_unix.go index dacbdf7d89d8..d42dafc01bcb 100644 --- a/pkg/security/secl/model/event_deep_copy_unix.go +++ b/pkg/security/secl/model/event_deep_copy_unix.go @@ -253,11 +253,10 @@ func deepCopyProcessPtr(fieldToCopy *Process) *Process { copied.LinuxBinprm = deepCopyLinuxBinprm(fieldToCopy.LinuxBinprm) copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.Source = fieldToCopy.Source - copied.SpanID = fieldToCopy.SpanID + copied.SpanContext = deepCopySpanContext(fieldToCopy.SpanContext) copied.SymlinkBasenameStr = fieldToCopy.SymlinkBasenameStr copied.SymlinkPathnameStr = fieldToCopy.SymlinkPathnameStr copied.TTYName = fieldToCopy.TTYName - copied.TraceID = deepCopyTraceID(fieldToCopy.TraceID) copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) copied.UserSession = deepCopyUserSessionContext(fieldToCopy.UserSession) return copied @@ -400,6 +399,24 @@ func deepCopyLinuxBinprm(fieldToCopy LinuxBinprm) LinuxBinprm { copied.FileEvent = deepCopyFileEvent(fieldToCopy.FileEvent) return copied } +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 +} +func deepCopystringMap(fieldToCopy map[string]string) map[string]string { + if fieldToCopy == nil { + return nil + } + copied := make(map[string]string, len(fieldToCopy)) + for k, v := range fieldToCopy { + copied[k] = v + } + return copied +} func deepCopyTraceID(fieldToCopy utils.TraceID) utils.TraceID { copied := utils.TraceID{} copied.Hi = fieldToCopy.Hi @@ -498,11 +515,10 @@ func deepCopyProcess(fieldToCopy Process) Process { copied.LinuxBinprm = deepCopyLinuxBinprm(fieldToCopy.LinuxBinprm) copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.Source = fieldToCopy.Source - copied.SpanID = fieldToCopy.SpanID + copied.SpanContext = deepCopySpanContext(fieldToCopy.SpanContext) copied.SymlinkBasenameStr = fieldToCopy.SymlinkBasenameStr copied.SymlinkPathnameStr = fieldToCopy.SymlinkPathnameStr copied.TTYName = fieldToCopy.TTYName - copied.TraceID = deepCopyTraceID(fieldToCopy.TraceID) copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) copied.UserSession = deepCopyUserSessionContext(fieldToCopy.UserSession) return copied @@ -556,16 +572,6 @@ func deepCopyMatchedRulePtrArr(fieldToCopy []*MatchedRule) []*MatchedRule { } return copied } -func deepCopystringMap(fieldToCopy map[string]string) map[string]string { - if fieldToCopy == nil { - return nil - } - copied := make(map[string]string, len(fieldToCopy)) - for k, v := range fieldToCopy { - copied[k] = v - } - return copied -} func deepCopyMatchedRulePtr(fieldToCopy *MatchedRule) *MatchedRule { if fieldToCopy == nil { return nil @@ -1126,14 +1132,6 @@ func deepCopySocketEvent(fieldToCopy SocketEvent) SocketEvent { copied.Type = fieldToCopy.Type return copied } -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 -} func deepCopySpliceEvent(fieldToCopy SpliceEvent) SpliceEvent { copied := SpliceEvent{} copied.File = deepCopyFileEvent(fieldToCopy.File) diff --git a/pkg/security/secl/model/model_helpers_unix.go b/pkg/security/secl/model/model_helpers_unix.go index f6a28dfaec74..3da9b9523ce8 100644 --- a/pkg/security/secl/model/model_helpers_unix.go +++ b/pkg/security/secl/model/model_helpers_unix.go @@ -17,7 +17,6 @@ import ( "time" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" - "github.com/DataDog/datadog-agent/pkg/security/secl/model/utils" ) const ( @@ -149,10 +148,20 @@ func (c *Credentials) Equals(o *Credentials) bool { c.CapPermitted == o.CapPermitted } -// SetSpan sets the span -func (p *Process) SetSpan(spanID uint64, traceID utils.TraceID) { - p.SpanID = spanID - p.TraceID = traceID +// SetSpanContext attaches the captured APM correlation span context to the +// process. Used by AddForkEntry to persist the parent's span across fork. +// Carries SpanID, TraceID, HasExtraAttrs and any OTel extra Attributes. +func (p *Process) SetSpanContext(sc SpanContext) { + p.SpanContext = sc +} + +// SetSpanContextAttributes updates only the Attributes field on the process's +// already-populated SpanContext. Used when OTel extra attributes are resolved +// after the PCE was first stamped (resolveOTelSpanAttrs runs after +// AddForkEntry / AddExecEntry, which only had the index→name-less event +// SpanContext to copy from). +func (p *Process) SetSpanContextAttributes(attrs map[string]string) { + p.SpanContext.Attributes = attrs } // GetPathResolutionError returns the path resolution error as a string if there is one diff --git a/pkg/security/secl/model/model_unix.go b/pkg/security/secl/model/model_unix.go index b4f0fbe88882..46ac3333f26e 100644 --- a/pkg/security/secl/model/model_unix.go +++ b/pkg/security/secl/model/model_unix.go @@ -23,7 +23,6 @@ import ( tracermetadata "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" "github.com/DataDog/datadog-agent/pkg/security/secl/containerutils" - "github.com/DataDog/datadog-agent/pkg/security/secl/model/utils" ) const ( @@ -395,8 +394,7 @@ type Process struct { CGroup CGroupContext `field:"cgroup"` // SECLDoc[cgroup] Definition:`CGroup` ContainerContext ContainerContext `field:"container"` // SECLDoc[container] Definition:`Container` - SpanID uint64 `field:"-"` - TraceID utils.TraceID `field:"-"` + SpanContext SpanContext `field:"-"` TTYName string `field:"tty_name"` // SECLDoc[tty_name] Definition:`Name of the TTY associated with the process` Comm string `field:"comm"` // SECLDoc[comm] Definition:`Comm attribute of the process` diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 20ee392dfe3d..28bfbc83dd0e 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -1260,6 +1260,20 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(i } easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in, out.Tracer) } + case "span_context": + if in.IsNull() { + in.Skip() + out.SpanContext = nil + } else { + if out.SpanContext == nil { + out.SpanContext = new(DDContextSerializer) + } + if in.IsNull() { + in.Skip() + } else { + (*out.SpanContext).UnmarshalEasyJSON(in) + } + } case "variables": if in.IsNull() { in.Skip() @@ -1579,6 +1593,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(o out.RawString(prefix) easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out, *in.Tracer) } + if in.SpanContext != nil { + const prefix string = ",\"span_context\":" + out.RawString(prefix) + (*in.SpanContext).MarshalEasyJSON(out) + } if len(in.Variables) != 0 { const prefix string = ",\"variables\":" out.RawString(prefix) diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 4e90bb6dd037..439963987af3 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -336,6 +336,13 @@ type ProcessSerializer struct { AWSSecurityCredentials []*AWSSecurityCredentialsSerializer `json:"aws_security_credentials,omitempty"` // Metadata from APM tracer instrumentation Tracer *tracermetadata.TracerMetadata `json:"tracer,omitempty"` + // APM span context captured for this process. For a process that + // fork+exec'd a subprocess this carries the parent's span (the one + // captured by fill_span_context at sched_process_fork). The top-level + // event "dd" field is built by newDDContextSerializer which walks the + // ancestor lineage; this per-process field exposes the same data at + // each level of the ancestor chain. + SpanContext *DDContextSerializer `json:"span_context,omitempty"` // Variable values Variables Variables `json:"variables,omitempty"` } @@ -1026,6 +1033,14 @@ func newProcessSerializer(ps *model.Process, e *model.Event) *ProcessSerializer psSerializer.Tracer = &tmetaCopy } + if ps.SpanContext.SpanID != 0 && (ps.SpanContext.TraceID.Hi != 0 || ps.SpanContext.TraceID.Lo != 0) { + psSerializer.SpanContext = &DDContextSerializer{ + SpanID: strconv.FormatUint(ps.SpanContext.SpanID, 10), + TraceID: fmt.Sprintf("%x%x", ps.SpanContext.TraceID.Hi, ps.SpanContext.TraceID.Lo), + Attributes: ps.SpanContext.Attributes, + } + } + if len(ps.ContainerContext.ContainerID) != 0 { psSerializer.Container = &ContainerContextSerializer{ ID: string(ps.ContainerContext.ContainerID), @@ -1482,9 +1497,10 @@ func newDDContextSerializer(e *model.Event) *DDContextSerializer { for ptr != nil { pce := (*model.ProcessCacheEntry)(ptr) - if pce.SpanID != 0 && (pce.TraceID.Hi != 0 || pce.TraceID.Lo != 0) { - s.SpanID = strconv.FormatUint(pce.SpanID, 10) - s.TraceID = fmt.Sprintf("%x%x", pce.TraceID.Hi, pce.TraceID.Lo) + if pce.SpanContext.SpanID != 0 && (pce.SpanContext.TraceID.Hi != 0 || pce.SpanContext.TraceID.Lo != 0) { + s.SpanID = strconv.FormatUint(pce.SpanContext.SpanID, 10) + s.TraceID = fmt.Sprintf("%x%x", pce.SpanContext.TraceID.Hi, pce.SpanContext.TraceID.Lo) + s.Attributes = pce.SpanContext.Attributes break } diff --git a/pkg/security/serializers/serializers_linux_easyjson.go b/pkg/security/serializers/serializers_linux_easyjson.go index 5cdc17c79d8d..93fe6bddfde4 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.go +++ b/pkg/security/serializers/serializers_linux_easyjson.go @@ -2674,6 +2674,20 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( } easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in, out.Tracer) } + case "span_context": + if in.IsNull() { + in.Skip() + out.SpanContext = nil + } else { + if out.SpanContext == nil { + out.SpanContext = new(DDContextSerializer) + } + if in.IsNull() { + in.Skip() + } else { + (*out.SpanContext).UnmarshalEasyJSON(in) + } + } case "variables": if in.IsNull() { in.Skip() @@ -2950,6 +2964,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString(prefix) easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out, *in.Tracer) } + if in.SpanContext != nil { + const prefix string = ",\"span_context\":" + out.RawString(prefix) + (*in.SpanContext).MarshalEasyJSON(out) + } if len(in.Variables) != 0 { const prefix string = ",\"variables\":" out.RawString(prefix) diff --git a/pkg/security/serializers/serializers_linux_easyjson.mock b/pkg/security/serializers/serializers_linux_easyjson.mock index a9cd7e300908..839505acd187 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.mock +++ b/pkg/security/serializers/serializers_linux_easyjson.mock @@ -54,3 +54,11 @@ func (v *UserSessionContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { // MarshalEasyJSON supports easyjson.Marshaler interface func (v UserSessionContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { } + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v DDContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *DDContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { +} diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 2f2283360192..5472cca5d636 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -38,6 +38,96 @@ func splitTraceID(decimalTraceID string) (hi, lo uint64, ok bool) { return hi, lo, true } +// spanContextJSON mirrors the shape of DDContextSerializer in JSON. +type spanContextJSON struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + Attributes map[string]string `json:"attributes"` +} + +// spanLocations describes where the per-process span_context is expected to +// surface in the serialized event. The top-level "dd" field is always +// asserted; these flags govern where the per-PCE serializer copy must (or +// must not) appear. +type spanLocations struct { + // onTopLevelProcess: process.span_context must carry the expected values. + // Set for non-fork-exec scenarios where fill_span_context captured a + // span at prepare_binprm and AddExecEntry persisted it on the new PCE. + onTopLevelProcess bool + // onAncestor: at least one entry in process.ancestors[].span_context + // must carry the expected values. Set for fork+exec scenarios where the + // fork hook captured the parent's span on the child PCE which then + // became an ancestor of the exec'd image. + onAncestor bool +} + +// assertSerializedSpanContext parses the marshalled event and asserts the +// propagation wiring described by `loc`. Always asserts the top-level "dd" +// field (built by newDDContextSerializer); the rest is gated by loc. +// +// expectedAttrs may be nil; when non-nil, each expected key must be present +// on the asserted span_context.attributes with the expected value (subset +// match — the helper does not assert absence of unexpected keys). +func assertSerializedSpanContext(t *testing.T, jsonStr, expectedSpanID, expectedTraceID string, expectedAttrs map[string]string, loc spanLocations) { + t.Helper() + var parsed struct { + DD struct { + SpanID string `json:"span_id"` + TraceID string `json:"trace_id"` + } `json:"dd"` + Process struct { + SpanContext *spanContextJSON `json:"span_context"` + Ancestors []struct { + SpanContext *spanContextJSON `json:"span_context"` + } `json:"ancestors"` + } `json:"process"` + } + if !assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { + return + } + + // (1) Top-level "dd" — always asserted. + assert.Equal(t, expectedSpanID, parsed.DD.SpanID, "serialized dd.span_id") + assert.Equal(t, expectedTraceID, parsed.DD.TraceID, "serialized dd.trace_id") + + // (2) Top-level "process.span_context". + if loc.onTopLevelProcess { + if assert.NotNil(t, parsed.Process.SpanContext, + "process.span_context should be populated (AddExecEntry persisted event.SpanContext on the new PCE)") { + assertSpanFields(t, parsed.Process.SpanContext, expectedSpanID, expectedTraceID, expectedAttrs, "process.span_context") + } + } else { + assert.Nil(t, parsed.Process.SpanContext, + "process.span_context should be unset (event.SpanContext was zero at exec time; nothing for AddExecEntry to persist)") + } + + // (3) Ancestor "span_context". + if loc.onAncestor { + var ancestorSpan *spanContextJSON + for i := range parsed.Process.Ancestors { + if parsed.Process.Ancestors[i].SpanContext != nil { + ancestorSpan = parsed.Process.Ancestors[i].SpanContext + break + } + } + if assert.NotNil(t, ancestorSpan, + "at least one ancestor in process.ancestors[] should carry a serialized span_context") { + assertSpanFields(t, ancestorSpan, expectedSpanID, expectedTraceID, expectedAttrs, "ancestor.span_context") + } + } +} + +// assertSpanFields asserts the fields of a serialized span_context match the +// expected span/trace IDs and the expected attribute subset. +func assertSpanFields(t *testing.T, sc *spanContextJSON, expectedSpanID, expectedTraceID string, expectedAttrs map[string]string, prefix string) { + t.Helper() + assert.Equal(t, expectedSpanID, sc.SpanID, "%s.span_id", prefix) + assert.Equal(t, expectedTraceID, sc.TraceID, "%s.trace_id", prefix) + for k, v := range expectedAttrs { + assert.Equal(t, v, sc.Attributes[k], "%s.attributes[%q]", prefix, k) + } +} + func TestSpan(t *testing.T) { SkipIfNotAvailable(t) @@ -131,6 +221,21 @@ func TestSpan(t *testing.T) { assert.Equal(t, "204", strconv.FormatUint(event.SpanContext.SpanID, 10)) assert.Equal(t, fakeTraceID128b, event.SpanContext.TraceID.String()) + + // In-process exec (pthread + execv preserves the tgid): + // fill_span_context_legacy reads span_tls[tgid] at prepare_binprm, + // AddExecEntry persists event.SpanContext onto the new touch PCE, + // so process.span_context is populated on the top-level process. + expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) + if !assert.True(t, ok, "splitTraceID") { + return + } + jsonStr, err := test.marshalEvent(event) + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, "204", + fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + spanLocations{onTopLevelProcess: true}) + } }, "test_span_rule_exec") }) @@ -179,37 +284,31 @@ func TestSpan(t *testing.T) { // span captured by fill_span_context_legacy at fork time. var foundAncestor *model.ProcessCacheEntry for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanID != 0 { + if pce.SpanContext.SpanID != 0 { foundAncestor = pce break } } if assert.NotNil(t, foundAncestor, "an ancestor should carry the parent's legacy TLS span captured at fork time") { - assert.Equal(t, uint64(204), foundAncestor.SpanID, + assert.Equal(t, uint64(204), foundAncestor.SpanContext.SpanID, "fork-parent ancestor SpanID should equal the legacy-TLS span_id") - assert.Equal(t, fakeTraceID128b, foundAncestor.TraceID.String(), + assert.Equal(t, fakeTraceID128b, foundAncestor.SpanContext.TraceID.String(), "fork-parent ancestor TraceID should equal the legacy-TLS trace_id") } - // (3) Serialized dd field should carry the propagated values. + // (3) Serialized dd field (top-level) and per-process + // span_context on the ancestor should both carry the + // propagated legacy-TLS values. expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) if !assert.True(t, ok, "splitTraceID") { return } jsonStr, err := test.marshalEvent(event) - assert.NoError(t, err, "marshalEvent") - var parsed struct { - DD struct { - SpanID string `json:"span_id"` - TraceID string `json:"trace_id"` - } `json:"dd"` - } - if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { - assert.Equal(t, "204", parsed.DD.SpanID, - "serialized dd.span_id should equal the legacy-TLS span_id") - assert.Equal(t, fmt.Sprintf("%x%x", expectedHi, expectedLo), parsed.DD.TraceID, - "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the legacy-TLS trace_id") + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, "204", + fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + spanLocations{onAncestor: true}) } }, "test_span_rule_exec") }) @@ -401,6 +500,28 @@ func TestOTelSpan(t *testing.T) { assert.Equal(t, "204", strconv.FormatUint(event.SpanContext.SpanID, 10)) assert.Equal(t, fakeTraceID128b, event.SpanContext.TraceID.String()) + + // In-process exec (pthread + execv preserves the tgid): the + // OTel record is published before execv, fill_span_context_otel + // reads it at prepare_binprm, AddExecEntry persists + // event.SpanContext onto the new touch PCE. Top-level + // process.span_context is populated, including the OTel + // custom attributes resolved via resolveOTelSpanAttrs. + expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) + if !assert.True(t, ok, "splitTraceID") { + return + } + jsonStr, err := test.marshalEvent(event) + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, "204", + fmt.Sprintf("%x%x", expectedHi, expectedLo), + map[string]string{ + "http.method": "GET", + "http.target": "/test", + "http.user": "will@datadoghq.com", + }, + spanLocations{onTopLevelProcess: true}) + } }, "test_otel_span_rule_exec") }) }) @@ -499,37 +620,31 @@ func TestOTelSpan(t *testing.T) { // span captured by fill_span_context_otel at fork time. var foundAncestor *model.ProcessCacheEntry for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanID != 0 { + if pce.SpanContext.SpanID != 0 { foundAncestor = pce break } } if assert.NotNil(t, foundAncestor, "an ancestor should carry the parent's OTel TLS span captured at fork time") { - assert.Equal(t, uint64(204), foundAncestor.SpanID, + assert.Equal(t, uint64(204), foundAncestor.SpanContext.SpanID, "fork-parent ancestor SpanID should equal the OTel record span_id") - assert.Equal(t, fakeTraceID128b, foundAncestor.TraceID.String(), + assert.Equal(t, fakeTraceID128b, foundAncestor.SpanContext.TraceID.String(), "fork-parent ancestor TraceID should equal the OTel record trace_id") } - // (3) Serialized dd field should carry the propagated values. + // (3) Serialized dd field (top-level) and per-process + // span_context on the ancestor should both carry the + // propagated OTel-record values. expectedHi, expectedLo, ok := splitTraceID(fakeTraceID128b) if !assert.True(t, ok, "splitTraceID") { return } jsonStr, err := test.marshalEvent(event) - assert.NoError(t, err, "marshalEvent") - var parsed struct { - DD struct { - SpanID string `json:"span_id"` - TraceID string `json:"trace_id"` - } `json:"dd"` - } - if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { - assert.Equal(t, "204", parsed.DD.SpanID, - "serialized dd.span_id should equal the OTel record span_id") - assert.Equal(t, fmt.Sprintf("%x%x", expectedHi, expectedLo), parsed.DD.TraceID, - "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the OTel record trace_id") + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, "204", + fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + spanLocations{onAncestor: true}) } }, "test_otel_span_rule_exec") }) @@ -651,6 +766,19 @@ func TestGoSpan(t *testing.T) { "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") + + // In-process exec via syscall.Exec preserves the tgid: + // fill_span_context_go reads the goroutine's pprof labels at + // prepare_binprm, AddExecEntry persists event.SpanContext + // onto the new touch PCE → process.span_context populated. + jsonStr, err := test.marshalEvent(event) + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, + strconv.FormatUint(987654321, 10), + fmt.Sprintf("%x%x", uint64(0), uint64(123456789)), + nil, + spanLocations{onTopLevelProcess: true}) + } }, "test_go_span_rule_exec") }) }) @@ -773,11 +901,11 @@ func TestGoSpan(t *testing.T) { var foundSpan bool var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanID != 0 { + if pce.SpanContext.SpanID != 0 { foundSpan = true - ancestorSpanID = pce.SpanID - ancestorTraceIDLo = pce.TraceID.Lo - ancestorTraceIDHi = pce.TraceID.Hi + ancestorSpanID = pce.SpanContext.SpanID + ancestorTraceIDLo = pce.SpanContext.TraceID.Lo + ancestorTraceIDHi = pce.SpanContext.TraceID.Hi break } } @@ -790,23 +918,16 @@ func TestGoSpan(t *testing.T) { assert.Equal(t, uint64(0), ancestorTraceIDHi, "Go pprof labels only populate the low 64 bits of trace_id") - // (3) The serialized event's "dd" field should carry those - // same propagated values (newDDContextSerializer's ancestor - // fallback path). + // (3) Top-level "dd" field (newDDContextSerializer's ancestor + // fallback) AND the per-process "span_context" on the + // ancestor should both carry the parent's pprof-label span. jsonStr, err := test.marshalEvent(event) - assert.NoError(t, err, "marshalEvent") - var parsed struct { - DD struct { - SpanID string `json:"span_id"` - TraceID string `json:"trace_id"` - } `json:"dd"` - } - if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { - assert.Equal(t, strconv.FormatUint(parentSpanID, 10), parsed.DD.SpanID, - "serialized dd.span_id should equal the parent's pprof span_id") - expectedTraceID := fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID) - assert.Equal(t, expectedTraceID, parsed.DD.TraceID, - "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of the parent's local_root_span_id") + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, + strconv.FormatUint(parentSpanID, 10), + fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID), + nil, + spanLocations{onAncestor: true}) } }, "test_go_span_rule_exec") }) @@ -946,6 +1067,19 @@ func TestDDTraceGoSpan(t *testing.T) { "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") + + // In-process exec via syscall.Exec: dd-trace-go's pprof labels + // on the locked OS thread are read by fill_span_context_go at + // prepare_binprm, AddExecEntry persists event.SpanContext + // onto the new touch PCE → process.span_context populated. + jsonStr, err := test.marshalEvent(event) + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, + strconv.FormatUint(expectedSpanID, 10), + fmt.Sprintf("%x%x", uint64(0), expectedLocalRootSpanID), + nil, + spanLocations{onTopLevelProcess: true}) + } }, "test_ddtrace_span_rule_exec") }) }) @@ -1071,11 +1205,11 @@ func TestDDTraceGoSpan(t *testing.T) { var foundSpan bool var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanID != 0 { + if pce.SpanContext.SpanID != 0 { foundSpan = true - ancestorSpanID = pce.SpanID - ancestorTraceIDLo = pce.TraceID.Lo - ancestorTraceIDHi = pce.TraceID.Hi + ancestorSpanID = pce.SpanContext.SpanID + ancestorTraceIDLo = pce.SpanContext.TraceID.Lo + ancestorTraceIDHi = pce.SpanContext.TraceID.Hi break } } @@ -1088,23 +1222,16 @@ func TestDDTraceGoSpan(t *testing.T) { assert.Equal(t, uint64(0), ancestorTraceIDHi, "dd-trace-go pprof labels only populate the low 64 bits of trace_id") - // (3) The serialized event's "dd" field should carry the - // same propagated values (the ancestor-fallback path in - // newDDContextSerializer). + // (3) Top-level "dd" field AND the per-process "span_context" + // on the ancestor should both carry dd-trace-go's parent + // span values. jsonStr, err := test.marshalEvent(event) - assert.NoError(t, err, "marshalEvent") - var parsed struct { - DD struct { - SpanID string `json:"span_id"` - TraceID string `json:"trace_id"` - } `json:"dd"` - } - if assert.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed), "json.Unmarshal") { - assert.Equal(t, strconv.FormatUint(parentSpanID, 10), parsed.DD.SpanID, - "serialized dd.span_id should equal dd-trace-go's parent span_id") - expectedTraceID := fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID) - assert.Equal(t, expectedTraceID, parsed.DD.TraceID, - "serialized dd.trace_id should equal hex(Hi)+hex(Lo) of dd-trace-go's local_root_span_id") + if assert.NoError(t, err, "marshalEvent") { + assertSerializedSpanContext(t, jsonStr, + strconv.FormatUint(parentSpanID, 10), + fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID), + nil, + spanLocations{onAncestor: true}) } }, "test_ddtrace_span_rule_exec") }) From 84927996076e2cb566f4d37cada00544d3aa87b1 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Wed, 27 May 2026 16:44:13 +0200 Subject: [PATCH 10/14] [WP] Taxonomy update --- docs/cloud-workload-security/backend_linux.md | 240 +-- .../backend_linux.schema.json | 82 +- pkg/security/probe/probe_ebpf.go | 6 +- .../resolvers/process/resolver_ebpf.go | 2 +- .../secl/model/event_deep_copy_unix.go | 44 +- .../secl/model/event_deep_copy_windows.go | 41 +- pkg/security/secl/model/model.go | 11 +- pkg/security/secl/model/model_helpers_unix.go | 4 +- pkg/security/secl/model/model_unix.go | 5 +- pkg/security/secl/model/model_windows.go | 3 +- .../secl/model/process_cache_entry_unix.go | 2 +- .../secl/schemas/span_context.schema.json | 15 +- pkg/security/seclwin/model/model.go | 11 +- pkg/security/seclwin/model/model_win.go | 3 +- .../serializers_base_linux_easyjson.go | 258 +--- pkg/security/serializers/serializers_linux.go | 96 +- .../serializers/serializers_linux_easyjson.go | 1370 +++++++++-------- .../serializers_linux_easyjson.mock | 12 +- pkg/security/tests/span_test.go | 97 +- 19 files changed, 1169 insertions(+), 1133 deletions(-) diff --git a/docs/cloud-workload-security/backend_linux.md b/docs/cloud-workload-security/backend_linux.md index 36f819c7c8ea..119732b08eaa 100644 --- a/docs/cloud-workload-security/backend_linux.md +++ b/docs/cloud-workload-security/backend_linux.md @@ -330,28 +330,6 @@ Workload Protection events for Linux systems have the following JSON schema: "type": "object", "description": "ContainerContextSerializer serializes a container context to JSON" }, - "DDContext": { - "properties": { - "span_id": { - "type": "string", - "description": "Span ID used for APM correlation" - }, - "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, - "type": "object", - "description": "DDContextSerializer serializes a span context to JSON" - }, "DNSEvent": { "properties": { "id": { @@ -1505,12 +1483,8 @@ Workload Protection events for Linux systems have the following JSON schema: "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -1692,12 +1666,8 @@ Workload Protection events for Linux systems have the following JSON schema: "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -2294,6 +2264,43 @@ Workload Protection events for Linux systems have the following JSON schema: "type": "object", "description": "TLSContextSerializer defines a tls context serializer" }, + "Trace": { + "properties": { + "span_id": { + "type": "string", + "description": "Span ID used for APM correlation" + }, + "trace_id": { + "type": "string", + "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Custom OTel thread-local attributes from the span context" + } + }, + "additionalProperties": false, + "type": "object", + "description": "TraceSerializer serializes a span context to JSON" + }, + "Tracer": { + "properties": { + "trace": { + "$ref": "#/$defs/Trace", + "description": "Captured APM span context for this process." + }, + "metadata": { + "$ref": "#/$defs/TracerMetadata", + "description": "Metadata from APM tracer instrumentation (schema version, language,\nversion, thread-local attribute keys, ...)." + } + }, + "additionalProperties": false, + "type": "object", + "description": "TracerSerializer groups the per-process APM tracer information surfaced under the \"tracer\" key in the serialized process: the captured span context (.trace) and the static tracer metadata (.metadata)." + }, "TracerMetadata": { "properties": { "schema_version": { @@ -2466,7 +2473,12 @@ Workload Protection events for Linux systems have the following JSON schema: "$ref": "#/$defs/NetworkContext" }, "dd": { - "$ref": "#/$defs/DDContext" + "$ref": "#/$defs/Trace", + "description": "DD holds the APM correlation span context under the \"dd\" key, the\nshape the Datadog backend expects at ingest. This field is consumed\nby the intake and not surfaced back to end users." + }, + "trace": { + "$ref": "#/$defs/Trace", + "description": "Trace is the same span/trace/attributes payload, exposed under a\nuser-facing key. Built from newTraceSerializer just like the \"dd\"\nfield above \u2014 the two pointers reference the same serializer\ninstance, so the two views can never drift." }, "security_profile": { "$ref": "#/$defs/SecurityProfileContext" @@ -2575,7 +2587,8 @@ Workload Protection events for Linux systems have the following JSON schema: | `container` | $ref | Please see [ContainerContext](#containercontext) | | `signature` | string | | | `network` | $ref | Please see [NetworkContext](#networkcontext) | -| `dd` | $ref | Please see [DDContext](#ddcontext) | +| `dd` | $ref | Please see [Trace](#trace) | +| `trace` | $ref | Please see [Trace](#trace) | | `security_profile` | $ref | Please see [SecurityProfileContext](#securityprofilecontext) | | `cgroup` | $ref | Please see [CGroupContext](#cgroupcontext) | | `selinux` | $ref | Please see [SELinuxEvent](#selinuxevent) | @@ -3118,42 +3131,6 @@ Workload Protection events for Linux systems have the following JSON schema: | ---------- | | [Variables](#variables) | -## `DDContext` - - -{{< code-block lang="json" collapsible="true" >}} -{ - "properties": { - "span_id": { - "type": "string", - "description": "Span ID used for APM correlation" - }, - "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, - "type": "object", - "description": "DDContextSerializer serializes a span context to JSON" -} - -{{< /code-block >}} - -| Field | Description | -| ----- | ----------- | -| `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` @@ -4776,12 +4753,8 @@ Workload Protection events for Linux systems have the following JSON schema: "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -4839,13 +4812,13 @@ Workload Protection events for Linux systems have the following JSON schema: | `source` | Process source | | `syscalls` | List of syscalls captured to generate the event | | `aws_security_credentials` | List of AWS Security Credentials that the process had access to | -| `tracer` | Metadata from APM tracer instrumentation | -| `span_context` | APM span context captured for this process. For a process that -fork+exec'd a subprocess this carries the parent's span (the one -captured by fill_span_context at sched_process_fork). The top-level -event "dd" field is built by newDDContextSerializer which walks the -ancestor lineage; this per-process field exposes the same data at -each level of the ancestor chain. | +| `tracer` | Tracer bundles the per-process APM tracer state: the captured span +(trace_id / span_id / attributes) under "trace", and the tracer +metadata under "metadata". For a process that fork+exec'd a +subprocess, .trace carries the parent's span captured by +fill_span_context at sched_process_fork; the top-level event +"dd"/"trace" fields are built by newTraceSerializer which walks the +ancestor lineage to find the same value. | | `variables` | Variable values | | References | @@ -4856,8 +4829,7 @@ each level of the ancestor chain. | | [CGroupContext](#cgroupcontext) | | [ContainerContext](#containercontext) | | [SyscallsEvent](#syscallsevent) | -| [TracerMetadata](#tracermetadata) | -| [DDContext](#ddcontext) | +| [Tracer](#tracer) | | [Variables](#variables) | ## `ProcessContext` @@ -5029,12 +5001,8 @@ each level of the ancestor chain. | "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -5107,13 +5075,13 @@ each level of the ancestor chain. | | `source` | Process source | | `syscalls` | List of syscalls captured to generate the event | | `aws_security_credentials` | List of AWS Security Credentials that the process had access to | -| `tracer` | Metadata from APM tracer instrumentation | -| `span_context` | APM span context captured for this process. For a process that -fork+exec'd a subprocess this carries the parent's span (the one -captured by fill_span_context at sched_process_fork). The top-level -event "dd" field is built by newDDContextSerializer which walks the -ancestor lineage; this per-process field exposes the same data at -each level of the ancestor chain. | +| `tracer` | Tracer bundles the per-process APM tracer state: the captured span +(trace_id / span_id / attributes) under "trace", and the tracer +metadata under "metadata". For a process that fork+exec'd a +subprocess, .trace carries the parent's span captured by +fill_span_context at sched_process_fork; the top-level event +"dd"/"trace" fields are built by newTraceSerializer which walks the +ancestor lineage to find the same value. | | `variables` | Variable values | | `parent` | Parent process | | `ancestors` | Ancestor processes | @@ -5127,8 +5095,7 @@ each level of the ancestor chain. | | [CGroupContext](#cgroupcontext) | | [ContainerContext](#containercontext) | | [SyscallsEvent](#syscallsevent) | -| [TracerMetadata](#tracermetadata) | -| [DDContext](#ddcontext) | +| [Tracer](#tracer) | | [Variables](#variables) | | [Process](#process) | @@ -5993,6 +5960,75 @@ each level of the ancestor chain. | +## `Trace` + + +{{< code-block lang="json" collapsible="true" >}} +{ + "properties": { + "span_id": { + "type": "string", + "description": "Span ID used for APM correlation" + }, + "trace_id": { + "type": "string", + "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Custom OTel thread-local attributes from the span context" + } + }, + "additionalProperties": false, + "type": "object", + "description": "TraceSerializer serializes a span context to JSON" +} + +{{< /code-block >}} + +| Field | Description | +| ----- | ----------- | +| `span_id` | Span ID used for APM correlation | +| `trace_id` | Trace ID used for APM correlation | +| `attributes` | Custom OTel thread-local attributes from the span context | + + +## `Tracer` + + +{{< code-block lang="json" collapsible="true" >}} +{ + "properties": { + "trace": { + "$ref": "#/$defs/Trace", + "description": "Captured APM span context for this process." + }, + "metadata": { + "$ref": "#/$defs/TracerMetadata", + "description": "Metadata from APM tracer instrumentation (schema version, language,\nversion, thread-local attribute keys, ...)." + } + }, + "additionalProperties": false, + "type": "object", + "description": "TracerSerializer groups the per-process APM tracer information surfaced under the \"tracer\" key in the serialized process: the captured span context (.trace) and the static tracer metadata (.metadata)." +} + +{{< /code-block >}} + +| Field | Description | +| ----- | ----------- | +| `trace` | Captured APM span context for this process. | +| `metadata` | Metadata from APM tracer instrumentation (schema version, language, +version, thread-local attribute keys, ...). | + +| References | +| ---------- | +| [Trace](#trace) | +| [TracerMetadata](#tracermetadata) | + ## `TracerMetadata` diff --git a/docs/cloud-workload-security/backend_linux.schema.json b/docs/cloud-workload-security/backend_linux.schema.json index 83e6efdc55e5..ddd7717b6a6f 100644 --- a/docs/cloud-workload-security/backend_linux.schema.json +++ b/docs/cloud-workload-security/backend_linux.schema.json @@ -319,28 +319,6 @@ "type": "object", "description": "ContainerContextSerializer serializes a container context to JSON" }, - "DDContext": { - "properties": { - "span_id": { - "type": "string", - "description": "Span ID used for APM correlation" - }, - "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, - "type": "object", - "description": "DDContextSerializer serializes a span context to JSON" - }, "DNSEvent": { "properties": { "id": { @@ -1494,12 +1472,8 @@ "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -1681,12 +1655,8 @@ "description": "List of AWS Security Credentials that the process had access to" }, "tracer": { - "$ref": "#/$defs/TracerMetadata", - "description": "Metadata from APM tracer instrumentation" - }, - "span_context": { - "$ref": "#/$defs/DDContext", - "description": "APM span context captured for this process. For a process that\nfork+exec'd a subprocess this carries the parent's span (the one\ncaptured by fill_span_context at sched_process_fork). The top-level\nevent \"dd\" field is built by newDDContextSerializer which walks the\nancestor lineage; this per-process field exposes the same data at\neach level of the ancestor chain." + "$ref": "#/$defs/Tracer", + "description": "Tracer bundles the per-process APM tracer state: the captured span\n(trace_id / span_id / attributes) under \"trace\", and the tracer\nmetadata under \"metadata\". For a process that fork+exec'd a\nsubprocess, .trace carries the parent's span captured by\nfill_span_context at sched_process_fork; the top-level event\n\"dd\"/\"trace\" fields are built by newTraceSerializer which walks the\nancestor lineage to find the same value." }, "variables": { "$ref": "#/$defs/Variables", @@ -2283,6 +2253,43 @@ "type": "object", "description": "TLSContextSerializer defines a tls context serializer" }, + "Trace": { + "properties": { + "span_id": { + "type": "string", + "description": "Span ID used for APM correlation" + }, + "trace_id": { + "type": "string", + "description": "Trace ID used for APM correlation" + }, + "attributes": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Custom OTel thread-local attributes from the span context" + } + }, + "additionalProperties": false, + "type": "object", + "description": "TraceSerializer serializes a span context to JSON" + }, + "Tracer": { + "properties": { + "trace": { + "$ref": "#/$defs/Trace", + "description": "Captured APM span context for this process." + }, + "metadata": { + "$ref": "#/$defs/TracerMetadata", + "description": "Metadata from APM tracer instrumentation (schema version, language,\nversion, thread-local attribute keys, ...)." + } + }, + "additionalProperties": false, + "type": "object", + "description": "TracerSerializer groups the per-process APM tracer information surfaced under the \"tracer\" key in the serialized process: the captured span context (.trace) and the static tracer metadata (.metadata)." + }, "TracerMetadata": { "properties": { "schema_version": { @@ -2455,7 +2462,12 @@ "$ref": "#/$defs/NetworkContext" }, "dd": { - "$ref": "#/$defs/DDContext" + "$ref": "#/$defs/Trace", + "description": "DD holds the APM correlation span context under the \"dd\" key, the\nshape the Datadog backend expects at ingest. This field is consumed\nby the intake and not surfaced back to end users." + }, + "trace": { + "$ref": "#/$defs/Trace", + "description": "Trace is the same span/trace/attributes payload, exposed under a\nuser-facing key. Built from newTraceSerializer just like the \"dd\"\nfield above — the two pointers reference the same serializer\ninstance, so the two views can never drift." }, "security_profile": { "$ref": "#/$defs/SecurityProfileContext" diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 8a41585feef8..69c368fcf250 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -1113,11 +1113,11 @@ func (p *EBPFProbe) resolveOTelSpanAttrs(event *model.Event, eventType model.Eve // ThreadlocalAttributeKeys so we can map index → name correctly. var keyNames []string if event.ProcessContext != nil { - keyNames = event.ProcessContext.Process.TracerMetadata.ThreadlocalAttributeKeys + keyNames = event.ProcessContext.Process.Tracer.Metadata.ThreadlocalAttributeKeys if len(keyNames) == 0 { for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if len(pce.Process.TracerMetadata.ThreadlocalAttributeKeys) > 0 { - keyNames = pce.Process.TracerMetadata.ThreadlocalAttributeKeys + if len(pce.Process.Tracer.Metadata.ThreadlocalAttributeKeys) > 0 { + keyNames = pce.Process.Tracer.Metadata.ThreadlocalAttributeKeys break } } diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index 9cca6d7cdfcf..4eed458e8832 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -1490,7 +1490,7 @@ func (p *EBPFResolver) AddTracerMetadata(pid uint32, event *model.Event) error { p.Lock() entry := p.entryCache[pid] if entry != nil { - entry.TracerMetadata = tmeta + entry.Tracer.Metadata = tmeta } p.Unlock() diff --git a/pkg/security/secl/model/event_deep_copy_unix.go b/pkg/security/secl/model/event_deep_copy_unix.go index d42dafc01bcb..74245c525868 100644 --- a/pkg/security/secl/model/event_deep_copy_unix.go +++ b/pkg/security/secl/model/event_deep_copy_unix.go @@ -253,11 +253,10 @@ func deepCopyProcessPtr(fieldToCopy *Process) *Process { copied.LinuxBinprm = deepCopyLinuxBinprm(fieldToCopy.LinuxBinprm) copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.Source = fieldToCopy.Source - copied.SpanContext = deepCopySpanContext(fieldToCopy.SpanContext) copied.SymlinkBasenameStr = fieldToCopy.SymlinkBasenameStr copied.SymlinkPathnameStr = fieldToCopy.SymlinkPathnameStr copied.TTYName = fieldToCopy.TTYName - copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) + copied.Tracer = deepCopyTracer(fieldToCopy.Tracer) copied.UserSession = deepCopyUserSessionContext(fieldToCopy.UserSession) return copied } @@ -399,6 +398,28 @@ func deepCopyLinuxBinprm(fieldToCopy LinuxBinprm) LinuxBinprm { copied.FileEvent = deepCopyFileEvent(fieldToCopy.FileEvent) return copied } +func deepCopyTracer(fieldToCopy Tracer) Tracer { + copied := Tracer{} + copied.Metadata = deepCopyTracerMetadata(fieldToCopy.Metadata) + copied.Trace = deepCopySpanContext(fieldToCopy.Trace) + return copied +} +func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermetadata.TracerMetadata { + copied := tracermetadata.TracerMetadata{} + copied.ContainerID = fieldToCopy.ContainerID + copied.Hostname = fieldToCopy.Hostname + copied.LogsCollected = fieldToCopy.LogsCollected + copied.ProcessTags = fieldToCopy.ProcessTags + copied.RuntimeID = fieldToCopy.RuntimeID + copied.SchemaVersion = fieldToCopy.SchemaVersion + 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 +} func deepCopySpanContext(fieldToCopy SpanContext) SpanContext { copied := SpanContext{} copied.Attributes = deepCopystringMap(fieldToCopy.Attributes) @@ -423,22 +444,6 @@ func deepCopyTraceID(fieldToCopy utils.TraceID) utils.TraceID { copied.Lo = fieldToCopy.Lo return copied } -func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermetadata.TracerMetadata { - copied := tracermetadata.TracerMetadata{} - copied.ContainerID = fieldToCopy.ContainerID - copied.Hostname = fieldToCopy.Hostname - copied.LogsCollected = fieldToCopy.LogsCollected - copied.ProcessTags = fieldToCopy.ProcessTags - copied.RuntimeID = fieldToCopy.RuntimeID - copied.SchemaVersion = fieldToCopy.SchemaVersion - 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 -} func deepCopyUserSessionContext(fieldToCopy UserSessionContext) UserSessionContext { copied := UserSessionContext{} copied.ID = fieldToCopy.ID @@ -515,11 +520,10 @@ func deepCopyProcess(fieldToCopy Process) Process { copied.LinuxBinprm = deepCopyLinuxBinprm(fieldToCopy.LinuxBinprm) copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.Source = fieldToCopy.Source - copied.SpanContext = deepCopySpanContext(fieldToCopy.SpanContext) copied.SymlinkBasenameStr = fieldToCopy.SymlinkBasenameStr copied.SymlinkPathnameStr = fieldToCopy.SymlinkPathnameStr copied.TTYName = fieldToCopy.TTYName - copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) + copied.Tracer = deepCopyTracer(fieldToCopy.Tracer) copied.UserSession = deepCopyUserSessionContext(fieldToCopy.UserSession) 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 ad9559cc4bf1..cb0ee3608f38 100644 --- a/pkg/security/secl/model/event_deep_copy_windows.go +++ b/pkg/security/secl/model/event_deep_copy_windows.go @@ -97,7 +97,7 @@ func deepCopyProcessPtr(fieldToCopy *Process) *Process { copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.PPid = fieldToCopy.PPid copied.ScrubbedCmdLineResolved = fieldToCopy.ScrubbedCmdLineResolved - copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) + copied.Tracer = deepCopyTracer(fieldToCopy.Tracer) copied.User = fieldToCopy.User return copied } @@ -154,6 +154,12 @@ func deepCopyFileEvent(fieldToCopy FileEvent) FileEvent { copied.PathnameStr = fieldToCopy.PathnameStr return copied } +func deepCopyTracer(fieldToCopy Tracer) Tracer { + copied := Tracer{} + copied.Metadata = deepCopyTracerMetadata(fieldToCopy.Metadata) + copied.Trace = deepCopySpanContext(fieldToCopy.Trace) + return copied +} func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermetadata.TracerMetadata { copied := tracermetadata.TracerMetadata{} copied.ContainerID = fieldToCopy.ContainerID @@ -170,6 +176,27 @@ func deepCopyTracerMetadata(fieldToCopy tracermetadata.TracerMetadata) tracermet copied.TracerVersion = fieldToCopy.TracerVersion return copied } +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 +} +func deepCopystringMap(fieldToCopy map[string]string) map[string]string { + if fieldToCopy == nil { + return nil + } + copied := make(map[string]string, len(fieldToCopy)) + for k, v := range fieldToCopy { + copied[k] = v + } + return copied +} +func deepCopyTraceID(fieldToCopy utils.TraceID) utils.TraceID { + return fieldToCopy +} func deepCopyProcess(fieldToCopy Process) Process { copied := Process{} copied.ArgsEntry = deepCopyArgsEntryPtr(fieldToCopy.ArgsEntry) @@ -187,7 +214,7 @@ func deepCopyProcess(fieldToCopy Process) Process { copied.PIDContext = deepCopyPIDContext(fieldToCopy.PIDContext) copied.PPid = fieldToCopy.PPid copied.ScrubbedCmdLineResolved = fieldToCopy.ScrubbedCmdLineResolved - copied.TracerMetadata = deepCopyTracerMetadata(fieldToCopy.TracerMetadata) + copied.Tracer = deepCopyTracer(fieldToCopy.Tracer) copied.User = fieldToCopy.User return copied } @@ -240,16 +267,6 @@ func deepCopyMatchedRulePtrArr(fieldToCopy []*MatchedRule) []*MatchedRule { } return copied } -func deepCopystringMap(fieldToCopy map[string]string) map[string]string { - if fieldToCopy == nil { - return nil - } - copied := make(map[string]string, len(fieldToCopy)) - for k, v := range fieldToCopy { - copied[k] = v - } - return copied -} func deepCopyMatchedRulePtr(fieldToCopy *MatchedRule) *MatchedRule { if fieldToCopy == nil { return nil diff --git a/pkg/security/secl/model/model.go b/pkg/security/secl/model/model.go index d5bdaade1414..9384c8259092 100644 --- a/pkg/security/secl/model/model.go +++ b/pkg/security/secl/model/model.go @@ -201,6 +201,15 @@ type SpanContext struct { Attributes map[string]string `field:"-"` } +// Tracer bundles the per-process APM tracer state: static metadata captured +// from the tracer-info memfd, plus the most recent span context observed for +// this process. Cross-platform so the model.go-level accessors compile on +// both Linux and Windows builds. +type Tracer struct { + Metadata tracermetadata.TracerMetadata + Trace SpanContext +} + // RuleContext defines a rule context type RuleContext struct { Expression string `field:"-"` @@ -400,7 +409,7 @@ func (e *Event) GetProcessTracerMetadata() tracermetadata.TracerMetadata { if e.BaseEvent.ProcessContext == nil { return tracermetadata.TracerMetadata{} } - return e.BaseEvent.ProcessContext.Process.TracerMetadata + return e.BaseEvent.ProcessContext.Process.Tracer.Metadata } // UserSessionContext describes the user session context diff --git a/pkg/security/secl/model/model_helpers_unix.go b/pkg/security/secl/model/model_helpers_unix.go index 3da9b9523ce8..e95d4bddf417 100644 --- a/pkg/security/secl/model/model_helpers_unix.go +++ b/pkg/security/secl/model/model_helpers_unix.go @@ -152,7 +152,7 @@ func (c *Credentials) Equals(o *Credentials) bool { // process. Used by AddForkEntry to persist the parent's span across fork. // Carries SpanID, TraceID, HasExtraAttrs and any OTel extra Attributes. func (p *Process) SetSpanContext(sc SpanContext) { - p.SpanContext = sc + p.Tracer.Trace = sc } // SetSpanContextAttributes updates only the Attributes field on the process's @@ -161,7 +161,7 @@ func (p *Process) SetSpanContext(sc SpanContext) { // AddForkEntry / AddExecEntry, which only had the index→name-less event // SpanContext to copy from). func (p *Process) SetSpanContextAttributes(attrs map[string]string) { - p.SpanContext.Attributes = attrs + p.Tracer.Trace.Attributes = attrs } // GetPathResolutionError returns the path resolution error as a string if there is one diff --git a/pkg/security/secl/model/model_unix.go b/pkg/security/secl/model/model_unix.go index 46ac3333f26e..6b15c482bffa 100644 --- a/pkg/security/secl/model/model_unix.go +++ b/pkg/security/secl/model/model_unix.go @@ -20,7 +20,6 @@ import ( "github.com/google/gopacket" - tracermetadata "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" "github.com/DataDog/datadog-agent/pkg/security/secl/containerutils" ) @@ -394,8 +393,6 @@ type Process struct { CGroup CGroupContext `field:"cgroup"` // SECLDoc[cgroup] Definition:`CGroup` ContainerContext ContainerContext `field:"container"` // SECLDoc[container] Definition:`Container` - SpanContext SpanContext `field:"-"` - TTYName string `field:"tty_name"` // SECLDoc[tty_name] Definition:`Name of the TTY associated with the process` Comm string `field:"comm"` // SECLDoc[comm] Definition:`Comm attribute of the process` LinuxBinprm LinuxBinprm `field:"interpreter,check:HasInterpreter,set_handler:SetInterpreterFields"` // Script interpreter as identified by the shebang @@ -422,7 +419,7 @@ type Process struct { AWSSecurityCredentials []AWSSecurityCredentials `field:"-"` - TracerMetadata tracermetadata.TracerMetadata `field:"-"` // Metadata from APM tracer instrumentation + Tracer Tracer `field:"-"` ArgsID uint64 `field:"-"` EnvsID uint64 `field:"-"` diff --git a/pkg/security/secl/model/model_windows.go b/pkg/security/secl/model/model_windows.go index d101d14c34a2..55b8bce4230c 100644 --- a/pkg/security/secl/model/model_windows.go +++ b/pkg/security/secl/model/model_windows.go @@ -14,7 +14,6 @@ import ( "time" "unsafe" - tracermetadata "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" ) @@ -157,7 +156,7 @@ type Process struct { PPid uint32 `field:"ppid"` // SECLDoc[ppid] Definition:`Parent process ID` - TracerMetadata tracermetadata.TracerMetadata `field:"-"` // Metadata from APM tracer instrumentation + Tracer Tracer `field:"-"` // APM tracer state: metadata + captured span ArgsEntry *ArgsEntry `field:"-"` EnvsEntry *EnvsEntry `field:"-"` diff --git a/pkg/security/secl/model/process_cache_entry_unix.go b/pkg/security/secl/model/process_cache_entry_unix.go index 855c3b38db6f..4c8dd9f7bf2a 100644 --- a/pkg/security/secl/model/process_cache_entry_unix.go +++ b/pkg/security/secl/model/process_cache_entry_unix.go @@ -200,7 +200,7 @@ func (pc *ProcessCacheEntry) Fork(child *ProcessCacheEntry) { child.Credentials = pc.Credentials child.LinuxBinprm = pc.LinuxBinprm child.Cookie = pc.Cookie - child.TracerMetadata = pc.TracerMetadata + child.Tracer.Metadata = pc.Tracer.Metadata child.SetForkParent(pc) } diff --git a/pkg/security/secl/schemas/span_context.schema.json b/pkg/security/secl/schemas/span_context.schema.json index 42e423c782e7..d7122acf19bd 100644 --- a/pkg/security/secl/schemas/span_context.schema.json +++ b/pkg/security/secl/schemas/span_context.schema.json @@ -14,9 +14,22 @@ "span_id", "trace_id" ] + }, + "trace": { + "span_id": { + "type": "string" + }, + "trace_id": { + "type": "string" + }, + "required": [ + "span_id", + "trace_id" + ] } }, "required": [ - "dd" + "dd", + "trace" ] } diff --git a/pkg/security/seclwin/model/model.go b/pkg/security/seclwin/model/model.go index d5bdaade1414..9384c8259092 100644 --- a/pkg/security/seclwin/model/model.go +++ b/pkg/security/seclwin/model/model.go @@ -201,6 +201,15 @@ type SpanContext struct { Attributes map[string]string `field:"-"` } +// Tracer bundles the per-process APM tracer state: static metadata captured +// from the tracer-info memfd, plus the most recent span context observed for +// this process. Cross-platform so the model.go-level accessors compile on +// both Linux and Windows builds. +type Tracer struct { + Metadata tracermetadata.TracerMetadata + Trace SpanContext +} + // RuleContext defines a rule context type RuleContext struct { Expression string `field:"-"` @@ -400,7 +409,7 @@ func (e *Event) GetProcessTracerMetadata() tracermetadata.TracerMetadata { if e.BaseEvent.ProcessContext == nil { return tracermetadata.TracerMetadata{} } - return e.BaseEvent.ProcessContext.Process.TracerMetadata + return e.BaseEvent.ProcessContext.Process.Tracer.Metadata } // UserSessionContext describes the user session context diff --git a/pkg/security/seclwin/model/model_win.go b/pkg/security/seclwin/model/model_win.go index d101d14c34a2..55b8bce4230c 100644 --- a/pkg/security/seclwin/model/model_win.go +++ b/pkg/security/seclwin/model/model_win.go @@ -14,7 +14,6 @@ import ( "time" "unsafe" - tracermetadata "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" ) @@ -157,7 +156,7 @@ type Process struct { PPid uint32 `field:"ppid"` // SECLDoc[ppid] Definition:`Parent process ID` - TracerMetadata tracermetadata.TracerMetadata `field:"-"` // Metadata from APM tracer instrumentation + Tracer Tracer `field:"-"` // APM tracer state: metadata + captured span ArgsEntry *ArgsEntry `field:"-"` EnvsEntry *EnvsEntry `field:"-"` diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 28bfbc83dd0e..7b531580bedf 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -7,7 +7,6 @@ package serializers import ( json "encoding/json" - model "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" utils "github.com/DataDog/datadog-agent/pkg/security/utils" easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" @@ -1256,22 +1255,12 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(i out.Tracer = nil } else { if out.Tracer == nil { - out.Tracer = new(model.TracerMetadata) - } - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in, out.Tracer) - } - case "span_context": - if in.IsNull() { - in.Skip() - out.SpanContext = nil - } else { - if out.SpanContext == nil { - out.SpanContext = new(DDContextSerializer) + out.Tracer = new(TracerSerializer) } if in.IsNull() { in.Skip() } else { - (*out.SpanContext).UnmarshalEasyJSON(in) + (*out.Tracer).UnmarshalEasyJSON(in) } } case "variables": @@ -1591,12 +1580,7 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(o if in.Tracer != nil { const prefix string = ",\"tracer\":" out.RawString(prefix) - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out, *in.Tracer) - } - if in.SpanContext != nil { - const prefix string = ",\"span_context\":" - out.RawString(prefix) - (*in.SpanContext).MarshalEasyJSON(out) + (*in.Tracer).MarshalEasyJSON(out) } if len(in.Variables) != 0 { const prefix string = ",\"variables\":" @@ -1615,198 +1599,6 @@ func (v ProcessContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { func (v *ProcessContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in *jlexer.Lexer, out *model.TracerMetadata) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "schema_version": - if in.IsNull() { - in.Skip() - } else { - out.SchemaVersion = uint8(in.Uint8()) - } - case "runtime_id": - if in.IsNull() { - in.Skip() - } else { - out.RuntimeID = string(in.String()) - } - case "tracer_language": - if in.IsNull() { - in.Skip() - } else { - out.TracerLanguage = string(in.String()) - } - case "tracer_version": - if in.IsNull() { - in.Skip() - } else { - out.TracerVersion = string(in.String()) - } - case "hostname": - if in.IsNull() { - in.Skip() - } else { - out.Hostname = string(in.String()) - } - case "service_name": - if in.IsNull() { - in.Skip() - } else { - out.ServiceName = string(in.String()) - } - case "service_env": - if in.IsNull() { - in.Skip() - } else { - out.ServiceEnv = string(in.String()) - } - case "service_version": - if in.IsNull() { - in.Skip() - } else { - out.ServiceVersion = string(in.String()) - } - case "process_tags": - if in.IsNull() { - in.Skip() - } else { - out.ProcessTags = string(in.String()) - } - case "container_id": - if in.IsNull() { - in.Skip() - } else { - out.ContainerID = string(in.String()) - } - case "logs_collected": - if in.IsNull() { - in.Skip() - } 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() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out *jwriter.Writer, in model.TracerMetadata) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"schema_version\":" - out.RawString(prefix[1:]) - out.Uint8(uint8(in.SchemaVersion)) - } - if in.RuntimeID != "" { - const prefix string = ",\"runtime_id\":" - out.RawString(prefix) - out.String(string(in.RuntimeID)) - } - { - const prefix string = ",\"tracer_language\":" - out.RawString(prefix) - out.String(string(in.TracerLanguage)) - } - { - const prefix string = ",\"tracer_version\":" - out.RawString(prefix) - out.String(string(in.TracerVersion)) - } - { - const prefix string = ",\"hostname\":" - out.RawString(prefix) - out.String(string(in.Hostname)) - } - if in.ServiceName != "" { - const prefix string = ",\"service_name\":" - out.RawString(prefix) - out.String(string(in.ServiceName)) - } - if in.ServiceEnv != "" { - const prefix string = ",\"service_env\":" - out.RawString(prefix) - out.String(string(in.ServiceEnv)) - } - if in.ServiceVersion != "" { - const prefix string = ",\"service_version\":" - out.RawString(prefix) - out.String(string(in.ServiceVersion)) - } - if in.ProcessTags != "" { - const prefix string = ",\"process_tags\":" - out.RawString(prefix) - out.String(string(in.ProcessTags)) - } - if in.ContainerID != "" { - const prefix string = ",\"container_id\":" - out.RawString(prefix) - out.String(string(in.ContainerID)) - } - if in.LogsCollected { - const prefix string = ",\"logs_collected\":" - 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) { isTopLevel := in.IsStart() if in.IsNull() { @@ -1971,21 +1763,21 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(i out.Flows = (out.Flows)[:0] } for !in.IsDelim(']') { - var v35 *FlowSerializer + var v32 *FlowSerializer if in.IsNull() { in.Skip() - v35 = nil + v32 = nil } else { - if v35 == nil { - v35 = new(FlowSerializer) + if v32 == nil { + v32 = new(FlowSerializer) } if in.IsNull() { in.Skip() } else { - (*v35).UnmarshalEasyJSON(in) + (*v32).UnmarshalEasyJSON(in) } } - out.Flows = append(out.Flows, v35) + out.Flows = append(out.Flows, v32) in.WantComma() } in.Delim(']') @@ -2020,14 +1812,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(o } { out.RawByte('[') - for v36, v37 := range in.Flows { - if v36 > 0 { + for v33, v34 := range in.Flows { + if v33 > 0 { out.RawByte(',') } - if v37 == nil { + if v34 == nil { out.RawString("null") } else { - (*v37).MarshalEasyJSON(out) + (*v34).MarshalEasyJSON(out) } } out.RawByte(']') @@ -2313,13 +2105,13 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v38 string + var v35 string if in.IsNull() { in.Skip() } else { - v38 = string(in.String()) + v35 = string(in.String()) } - out.Tags = append(out.Tags, v38) + out.Tags = append(out.Tags, v35) in.WantComma() } in.Delim(']') @@ -2376,11 +2168,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( } { out.RawByte('[') - for v39, v40 := range in.Tags { - if v39 > 0 { + for v36, v37 := range in.Tags { + if v36 > 0 { out.RawByte(',') } - out.String(string(v40)) + out.String(string(v37)) } out.RawByte(']') } @@ -2915,13 +2707,13 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( out.MatchedRules = (out.MatchedRules)[:0] } for !in.IsDelim(']') { - var v41 MatchedRuleSerializer + var v38 MatchedRuleSerializer if in.IsNull() { in.Skip() } else { - (v41).UnmarshalEasyJSON(in) + (v38).UnmarshalEasyJSON(in) } - out.MatchedRules = append(out.MatchedRules, v41) + out.MatchedRules = append(out.MatchedRules, v38) in.WantComma() } in.Delim(']') @@ -3004,11 +2796,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( } { out.RawByte('[') - for v42, v43 := range in.MatchedRules { - if v42 > 0 { + for v39, v40 := range in.MatchedRules { + if v39 > 0 { out.RawByte(',') } - (v43).MarshalEasyJSON(out) + (v40).MarshalEasyJSON(out) } out.RawByte(']') } diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 439963987af3..546e0c1ae3cf 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -334,19 +334,30 @@ type ProcessSerializer struct { Syscalls *SyscallsEventSerializer `json:"syscalls,omitempty"` // List of AWS Security Credentials that the process had access to AWSSecurityCredentials []*AWSSecurityCredentialsSerializer `json:"aws_security_credentials,omitempty"` - // Metadata from APM tracer instrumentation - Tracer *tracermetadata.TracerMetadata `json:"tracer,omitempty"` - // APM span context captured for this process. For a process that - // fork+exec'd a subprocess this carries the parent's span (the one - // captured by fill_span_context at sched_process_fork). The top-level - // event "dd" field is built by newDDContextSerializer which walks the - // ancestor lineage; this per-process field exposes the same data at - // each level of the ancestor chain. - SpanContext *DDContextSerializer `json:"span_context,omitempty"` + // Tracer bundles the per-process APM tracer state: the captured span + // (trace_id / span_id / attributes) under "trace", and the tracer + // metadata under "metadata". For a process that fork+exec'd a + // subprocess, .trace carries the parent's span captured by + // fill_span_context at sched_process_fork; the top-level event + // "dd"/"trace" fields are built by newTraceSerializer which walks the + // ancestor lineage to find the same value. + Tracer *TracerSerializer `json:"tracer,omitempty"` // Variable values Variables Variables `json:"variables,omitempty"` } +// TracerSerializer groups the per-process APM tracer information surfaced +// under the "tracer" key in the serialized process: the captured span +// context (.trace) and the static tracer metadata (.metadata). +// easyjson:json +type TracerSerializer struct { + // Captured APM span context for this process. + Trace *TraceSerializer `json:"trace,omitempty"` + // Metadata from APM tracer instrumentation (schema version, language, + // version, thread-local attribute keys, ...). + Metadata *tracermetadata.TracerMetadata `json:"metadata,omitempty"` +} + // FileEventSerializer serializes a file event to JSON // easyjson:json type FileEventSerializer struct { @@ -808,8 +819,16 @@ type EventSerializer struct { *BaseEventSerializer Signature string `json:"signature,omitempty"` - *NetworkContextSerializer `json:"network,omitempty"` - *DDContextSerializer `json:"dd,omitempty"` + *NetworkContextSerializer `json:"network,omitempty"` + // DD holds the APM correlation span context under the "dd" key, the + // shape the Datadog backend expects at ingest. This field is consumed + // by the intake and not surfaced back to end users. + DD *TraceSerializer `json:"dd,omitempty"` + // Trace is the same span/trace/attributes payload, exposed under a + // user-facing key. Built from newTraceSerializer just like the "dd" + // field above — the two pointers reference the same serializer + // instance, so the two views can never drift. + Trace *TraceSerializer `json:"trace,omitempty"` *SecurityProfileContextSerializer `json:"security_profile,omitempty"` *CGroupContextSerializer `json:"cgroup,omitempty"` @@ -1028,18 +1047,29 @@ func newProcessSerializer(ps *model.Process, e *model.Event) *ProcessSerializer } } - if !ps.TracerMetadata.IsZero() { - tmetaCopy := ps.TracerMetadata - psSerializer.Tracer = &tmetaCopy + // Build the per-process "tracer" object lazily — it appears only + // when either the static metadata or the captured span is set. + var tracer *TracerSerializer + if !ps.Tracer.Metadata.IsZero() { + tmetaCopy := ps.Tracer.Metadata + if tracer == nil { + tracer = &TracerSerializer{} + } + tracer.Metadata = &tmetaCopy } - - if ps.SpanContext.SpanID != 0 && (ps.SpanContext.TraceID.Hi != 0 || ps.SpanContext.TraceID.Lo != 0) { - psSerializer.SpanContext = &DDContextSerializer{ - SpanID: strconv.FormatUint(ps.SpanContext.SpanID, 10), - TraceID: fmt.Sprintf("%x%x", ps.SpanContext.TraceID.Hi, ps.SpanContext.TraceID.Lo), - Attributes: ps.SpanContext.Attributes, + if ps.Tracer.Trace.SpanID != 0 && (ps.Tracer.Trace.TraceID.Hi != 0 || ps.Tracer.Trace.TraceID.Lo != 0) { + if tracer == nil { + tracer = &TracerSerializer{} + } + tracer.Trace = &TraceSerializer{ + SpanID: strconv.FormatUint(ps.Tracer.Trace.SpanID, 10), + TraceID: fmt.Sprintf("%x%x", ps.Tracer.Trace.TraceID.Hi, ps.Tracer.Trace.TraceID.Lo), + Attributes: ps.Tracer.Trace.Attributes, } } + if tracer != nil { + psSerializer.Tracer = tracer + } if len(ps.ContainerContext.ContainerID) != 0 { psSerializer.Container = &ContainerContextSerializer{ @@ -1470,19 +1500,19 @@ func newProcessContextSerializer(pc *model.ProcessContext, e *model.Event, rule return &ps } -// DDContextSerializer serializes a span context to JSON +// TraceSerializer serializes a span context to JSON // easyjson:json -type DDContextSerializer struct { +type TraceSerializer struct { // Span ID used for APM correlation 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 + // Custom OTel thread-local attributes from the span context Attributes map[string]string `json:"attributes,omitempty"` } -func newDDContextSerializer(e *model.Event) *DDContextSerializer { - s := &DDContextSerializer{} +func newTraceSerializer(e *model.Event) *TraceSerializer { + s := &TraceSerializer{} 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) @@ -1497,10 +1527,10 @@ func newDDContextSerializer(e *model.Event) *DDContextSerializer { for ptr != nil { pce := (*model.ProcessCacheEntry)(ptr) - if pce.SpanContext.SpanID != 0 && (pce.SpanContext.TraceID.Hi != 0 || pce.SpanContext.TraceID.Lo != 0) { - s.SpanID = strconv.FormatUint(pce.SpanContext.SpanID, 10) - s.TraceID = fmt.Sprintf("%x%x", pce.SpanContext.TraceID.Hi, pce.SpanContext.TraceID.Lo) - s.Attributes = pce.SpanContext.Attributes + if pce.Tracer.Trace.SpanID != 0 && (pce.Tracer.Trace.TraceID.Hi != 0 || pce.Tracer.Trace.TraceID.Lo != 0) { + s.SpanID = strconv.FormatUint(pce.Tracer.Trace.SpanID, 10) + s.TraceID = fmt.Sprintf("%x%x", pce.Tracer.Trace.TraceID.Hi, pce.Tracer.Trace.TraceID.Lo) + s.Attributes = pce.Tracer.Trace.Attributes break } @@ -1623,10 +1653,16 @@ func MarshalCustomEvent(event *events.CustomEvent) ([]byte, error) { // NewEventSerializer creates a new event serializer based on the event type func NewEventSerializer(event *model.Event, rule *rules.Rule, scrubber *utils.Scrubber) *EventSerializer { + // One trace serializer powers two top-level JSON keys: "dd" (consumed + // by the Datadog intake, not visible to users) and "trace" (user- + // facing). Sharing the pointer guarantees both views stay in lock step + // with no risk of divergence. + spanCtx := newTraceSerializer(event) s := &EventSerializer{ BaseEventSerializer: NewBaseEventSerializer(event, rule, scrubber), UserContextSerializer: newUserContextSerializer(event), - DDContextSerializer: newDDContextSerializer(event), + DD: spanCtx, + Trace: spanCtx, } s.Async = event.FieldHandlers.ResolveAsync(event) s.Signature = event.FieldHandlers.ResolveSignature(event) diff --git a/pkg/security/serializers/serializers_linux_easyjson.go b/pkg/security/serializers/serializers_linux_easyjson.go index 93fe6bddfde4..e17559e5aa85 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.go +++ b/pkg/security/serializers/serializers_linux_easyjson.go @@ -428,7 +428,395 @@ func (v UserContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { func (v *UserContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(in *jlexer.Lexer, out *SyscallContextSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(in *jlexer.Lexer, out *TracerSerializer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "trace": + if in.IsNull() { + in.Skip() + out.Trace = nil + } else { + if out.Trace == nil { + out.Trace = new(TraceSerializer) + } + if in.IsNull() { + in.Skip() + } else { + (*out.Trace).UnmarshalEasyJSON(in) + } + } + case "metadata": + if in.IsNull() { + in.Skip() + out.Metadata = nil + } else { + if out.Metadata == nil { + out.Metadata = new(model.TracerMetadata) + } + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in, out.Metadata) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(out *jwriter.Writer, in TracerSerializer) { + out.RawByte('{') + first := true + _ = first + if in.Trace != nil { + const prefix string = ",\"trace\":" + first = false + out.RawString(prefix[1:]) + (*in.Trace).MarshalEasyJSON(out) + } + if in.Metadata != nil { + const prefix string = ",\"metadata\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out, *in.Metadata) + } + out.RawByte('}') +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v TracerSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *TracerSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(l, v) +} +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in *jlexer.Lexer, out *model.TracerMetadata) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "schema_version": + if in.IsNull() { + in.Skip() + } else { + out.SchemaVersion = uint8(in.Uint8()) + } + case "runtime_id": + if in.IsNull() { + in.Skip() + } else { + out.RuntimeID = string(in.String()) + } + case "tracer_language": + if in.IsNull() { + in.Skip() + } else { + out.TracerLanguage = string(in.String()) + } + case "tracer_version": + if in.IsNull() { + in.Skip() + } else { + out.TracerVersion = string(in.String()) + } + case "hostname": + if in.IsNull() { + in.Skip() + } else { + out.Hostname = string(in.String()) + } + case "service_name": + if in.IsNull() { + in.Skip() + } else { + out.ServiceName = string(in.String()) + } + case "service_env": + if in.IsNull() { + in.Skip() + } else { + out.ServiceEnv = string(in.String()) + } + case "service_version": + if in.IsNull() { + in.Skip() + } else { + out.ServiceVersion = string(in.String()) + } + case "process_tags": + if in.IsNull() { + in.Skip() + } else { + out.ProcessTags = string(in.String()) + } + case "container_id": + if in.IsNull() { + in.Skip() + } else { + out.ContainerID = string(in.String()) + } + case "logs_collected": + if in.IsNull() { + in.Skip() + } 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 v9 string + if in.IsNull() { + in.Skip() + } else { + v9 = string(in.String()) + } + out.ThreadlocalAttributeKeys = append(out.ThreadlocalAttributeKeys, v9) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out *jwriter.Writer, in model.TracerMetadata) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"schema_version\":" + out.RawString(prefix[1:]) + out.Uint8(uint8(in.SchemaVersion)) + } + if in.RuntimeID != "" { + const prefix string = ",\"runtime_id\":" + out.RawString(prefix) + out.String(string(in.RuntimeID)) + } + { + const prefix string = ",\"tracer_language\":" + out.RawString(prefix) + out.String(string(in.TracerLanguage)) + } + { + const prefix string = ",\"tracer_version\":" + out.RawString(prefix) + out.String(string(in.TracerVersion)) + } + { + const prefix string = ",\"hostname\":" + out.RawString(prefix) + out.String(string(in.Hostname)) + } + if in.ServiceName != "" { + const prefix string = ",\"service_name\":" + out.RawString(prefix) + out.String(string(in.ServiceName)) + } + if in.ServiceEnv != "" { + const prefix string = ",\"service_env\":" + out.RawString(prefix) + out.String(string(in.ServiceEnv)) + } + if in.ServiceVersion != "" { + const prefix string = ",\"service_version\":" + out.RawString(prefix) + out.String(string(in.ServiceVersion)) + } + if in.ProcessTags != "" { + const prefix string = ",\"process_tags\":" + out.RawString(prefix) + out.String(string(in.ProcessTags)) + } + if in.ContainerID != "" { + const prefix string = ",\"container_id\":" + out.RawString(prefix) + out.String(string(in.ContainerID)) + } + if in.LogsCollected { + const prefix string = ",\"logs_collected\":" + 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 v10, v11 := range in.ThreadlocalAttributeKeys { + if v10 > 0 { + out.RawByte(',') + } + out.String(string(v11)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(in *jlexer.Lexer, out *TraceSerializer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "span_id": + if in.IsNull() { + in.Skip() + } else { + out.SpanID = string(in.String()) + } + case "trace_id": + if in.IsNull() { + in.Skip() + } 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 v12 string + if in.IsNull() { + in.Skip() + } else { + v12 = string(in.String()) + } + (out.Attributes)[key] = v12 + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(out *jwriter.Writer, in TraceSerializer) { + out.RawByte('{') + first := true + _ = first + if in.SpanID != "" { + const prefix string = ",\"span_id\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.SpanID)) + } + if in.TraceID != "" { + const prefix string = ",\"trace_id\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + 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('{') + v13First := true + for v13Name, v13Value := range in.Attributes { + if v13First { + v13First = false + } else { + out.RawByte(',') + } + out.String(string(v13Name)) + out.RawByte(':') + out.String(string(v13Value)) + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v TraceSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *TraceSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(l, v) +} +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(in *jlexer.Lexer, out *SyscallContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -648,7 +1036,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(out *jwriter.Writer, in SyscallContextSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(out *jwriter.Writer, in SyscallContextSerializer) { out.RawByte('{') first := true _ = first @@ -793,14 +1181,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SyscallContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SyscallContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(in *jlexer.Lexer, out *SyscallArgsSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(in *jlexer.Lexer, out *SyscallArgsSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -936,7 +1324,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(out *jwriter.Writer, in SyscallArgsSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(out *jwriter.Writer, in SyscallArgsSerializer) { out.RawByte('{') first := true _ = first @@ -1021,14 +1409,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SyscallArgsSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SyscallArgsSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers3(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(in *jlexer.Lexer, out *SpliceEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(in *jlexer.Lexer, out *SpliceEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1064,7 +1452,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(out *jwriter.Writer, in SpliceEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(out *jwriter.Writer, in SpliceEventSerializer) { out.RawByte('{') first := true _ = first @@ -1083,14 +1471,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SpliceEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SpliceEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(in *jlexer.Lexer, out *SocketEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(in *jlexer.Lexer, out *SocketEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1132,7 +1520,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(out *jwriter.Writer, in SocketEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(out *jwriter.Writer, in SocketEventSerializer) { out.RawByte('{') first := true _ = first @@ -1156,14 +1544,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SocketEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SocketEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(in *jlexer.Lexer, out *SignalEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(in *jlexer.Lexer, out *SignalEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1213,7 +1601,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(out *jwriter.Writer, in SignalEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(out *jwriter.Writer, in SignalEventSerializer) { out.RawByte('{') first := true _ = first @@ -1237,14 +1625,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SignalEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SignalEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(in *jlexer.Lexer, out *SetuidSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(in *jlexer.Lexer, out *SetuidSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1304,7 +1692,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(out *jwriter.Writer, in SetuidSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(out *jwriter.Writer, in SetuidSerializer) { out.RawByte('{') first := true _ = first @@ -1343,14 +1731,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SetuidSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SetuidSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(in *jlexer.Lexer, out *SetrlimitEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(in *jlexer.Lexer, out *SetrlimitEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1406,7 +1794,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(out *jwriter.Writer, in SetrlimitEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(out *jwriter.Writer, in SetrlimitEventSerializer) { out.RawByte('{') first := true _ = first @@ -1435,14 +1823,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SetrlimitEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SetrlimitEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(in *jlexer.Lexer, out *SetgidSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in *jlexer.Lexer, out *SetgidSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1502,7 +1890,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(i in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(out *jwriter.Writer, in SetgidSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out *jwriter.Writer, in SetgidSerializer) { out.RawByte('{') first := true _ = first @@ -1541,14 +1929,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v SetgidSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SetgidSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(in *jlexer.Lexer, out *SetSockOptEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(in *jlexer.Lexer, out *SetSockOptEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1626,7 +2014,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(out *jwriter.Writer, in SetSockOptEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(out *jwriter.Writer, in SetSockOptEventSerializer) { out.RawByte('{') first := true _ = first @@ -1680,14 +2068,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SetSockOptEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SetSockOptEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in *jlexer.Lexer, out *SecurityProfileContextSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(in *jlexer.Lexer, out *SecurityProfileContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1729,13 +2117,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v9 string + var v14 string if in.IsNull() { in.Skip() } else { - v9 = string(in.String()) + v14 = string(in.String()) } - out.Tags = append(out.Tags, v9) + out.Tags = append(out.Tags, v14) in.WantComma() } in.Delim(']') @@ -1762,7 +2150,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out *jwriter.Writer, in SecurityProfileContextSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(out *jwriter.Writer, in SecurityProfileContextSerializer) { out.RawByte('{') first := true _ = first @@ -1783,11 +2171,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( out.RawString("null") } else { out.RawByte('[') - for v10, v11 := range in.Tags { - if v10 > 0 { + for v15, v16 := range in.Tags { + if v15 > 0 { out.RawByte(',') } - out.String(string(v11)) + out.String(string(v16)) } out.RawByte(']') } @@ -1807,14 +2195,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SecurityProfileContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SecurityProfileContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(in *jlexer.Lexer, out *SSHSessionContextSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(in *jlexer.Lexer, out *SSHSessionContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1868,7 +2256,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(out *jwriter.Writer, in SSHSessionContextSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(out *jwriter.Writer, in SSHSessionContextSerializer) { out.RawByte('{') first := true _ = first @@ -1923,14 +2311,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SSHSessionContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SSHSessionContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(in *jlexer.Lexer, out *SELinuxEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(in *jlexer.Lexer, out *SELinuxEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1996,7 +2384,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(out *jwriter.Writer, in SELinuxEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(out *jwriter.Writer, in SELinuxEventSerializer) { out.RawByte('{') first := true _ = first @@ -2031,14 +2419,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SELinuxEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SELinuxEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(in *jlexer.Lexer, out *SELinuxEnforceStatusSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(in *jlexer.Lexer, out *SELinuxEnforceStatusSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2068,7 +2456,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(out *jwriter.Writer, in SELinuxEnforceStatusSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(out *jwriter.Writer, in SELinuxEnforceStatusSerializer) { out.RawByte('{') first := true _ = first @@ -2083,14 +2471,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SELinuxEnforceStatusSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SELinuxEnforceStatusSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(in *jlexer.Lexer, out *SELinuxBoolCommitSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(in *jlexer.Lexer, out *SELinuxBoolCommitSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2120,7 +2508,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(out *jwriter.Writer, in SELinuxBoolCommitSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(out *jwriter.Writer, in SELinuxBoolCommitSerializer) { out.RawByte('{') first := true _ = first @@ -2135,14 +2523,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SELinuxBoolCommitSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SELinuxBoolCommitSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(in *jlexer.Lexer, out *SELinuxBoolChangeSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in *jlexer.Lexer, out *SELinuxBoolChangeSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2178,7 +2566,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(out *jwriter.Writer, in SELinuxBoolChangeSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out *jwriter.Writer, in SELinuxBoolChangeSerializer) { out.RawByte('{') first := true _ = first @@ -2203,14 +2591,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16( // MarshalEasyJSON supports easyjson.Marshaler interface func (v SELinuxBoolChangeSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SELinuxBoolChangeSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(in *jlexer.Lexer, out *ProcessSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(in *jlexer.Lexer, out *ProcessSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2382,13 +2770,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.CapsAttempted = (out.CapsAttempted)[:0] } for !in.IsDelim(']') { - var v12 string + var v17 string if in.IsNull() { in.Skip() } else { - v12 = string(in.String()) + v17 = string(in.String()) } - out.CapsAttempted = append(out.CapsAttempted, v12) + out.CapsAttempted = append(out.CapsAttempted, v17) in.WantComma() } in.Delim(']') @@ -2409,13 +2797,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.CapsUsed = (out.CapsUsed)[:0] } for !in.IsDelim(']') { - var v13 string + var v18 string if in.IsNull() { in.Skip() } else { - v13 = string(in.String()) + v18 = string(in.String()) } - out.CapsUsed = append(out.CapsUsed, v13) + out.CapsUsed = append(out.CapsUsed, v18) in.WantComma() } in.Delim(']') @@ -2512,13 +2900,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.Args = (out.Args)[:0] } for !in.IsDelim(']') { - var v14 string + var v19 string if in.IsNull() { in.Skip() } else { - v14 = string(in.String()) + v19 = string(in.String()) } - out.Args = append(out.Args, v14) + out.Args = append(out.Args, v19) in.WantComma() } in.Delim(']') @@ -2545,13 +2933,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.Envs = (out.Envs)[:0] } for !in.IsDelim(']') { - var v15 string + var v20 string if in.IsNull() { in.Skip() } else { - v15 = string(in.String()) + v20 = string(in.String()) } - out.Envs = append(out.Envs, v15) + out.Envs = append(out.Envs, v20) in.WantComma() } in.Delim(']') @@ -2621,9 +3009,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( *out.Syscalls = (*out.Syscalls)[:0] } for !in.IsDelim(']') { - var v16 SyscallSerializer - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in, &v16) - *out.Syscalls = append(*out.Syscalls, v16) + var v21 SyscallSerializer + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(in, &v21) + *out.Syscalls = append(*out.Syscalls, v21) in.WantComma() } in.Delim(']') @@ -2645,21 +3033,21 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.AWSSecurityCredentials = (out.AWSSecurityCredentials)[:0] } for !in.IsDelim(']') { - var v17 *AWSSecurityCredentialsSerializer + var v22 *AWSSecurityCredentialsSerializer if in.IsNull() { in.Skip() - v17 = nil + v22 = nil } else { - if v17 == nil { - v17 = new(AWSSecurityCredentialsSerializer) + if v22 == nil { + v22 = new(AWSSecurityCredentialsSerializer) } if in.IsNull() { in.Skip() } else { - (*v17).UnmarshalEasyJSON(in) + (*v22).UnmarshalEasyJSON(in) } } - out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v17) + out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v22) in.WantComma() } in.Delim(']') @@ -2670,22 +3058,12 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.Tracer = nil } else { if out.Tracer == nil { - out.Tracer = new(model.TracerMetadata) - } - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in, out.Tracer) - } - case "span_context": - if in.IsNull() { - in.Skip() - out.SpanContext = nil - } else { - if out.SpanContext == nil { - out.SpanContext = new(DDContextSerializer) + out.Tracer = new(TracerSerializer) } if in.IsNull() { in.Skip() } else { - (*out.SpanContext).UnmarshalEasyJSON(in) + (*out.Tracer).UnmarshalEasyJSON(in) } } case "variables": @@ -2704,7 +3082,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(out *jwriter.Writer, in ProcessSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(out *jwriter.Writer, in ProcessSerializer) { out.RawByte('{') first := true _ = first @@ -2804,11 +3182,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString(prefix) { out.RawByte('[') - for v18, v19 := range in.CapsAttempted { - if v18 > 0 { + for v23, v24 := range in.CapsAttempted { + if v23 > 0 { out.RawByte(',') } - out.String(string(v19)) + out.String(string(v24)) } out.RawByte(']') } @@ -2818,11 +3196,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString(prefix) { out.RawByte('[') - for v20, v21 := range in.CapsUsed { - if v20 > 0 { + for v25, v26 := range in.CapsUsed { + if v25 > 0 { out.RawByte(',') } - out.String(string(v21)) + out.String(string(v26)) } out.RawByte(']') } @@ -2862,11 +3240,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString(prefix) { out.RawByte('[') - for v22, v23 := range in.Args { - if v22 > 0 { + for v27, v28 := range in.Args { + if v27 > 0 { out.RawByte(',') } - out.String(string(v23)) + out.String(string(v28)) } out.RawByte(']') } @@ -2881,11 +3259,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString(prefix) { out.RawByte('[') - for v24, v25 := range in.Envs { - if v24 > 0 { + for v29, v30 := range in.Envs { + if v29 > 0 { out.RawByte(',') } - out.String(string(v25)) + out.String(string(v30)) } out.RawByte(']') } @@ -2932,253 +3310,56 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17( out.RawString("null") } else { out.RawByte('[') - for v26, v27 := range *in.Syscalls { - if v26 > 0 { - out.RawByte(',') - } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out, v27) - } - out.RawByte(']') - } - } - if len(in.AWSSecurityCredentials) != 0 { - const prefix string = ",\"aws_security_credentials\":" - out.RawString(prefix) - { - out.RawByte('[') - for v28, v29 := range in.AWSSecurityCredentials { - if v28 > 0 { - out.RawByte(',') - } - if v29 == nil { - out.RawString("null") - } else { - (*v29).MarshalEasyJSON(out) - } - } - out.RawByte(']') - } - } - if in.Tracer != nil { - const prefix string = ",\"tracer\":" - out.RawString(prefix) - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out, *in.Tracer) - } - if in.SpanContext != nil { - const prefix string = ",\"span_context\":" - out.RawString(prefix) - (*in.SpanContext).MarshalEasyJSON(out) - } - if len(in.Variables) != 0 { - const prefix string = ",\"variables\":" - out.RawString(prefix) - (in.Variables).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v ProcessSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(w, v) -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *ProcessSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers17(l, v) -} -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(in *jlexer.Lexer, out *model.TracerMetadata) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "schema_version": - if in.IsNull() { - in.Skip() - } else { - out.SchemaVersion = uint8(in.Uint8()) - } - case "runtime_id": - if in.IsNull() { - in.Skip() - } else { - out.RuntimeID = string(in.String()) - } - case "tracer_language": - if in.IsNull() { - in.Skip() - } else { - out.TracerLanguage = string(in.String()) - } - case "tracer_version": - if in.IsNull() { - in.Skip() - } else { - out.TracerVersion = string(in.String()) - } - case "hostname": - if in.IsNull() { - in.Skip() - } else { - out.Hostname = string(in.String()) - } - case "service_name": - if in.IsNull() { - in.Skip() - } else { - out.ServiceName = string(in.String()) - } - case "service_env": - if in.IsNull() { - in.Skip() - } else { - out.ServiceEnv = string(in.String()) - } - case "service_version": - if in.IsNull() { - in.Skip() - } else { - out.ServiceVersion = string(in.String()) - } - case "process_tags": - if in.IsNull() { - in.Skip() - } else { - out.ProcessTags = string(in.String()) - } - case "container_id": - if in.IsNull() { - in.Skip() - } else { - out.ContainerID = string(in.String()) - } - case "logs_collected": - if in.IsNull() { - in.Skip() - } 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() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgDiscoveryTracermetadataModel(out *jwriter.Writer, in model.TracerMetadata) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"schema_version\":" - out.RawString(prefix[1:]) - out.Uint8(uint8(in.SchemaVersion)) - } - if in.RuntimeID != "" { - const prefix string = ",\"runtime_id\":" - out.RawString(prefix) - out.String(string(in.RuntimeID)) - } - { - const prefix string = ",\"tracer_language\":" - out.RawString(prefix) - out.String(string(in.TracerLanguage)) - } - { - const prefix string = ",\"tracer_version\":" - out.RawString(prefix) - out.String(string(in.TracerVersion)) - } - { - const prefix string = ",\"hostname\":" - out.RawString(prefix) - out.String(string(in.Hostname)) - } - if in.ServiceName != "" { - const prefix string = ",\"service_name\":" - out.RawString(prefix) - out.String(string(in.ServiceName)) - } - if in.ServiceEnv != "" { - const prefix string = ",\"service_env\":" - out.RawString(prefix) - out.String(string(in.ServiceEnv)) - } - if in.ServiceVersion != "" { - const prefix string = ",\"service_version\":" - out.RawString(prefix) - out.String(string(in.ServiceVersion)) - } - if in.ProcessTags != "" { - const prefix string = ",\"process_tags\":" - out.RawString(prefix) - out.String(string(in.ProcessTags)) - } - if in.ContainerID != "" { - const prefix string = ",\"container_id\":" - out.RawString(prefix) - out.String(string(in.ContainerID)) - } - if in.LogsCollected { - const prefix string = ",\"logs_collected\":" - out.RawString(prefix) - out.Bool(bool(in.LogsCollected)) + for v31, v32 := range *in.Syscalls { + if v31 > 0 { + out.RawByte(',') + } + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(out, v32) + } + out.RawByte(']') + } } - if len(in.ThreadlocalAttributeKeys) != 0 { - const prefix string = ",\"threadlocal_attribute_keys\":" + if len(in.AWSSecurityCredentials) != 0 { + const prefix string = ",\"aws_security_credentials\":" out.RawString(prefix) { out.RawByte('[') - for v31, v32 := range in.ThreadlocalAttributeKeys { - if v31 > 0 { + for v33, v34 := range in.AWSSecurityCredentials { + if v33 > 0 { out.RawByte(',') } - out.String(string(v32)) + if v34 == nil { + out.RawString("null") + } else { + (*v34).MarshalEasyJSON(out) + } } out.RawByte(']') } } + if in.Tracer != nil { + const prefix string = ",\"tracer\":" + out.RawString(prefix) + (*in.Tracer).MarshalEasyJSON(out) + } + if len(in.Variables) != 0 { + const prefix string = ",\"variables\":" + out.RawString(prefix) + (in.Variables).MarshalEasyJSON(out) + } out.RawByte('}') } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in *jlexer.Lexer, out *SyscallSerializer) { + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ProcessSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ProcessSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(l, v) +} +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(in *jlexer.Lexer, out *SyscallSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3214,7 +3395,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out *jwriter.Writer, in SyscallSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(out *jwriter.Writer, in SyscallSerializer) { out.RawByte('{') first := true _ = first @@ -3230,7 +3411,7 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18( } out.RawByte('}') } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(in *jlexer.Lexer, out *ProcessCredentialsSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(in *jlexer.Lexer, out *ProcessCredentialsSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3347,13 +3528,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v33 string + var v35 string if in.IsNull() { in.Skip() } else { - v33 = string(in.String()) + v35 = string(in.String()) } - out.CapEffective = append(out.CapEffective, v33) + out.CapEffective = append(out.CapEffective, v35) in.WantComma() } in.Delim(']') @@ -3374,13 +3555,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v34 string + var v36 string if in.IsNull() { in.Skip() } else { - v34 = string(in.String()) + v36 = string(in.String()) } - out.CapPermitted = append(out.CapPermitted, v34) + out.CapPermitted = append(out.CapPermitted, v36) in.WantComma() } in.Delim(']') @@ -3395,7 +3576,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(out *jwriter.Writer, in ProcessCredentialsSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(out *jwriter.Writer, in ProcessCredentialsSerializer) { out.RawByte('{') first := true _ = first @@ -3488,11 +3669,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.RawString("null") } else { out.RawByte('[') - for v35, v36 := range in.CapEffective { - if v35 > 0 { + for v37, v38 := range in.CapEffective { + if v37 > 0 { out.RawByte(',') } - out.String(string(v36)) + out.String(string(v38)) } out.RawByte(']') } @@ -3504,11 +3685,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.RawString("null") } else { out.RawByte('[') - for v37, v38 := range in.CapPermitted { - if v37 > 0 { + for v39, v40 := range in.CapPermitted { + if v39 > 0 { out.RawByte(',') } - out.String(string(v38)) + out.String(string(v40)) } out.RawByte(']') } @@ -3518,14 +3699,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( // MarshalEasyJSON supports easyjson.Marshaler interface func (v ProcessCredentialsSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ProcessCredentialsSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(in *jlexer.Lexer, out *PrCtlEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(in *jlexer.Lexer, out *PrCtlEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3567,7 +3748,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(out *jwriter.Writer, in PrCtlEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(out *jwriter.Writer, in PrCtlEventSerializer) { out.RawByte('{') first := true _ = first @@ -3591,14 +3772,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( // MarshalEasyJSON supports easyjson.Marshaler interface func (v PrCtlEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *PrCtlEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(in *jlexer.Lexer, out *PTraceEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(in *jlexer.Lexer, out *PTraceEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3648,7 +3829,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(out *jwriter.Writer, in PTraceEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(out *jwriter.Writer, in PTraceEventSerializer) { out.RawByte('{') first := true _ = first @@ -3672,14 +3853,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( // MarshalEasyJSON supports easyjson.Marshaler interface func (v PTraceEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *PTraceEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(in *jlexer.Lexer, out *NetworkDeviceSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(in *jlexer.Lexer, out *NetworkDeviceSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3721,7 +3902,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(out *jwriter.Writer, in NetworkDeviceSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(out *jwriter.Writer, in NetworkDeviceSerializer) { out.RawByte('{') first := true _ = first @@ -3745,14 +3926,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22( // MarshalEasyJSON supports easyjson.Marshaler interface func (v NetworkDeviceSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *NetworkDeviceSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(in *jlexer.Lexer, out *MountEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(in *jlexer.Lexer, out *MountEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3870,7 +4051,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(out *jwriter.Writer, in MountEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(out *jwriter.Writer, in MountEventSerializer) { out.RawByte('{') first := true _ = first @@ -3955,14 +4136,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( // MarshalEasyJSON supports easyjson.Marshaler interface func (v MountEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MountEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(in *jlexer.Lexer, out *ModuleEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(in *jlexer.Lexer, out *ModuleEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4012,13 +4193,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.Argv = (out.Argv)[:0] } for !in.IsDelim(']') { - var v39 string + var v41 string if in.IsNull() { in.Skip() } else { - v39 = string(in.String()) + v41 = string(in.String()) } - out.Argv = append(out.Argv, v39) + out.Argv = append(out.Argv, v41) in.WantComma() } in.Delim(']') @@ -4047,7 +4228,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(out *jwriter.Writer, in ModuleEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(out *jwriter.Writer, in ModuleEventSerializer) { out.RawByte('{') first := true _ = first @@ -4066,11 +4247,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.RawString(prefix) { out.RawByte('[') - for v40, v41 := range in.Argv { - if v40 > 0 { + for v42, v43 := range in.Argv { + if v42 > 0 { out.RawByte(',') } - out.String(string(v41)) + out.String(string(v43)) } out.RawByte(']') } @@ -4085,14 +4266,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( // MarshalEasyJSON supports easyjson.Marshaler interface func (v ModuleEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ModuleEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(in *jlexer.Lexer, out *MProtectEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(in *jlexer.Lexer, out *MProtectEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4140,7 +4321,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers25( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(out *jwriter.Writer, in MProtectEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(out *jwriter.Writer, in MProtectEventSerializer) { out.RawByte('{') first := true _ = first @@ -4169,14 +4350,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers25( // MarshalEasyJSON supports easyjson.Marshaler interface func (v MProtectEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MProtectEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers25(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(in *jlexer.Lexer, out *MMapEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(in *jlexer.Lexer, out *MMapEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4230,7 +4411,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(out *jwriter.Writer, in MMapEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(out *jwriter.Writer, in MMapEventSerializer) { out.RawByte('{') first := true _ = first @@ -4264,14 +4445,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( // MarshalEasyJSON supports easyjson.Marshaler interface func (v MMapEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MMapEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(in *jlexer.Lexer, out *K8SSessionContextSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(in *jlexer.Lexer, out *K8SSessionContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4319,13 +4500,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( out.K8SGroups = (out.K8SGroups)[:0] } for !in.IsDelim(']') { - var v42 string + var v44 string if in.IsNull() { in.Skip() } else { - v42 = string(in.String()) + v44 = string(in.String()) } - out.K8SGroups = append(out.K8SGroups, v42) + out.K8SGroups = append(out.K8SGroups, v44) in.WantComma() } in.Delim(']') @@ -4343,34 +4524,34 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v43 []string + var v45 []string if in.IsNull() { in.Skip() - v43 = nil + v45 = nil } else { in.Delim('[') - if v43 == nil { + if v45 == nil { if !in.IsDelim(']') { - v43 = make([]string, 0, 4) + v45 = make([]string, 0, 4) } else { - v43 = []string{} + v45 = []string{} } } else { - v43 = (v43)[:0] + v45 = (v45)[:0] } for !in.IsDelim(']') { - var v44 string + var v46 string if in.IsNull() { in.Skip() } else { - v44 = string(in.String()) + v46 = string(in.String()) } - v43 = append(v43, v44) + v45 = append(v45, v46) in.WantComma() } in.Delim(']') } - (out.K8SExtra)[key] = v43 + (out.K8SExtra)[key] = v45 in.WantComma() } in.Delim('}') @@ -4385,7 +4566,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(out *jwriter.Writer, in K8SSessionContextSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(out *jwriter.Writer, in K8SSessionContextSerializer) { out.RawByte('{') first := true _ = first @@ -4425,11 +4606,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( } { out.RawByte('[') - for v45, v46 := range in.K8SGroups { - if v45 > 0 { + for v47, v48 := range in.K8SGroups { + if v47 > 0 { out.RawByte(',') } - out.String(string(v46)) + out.String(string(v48)) } out.RawByte(']') } @@ -4444,24 +4625,24 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( } { out.RawByte('{') - v47First := true - for v47Name, v47Value := range in.K8SExtra { - if v47First { - v47First = false + v49First := true + for v49Name, v49Value := range in.K8SExtra { + if v49First { + v49First = false } else { out.RawByte(',') } - out.String(string(v47Name)) + out.String(string(v49Name)) out.RawByte(':') - if v47Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + if v49Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { out.RawString("null") } else { out.RawByte('[') - for v48, v49 := range v47Value { - if v48 > 0 { + for v50, v51 := range v49Value { + if v50 > 0 { out.RawByte(',') } - out.String(string(v49)) + out.String(string(v51)) } out.RawByte(']') } @@ -4474,14 +4655,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27( // MarshalEasyJSON supports easyjson.Marshaler interface func (v K8SSessionContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *K8SSessionContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers27(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(in *jlexer.Lexer, out *FileSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(in *jlexer.Lexer, out *FileSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4633,13 +4814,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v50 string + var v52 string if in.IsNull() { in.Skip() } else { - v50 = string(in.String()) + v52 = string(in.String()) } - out.Flags = append(out.Flags, v50) + out.Flags = append(out.Flags, v52) in.WantComma() } in.Delim(']') @@ -4750,13 +4931,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v51 string + var v53 string if in.IsNull() { in.Skip() } else { - v51 = string(in.String()) + v53 = string(in.String()) } - out.Hashes = append(out.Hashes, v51) + out.Hashes = append(out.Hashes, v53) in.WantComma() } in.Delim(']') @@ -4837,7 +5018,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(out *jwriter.Writer, in FileSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(out *jwriter.Writer, in FileSerializer) { out.RawByte('{') first := true _ = first @@ -4967,11 +5148,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( out.RawString(prefix) { out.RawByte('[') - for v52, v53 := range in.Flags { - if v52 > 0 { + for v54, v55 := range in.Flags { + if v54 > 0 { out.RawByte(',') } - out.String(string(v53)) + out.String(string(v55)) } out.RawByte(']') } @@ -5031,11 +5212,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( out.RawString(prefix) { out.RawByte('[') - for v54, v55 := range in.Hashes { - if v54 > 0 { + for v56, v57 := range in.Hashes { + if v56 > 0 { out.RawByte(',') } - out.String(string(v55)) + out.String(string(v57)) } out.RawByte(']') } @@ -5080,14 +5261,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28( // MarshalEasyJSON supports easyjson.Marshaler interface func (v FileSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FileSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers28(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(in *jlexer.Lexer, out *FileMetadataSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(in *jlexer.Lexer, out *FileMetadataSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5159,7 +5340,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(out *jwriter.Writer, in FileMetadataSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(out *jwriter.Writer, in FileMetadataSerializer) { out.RawByte('{') first := true _ = first @@ -5244,14 +5425,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29( // MarshalEasyJSON supports easyjson.Marshaler interface func (v FileMetadataSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FileMetadataSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers29(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(in *jlexer.Lexer, out *FileEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(in *jlexer.Lexer, out *FileEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5435,13 +5616,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v56 string + var v58 string if in.IsNull() { in.Skip() } else { - v56 = string(in.String()) + v58 = string(in.String()) } - out.Flags = append(out.Flags, v56) + out.Flags = append(out.Flags, v58) in.WantComma() } in.Delim(']') @@ -5552,13 +5733,13 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v57 string + var v59 string if in.IsNull() { in.Skip() } else { - v57 = string(in.String()) + v59 = string(in.String()) } - out.Hashes = append(out.Hashes, v57) + out.Hashes = append(out.Hashes, v59) in.WantComma() } in.Delim(']') @@ -5639,7 +5820,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(out *jwriter.Writer, in FileEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(out *jwriter.Writer, in FileEventSerializer) { out.RawByte('{') first := true _ = first @@ -5809,11 +5990,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( out.RawString(prefix) { out.RawByte('[') - for v58, v59 := range in.Flags { - if v58 > 0 { + for v60, v61 := range in.Flags { + if v60 > 0 { out.RawByte(',') } - out.String(string(v59)) + out.String(string(v61)) } out.RawByte(']') } @@ -5873,11 +6054,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( out.RawString(prefix) { out.RawByte('[') - for v60, v61 := range in.Hashes { - if v60 > 0 { + for v62, v63 := range in.Hashes { + if v62 > 0 { out.RawByte(',') } - out.String(string(v61)) + out.String(string(v63)) } out.RawByte(']') } @@ -5922,14 +6103,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30( // MarshalEasyJSON supports easyjson.Marshaler interface func (v FileEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FileEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers30(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(in *jlexer.Lexer, out *EventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(in *jlexer.Lexer, out *EventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5940,7 +6121,6 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( } out.BaseEventSerializer = new(BaseEventSerializer) out.NetworkContextSerializer = new(NetworkContextSerializer) - out.DDContextSerializer = new(DDContextSerializer) out.SecurityProfileContextSerializer = new(SecurityProfileContextSerializer) out.CGroupContextSerializer = new(CGroupContextSerializer) out.SELinuxEventSerializer = new(SELinuxEventSerializer) @@ -5997,15 +6177,29 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( case "dd": if in.IsNull() { in.Skip() - out.DDContextSerializer = nil + out.DD = nil + } else { + if out.DD == nil { + out.DD = new(TraceSerializer) + } + if in.IsNull() { + in.Skip() + } else { + (*out.DD).UnmarshalEasyJSON(in) + } + } + case "trace": + if in.IsNull() { + in.Skip() + out.Trace = nil } else { - if out.DDContextSerializer == nil { - out.DDContextSerializer = new(DDContextSerializer) + if out.Trace == nil { + out.Trace = new(TraceSerializer) } if in.IsNull() { in.Skip() } else { - (*out.DDContextSerializer).UnmarshalEasyJSON(in) + (*out.Trace).UnmarshalEasyJSON(in) } } case "security_profile": @@ -6255,9 +6449,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( *out.SyscallsEventSerializer = (*out.SyscallsEventSerializer)[:0] } for !in.IsDelim(']') { - var v62 SyscallSerializer - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(in, &v62) - *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v62) + var v64 SyscallSerializer + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(in, &v64) + *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v64) in.WantComma() } in.Delim(']') @@ -6497,7 +6691,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(out *jwriter.Writer, in EventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(out *jwriter.Writer, in EventSerializer) { out.RawByte('{') first := true _ = first @@ -6517,7 +6711,7 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( } (*in.NetworkContextSerializer).MarshalEasyJSON(out) } - if in.DDContextSerializer != nil { + if in.DD != nil { const prefix string = ",\"dd\":" if first { first = false @@ -6525,7 +6719,17 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( } else { out.RawString(prefix) } - (*in.DDContextSerializer).MarshalEasyJSON(out) + (*in.DD).MarshalEasyJSON(out) + } + if in.Trace != nil { + const prefix string = ",\"trace\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + (*in.Trace).MarshalEasyJSON(out) } if in.SecurityProfileContextSerializer != nil { const prefix string = ",\"security_profile\":" @@ -6699,11 +6903,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( out.RawString("null") } else { out.RawByte('[') - for v63, v64 := range *in.SyscallsEventSerializer { - if v63 > 0 { + for v65, v66 := range *in.SyscallsEventSerializer { + if v65 > 0 { out.RawByte(',') } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers18(out, v64) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20(out, v66) } out.RawByte(']') } @@ -6883,130 +7087,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31( // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers31(l, v) -} -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(in *jlexer.Lexer, out *DDContextSerializer) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "span_id": - if in.IsNull() { - in.Skip() - } else { - out.SpanID = string(in.String()) - } - case "trace_id": - if in.IsNull() { - in.Skip() - } 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() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(out *jwriter.Writer, in DDContextSerializer) { - out.RawByte('{') - first := true - _ = first - if in.SpanID != "" { - const prefix string = ",\"span_id\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.SpanID)) - } - if in.TraceID != "" { - const prefix string = ",\"trace_id\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - 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('}') -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v DDContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(w, v) -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *DDContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers32(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(in *jlexer.Lexer, out *CredentialsSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(in *jlexer.Lexer, out *CredentialsSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7162,7 +7250,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(out *jwriter.Writer, in CredentialsSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(out *jwriter.Writer, in CredentialsSerializer) { out.RawByte('{') first := true _ = first @@ -7268,14 +7356,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33( // MarshalEasyJSON supports easyjson.Marshaler interface func (v CredentialsSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CredentialsSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers33(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(in *jlexer.Lexer, out *ConnectEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(in *jlexer.Lexer, out *ConnectEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7338,7 +7426,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(out *jwriter.Writer, in ConnectEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(out *jwriter.Writer, in ConnectEventSerializer) { out.RawByte('{') first := true _ = first @@ -7373,14 +7461,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34( // MarshalEasyJSON supports easyjson.Marshaler interface func (v ConnectEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ConnectEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers34(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(in *jlexer.Lexer, out *CapsetSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(in *jlexer.Lexer, out *CapsetSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7458,7 +7546,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(out *jwriter.Writer, in CapsetSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(out *jwriter.Writer, in CapsetSerializer) { out.RawByte('{') first := true _ = first @@ -7499,14 +7587,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35( // MarshalEasyJSON supports easyjson.Marshaler interface func (v CapsetSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CapsetSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers35(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(in *jlexer.Lexer, out *CapabilitiesEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(in *jlexer.Lexer, out *CapabilitiesEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7584,7 +7672,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(out *jwriter.Writer, in CapabilitiesEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(out *jwriter.Writer, in CapabilitiesEventSerializer) { out.RawByte('{') first := true _ = first @@ -7627,14 +7715,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36( // MarshalEasyJSON supports easyjson.Marshaler interface func (v CapabilitiesEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CapabilitiesEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers36(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(in *jlexer.Lexer, out *CGroupWriteEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(in *jlexer.Lexer, out *CGroupWriteEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7678,7 +7766,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers37( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(out *jwriter.Writer, in CGroupWriteEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(out *jwriter.Writer, in CGroupWriteEventSerializer) { out.RawByte('{') first := true _ = first @@ -7703,14 +7791,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers37( // MarshalEasyJSON supports easyjson.Marshaler interface func (v CGroupWriteEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CGroupWriteEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers37(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(in *jlexer.Lexer, out *BindEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(in *jlexer.Lexer, out *BindEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7746,7 +7834,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(out *jwriter.Writer, in BindEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(out *jwriter.Writer, in BindEventSerializer) { out.RawByte('{') first := true _ = first @@ -7765,14 +7853,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38( // MarshalEasyJSON supports easyjson.Marshaler interface func (v BindEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *BindEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers38(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(in *jlexer.Lexer, out *BPFProgramSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(in *jlexer.Lexer, out *BPFProgramSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7847,7 +7935,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(out *jwriter.Writer, in BPFProgramSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(out *jwriter.Writer, in BPFProgramSerializer) { out.RawByte('{') first := true _ = first @@ -7911,14 +7999,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39( // MarshalEasyJSON supports easyjson.Marshaler interface func (v BPFProgramSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *BPFProgramSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers39(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(in *jlexer.Lexer, out *BPFMapSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(in *jlexer.Lexer, out *BPFMapSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -7954,7 +8042,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers40( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(out *jwriter.Writer, in BPFMapSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(out *jwriter.Writer, in BPFMapSerializer) { out.RawByte('{') first := true _ = first @@ -7979,14 +8067,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers40( // MarshalEasyJSON supports easyjson.Marshaler interface func (v BPFMapSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *BPFMapSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers40(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(in *jlexer.Lexer, out *BPFEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(in *jlexer.Lexer, out *BPFEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -8044,7 +8132,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers41( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(out *jwriter.Writer, in BPFEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(out *jwriter.Writer, in BPFEventSerializer) { out.RawByte('{') first := true _ = first @@ -8068,14 +8156,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers41( // MarshalEasyJSON supports easyjson.Marshaler interface func (v BPFEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *BPFEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers41(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(in *jlexer.Lexer, out *AnomalyDetectionSyscallEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(in *jlexer.Lexer, out *AnomalyDetectionSyscallEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -8105,7 +8193,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(out *jwriter.Writer, in AnomalyDetectionSyscallEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(out *jwriter.Writer, in AnomalyDetectionSyscallEventSerializer) { out.RawByte('{') first := true _ = first @@ -8119,14 +8207,14 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42( // MarshalEasyJSON supports easyjson.Marshaler interface func (v AnomalyDetectionSyscallEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AnomalyDetectionSyscallEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers42(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(l, v) } -func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(in *jlexer.Lexer, out *AcceptEventSerializer) { +func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers44(in *jlexer.Lexer, out *AcceptEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -8183,7 +8271,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43( in.Consumed() } } -func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(out *jwriter.Writer, in AcceptEventSerializer) { +func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers44(out *jwriter.Writer, in AcceptEventSerializer) { out.RawByte('{') first := true _ = first @@ -8213,10 +8301,10 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43( // MarshalEasyJSON supports easyjson.Marshaler interface func (v AcceptEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(w, v) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers44(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AcceptEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers43(l, v) + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers44(l, v) } diff --git a/pkg/security/serializers/serializers_linux_easyjson.mock b/pkg/security/serializers/serializers_linux_easyjson.mock index 839505acd187..760486fa02a9 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.mock +++ b/pkg/security/serializers/serializers_linux_easyjson.mock @@ -56,9 +56,17 @@ func (v UserSessionContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v DDContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { +func (v TraceSerializer) MarshalEasyJSON(w *jwriter.Writer) { } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *DDContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { +func (v *TraceSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v TracerSerializer) MarshalEasyJSON(w *jwriter.Writer) { +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *TracerSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { } diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 5472cca5d636..873ab0066076 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -38,8 +38,8 @@ func splitTraceID(decimalTraceID string) (hi, lo uint64, ok bool) { return hi, lo, true } -// spanContextJSON mirrors the shape of DDContextSerializer in JSON. -type spanContextJSON struct { +// traceJSON mirrors the shape of SpanContextSerializer in JSON. +type traceJSON struct { SpanID string `json:"span_id"` TraceID string `json:"trace_id"` Attributes map[string]string `json:"attributes"` @@ -63,22 +63,26 @@ type spanLocations struct { // assertSerializedSpanContext parses the marshalled event and asserts the // propagation wiring described by `loc`. Always asserts the top-level "dd" -// field (built by newDDContextSerializer); the rest is gated by loc. +// and "tracer" fields (built by newDDContextSerializer; both share the same +// underlying serializer in EventSerializer); the rest is gated by loc. // // expectedAttrs may be nil; when non-nil, each expected key must be present // on the asserted span_context.attributes with the expected value (subset // match — the helper does not assert absence of unexpected keys). func assertSerializedSpanContext(t *testing.T, jsonStr, expectedSpanID, expectedTraceID string, expectedAttrs map[string]string, loc spanLocations) { t.Helper() + // processTracerJSON mirrors the serialized "tracer" wrapper on a + // process node: a "trace" span context plus optional tracer metadata. + type processTracerJSON struct { + Trace *traceJSON `json:"trace"` + } var parsed struct { - DD struct { - SpanID string `json:"span_id"` - TraceID string `json:"trace_id"` - } `json:"dd"` + DD *traceJSON `json:"dd"` + Trace *traceJSON `json:"trace"` Process struct { - SpanContext *spanContextJSON `json:"span_context"` - Ancestors []struct { - SpanContext *spanContextJSON `json:"span_context"` + Tracer *processTracerJSON `json:"tracer"` + Ancestors []struct { + Tracer *processTracerJSON `json:"tracer"` } `json:"ancestors"` } `json:"process"` } @@ -86,40 +90,53 @@ func assertSerializedSpanContext(t *testing.T, jsonStr, expectedSpanID, expected return } - // (1) Top-level "dd" — always asserted. - assert.Equal(t, expectedSpanID, parsed.DD.SpanID, "serialized dd.span_id") - assert.Equal(t, expectedTraceID, parsed.DD.TraceID, "serialized dd.trace_id") + // (1) Top-level "dd" (intake-consumed) — always asserted. + if assert.NotNil(t, parsed.DD, "serialized dd field should be populated") { + assertSpanFields(t, parsed.DD, expectedSpanID, expectedTraceID, expectedAttrs, "dd") + } - // (2) Top-level "process.span_context". + // (1b) Top-level "trace" (user-facing) — always asserted. Both fields + // are populated from the same serializer instance in EventSerializer, + // so any divergence between dd and trace would indicate a + // serialization bug. + if assert.NotNil(t, parsed.Trace, "serialized trace field should be populated") { + assertSpanFields(t, parsed.Trace, expectedSpanID, expectedTraceID, expectedAttrs, "trace") + } + + // (2) "process.tracer.trace" — populated when AddExecEntry persisted + // event.SpanContext onto the new PCE (in-process exec scenarios). if loc.onTopLevelProcess { - if assert.NotNil(t, parsed.Process.SpanContext, - "process.span_context should be populated (AddExecEntry persisted event.SpanContext on the new PCE)") { - assertSpanFields(t, parsed.Process.SpanContext, expectedSpanID, expectedTraceID, expectedAttrs, "process.span_context") + if assert.NotNil(t, parsed.Process.Tracer, "process.tracer should be populated") && + assert.NotNil(t, parsed.Process.Tracer.Trace, "process.tracer.trace should be populated") { + assertSpanFields(t, parsed.Process.Tracer.Trace, expectedSpanID, expectedTraceID, expectedAttrs, "process.tracer.trace") } } else { - assert.Nil(t, parsed.Process.SpanContext, - "process.span_context should be unset (event.SpanContext was zero at exec time; nothing for AddExecEntry to persist)") + if parsed.Process.Tracer != nil { + assert.Nil(t, parsed.Process.Tracer.Trace, + "process.tracer.trace should be unset (event.SpanContext was zero at exec time; nothing for AddExecEntry to persist)") + } } - // (3) Ancestor "span_context". + // (3) "process.ancestors[].tracer.trace" — populated on the fork + // parent's PCE in fork+exec scenarios. if loc.onAncestor { - var ancestorSpan *spanContextJSON + var ancestorSpan *traceJSON for i := range parsed.Process.Ancestors { - if parsed.Process.Ancestors[i].SpanContext != nil { - ancestorSpan = parsed.Process.Ancestors[i].SpanContext + if parsed.Process.Ancestors[i].Tracer != nil && parsed.Process.Ancestors[i].Tracer.Trace != nil { + ancestorSpan = parsed.Process.Ancestors[i].Tracer.Trace break } } if assert.NotNil(t, ancestorSpan, - "at least one ancestor in process.ancestors[] should carry a serialized span_context") { - assertSpanFields(t, ancestorSpan, expectedSpanID, expectedTraceID, expectedAttrs, "ancestor.span_context") + "at least one ancestor in process.ancestors[] should carry a serialized tracer.trace") { + assertSpanFields(t, ancestorSpan, expectedSpanID, expectedTraceID, expectedAttrs, "ancestor.tracer.trace") } } } // assertSpanFields asserts the fields of a serialized span_context match the // expected span/trace IDs and the expected attribute subset. -func assertSpanFields(t *testing.T, sc *spanContextJSON, expectedSpanID, expectedTraceID string, expectedAttrs map[string]string, prefix string) { +func assertSpanFields(t *testing.T, sc *traceJSON, expectedSpanID, expectedTraceID string, expectedAttrs map[string]string, prefix string) { t.Helper() assert.Equal(t, expectedSpanID, sc.SpanID, "%s.span_id", prefix) assert.Equal(t, expectedTraceID, sc.TraceID, "%s.trace_id", prefix) @@ -284,16 +301,16 @@ func TestSpan(t *testing.T) { // span captured by fill_span_context_legacy at fork time. var foundAncestor *model.ProcessCacheEntry for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanContext.SpanID != 0 { + if pce.Tracer.Trace.SpanID != 0 { foundAncestor = pce break } } if assert.NotNil(t, foundAncestor, "an ancestor should carry the parent's legacy TLS span captured at fork time") { - assert.Equal(t, uint64(204), foundAncestor.SpanContext.SpanID, + assert.Equal(t, uint64(204), foundAncestor.Tracer.Trace.SpanID, "fork-parent ancestor SpanID should equal the legacy-TLS span_id") - assert.Equal(t, fakeTraceID128b, foundAncestor.SpanContext.TraceID.String(), + assert.Equal(t, fakeTraceID128b, foundAncestor.Tracer.Trace.TraceID.String(), "fork-parent ancestor TraceID should equal the legacy-TLS trace_id") } @@ -620,16 +637,16 @@ func TestOTelSpan(t *testing.T) { // span captured by fill_span_context_otel at fork time. var foundAncestor *model.ProcessCacheEntry for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanContext.SpanID != 0 { + if pce.Tracer.Trace.SpanID != 0 { foundAncestor = pce break } } if assert.NotNil(t, foundAncestor, "an ancestor should carry the parent's OTel TLS span captured at fork time") { - assert.Equal(t, uint64(204), foundAncestor.SpanContext.SpanID, + assert.Equal(t, uint64(204), foundAncestor.Tracer.Trace.SpanID, "fork-parent ancestor SpanID should equal the OTel record span_id") - assert.Equal(t, fakeTraceID128b, foundAncestor.SpanContext.TraceID.String(), + assert.Equal(t, fakeTraceID128b, foundAncestor.Tracer.Trace.TraceID.String(), "fork-parent ancestor TraceID should equal the OTel record trace_id") } @@ -901,11 +918,11 @@ func TestGoSpan(t *testing.T) { var foundSpan bool var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanContext.SpanID != 0 { + if pce.Tracer.Trace.SpanID != 0 { foundSpan = true - ancestorSpanID = pce.SpanContext.SpanID - ancestorTraceIDLo = pce.SpanContext.TraceID.Lo - ancestorTraceIDHi = pce.SpanContext.TraceID.Hi + ancestorSpanID = pce.Tracer.Trace.SpanID + ancestorTraceIDLo = pce.Tracer.Trace.TraceID.Lo + ancestorTraceIDHi = pce.Tracer.Trace.TraceID.Hi break } } @@ -1205,11 +1222,11 @@ func TestDDTraceGoSpan(t *testing.T) { var foundSpan bool var ancestorSpanID, ancestorTraceIDLo, ancestorTraceIDHi uint64 for pce := event.ProcessContext.Ancestor; pce != nil; pce = pce.Ancestor { - if pce.SpanContext.SpanID != 0 { + if pce.Tracer.Trace.SpanID != 0 { foundSpan = true - ancestorSpanID = pce.SpanContext.SpanID - ancestorTraceIDLo = pce.SpanContext.TraceID.Lo - ancestorTraceIDHi = pce.SpanContext.TraceID.Hi + ancestorSpanID = pce.Tracer.Trace.SpanID + ancestorTraceIDLo = pce.Tracer.Trace.TraceID.Lo + ancestorTraceIDHi = pce.Tracer.Trace.TraceID.Hi break } } From c650495b4d4d4167fd99831b23037d722eb1d703 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Thu, 28 May 2026 10:42:56 +0200 Subject: [PATCH 11/14] [Network] Fix network events monitor tests --- pkg/network/events/monitor_test.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/network/events/monitor_test.go b/pkg/network/events/monitor_test.go index bc3814a04850..a5891dcb6b7e 100644 --- a/pkg/network/events/monitor_test.go +++ b/pkg/network/events/monitor_test.go @@ -260,11 +260,13 @@ func TestEventHandleTracerTags(t *testing.T) { "DD_ENV=env-from-envp", "DD_VERSION=version-from-envp", }, - TracerMetadata: tracermetadata.TracerMetadata{ - ServiceName: "my-service", - ServiceEnv: "my-env", - ServiceVersion: "my-version", - ProcessTags: "entrypoint.name:my-entrypoint", + Tracer: model.Tracer{ + Metadata: tracermetadata.TracerMetadata{ + ServiceName: "my-service", + ServiceEnv: "my-env", + ServiceVersion: "my-version", + ProcessTags: "entrypoint.name:my-entrypoint", + }, }, }, }, @@ -304,11 +306,13 @@ func TestEventHandleTracerTags(t *testing.T) { "DD_ENV=my-env", "DD_VERSION=my-version", }, - TracerMetadata: tracermetadata.TracerMetadata{ - ServiceName: "my-service", - ServiceEnv: "my-env", - ServiceVersion: "my-version", - ProcessTags: "entrypoint.name:my-entrypoint", + Tracer: model.Tracer{ + Metadata: tracermetadata.TracerMetadata{ + ServiceName: "my-service", + ServiceEnv: "my-env", + ServiceVersion: "my-version", + ProcessTags: "entrypoint.name:my-entrypoint", + }, }, }, }, From 690368cb2165aab8b0200f2e4a55cb30ccb4c3e9 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Thu, 28 May 2026 17:00:06 +0200 Subject: [PATCH 12/14] [WP] Fix trace ID serialization --- pkg/security/secl/model/utils/BUILD.bazel | 8 ++- pkg/security/secl/model/utils/uint128.go | 19 ++++++ pkg/security/secl/model/utils/uint128_test.go | 61 +++++++++++++++++++ pkg/security/serializers/serializers_linux.go | 6 +- pkg/security/tests/span_test.go | 17 +++--- 5 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 pkg/security/secl/model/utils/uint128_test.go diff --git a/pkg/security/secl/model/utils/BUILD.bazel b/pkg/security/secl/model/utils/BUILD.bazel index fd4909e61305..6c9e8f341c9c 100644 --- a/pkg/security/secl/model/utils/BUILD.bazel +++ b/pkg/security/secl/model/utils/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") # Source-level export so the //pkg/security/secl/model:event_deep_copy_* # Bazel codegens can resolve types defined here (e.g. utils.TraceID). @@ -13,3 +13,9 @@ go_library( importpath = "github.com/DataDog/datadog-agent/pkg/security/secl/model/utils", visibility = ["//visibility:public"], ) + +go_test( + name = "utils_test", + srcs = ["uint128_test.go"], + embed = [":utils"], +) diff --git a/pkg/security/secl/model/utils/uint128.go b/pkg/security/secl/model/utils/uint128.go index a6f69839ec54..6b07a030b154 100644 --- a/pkg/security/secl/model/utils/uint128.go +++ b/pkg/security/secl/model/utils/uint128.go @@ -7,6 +7,7 @@ package utils import ( + "fmt" "math/big" ) @@ -30,3 +31,21 @@ func (t TraceID) bigInt() *big.Int { func (t TraceID) String() string { return t.bigInt().String() } + +// HexString returns the trace ID as lowercase hex. +// +// When Hi == 0 (the high half was not collected, e.g. dd-trace-go pprof +// labels expose only the lower 64 bits), Lo is emitted unpadded so the +// result is byte-exact for backend pattern searches against the uint64 +// lower half. +// +// When Hi != 0 the full 128-bit ID was collected and the result must +// match the canonical 16-byte form (e.g. APM's 32-char display): Hi is +// emitted unpadded but Lo is zero-padded to 16 hex chars so leading +// zeros in Lo — which are part of the real 128-bit ID — are preserved. +func (t TraceID) HexString() string { + if t.Hi == 0 { + return fmt.Sprintf("%x", t.Lo) + } + return fmt.Sprintf("%x%016x", t.Hi, t.Lo) +} diff --git a/pkg/security/secl/model/utils/uint128_test.go b/pkg/security/secl/model/utils/uint128_test.go new file mode 100644 index 000000000000..c1e0612f0c80 --- /dev/null +++ b/pkg/security/secl/model/utils/uint128_test.go @@ -0,0 +1,61 @@ +// 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. + +package utils + +import "testing" + +func TestTraceIDHexString(t *testing.T) { + tests := []struct { + name string + id TraceID + want string + }{ + { + // dd-trace-go pprof-label path: only the lower 64 bits are + // collected; Hi must not contribute a stray "0" prefix. + name: "lower_only", + id: TraceID{Lo: 0x37e0414e1b3d14f2}, + want: "37e0414e1b3d14f2", + }, + { + // OTel TLS path with full 128-bit ID. dd-trace-go's 128-bit form + // uses in the high half; the trailing + // zeros are real bits and must be preserved by %x. + name: "full_128bit", + id: TraceID{Hi: 0x6a18137300000000, Lo: 0x37e0414e1b3d14f2}, + want: "6a18137300000000" + "37e0414e1b3d14f2", + }, + { + // Sanity: Hi with leading zero nibbles still renders unpadded. + name: "hi_with_leading_zero_nibble", + id: TraceID{Hi: 0x0a18137300000000, Lo: 0x37e0414e1b3d14f2}, + want: "a18137300000000" + "37e0414e1b3d14f2", + }, + { + // Hi==0: Lo's leading-zero nibbles must NOT be padded — the + // backend's "real" 64-bit trace ID has no such zeros and + // pattern search must match the uint64-as-hex form exactly. + name: "lower_only_with_leading_zero", + id: TraceID{Lo: 0x00abcdef12345678}, + want: "abcdef12345678", + }, + { + // Hi!=0: Lo's leading-zero nibbles ARE part of the canonical + // 16-byte trace ID; the result must stay 32 chars wide to + // match APM's display form byte-for-byte. + name: "full_128bit_lo_with_leading_zero", + id: TraceID{Hi: 0x6a18137300000000, Lo: 0x00abcdef12345678}, + want: "6a18137300000000" + "00abcdef12345678", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.id.HexString(); got != tc.want { + t.Fatalf("HexString() = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 546e0c1ae3cf..63c5e0b1729f 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -1063,7 +1063,7 @@ func newProcessSerializer(ps *model.Process, e *model.Event) *ProcessSerializer } tracer.Trace = &TraceSerializer{ SpanID: strconv.FormatUint(ps.Tracer.Trace.SpanID, 10), - TraceID: fmt.Sprintf("%x%x", ps.Tracer.Trace.TraceID.Hi, ps.Tracer.Trace.TraceID.Lo), + TraceID: ps.Tracer.Trace.TraceID.HexString(), Attributes: ps.Tracer.Trace.Attributes, } } @@ -1515,7 +1515,7 @@ func newTraceSerializer(e *model.Event) *TraceSerializer { s := &TraceSerializer{} 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.TraceID = e.SpanContext.TraceID.HexString() s.Attributes = e.SpanContext.Attributes return s } @@ -1529,7 +1529,7 @@ func newTraceSerializer(e *model.Event) *TraceSerializer { if pce.Tracer.Trace.SpanID != 0 && (pce.Tracer.Trace.TraceID.Hi != 0 || pce.Tracer.Trace.TraceID.Lo != 0) { s.SpanID = strconv.FormatUint(pce.Tracer.Trace.SpanID, 10) - s.TraceID = fmt.Sprintf("%x%x", pce.Tracer.Trace.TraceID.Hi, pce.Tracer.Trace.TraceID.Lo) + s.TraceID = pce.Tracer.Trace.TraceID.HexString() s.Attributes = pce.Tracer.Trace.Attributes break } diff --git a/pkg/security/tests/span_test.go b/pkg/security/tests/span_test.go index 873ab0066076..c0b6242513d7 100644 --- a/pkg/security/tests/span_test.go +++ b/pkg/security/tests/span_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/DataDog/datadog-agent/pkg/security/secl/model" + "github.com/DataDog/datadog-agent/pkg/security/secl/model/utils" "github.com/DataDog/datadog-agent/pkg/security/secl/rules" ) @@ -250,7 +251,7 @@ func TestSpan(t *testing.T) { jsonStr, err := test.marshalEvent(event) if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, "204", - fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + utils.TraceID{Hi: expectedHi, Lo: expectedLo}.HexString(), nil, spanLocations{onTopLevelProcess: true}) } }, "test_span_rule_exec") @@ -324,7 +325,7 @@ func TestSpan(t *testing.T) { jsonStr, err := test.marshalEvent(event) if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, "204", - fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + utils.TraceID{Hi: expectedHi, Lo: expectedLo}.HexString(), nil, spanLocations{onAncestor: true}) } }, "test_span_rule_exec") @@ -531,7 +532,7 @@ func TestOTelSpan(t *testing.T) { jsonStr, err := test.marshalEvent(event) if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, "204", - fmt.Sprintf("%x%x", expectedHi, expectedLo), + utils.TraceID{Hi: expectedHi, Lo: expectedLo}.HexString(), map[string]string{ "http.method": "GET", "http.target": "/test", @@ -660,7 +661,7 @@ func TestOTelSpan(t *testing.T) { jsonStr, err := test.marshalEvent(event) if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, "204", - fmt.Sprintf("%x%x", expectedHi, expectedLo), nil, + utils.TraceID{Hi: expectedHi, Lo: expectedLo}.HexString(), nil, spanLocations{onAncestor: true}) } }, "test_otel_span_rule_exec") @@ -792,7 +793,7 @@ func TestGoSpan(t *testing.T) { if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, strconv.FormatUint(987654321, 10), - fmt.Sprintf("%x%x", uint64(0), uint64(123456789)), + utils.TraceID{Lo: 123456789}.HexString(), nil, spanLocations{onTopLevelProcess: true}) } @@ -942,7 +943,7 @@ func TestGoSpan(t *testing.T) { if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, strconv.FormatUint(parentSpanID, 10), - fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID), + utils.TraceID{Lo: parentLocalRootSpanID}.HexString(), nil, spanLocations{onAncestor: true}) } @@ -1093,7 +1094,7 @@ func TestDDTraceGoSpan(t *testing.T) { if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, strconv.FormatUint(expectedSpanID, 10), - fmt.Sprintf("%x%x", uint64(0), expectedLocalRootSpanID), + utils.TraceID{Lo: expectedLocalRootSpanID}.HexString(), nil, spanLocations{onTopLevelProcess: true}) } @@ -1246,7 +1247,7 @@ func TestDDTraceGoSpan(t *testing.T) { if assert.NoError(t, err, "marshalEvent") { assertSerializedSpanContext(t, jsonStr, strconv.FormatUint(parentSpanID, 10), - fmt.Sprintf("%x%x", uint64(0), parentLocalRootSpanID), + utils.TraceID{Lo: parentLocalRootSpanID}.HexString(), nil, spanLocations{onAncestor: true}) } From 07480cb5d3a48a56afdc77d81c30c464f58b1800 Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 29 May 2026 14:01:44 +0200 Subject: [PATCH 13/14] [WP] Support <6.12 kernels --- pkg/security/ebpf/c/include/hooks/exec.h | 48 +++++++++---------- pkg/security/ebpf/c/include/hooks/module.h | 37 ++++++++------ .../ebpf/c/include/hooks/raw_syscalls.h | 26 ++++++---- pkg/security/ebpf/c/include/hooks/setxattr.h | 33 ++++++++----- pkg/security/ebpf/c/include/maps.h | 13 +++++ 5 files changed, 99 insertions(+), 58 deletions(-) diff --git a/pkg/security/ebpf/c/include/hooks/exec.h b/pkg/security/ebpf/c/include/hooks/exec.h index 743d638912d2..537a0c3f4d9a 100644 --- a/pkg/security/ebpf/c/include/hooks/exec.h +++ b/pkg/security/ebpf/c/include/hooks/exec.h @@ -762,25 +762,25 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { bpf_map_delete_elem(&exec_pid_transfer, &tgid); - struct proc_cache_t pc = { - .entry = { - .executable = { - .path_key = { - .ino = syscall->exec.file.path_key.ino, - .mount_id = syscall->exec.file.path_key.mount_id, - .path_id = syscall->exec.file.path_key.path_id, - }, - .flags = syscall->exec.file.flags, - .metadata = { - .nlink = syscall->exec.file.metadata.nlink - }, - }, - .exec_timestamp = now, - }, - .cgroup = {}, - }; - fill_file(syscall->exec.dentry, &pc.entry.executable); - bpf_get_current_comm(&pc.entry.comm, sizeof(pc.entry.comm)); + // proc_cache_t (~200b) lives in a per-CPU scratch map rather than on the + // stack — combined with send_exec_event's other locals, keeping it on the + // stack would leave no room for the bpf-to-bpf call into + // fill_span_context_go reachable via the tail_call_flush_network_stats_exec + // wrapper (pre-6.17 verifiers enforce a 512-byte combined budget). + u32 pc_key = 0; + struct proc_cache_t *pc = bpf_map_lookup_elem(&exec_proc_cache_gen, &pc_key); + if (!pc) { + return 0; + } + __builtin_memset(pc, 0, sizeof(*pc)); + pc->entry.executable.path_key.ino = syscall->exec.file.path_key.ino; + pc->entry.executable.path_key.mount_id = syscall->exec.file.path_key.mount_id; + pc->entry.executable.path_key.path_id = syscall->exec.file.path_key.path_id; + pc->entry.executable.flags = syscall->exec.file.flags; + pc->entry.executable.metadata.nlink = syscall->exec.file.metadata.nlink; + pc->entry.exec_timestamp = now; + fill_file(syscall->exec.dentry, &pc->entry.executable); + bpf_get_current_comm(&pc->entry.comm, sizeof(pc->entry.comm)); // store the process path key (copy to stack for older kernel verifiers) struct path_key_t on_stack_exec_path_key = syscall->exec.file.path_key; @@ -800,11 +800,11 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { // inherit the parent cgroup context if ((fork_entry->fork_flags & CLONE_INTO_CGROUP) == 0) { - fill_cgroup_context(parent_pc, &pc.cgroup); + fill_cgroup_context(parent_pc, &pc->cgroup); } else { u64 cgroup_id = get_current_cgroup_id(); if (cgroup_id) { - pc.cgroup.path_key.ino = cgroup_id; + pc->cgroup.path_key.ino = cgroup_id; } } } @@ -813,7 +813,7 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { // Insert new proc cache entry (Note: do not move the order of this block with the previous one, we need to inherit // the cgroup before saving the entry in proc_cache. Modifying entry after insertion won't work.) u64 cookie = rand64(); - bpf_map_update_elem(&proc_cache, &cookie, &pc, BPF_ANY); + bpf_map_update_elem(&proc_cache, &cookie, pc, BPF_ANY); // update pid <-> cookie mapping if (fork_entry) { @@ -836,8 +836,8 @@ int __attribute__((always_inline)) send_exec_event(ctx_t *ctx) { } // copy proc_cache data - fill_cgroup_context(&pc, &event->cgroup); - copy_proc_entry(&pc.entry, &event->proc_entry); + fill_cgroup_context(pc, &event->cgroup); + copy_proc_entry(&pc->entry, &event->proc_entry); // copy pid_cache entry data copy_pid_cache_except_exit_ts(fork_entry, &event->pid_entry); diff --git a/pkg/security/ebpf/c/include/hooks/module.h b/pkg/security/ebpf/c/include/hooks/module.h index 3d5ccdbb109b..51c85a012b74 100644 --- a/pkg/security/ebpf/c/include/hooks/module.h +++ b/pkg/security/ebpf/c/include/hooks/module.h @@ -100,30 +100,39 @@ int __attribute__((always_inline)) trace_init_module_ret(void *ctx, int retval, return 0; } - struct init_module_event_t event = { - .syscall.retval = retval, - .file = syscall->init_module.file, - .loaded_from_memory = syscall->init_module.loaded_from_memory, - }; + // init_module_event_t lives in a per-CPU map rather than on the stack: at + // ~480 bytes it leaves no room for the bpf-to-bpf call into + // fill_span_context_go (combined stack budget is 512 bytes on pre-6.17 + // verifiers). + u32 zero = 0; + struct init_module_event_t *event = bpf_map_lookup_elem(&init_module_event_gen, &zero); + if (!event) { + return 0; + } + __builtin_memset(event, 0, sizeof(*event)); - bpf_probe_read_str(&event.args, sizeof(event.args), &syscall->init_module.args); - event.args_truncated = syscall->init_module.args_truncated; + event->syscall.retval = retval; + event->file = syscall->init_module.file; + event->loaded_from_memory = syscall->init_module.loaded_from_memory; + + bpf_probe_read_str(&event->args, sizeof(event->args), &syscall->init_module.args); + event->args_truncated = syscall->init_module.args_truncated; if (!modname) { - bpf_probe_read_str(&event.name, sizeof(event.name), &syscall->init_module.name[0]); + bpf_probe_read_str(&event->name, sizeof(event->name), &syscall->init_module.name[0]); } else { - bpf_probe_read_str(&event.name, sizeof(event.name), modname); + bpf_probe_read_str(&event->name, sizeof(event->name), modname); } if (syscall->init_module.dentry != NULL) { - fill_file(syscall->init_module.dentry, &event.file); + fill_file(syscall->init_module.dentry, &event->file); } - struct proc_cache_t *entry = fill_process_context(&event.process); - fill_cgroup_context(entry, &event.cgroup); - fill_span_context(&event.span); + struct proc_cache_t *entry = fill_process_context(&event->process); + fill_cgroup_context(entry, &event->cgroup); + fill_span_context(&event->span); - send_event(ctx, EVENT_INIT_MODULE, event); + send_event_ptr(ctx, EVENT_INIT_MODULE, event); return 0; } diff --git a/pkg/security/ebpf/c/include/hooks/raw_syscalls.h b/pkg/security/ebpf/c/include/hooks/raw_syscalls.h index e33d0842ff4c..d020299d928b 100644 --- a/pkg/security/ebpf/c/include/hooks/raw_syscalls.h +++ b/pkg/security/ebpf/c/include/hooks/raw_syscalls.h @@ -15,14 +15,24 @@ int sys_enter(struct _tracepoint_raw_syscalls_sys_enter *args) { send_signal(pid); - struct syscall_monitor_event_t event = {}; - struct proc_cache_t *proc_cache_entry = fill_process_context(&event.process); - fill_cgroup_context(proc_cache_entry, &event.cgroup); + // syscall_monitor_event_t lives in a per-CPU map rather than on the stack: + // it carries a 64-byte syscall encoding table plus process/cgroup/span + // context, which together leave too little room for the bpf-to-bpf call + // into fill_span_context_go on pre-6.17 verifiers (combined budget 512b). + u32 key = 0; + struct syscall_monitor_event_t *event = bpf_map_lookup_elem(&syscall_monitor_event_gen, &key); + if (!event) { + return 0; + } + __builtin_memset(event, 0, sizeof(*event)); + + struct proc_cache_t *proc_cache_entry = fill_process_context(&event->process); + fill_cgroup_context(proc_cache_entry, &event->cgroup); // check if this event should trigger a syscall drift event if (is_anomaly_syscalls_enabled()) { // fetch the profile for the current cgroup - struct security_profile_t *profile = bpf_map_lookup_elem(&security_profiles, &event.cgroup.path_key.ino); + struct security_profile_t *profile = bpf_map_lookup_elem(&security_profiles, &event->cgroup.path_key.ino); if (profile) { u64 cookie = profile->cookie; struct security_profile_syscalls_t *syscalls = bpf_map_lookup_elem(&secprofs_syscalls, &cookie); @@ -38,8 +48,8 @@ int sys_enter(struct _tracepoint_raw_syscalls_sys_enter *args) { syscall_monitor_entry_insert(entry, args->id); } // send an event if need be - event.event.flags = EVENT_FLAGS_ANOMALY_DETECTION_EVENT; - send_or_skip_syscall_monitor_event(args, &event, entry, &zero, SYSCALL_MONITOR_TYPE_DRIFT); + event->event.flags = EVENT_FLAGS_ANOMALY_DETECTION_EVENT; + send_or_skip_syscall_monitor_event(args, event, entry, &zero, SYSCALL_MONITOR_TYPE_DRIFT); } } } @@ -57,8 +67,8 @@ int sys_enter(struct _tracepoint_raw_syscalls_sys_enter *args) { // insert the current syscall in the map syscall_monitor_entry_insert(entry, args->id); // send an event if need be - event.event.flags = EVENT_FLAGS_ACTIVITY_DUMP_SAMPLE; - send_or_skip_syscall_monitor_event(args, &event, entry, &zero, SYSCALL_MONITOR_TYPE_DUMP); + event->event.flags = EVENT_FLAGS_ACTIVITY_DUMP_SAMPLE; + send_or_skip_syscall_monitor_event(args, event, entry, &zero, SYSCALL_MONITOR_TYPE_DUMP); } } diff --git a/pkg/security/ebpf/c/include/hooks/setxattr.h b/pkg/security/ebpf/c/include/hooks/setxattr.h index a13988e71318..c2f426258fdc 100644 --- a/pkg/security/ebpf/c/include/hooks/setxattr.h +++ b/pkg/security/ebpf/c/include/hooks/setxattr.h @@ -153,27 +153,36 @@ int __attribute__((always_inline)) sys_xattr_ret(void *ctx, int retval, u64 even return 0; } - struct setxattr_event_t event = { - .event.flags = syscall->async ? EVENT_FLAGS_ASYNC : 0, - .syscall.retval = retval, - .file = syscall->xattr.file, - }; + // setxattr_event_t lives in a per-CPU map rather than on the stack: at + // ~480 bytes it leaves no room for the bpf-to-bpf call into + // fill_span_context_go (combined stack budget is 512 bytes on pre-6.17 + // verifiers). + u32 zero = 0; + struct setxattr_event_t *event = bpf_map_lookup_elem(&setxattr_event_gen, &zero); + if (!event) { + return 0; + } + __builtin_memset(event, 0, sizeof(*event)); + + event->event.flags = syscall->async ? EVENT_FLAGS_ASYNC : 0; + event->syscall.retval = retval; + event->file = syscall->xattr.file; // copy xattr name - bpf_probe_read_str(&event.name, MAX_XATTR_NAME_LEN, (void *)syscall->xattr.name); + bpf_probe_read_str(&event->name, MAX_XATTR_NAME_LEN, (void *)syscall->xattr.name); struct proc_cache_t *entry; if (syscall->xattr.pid_tgid != 0) { - entry = fill_process_context_with_pid_tgid(&event.process, syscall->xattr.pid_tgid); + entry = fill_process_context_with_pid_tgid(&event->process, syscall->xattr.pid_tgid); } else { - entry = fill_process_context(&event.process); + entry = fill_process_context(&event->process); } - fill_cgroup_context(entry, &event.cgroup); - fill_file(syscall->xattr.dentry, &event.file); - fill_span_context(&event.span); + fill_cgroup_context(entry, &event->cgroup); + fill_file(syscall->xattr.dentry, &event->file); + fill_span_context(&event->span); - send_event(ctx, event_type, event); + send_event_ptr(ctx, event_type, event); return 0; } diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index e0a18ee5741d..ee535b8a5a60 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -142,6 +142,19 @@ BPF_PERCPU_ARRAY_MAP(raw_packet_enabled, u32, 1) BPF_PERCPU_ARRAY_MAP(sysctl_event_gen, struct sysctl_event_t, 1) BPF_PERCPU_ARRAY_MAP(on_demand_event_gen, struct on_demand_event_t, 1) BPF_PERCPU_ARRAY_MAP(setsockopt_event, struct setsockopt_event_t, 1) +// Per-CPU scratch slots for events whose on-stack form is too large to leave +// any room for a bpf-to-bpf call from the hook (pre-6.17 verifier enforces +// combined bpf-to-bpf stack ≤ 512 bytes; fill_span_context_go is the call +// that runs into this). +BPF_PERCPU_ARRAY_MAP(setxattr_event_gen, struct setxattr_event_t, 1) +BPF_PERCPU_ARRAY_MAP(init_module_event_gen, struct init_module_event_t, 1) +BPF_PERCPU_ARRAY_MAP(syscall_monitor_event_gen, struct syscall_monitor_event_t, 1) +// Per-CPU scratch slot for the proc_cache_t aggregator built by send_exec_event +// before it is committed to the proc_cache map. Keeping it off the stack lets +// the tail-called flush_network_stats_exec wrapper (which inlines +// send_exec_event) fit within the 512-byte combined budget alongside +// fill_span_context_go. +BPF_PERCPU_ARRAY_MAP(exec_proc_cache_gen, struct proc_cache_t, 1) BPF_PROG_ARRAY(args_envs_progs, 3) BPF_PROG_ARRAY(dentry_resolver_kprobe_or_fentry_callbacks, EVENT_MAX) From 75c8f9871ec95060a0a3c7a0c8e26254b89e3aad Mon Sep 17 00:00:00 2001 From: Gui774ume Date: Fri, 29 May 2026 15:29:48 +0200 Subject: [PATCH 14/14] [WP] Fetch go label offsets and otel offsets on snapshot --- .../resolvers/process/resolver_ebpf.go | 56 +++++++++++++++---- pkg/security/resolvers/resolvers_ebpf.go | 6 ++ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index 4eed458e8832..2acb00e65684 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -30,6 +30,7 @@ import ( "go.uber.org/atomic" "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata" + tracermetadatamodel "github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata/model" "github.com/DataDog/datadog-agent/pkg/security/ebpf" "github.com/DataDog/datadog-agent/pkg/security/metrics" "github.com/DataDog/datadog-agent/pkg/security/probe/config" @@ -1487,30 +1488,61 @@ func (p *EBPFResolver) AddTracerMetadata(pid uint32, event *model.Event) error { return fmt.Errorf("failed to read tracer metadata: %w", err) } + p.applyTracerMetadata(pid, tmeta) + return nil +} + +// SnapshotTracer detects whether a pre-existing process (one that started +// before the agent) is running a Datadog tracer and, if so, populates the +// user-space tracer metadata and the kernel-side offset maps (go_labels_procs +// or otel_tls) the same way the runtime tracer_memfd_seal event handler does. +// +// Called from the startup snapshot for every pid; processes without a tracer +// memfd return cheaply via the GetTracerMetadata error path. +func (p *EBPFResolver) SnapshotTracer(pid uint32) { + // Only do the (mildly expensive) /proc//fd scan for pids that + // SyncCache actually entered into the cache — anything else can't be + // updated downstream anyway. + p.RLock() + hasEntry := p.entryCache[pid] != nil + p.RUnlock() + if !hasEntry { + return + } + + tmeta, err := tracermetadata.GetTracerMetadata(int(pid), kernel.HostProc()) + if err != nil { + // The common case for non-tracer processes — silent. + return + } + + p.applyTracerMetadata(pid, tmeta) +} + +// applyTracerMetadata stores tracer metadata on the process cache entry and +// resolves the language-appropriate offset map (Go pprof labels or native +// OTel TLS). Must be called WITHOUT the resolver lock held; ELF I/O happens +// outside the lock. +func (p *EBPFResolver) applyTracerMetadata(pid uint32, tmeta tracermetadatamodel.TracerMetadata) { p.Lock() - entry := p.entryCache[pid] - if entry != nil { + if entry := p.entryCache[pid]; entry != nil { entry.Tracer.Metadata = 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 + } + // 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 diff --git a/pkg/security/resolvers/resolvers_ebpf.go b/pkg/security/resolvers/resolvers_ebpf.go index b1eb6b1d7f55..cd9021e49457 100644 --- a/pkg/security/resolvers/resolvers_ebpf.go +++ b/pkg/security/resolvers/resolvers_ebpf.go @@ -317,6 +317,12 @@ func (r *EBPFResolvers) snapshot() error { for _, proc := range processes { // Sync the process cache r.ProcessResolver.SyncCache(proc) + // If the process is running a Datadog tracer, populate the user-space + // metadata and the kernel-side go_labels_procs / otel_tls offset maps + // — these otherwise only get populated by the runtime + // tracer_memfd_seal event, which has already fired and been missed for + // processes that started before the agent. + r.ProcessResolver.SnapshotTracer(uint32(proc.Pid)) } return nil