diff --git a/pkg/containerprofilemanager/v1/event_reporting.go b/pkg/containerprofilemanager/v1/event_reporting.go index 077875fe1a..dee91b4a68 100644 --- a/pkg/containerprofilemanager/v1/event_reporting.go +++ b/pkg/containerprofilemanager/v1/event_reporting.go @@ -19,6 +19,13 @@ import ( var procRegex = regexp.MustCompile(`^/proc/\d+`) +// maxIdentifiedCallStacksPerContainer bounds the number of distinct identified +// call stacks retained per container between reporting flushes. Each distinct +// call site produces a distinct (deduped-by-hash) call stack; without a cap a +// workload that launches many distinct executables grows this map unbounded and +// drives the node-agent OOM. +const maxIdentifiedCallStacksPerContainer = 512 + // ReportCapability reports a capability event for a container func (cpm *ContainerProfileManager) ReportCapability(containerID, capability string) { err := cpm.withContainer(containerID, func(data *containerData) (int, error) { @@ -233,6 +240,15 @@ func (cpm *ContainerProfileManager) ReportIdentifiedCallStack(containerID string return 0, nil } + // Bound memory: a container that launches many distinct executables (or a + // process-enumeration/fork-churn attack) produces a distinct call stack per + // call site, which would otherwise grow this map unbounded within a + // reporting window and drive the node-agent OOM. The profile only needs a + // representative set, so stop retaining new call stacks past the cap. + if data.callStacks.Len() >= maxIdentifiedCallStacksPerContainer { + return 0, nil + } + // Add to call stacks map data.callStacks.Set(callStackIdentifier, callStack) return size.Of(callStack), nil diff --git a/pkg/objectcache/callstackcache/callstackcache.go b/pkg/objectcache/callstackcache/callstackcache.go index c709281a81..ad86098aca 100644 --- a/pkg/objectcache/callstackcache/callstackcache.go +++ b/pkg/objectcache/callstackcache/callstackcache.go @@ -7,6 +7,12 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) +// maxCallStacksInTree bounds the number of distinct call sites (CallIDs) indexed +// in a container's search tree. A container that launches many distinct +// executables produces many distinct call sites; without a cap the tree, trie and +// per-CallID paths grow unbounded and inflate node-agent memory. +const maxCallStacksInTree = 512 + // BidirectionalNode extends the call stack node with parent reference type BidirectionalNode struct { Frame v1beta1.StackFrame @@ -39,6 +45,12 @@ func (t *CallStackSearchTree) AddCallStack(stack v1beta1.IdentifiedCallStack) { return } + // Bound memory: stop indexing new call sites past the cap (see + // maxCallStacksInTree). The tree already retains a representative set. + if len(t.Roots) >= maxCallStacksInTree { + return + } + // Get all paths from the call stack paths := getCallStackPaths(stack.CallStack) if len(paths) == 0 {