From fef6e38a78cff9c0cb0a5cecaa2c2811d7ad4d92 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Thu, 17 Sep 2026 14:39:24 +0000 Subject: [PATCH] rego: Fix exponential growth of `matches` metadata when exec_processes contains duplicate entries When a policy has two or more duplicated exec_processes entries in the container definition, we end up storing duplicated matches in the metadata due to this list comprehension: possible_after_initial_containers := [container | container := data.metadata.matches[input.containerID][_] ... some process in container.exec_processes command_ok(process.command) ] ... containers := possible_after_caps_containers updateMatches := { "name": "matches", "action": "update", "key": input.containerID, "value": containers, } Then the next time the same exec is attempted, we will end up doubling it again, and eventually if this happens repeatedly (for example if it's a liveness probe), we will run out of memory. Fix this by getting rid of the `some` in this list comprehension (as well as a similar case in the exec process's signals enforcement), and instead extracting it out into a rule, which can have `some`s without creating duplicate entries in the caller's list. Add test that checks we can do this 100 times without blowing up. Assisted-by: GitHub-Copilot Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 34 +++++---- pkg/securitypolicy/regopolicy_linux_test.go | 74 ++++++++++++++++++ pkg/securitypolicy/regopolicy_windows_test.go | 76 +++++++++++++++++++ 3 files changed, 168 insertions(+), 16 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index e462f4f34e..fba22bac02 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -250,6 +250,17 @@ command_ok(command) if { } } +exec_process_matches(container) if { + some process in container.exec_processes + command_ok(process.command) +} + +exec_process_signal_matches(container) if { + some process in container.exec_processes + command_ok(process.command) + signal_ok(process.signals) +} + # An env rule can be of two forms: # { # "pattern": "name=value", @@ -933,8 +944,7 @@ exec_in_container := {"metadata": [updateMatches], workingDirectory_ok(container.working_dir) noNewPrivileges_ok(container.no_new_privileges) user_ok(container.user) - some process in container.exec_processes - command_ok(process.command) + exec_process_matches(container) ] count(possible_after_initial_containers) > 0 @@ -985,8 +995,7 @@ exec_in_container := {"metadata": [updateMatches], # the narrowing process. workingDirectory_ok(container.working_dir) user_ok(container.user) - some process in container.exec_processes - command_ok(process.command) + exec_process_matches(container) ] count(possible_after_initial_containers) > 0 @@ -1047,9 +1056,7 @@ signal_container_process := {"metadata": [updateMatches], "allowed": true} if { not input.isInitProcess containers := [container | container := data.metadata.matches[input.containerID][_] - some process in container.exec_processes - command_ok(process.command) - signal_ok(process.signals) + exec_process_signal_matches(container) ] count(containers) > 0 @@ -2037,8 +2044,7 @@ command_matches if { command_matches if { input.rule == "exec_in_container" some container in data.metadata.matches[input.containerID] - some process in container.exec_processes - command_ok(process.command) + exec_process_matches(container) } command_matches if { @@ -2126,8 +2132,7 @@ errors contains "missing required environment variable" if { noNewPrivileges_ok(container.no_new_privileges) user_ok(container.user) workingDirectory_ok(container.working_dir) - some process in container.exec_processes - command_ok(process.command) + exec_process_matches(container) ] count(possible_containers) > 0 @@ -2243,9 +2248,7 @@ signal_allowed if { signal_allowed if { not input.isInitProcess some container in data.metadata.matches[input.containerID] - some process in container.exec_processes - command_ok(process.command) - signal_ok(process.signals) + exec_process_signal_matches(container) } errors contains "target isn't allowed to receive the signal" if { @@ -2686,8 +2689,7 @@ errors contains "capabilities don't match" if { workingDirectory_ok(container.working_dir) noNewPrivileges_ok(container.no_new_privileges) user_ok(container.user) - some process in container.exec_processes - command_ok(process.command) + exec_process_matches(container) ] count(possible_after_initial_containers) > 0 diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index 52500dfcc6..295af14fa4 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -2656,6 +2656,80 @@ func Test_Rego_ExecInContainerPolicy(t *testing.T) { } } +func Test_Rego_ExecInContainerPolicy_DuplicatedCommand(t *testing.T) { + const execCount = 100 + + constraints := generateConstraints(testRand, 1) + container := constraints.containers[0] + process := generateContainerExecProcess(testRand) + container.ExecProcesses = []containerExecProcess{process.clone(), process.clone()} + + tc, err := setupRegoRunningContainerTest(constraints, false) + if err != nil { + t.Fatal(err) + } + + running := tc.runningContainers[0] + capabilities := container.Capabilities.toExternal() + user := buildIDNameFromConfig(container.User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(container.User, testRand) + + for i := 0; i < execCount; i++ { + _, _, _, err := tc.policy.EnforceExecInContainerPolicy(constraints.ctx, running.containerID, process.Command, running.envList, container.WorkingDir, container.NoNewPrivileges, user, groups, container.User.Umask, &capabilities) + if err != nil { + t.Fatalf("exec %d failed: %v", i+1, err) + } + + rawMatches, err := tc.policy.rego.GetMetadataMapValue("matches", running.containerID) + if err != nil { + t.Fatalf("get matches metadata after exec %d: %v", i+1, err) + } + matches, ok := rawMatches.([]interface{}) + if !ok { + t.Fatalf("matches metadata has type %T, want []interface{}", rawMatches) + } + if len(matches) != 1 { + t.Fatalf("matches metadata has %d entries after exec %d, want 1", len(matches), i+1) + } + } +} + +func Test_Rego_SignalContainerProcessPolicy_DuplicatedCommand(t *testing.T) { + const signalCount = 100 + + constraints := generateConstraints(testRand, 1) + container := constraints.containers[0] + process := generateContainerExecProcess(testRand) + process.Signals = generateListOfSignals(testRand, 1, 4) + container.ExecProcesses = []containerExecProcess{process.clone(), process.clone()} + + tc, err := setupRegoRunningContainerTest(constraints, false) + if err != nil { + t.Fatal(err) + } + + running := tc.runningContainers[0] + signal := selectSignalFromSignals(testRand, process.Signals) + + for i := 0; i < signalCount; i++ { + if err := tc.policy.EnforceSignalContainerProcessPolicy(constraints.ctx, running.containerID, signal, false, process.Command); err != nil { + t.Fatalf("signal %d failed: %v", i+1, err) + } + + rawMatches, err := tc.policy.rego.GetMetadataMapValue("matches", running.containerID) + if err != nil { + t.Fatalf("get matches metadata after signal %d: %v", i+1, err) + } + matches, ok := rawMatches.([]interface{}) + if !ok { + t.Fatalf("matches metadata has type %T, want []interface{}", rawMatches) + } + if len(matches) != 1 { + t.Fatalf("matches metadata has %d entries after signal %d, want 1", len(matches), i+1) + } + } +} + func Test_Rego_ExecInContainerPolicy_No_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { tc, err := setupRegoRunningContainerTest(p, false) diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go index e9eeff622d..a5b384620d 100644 --- a/pkg/securitypolicy/regopolicy_windows_test.go +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -394,6 +394,82 @@ func Test_Rego_ExecInContainerPolicy_Windows(t *testing.T) { } } +func Test_Rego_ExecInContainerPolicy_DuplicatedCommand_Windows(t *testing.T) { + const execCount = 100 + + constraints := generateWindowsConstraints(testRand, 1) + container := constraints.containers[0] + process := generateWindowsContainerExecProcess(testRand) + container.ExecProcesses = []windowsContainerExecProcess{process, process} + + tc, err := setupRegoRunningWindowsContainerTest(constraints) + if err != nil { + t.Fatal(err) + } + + running := tc.runningContainers[0] + user := IDName{Name: container.User} + commandLine := []string{process.Command} + + for i := 0; i < execCount; i++ { + _, _, _, err := tc.policy.EnforceExecInContainerPolicyV2(constraints.ctx, running.containerID, commandLine, running.envList, container.WorkingDir, user, nil) + if err != nil { + t.Fatalf("exec %d failed: %v", i+1, err) + } + + rawMatches, err := tc.policy.rego.GetMetadataMapValue("matches", running.containerID) + if err != nil { + t.Fatalf("get matches metadata after exec %d: %v", i+1, err) + } + matches, ok := rawMatches.([]interface{}) + if !ok { + t.Fatalf("matches metadata has type %T, want []interface{}", rawMatches) + } + if len(matches) != 1 { + t.Fatalf("matches metadata has %d entries after exec %d, want 1", len(matches), i+1) + } + } +} + +func Test_Rego_SignalContainerProcessPolicy_DuplicatedCommand_Windows(t *testing.T) { + const signalCount = 100 + + constraints := generateWindowsConstraints(testRand, 1) + container := constraints.containers[0] + process := generateWindowsContainerExecProcess(testRand) + container.ExecProcesses = []windowsContainerExecProcess{process, process} + + tc, err := setupRegoRunningWindowsContainerTest(constraints) + if err != nil { + t.Fatal(err) + } + + running := tc.runningContainers[0] + signal := selectSignalFromWindowsSignals(testRand, process.Signals) + opts := &SignalContainerOptions{ + WindowsSignal: signal, + WindowsCommand: []string{process.Command}, + } + + for i := 0; i < signalCount; i++ { + if err := tc.policy.EnforceSignalContainerProcessPolicyV2(constraints.ctx, running.containerID, opts); err != nil { + t.Fatalf("signal %d failed: %v", i+1, err) + } + + rawMatches, err := tc.policy.rego.GetMetadataMapValue("matches", running.containerID) + if err != nil { + t.Fatalf("get matches metadata after signal %d: %v", i+1, err) + } + matches, ok := rawMatches.([]interface{}) + if !ok { + t.Fatalf("matches metadata has type %T, want []interface{}", rawMatches) + } + if len(matches) != 1 { + t.Fatalf("matches metadata has %d entries after signal %d, want 1", len(matches), i+1) + } + } +} + func Test_Rego_ExecInContainerPolicy_No_Matches_Windows(t *testing.T) { f := func(p *generatedWindowsConstraints) bool { tc, err := setupRegoRunningWindowsContainerTest(p)