Skip to content

Commit e931899

Browse files
committed
Add tests for sandbox sysfs special-case
Assisted-by: GitHub Copilot:claude-opus-4.7 Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
1 parent 025c7eb commit e931899

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

pkg/securitypolicy/regopolicy_linux_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7367,3 +7367,169 @@ func substituteUVMPath(sandboxID string, m mountInternal) mountInternal {
73677367
}
73687368
return m
73697369
}
7370+
7371+
// setupSandboxSysfsTest builds a policy with exactly two containers: a
7372+
// non-elevated container that the test request will match (acting as the
7373+
// sandbox/pause container in the policy) and a separate elevated container so
7374+
// that candidate_containers has at least one container with allow_elevated set
7375+
// - this gates the sandbox sysfs carve-out in framework.rego.
7376+
//
7377+
// DefaultCRIMounts() is used as the policy's default mount set so the /sys
7378+
// "ro" mount is in data.defaultMounts.
7379+
func setupSandboxSysfsTest(t *testing.T) (
7380+
policy *regoEnforcer,
7381+
sandboxContainer *securityPolicyContainer,
7382+
containerID string,
7383+
envList []string,
7384+
user IDName,
7385+
groups []IDName,
7386+
capabilities *oci.LinuxCapabilities,
7387+
) {
7388+
t.Helper()
7389+
7390+
gc := generateConstraints(testRand, 2)
7391+
if len(gc.containers) < 2 {
7392+
// Force exactly two containers if generator under-generated.
7393+
for len(gc.containers) < 2 {
7394+
gc.containers = append(gc.containers, generateConstraintsContainer(testRand, 1, maxLayersInGeneratedContainer))
7395+
}
7396+
}
7397+
7398+
// containers[0] is the one the test will exercise: act as the pause
7399+
// container, never elevated. Strip its own mount constraints so the
7400+
// input mount list is matched purely against defaultMounts and the
7401+
// sandbox carve-out.
7402+
sandboxContainer = gc.containers[0]
7403+
sandboxContainer.AllowElevated = false
7404+
sandboxContainer.Mounts = nil
7405+
7406+
// At least one other container in the policy must be elevated to enable
7407+
// the sandbox sysfs carve-out.
7408+
gc.containers[1].AllowElevated = true
7409+
7410+
defaultMounts := DefaultCRIMounts()
7411+
privilegedMounts := DefaultCRIPrivilegedMounts()
7412+
7413+
var err error
7414+
policy, err = newRegoPolicy(gc.toPolicy().marshalRego(), defaultMounts, privilegedMounts, testOSType)
7415+
if err != nil {
7416+
t.Fatalf("failed to create policy: %v", err)
7417+
}
7418+
7419+
containerID, err = mountImageForContainer(policy, sandboxContainer)
7420+
if err != nil {
7421+
t.Fatalf("failed to mount image for sandbox container: %v", err)
7422+
}
7423+
7424+
envList = buildEnvironmentVariablesFromEnvRules(sandboxContainer.EnvRules, testRand)
7425+
user = buildIDNameFromConfig(sandboxContainer.User.UserIDName, testRand)
7426+
groups = buildGroupIDNamesFromUser(sandboxContainer.User, testRand)
7427+
capsExternal := copyLinuxCapabilities(sandboxContainer.Capabilities.toExternal())
7428+
capabilities = &capsExternal
7429+
return policy, sandboxContainer, containerID, envList, user, groups, capabilities
7430+
}
7431+
7432+
// sysfsMount returns a /sys sysfs mount with the given mode ("ro" or "rw"),
7433+
// matching the option set produced by containerd's CRI sandbox spec.
7434+
func sysfsMount(mode string) oci.Mount {
7435+
return oci.Mount{
7436+
Source: "sysfs",
7437+
Destination: "/sys",
7438+
Type: "sysfs",
7439+
Options: []string{"nosuid", "noexec", "nodev", mode},
7440+
}
7441+
}
7442+
7443+
func Test_Rego_SandboxSysfsCarveOut(t *testing.T) {
7444+
cases := []struct {
7445+
name string
7446+
isSandboxContainer bool
7447+
mode string
7448+
expectAllowed bool
7449+
}{
7450+
{"sandbox_ro", true, "ro", true},
7451+
{"sandbox_rw", true, "rw", true},
7452+
{"non_sandbox_ro", false, "ro", true},
7453+
{"non_sandbox_rw", false, "rw", false},
7454+
}
7455+
7456+
for _, tc := range cases {
7457+
t.Run(tc.name, func(t *testing.T) {
7458+
// Each subtest gets its own policy because a successful
7459+
// create_container records the container as "started" and a
7460+
// second call for the same containerID would be denied.
7461+
policy, sandboxContainer, containerID, envList, user, groups, capabilities :=
7462+
setupSandboxSysfsTest(t)
7463+
noNewPriv := sandboxContainer.NoNewPrivileges
7464+
privileged := false
7465+
mounts := []oci.Mount{sysfsMount(tc.mode)}
7466+
_, _, _, err := policy.EnforceCreateContainerPolicyV2(
7467+
context.Background(),
7468+
containerID,
7469+
sandboxContainer.Command,
7470+
envList,
7471+
sandboxContainer.WorkingDir,
7472+
mounts,
7473+
user,
7474+
&CreateContainerOptions{
7475+
SandboxID: testDataGenerator.uniqueSandboxID(),
7476+
Privileged: &privileged,
7477+
NoNewPrivileges: &noNewPriv,
7478+
Groups: groups,
7479+
Umask: sandboxContainer.User.Umask,
7480+
Capabilities: capabilities,
7481+
SeccompProfileSHA256: sandboxContainer.SeccompProfileSHA256,
7482+
IsSandboxContainer: tc.isSandboxContainer,
7483+
},
7484+
)
7485+
if tc.expectAllowed {
7486+
if err != nil {
7487+
t.Errorf("expected allowed, got error: %v", err)
7488+
}
7489+
} else {
7490+
if err == nil {
7491+
t.Errorf("expected denied, got allowed")
7492+
} else {
7493+
assertDecisionJSONContains(t, err, "invalid mount list", "/sys")
7494+
}
7495+
}
7496+
})
7497+
}
7498+
}
7499+
7500+
// Test_Rego_SandboxSysfsCarveOut_PrivilegedRequestDenied verifies that the
7501+
// sysfs carve-out for the sandbox container does NOT also grant privilege:
7502+
// even with IsSandboxContainer=true and the /sys rw mount accepted, if the
7503+
// host requests Privileged=true for a sandbox container whose policy entry
7504+
// does not allow elevation, create_container must still be denied.
7505+
func Test_Rego_SandboxSysfsCarveOut_PrivilegedRequestDenied(t *testing.T) {
7506+
policy, sandboxContainer, containerID, envList, user, groups, capabilities :=
7507+
setupSandboxSysfsTest(t)
7508+
7509+
noNewPriv := sandboxContainer.NoNewPrivileges
7510+
privileged := true
7511+
mounts := []oci.Mount{sysfsMount("rw")}
7512+
_, _, _, err := policy.EnforceCreateContainerPolicyV2(
7513+
context.Background(),
7514+
containerID,
7515+
sandboxContainer.Command,
7516+
envList,
7517+
sandboxContainer.WorkingDir,
7518+
mounts,
7519+
user,
7520+
&CreateContainerOptions{
7521+
SandboxID: testDataGenerator.uniqueSandboxID(),
7522+
Privileged: &privileged,
7523+
NoNewPrivileges: &noNewPriv,
7524+
Groups: groups,
7525+
Umask: sandboxContainer.User.Umask,
7526+
Capabilities: capabilities,
7527+
SeccompProfileSHA256: sandboxContainer.SeccompProfileSHA256,
7528+
IsSandboxContainer: true,
7529+
},
7530+
)
7531+
if err == nil {
7532+
t.Fatal("expected create_container to be denied when Privileged=true for a non-elevated sandbox container, but it was allowed")
7533+
}
7534+
assertDecisionJSONContains(t, err, "privileged escalation not allowed")
7535+
}

0 commit comments

Comments
 (0)