@@ -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