From 75ff9dbe76c6438ecfb4260d1243921606e4def5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momar=20TOUR=C3=89?= Date: Thu, 17 Sep 2026 16:47:21 +0200 Subject: [PATCH 1/4] add insertion of connect events in the activity tree --- go.mod | 2 +- go.sum | 2 + pkg/config/schema/yaml/system-probe-cws.yaml | 3 +- pkg/config/setup/system_probe_test.go | 4 +- pkg/security/config/config.go | 2 +- .../activity_tree/activity_tree.go | 23 +++++ .../activity_tree/activity_tree_graph.go | 27 +++++- .../activity_tree_proto_dec_v1.go | 24 ++++++ .../activity_tree_proto_enc_v1.go | 21 ++++- .../activity_tree/activity_tree_stats.go | 2 + .../activity_tree/process_node.go | 47 +++++++++++ .../activity_tree/size_test.go | 1 + .../activity_tree/socket_node.go | 84 ++++++++++++++++++- 13 files changed, 233 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index d6f3227a8377..6d817f5772b5 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.5.0 github.com/CycloneDX/cyclonedx-go v0.12.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/DataDog/agent-payload/v5 v5.0.210 + github.com/DataDog/agent-payload/v5 v5.0.212-0.20260917144100-9eb352e40a01 github.com/DataDog/datadog-agent/comp/anomalydetection/observer/def v0.0.0-00010101000000-000000000000 github.com/DataDog/datadog-agent/comp/anomalydetection/recorder/def v0.0.0-00010101000000-000000000000 github.com/DataDog/datadog-agent/comp/anomalydetection/severityevents/def v0.0.0-00010101000000-000000000000 diff --git a/go.sum b/go.sum index 59ed98f9d018..eb7b902a89bc 100644 --- a/go.sum +++ b/go.sum @@ -2530,6 +2530,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/agent-payload/v5 v5.0.210 h1:yLEYo1V20zPeh1bENaUApCtdaTdZP8WOvYWZQX9A7Yw= github.com/DataDog/agent-payload/v5 v5.0.210/go.mod h1:z6tbYIJQWWQa3ozmZdit7jR3ROOKhgs/va4tNLu80mI= +github.com/DataDog/agent-payload/v5 v5.0.212-0.20260917144100-9eb352e40a01 h1:gBbfccPUy+TNzpE67FCZA1GhZ9EHFv+Gt6nde/0Ck3w= +github.com/DataDog/agent-payload/v5 v5.0.212-0.20260917144100-9eb352e40a01/go.mod h1:z6tbYIJQWWQa3ozmZdit7jR3ROOKhgs/va4tNLu80mI= github.com/DataDog/aptly v1.4.1-0.20260507130549-83af7604fdc9 h1:SqGEmv63IrjqqDmXf49ZjlZnyBiHiBbF/1AVaM3q+wI= github.com/DataDog/aptly v1.4.1-0.20260507130549-83af7604fdc9/go.mod h1:UmLcF4Qcn6yaiqp5xrsVwEfeBSD2YTWkWLv69SIlvtE= github.com/DataDog/cast v1.8.0 h1:uooY8bMzq+cjgiNP1VTquCWve5emgk8fRspZojJwQa8= diff --git a/pkg/config/schema/yaml/system-probe-cws.yaml b/pkg/config/schema/yaml/system-probe-cws.yaml index 7ebda2e4e495..73d2c173e829 100644 --- a/pkg/config/schema/yaml/system-probe-cws.yaml +++ b/pkg/config/schema/yaml/system-probe-cws.yaml @@ -716,6 +716,7 @@ properties: - open - dns - bind + - connect items: type: string excluded_images: @@ -727,7 +728,7 @@ properties: max_dump_size: node_type: setting type: integer - default: 2560 + default: 3072 sample_refresh_period: node_type: setting type: string diff --git a/pkg/config/setup/system_probe_test.go b/pkg/config/setup/system_probe_test.go index d543fbf72008..d027a5abb83b 100644 --- a/pkg/config/setup/system_probe_test.go +++ b/pkg/config/setup/system_probe_test.go @@ -58,8 +58,8 @@ func TestSystemProbeDefaultConfig(t *testing.T) { {key: "discovery.service_collection_max_consecutive_timeouts", defaultValue: 5}, {key: "discovery.service_collection_min_process_age", defaultValue: time.Minute}, {key: "runtime_security_config.security_profile.v2.enabled", defaultValue: true}, - {key: "runtime_security_config.security_profile.v2.max_dump_size", defaultValue: 2560}, - {key: "runtime_security_config.security_profile.v2.event_types", defaultValue: []string{"exec", "open", "dns", "bind"}}, + {key: "runtime_security_config.security_profile.v2.max_dump_size", defaultValue: 3072}, + {key: "runtime_security_config.security_profile.v2.event_types", defaultValue: []string{"exec", "open", "dns", "bind", "connect"}}, } { t.Run(tc.key, func(t *testing.T) { switch expected := tc.defaultValue.(type) { diff --git a/pkg/security/config/config.go b/pkg/security/config/config.go index d3efbdca2683..4f7c0a259889 100644 --- a/pkg/security/config/config.go +++ b/pkg/security/config/config.go @@ -521,7 +521,7 @@ type RuntimeSecurityConfig struct { // description: SecurityProfileV2MaxDumpSize returns the V2-only max profile size in bytes. // visibility: private - // default_value: 5120 + // default_value: 3072 SecurityProfileV2MaxDumpSize func() int // description: AnomalyDetectionEventTypes defines the list of events that should be allowed to generate anomaly detections diff --git a/pkg/security/security_profile/activity_tree/activity_tree.go b/pkg/security/security_profile/activity_tree/activity_tree.go index a10b1cce6424..d115bbd87eb3 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree.go +++ b/pkg/security/security_profile/activity_tree/activity_tree.go @@ -36,6 +36,7 @@ const ( eventTypeReason NodeDroppedReason = iota invalidRootNodeReason bindFamilyReason + connectFamilyReason brokenEventReason minNodeDroppedReason = eventTypeReason @@ -51,6 +52,8 @@ func (reason NodeDroppedReason) String() string { return "invalid_root_node" case bindFamilyReason: return "bind_family" + case connectFamilyReason: + return "connect_family" case brokenEventReason: return "broken_event" default: @@ -68,6 +71,8 @@ func (reason NodeDroppedReason) Tag() string { return "reason:invalid_root_node" case bindFamilyReason: return "reason:bind_family" + case connectFamilyReason: + return "reason:connect_family" case brokenEventReason: return "reason:broken_event" default: @@ -82,6 +87,8 @@ var ( ErrNotValidRootNode = errors.New("root node not valid") // ErrInvalidBindFamily is returned when a bind event uses an unsupported address family ErrInvalidBindFamily = errors.New("invalid bind address family") + // ErrInvalidConnectFamily is returned when a connect event uses an unsupported address family + ErrInvalidConnectFamily = errors.New("invalid connect address family") // ErrIMDSMissingCredentials is returned when an IMDS response event has no access key ID ErrIMDSMissingCredentials = errors.New("IMDS response without credentials") // ErrIMDSMissingURL is returned when an IMDS request event has no URL @@ -330,6 +337,10 @@ func (at *ActivityTree) ComputeActivityTreeStats() { at.Stats.DNSNodes += int64(len(node.DNSNames)) at.Stats.SocketNodes += int64(len(node.Sockets)) + for _, sock := range node.Sockets { + at.Stats.ConnectNodes += int64(len(sock.Connect)) + } + for _, f := range node.Files { fnodes = append(fnodes, f) } @@ -392,6 +403,7 @@ func IsExpectedFilterError(err error) bool { errors.As(err, &pathResolutionNotCriticalErr) || errors.Is(err, ErrNotValidRootNode) || errors.Is(err, ErrInvalidBindFamily) || + errors.Is(err, ErrInvalidConnectFamily) || errors.Is(err, ErrIMDSMissingCredentials) || errors.Is(err, ErrIMDSMissingURL) } @@ -452,6 +464,14 @@ func (at *ActivityTree) isEventValid(event *model.Event, dryRun bool) (bool, err } return false, fmt.Errorf("%w: %s", ErrInvalidBindFamily, model.AddressFamily(event.Bind.AddrFamily)) } + case model.ConnectEventType: + // ignore non IPv4 / IPv6 connect events for now + if event.Connect.AddrFamily != unix.AF_INET && event.Connect.AddrFamily != unix.AF_INET6 { + if !dryRun { + at.Stats.counts[model.ConnectEventType].droppedCount[connectFamilyReason].Inc() + } + return false, fmt.Errorf("%w: %s", ErrInvalidConnectFamily, model.AddressFamily(event.Connect.AddrFamily)) + } case model.IMDSEventType: // ignore IMDS answers without AccessKeyIDS if event.IMDS.Type == model.IMDSResponseType && len(event.IMDS.AWS.SecurityCredentials.AccessKeyID) == 0 { @@ -540,6 +560,9 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi case model.BindEventType: newEntry, eventNodeBase := node.InsertBindEvent(event, imageTagID, generationType, at.Stats, dryRun) return newEntry, node, eventNodeBase, nil + case model.ConnectEventType: + newEntry, eventNodeBase := node.InsertConnectEvent(event, imageTagID, generationType, at.Stats, dryRun) + return newEntry, node, eventNodeBase, nil case model.SyscallsEventType: return node.InsertSyscalls(event, imageTagID, at.SyscallsMask, at.Stats, dryRun), node, nil, nil case model.NetworkFlowMonitorEventType: diff --git a/pkg/security/security_profile/activity_tree/activity_tree_graph.go b/pkg/security/security_profile/activity_tree/activity_tree_graph.go index bf929047e0d5..cdfa8f231f9b 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_graph.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_graph.go @@ -422,7 +422,7 @@ func (at *ActivityTree) prepareSocketNode(n *SocketNode, data *utils.Graph, proc for i, node := range n.Bind { bindNode := &utils.Node{ ID: processID.Derive(utils.NewNodeIDFromPtr(n), utils.NewNodeID(uint64(i+1))), - Label: "[" + node.IP + "]:" + strconv.FormatUint(uint64(node.Port), 10), + Label: "bind [" + node.IP + "]:" + strconv.FormatUint(uint64(node.Port), 10), Size: smallText, Color: networkColor, Shape: networkShape, @@ -442,6 +442,31 @@ func (at *ActivityTree) prepareSocketNode(n *SocketNode, data *utils.Graph, proc data.Nodes[bindNode.ID] = bindNode } + // prepare connect nodes + bindCount := uint64(len(n.Bind)) + for i, node := range n.Connect { + connectNode := &utils.Node{ + ID: processID.Derive(utils.NewNodeIDFromPtr(n), utils.NewNodeID(bindCount+uint64(i)+1)), + Label: "connect [" + node.IP + "]:" + strconv.FormatUint(uint64(node.Port), 10), + Size: smallText, + Color: networkColor, + Shape: networkShape, + } + + switch node.GenerationType { + case Runtime, Snapshot, Unknown: + connectNode.FillColor = networkRuntimeColor + case ProfileDrift: + connectNode.FillColor = networkProfileDriftColor + } + data.Edges = append(data.Edges, &utils.Edge{ + From: targetID, + To: connectNode.ID, + Color: networkColor, + }) + data.Nodes[connectNode.ID] = connectNode + } + return targetID } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go index 9183bb17b8ab..812b3378446d 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go @@ -496,6 +496,30 @@ func protoDecodeProtoSocket(sn *adproto.SocketNode, getIDFromImageTag func(strin socketNode.Bind = append(socketNode.Bind, psn) } + for _, connectNode := range sn.GetConnect() { + cn := &ConnectNode{ + MatchedRules: make([]*model.MatchedRule, 0, len(connectNode.MatchedRules)), + Port: uint16(connectNode.Port), + IP: connectNode.Ip, + Protocol: uint16(connectNode.Protocol), + NodeBase: NewNodeBase(), + } + + if connectNode.NodeBase != nil { + for tag, imageTagTimes := range connectNode.NodeBase.Seen { + firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) + lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) + cn.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) + } + } + + for _, rule := range connectNode.MatchedRules { + cn.MatchedRules = append(cn.MatchedRules, protoDecodeProtoMatchedRule(rule)) + } + + socketNode.Connect = append(socketNode.Connect, cn) + } + return socketNode } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go index 3eafeefe86dc..6388c0a9d55c 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go @@ -387,8 +387,9 @@ func socketNodeToProto(sn *SocketNode, tagIDToImageTag func(id uint64) string) * } psn := &adproto.SocketNode{ - Family: sn.Family, - Bind: make([]*adproto.BindNode, 0, len(sn.Bind)), + Family: sn.Family, + Bind: make([]*adproto.BindNode, 0, len(sn.Bind)), + Connect: make([]*adproto.ConnectNode, 0, len(sn.Connect)), } for _, bn := range sn.Bind { @@ -407,6 +408,22 @@ func socketNodeToProto(sn *SocketNode, tagIDToImageTag func(id uint64) string) * psn.Bind = append(psn.Bind, pbn) } + for _, cn := range sn.Connect { + pcn := &adproto.ConnectNode{ + MatchedRules: make([]*adproto.MatchedRule, 0, len(cn.MatchedRules)), + Port: uint32(cn.Port), + Ip: cn.IP, + Protocol: uint32(cn.Protocol), + NodeBase: nodeBaseToProto(&cn.NodeBase, tagIDToImageTag), + } + + for _, rule := range cn.MatchedRules { + pcn.MatchedRules = append(pcn.MatchedRules, matchedRuleToProto(rule)) + } + + psn.Connect = append(psn.Connect, pcn) + } + return psn } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_stats.go b/pkg/security/security_profile/activity_tree/activity_tree_stats.go index e9396b8058da..a923b1dcf901 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_stats.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_stats.go @@ -28,6 +28,7 @@ type Stats struct { IMDSNodes int64 SyscallNodes int64 FlowNodes int64 + ConnectNodes int64 CapabilityNodes int64 SizeBytes int64 @@ -78,6 +79,7 @@ func (stats *Stats) ApproximateSize() int64 { total += stats.IMDSNodes * int64(unsafe.Sizeof(IMDSNode{})) total += stats.SyscallNodes * int64(unsafe.Sizeof(SyscallNode{})) total += stats.FlowNodes * int64(unsafe.Sizeof(FlowNode{})) + total += stats.ConnectNodes * int64(unsafe.Sizeof(ConnectNode{})) total += stats.CapabilityNodes * int64(unsafe.Sizeof(CapabilityNode{})) return total } diff --git a/pkg/security/security_profile/activity_tree/process_node.go b/pkg/security/security_profile/activity_tree/process_node.go index d54a9b376c2d..ff8515428bc6 100644 --- a/pkg/security/security_profile/activity_tree/process_node.go +++ b/pkg/security/security_profile/activity_tree/process_node.go @@ -296,6 +296,14 @@ func (pn *ProcessNode) debug(w io.Writer, prefix string) { fmt.Fprintf(w, "%s - %s | %s\n", prefix, evt.CloudProvider, evt.Type) } } + for _, sock := range pn.Sockets { + if len(sock.Connect) > 0 { + fmt.Fprintf(w, "%s connect (%s):\n", prefix, sock.Family) + for _, conn := range sock.Connect { + fmt.Fprintf(w, "%s - %s:%d\n", prefix, conn.IP, conn.Port) + } + } + } if len(pn.Children) > 0 { fmt.Fprintf(w, "%s children:\n", prefix) for _, child := range pn.Children { @@ -554,6 +562,39 @@ func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTagID uint64, gene return newNode, bindNodeBase } +// InsertConnectEvent inserts a connect event in a process node. Returns whether a new entry was +// added and the NodeBase of the matched or newly created ConnectNode. +func (pn *ProcessNode) InsertConnectEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool) (bool, *NodeBase) { + if evt.Connect.SyscallEvent.Retval != 0 { + return false, nil + } + var newNode bool + evtFamily := model.AddressFamily(evt.Connect.AddrFamily).String() + + var sock *SocketNode + for _, s := range pn.Sockets { + if s.Family == evtFamily { + sock = s + } + } + if sock == nil { + sock = NewSocketNode(evtFamily, generationType) + if !dryRun { + stats.SocketNodes++ + stats.SizeBytes += sock.size() + pn.Sockets = append(pn.Sockets, sock) + } + newNode = true + } + + connectNew, connectNodeBase := sock.InsertConnectEvent(&evt.Connect, evt, imageTagID, generationType, evt.Rules, stats, dryRun) + if connectNew { + newNode = true + } + + return newNode, connectNodeBase +} + // InsertCapabilitiesUsageEvent inserts a capabilities usage event in a process node func (pn *ProcessNode) InsertCapabilitiesUsageEvent(evt *model.Event, imageTagID uint64, stats *Stats, dryRun bool) bool { hasNewCapabilitiesUsage := false @@ -613,6 +654,12 @@ func (pn *ProcessNode) TagAllNodes(imageTagID uint64, timestamp time.Time) { } for _, sock := range pn.Sockets { sock.AppendImageTagID(imageTagID, timestamp) + for _, bind := range sock.Bind { + bind.AppendImageTagID(imageTagID, timestamp) + } + for _, conn := range sock.Connect { + conn.AppendImageTagID(imageTagID, timestamp) + } } for _, scall := range pn.Syscalls { scall.AppendImageTagID(imageTagID, timestamp) diff --git a/pkg/security/security_profile/activity_tree/size_test.go b/pkg/security/security_profile/activity_tree/size_test.go index 7ba0726e3d28..31e20aaa5ea5 100644 --- a/pkg/security/security_profile/activity_tree/size_test.go +++ b/pkg/security/security_profile/activity_tree/size_test.go @@ -147,6 +147,7 @@ func TestApproximateSize_LegacyShallowSemantics(t *testing.T) { {"imds_only", Stats{IMDSNodes: 2}}, {"syscall_only", Stats{SyscallNodes: 4}}, {"flow_only", Stats{FlowNodes: 7}}, + {"connect_only", Stats{ConnectNodes: 3}}, {"capability_only", Stats{CapabilityNodes: 6}}, } for _, tt := range tests { diff --git a/pkg/security/security_profile/activity_tree/socket_node.go b/pkg/security/security_profile/activity_tree/socket_node.go index b23a84f47c2f..16f79c346f5a 100644 --- a/pkg/security/security_profile/activity_tree/socket_node.go +++ b/pkg/security/security_profile/activity_tree/socket_node.go @@ -27,12 +27,25 @@ type BindNode struct { Protocol uint16 } +// ConnectNode is used to store a connect node +type ConnectNode struct { + NodeBase + + MatchedRules []*model.MatchedRule + + GenerationType NodeGenerationType + Port uint16 + IP string + Protocol uint16 +} + // SocketNode is used to store a Socket node and associated events type SocketNode struct { NodeBase Family string GenerationType NodeGenerationType Bind []*BindNode + Connect []*ConnectNode } // size approximates this node's heap footprint, including all owned BindNodes. @@ -48,6 +61,10 @@ func (sn *SocketNode) size() int64 { for _, bind := range sn.Bind { s += bindSize(bind) } + s += sliceBackingBytes(cap(sn.Connect), unsafe.Sizeof((*ConnectNode)(nil))) + for _, conn := range sn.Connect { + s += connectSize(conn) + } return s } @@ -69,6 +86,23 @@ func (bn *BindNode) Matches(toMatch *BindNode) bool { return bn.Port == toMatch.Port && bn.IP == toMatch.IP && bn.Protocol == toMatch.Protocol } +// connectSize approximates the heap footprint of a single ConnectNode. +func connectSize(cn *ConnectNode) int64 { + if cn == nil { + return 0 + } + s := int64(unsafe.Sizeof(*cn)) + s += seenBytes(cn.NodeBase) + s += int64(len(cn.IP)) + s += sliceBackingBytes(cap(cn.MatchedRules), unsafe.Sizeof((*model.MatchedRule)(nil))) + return s +} + +// Matches returns true if ConnectNodes matches +func (cn *ConnectNode) Matches(toMatch *ConnectNode) bool { + return cn.Port == toMatch.Port && cn.IP == toMatch.IP && cn.Protocol == toMatch.Protocol +} + // Matches returns true if SocketNodes matches func (sn *SocketNode) Matches(toMatch *SocketNode) bool { return sn.Family == toMatch.Family @@ -92,7 +126,19 @@ func (sn *SocketNode) evictImageTag(imageTagID uint64) (bool, int64) { } clear(sn.Bind[len(newBind):]) sn.Bind = newBind - return len(newBind) == 0, removed + + newConnect := sn.Connect[:0] + for _, conn := range sn.Connect { + if conn.EvictImageTag(imageTagID) { + removed += connectSize(conn) + continue + } + newConnect = append(newConnect, conn) + } + clear(sn.Connect[len(newConnect):]) + sn.Connect = newConnect + + return len(newBind) == 0 && len(newConnect) == 0, removed } // InsertBindEvent inserts a bind event inside a socket node. When a new BindNode is @@ -142,3 +188,39 @@ func NewSocketNode(family string, generationType NodeGenerationType) *SocketNode node.NodeBase = NewNodeBase() return node } + +// InsertConnectEvent inserts a connect event inside a socket node. When a new ConnectNode is +// created the caller-provided stats is charged its size, keeping Stats.SizeBytes honest. +func (sn *SocketNode) InsertConnectEvent(evt *model.ConnectEvent, event *model.Event, imageTagID uint64, generationType NodeGenerationType, rules []*model.MatchedRule, stats *Stats, dryRun bool) (bool, *NodeBase) { + evtIP := utils.GetIPStringFromIPNet(evt.Addr.IPNet) + for _, n := range sn.Connect { + if evt.Addr.Port == n.Port && evtIP == n.IP && evt.Protocol == n.Protocol { + if !dryRun { + n.MatchedRules = model.AppendMatchedRule(n.MatchedRules, rules) + } + if imageTagID == 0 || n.HasImageTag(imageTagID) { + return false, &n.NodeBase + } + n.AppendImageTagID(imageTagID, event.ResolveEventTime()) + return false, &n.NodeBase + } + } + + if !dryRun { + node := &ConnectNode{ + MatchedRules: rules, + GenerationType: generationType, + Port: evt.Addr.Port, + IP: evtIP, + Protocol: evt.Protocol, + } + node.NodeBase = NewNodeBase() + + node.AppendImageTagID(imageTagID, event.ResolveEventTime()) + sn.Connect = append(sn.Connect, node) + stats.ConnectNodes++ + stats.SizeBytes += connectSize(node) + return true, &node.NodeBase + } + return true, nil +} From eac4904b82a4626fa578bbb6d360cf8f9b0486eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momar=20TOUR=C3=89?= Date: Thu, 17 Sep 2026 17:06:30 +0200 Subject: [PATCH 2/4] accept EINPROGRESS and EAGAIN --- pkg/security/security_profile/activity_tree/process_node.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/security/security_profile/activity_tree/process_node.go b/pkg/security/security_profile/activity_tree/process_node.go index ff8515428bc6..043b57390bb8 100644 --- a/pkg/security/security_profile/activity_tree/process_node.go +++ b/pkg/security/security_profile/activity_tree/process_node.go @@ -16,6 +16,7 @@ import ( "sort" "strconv" "strings" + "syscall" "time" "unsafe" @@ -565,7 +566,9 @@ func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTagID uint64, gene // InsertConnectEvent inserts a connect event in a process node. Returns whether a new entry was // added and the NodeBase of the matched or newly created ConnectNode. func (pn *ProcessNode) InsertConnectEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool) (bool, *NodeBase) { - if evt.Connect.SyscallEvent.Retval != 0 { + if evt.Connect.SyscallEvent.Retval != 0 && + evt.Connect.SyscallEvent.Retval != -int64(syscall.EINPROGRESS) && + evt.Connect.SyscallEvent.Retval != -int64(syscall.EAGAIN) { return false, nil } var newNode bool From ba52df7e492d15366a44665a3b0a566f5afef6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momar=20TOUR=C3=89?= Date: Wed, 23 Sep 2026 15:33:27 +0200 Subject: [PATCH 3/4] revert changes related to max_dump_size increase --- pkg/config/schema/yaml/system-probe-cws.yaml | 2 +- pkg/config/setup/system_probe_test.go | 2 +- pkg/security/config/config.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/config/schema/yaml/system-probe-cws.yaml b/pkg/config/schema/yaml/system-probe-cws.yaml index 73d2c173e829..82c6149ff4fe 100644 --- a/pkg/config/schema/yaml/system-probe-cws.yaml +++ b/pkg/config/schema/yaml/system-probe-cws.yaml @@ -728,7 +728,7 @@ properties: max_dump_size: node_type: setting type: integer - default: 3072 + default: 2560 sample_refresh_period: node_type: setting type: string diff --git a/pkg/config/setup/system_probe_test.go b/pkg/config/setup/system_probe_test.go index d027a5abb83b..863dac5ed5a3 100644 --- a/pkg/config/setup/system_probe_test.go +++ b/pkg/config/setup/system_probe_test.go @@ -58,7 +58,7 @@ func TestSystemProbeDefaultConfig(t *testing.T) { {key: "discovery.service_collection_max_consecutive_timeouts", defaultValue: 5}, {key: "discovery.service_collection_min_process_age", defaultValue: time.Minute}, {key: "runtime_security_config.security_profile.v2.enabled", defaultValue: true}, - {key: "runtime_security_config.security_profile.v2.max_dump_size", defaultValue: 3072}, + {key: "runtime_security_config.security_profile.v2.max_dump_size", defaultValue: 2560}, {key: "runtime_security_config.security_profile.v2.event_types", defaultValue: []string{"exec", "open", "dns", "bind", "connect"}}, } { t.Run(tc.key, func(t *testing.T) { diff --git a/pkg/security/config/config.go b/pkg/security/config/config.go index 4f7c0a259889..d3efbdca2683 100644 --- a/pkg/security/config/config.go +++ b/pkg/security/config/config.go @@ -521,7 +521,7 @@ type RuntimeSecurityConfig struct { // description: SecurityProfileV2MaxDumpSize returns the V2-only max profile size in bytes. // visibility: private - // default_value: 3072 + // default_value: 5120 SecurityProfileV2MaxDumpSize func() int // description: AnomalyDetectionEventTypes defines the list of events that should be allowed to generate anomaly detections From 3284795ab3a1937974bb2d9e7433de62616ba1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momar=20TOUR=C3=89?= Date: Wed, 23 Sep 2026 17:23:06 +0200 Subject: [PATCH 4/4] changes after review --- .../ebpf/c/include/hooks/network/connect.h | 8 ++-- .../activity_tree/process_node.go | 20 ++++++---- .../activity_tree/socket_node.go | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/pkg/security/ebpf/c/include/hooks/network/connect.h b/pkg/security/ebpf/c/include/hooks/network/connect.h index 4d4ff090386b..f6fc506f41c8 100644 --- a/pkg/security/ebpf/c/include/hooks/network/connect.h +++ b/pkg/security/ebpf/c/include/hooks/network/connect.h @@ -35,13 +35,15 @@ int __attribute__((always_inline)) sys_connect_ret_impl(void *ctx, int retval, e return 0; } - approve_syscall(syscall, connect_approvers); - - // EAGAIN may be returned on Fedora 37 (kernel 6.0.7-301.fc37.x86_64) + // Bail out on failed connects before the approvers, otherwise a dropped event would still + // pollute connect_samples and suppress later successful connects to the same endpoint. + // EAGAIN may be returned on Fedora 37 (kernel 6.0.7-301.fc37.x86_64). if (IS_UNHANDLED_ERROR(retval) && retval != -EINPROGRESS && retval != -EAGAIN) { return 0; } + approve_syscall(syscall, connect_approvers); + register_connecting_flow(syscall->connect.sk, syscall->connect.pid_tgid ? syscall->connect.pid_tgid : bpf_get_current_pid_tgid()); // these probes are also loaded with the network probes, only send the event when a rule asks for it diff --git a/pkg/security/security_profile/activity_tree/process_node.go b/pkg/security/security_profile/activity_tree/process_node.go index 043b57390bb8..028f23c1131e 100644 --- a/pkg/security/security_profile/activity_tree/process_node.go +++ b/pkg/security/security_profile/activity_tree/process_node.go @@ -866,16 +866,20 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // Note: NetworkDeviceNode doesn't embed NodeBase so we skip eviction for network devices - // Evict unused socket nodes - for i := len(pn.Sockets) - 1; i >= 0; i-- { - socketNode := pn.Sockets[i] - if socketNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if socketNode.SeenIsEmpty() { - removedBytes += socketNode.size() - pn.Sockets = append(pn.Sockets[:i], pn.Sockets[i+1:]...) - } + // Evict unused socket nodes: children age out by their own timestamps, and a socket is + // removed only once it holds no children (see SocketNode.evictBeforeTimestamp). + newSockets := pn.Sockets[:0] + for _, socketNode := range pn.Sockets { + socketEmpty, socketRemoved := socketNode.evictBeforeTimestamp(before) + removedBytes += socketRemoved + if socketEmpty { + removedBytes += socketNode.size() + continue } + newSockets = append(newSockets, socketNode) } + clear(pn.Sockets[len(newSockets):]) + pn.Sockets = newSockets // Evict unused capability nodes for i := len(pn.Capabilities) - 1; i >= 0; i-- { diff --git a/pkg/security/security_profile/activity_tree/socket_node.go b/pkg/security/security_profile/activity_tree/socket_node.go index 16f79c346f5a..5ed28f83b036 100644 --- a/pkg/security/security_profile/activity_tree/socket_node.go +++ b/pkg/security/security_profile/activity_tree/socket_node.go @@ -9,6 +9,7 @@ package activitytree import ( + "time" "unsafe" "github.com/DataDog/datadog-agent/pkg/security/secl/model" @@ -141,6 +142,42 @@ func (sn *SocketNode) evictImageTag(imageTagID uint64) (bool, int64) { return len(newBind) == 0 && len(newConnect) == 0, removed } +// evictBeforeTimestamp evicts bind/connect children by their own NodeBase timestamps (a +// SocketNode's base is only stamped by TagAllNodes, not on insert, so it can't drive child +// eviction). Returns (socketIsEmpty, bytesRemoved): the caller subtracts bytesRemoved and, when +// empty, sn.size(). A childless socket carries no info, so it's reported empty regardless of base. +func (sn *SocketNode) evictBeforeTimestamp(before time.Time) (bool, int64) { + var removed int64 + + // Filter in place, clearing the tail so evicted pointers aren't pinned (mirrors evictImageTag). + newBind := sn.Bind[:0] + for _, bind := range sn.Bind { + if bind.NodeBase.EvictBeforeTimestamp(before) > 0 && bind.SeenIsEmpty() { + removed += bindSize(bind) + continue + } + newBind = append(newBind, bind) + } + clear(sn.Bind[len(newBind):]) + sn.Bind = newBind + + newConnect := sn.Connect[:0] + for _, conn := range sn.Connect { + if conn.NodeBase.EvictBeforeTimestamp(before) > 0 && conn.SeenIsEmpty() { + removed += connectSize(conn) + continue + } + newConnect = append(newConnect, conn) + } + clear(sn.Connect[len(newConnect):]) + sn.Connect = newConnect + + // Age out the socket's own image-tag timestamps so stale tags don't accumulate. + sn.NodeBase.EvictBeforeTimestamp(before) + + return len(sn.Bind) == 0 && len(sn.Connect) == 0, removed +} + // InsertBindEvent inserts a bind event inside a socket node. When a new BindNode is // created the caller-provided stats is charged its size, keeping Stats.SizeBytes honest // for bind-heavy workloads where the previous accounting only charged the socket once