github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/devices/systemd_test.go (about) 1 package devices 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "strings" 9 "testing" 10 11 "github.com/opencontainers/runc/internal/testutil" 12 "github.com/opencontainers/runc/libcontainer/cgroups" 13 "github.com/opencontainers/runc/libcontainer/cgroups/systemd" 14 "github.com/opencontainers/runc/libcontainer/configs" 15 "github.com/opencontainers/runc/libcontainer/devices" 16 ) 17 18 // TestPodSkipDevicesUpdate checks that updating a pod having SkipDevices: true 19 // does not result in spurious "permission denied" errors in a container 20 // running under the pod. The test is somewhat similar in nature to the 21 // @test "update devices [minimal transition rules]" in tests/integration, 22 // but uses a pod. 23 func TestPodSkipDevicesUpdate(t *testing.T) { 24 if !systemd.IsRunningSystemd() { 25 t.Skip("Test requires systemd.") 26 } 27 if os.Geteuid() != 0 { 28 t.Skip("Test requires root.") 29 } 30 // https://github.com/opencontainers/runc/issues/3743. 31 testutil.SkipOnCentOS(t, "Flaky (#3743)", 7) 32 33 podName := "system-runc_test_pod" + t.Name() + ".slice" 34 podConfig := &configs.Cgroup{ 35 Systemd: true, 36 Parent: "system.slice", 37 Name: podName, 38 Resources: &configs.Resources{ 39 PidsLimit: 42, 40 Memory: 32 * 1024 * 1024, 41 SkipDevices: true, 42 }, 43 } 44 // Create "pod" cgroup (a systemd slice to hold containers). 45 pm := newManager(t, podConfig) 46 if err := pm.Apply(-1); err != nil { 47 t.Fatal(err) 48 } 49 if err := pm.Set(podConfig.Resources); err != nil { 50 t.Fatal(err) 51 } 52 53 containerConfig := &configs.Cgroup{ 54 Parent: podName, 55 ScopePrefix: "test", 56 Name: "PodSkipDevicesUpdate", 57 Resources: &configs.Resources{ 58 Devices: []*devices.Rule{ 59 // Allow access to /dev/null. 60 { 61 Type: devices.CharDevice, 62 Major: 1, 63 Minor: 3, 64 Permissions: "rwm", 65 Allow: true, 66 }, 67 }, 68 }, 69 } 70 71 // Create a "container" within the "pod" cgroup. 72 // This is not a real container, just a process in the cgroup. 73 cmd := exec.Command("sleep", "infinity") 74 cmd.Env = append(os.Environ(), "LANG=C") 75 var stderr bytes.Buffer 76 cmd.Stderr = &stderr 77 if err := cmd.Start(); err != nil { 78 t.Fatal(err) 79 } 80 // Make sure to not leave a zombie. 81 defer func() { 82 // These may fail, we don't care. 83 _ = cmd.Process.Kill() 84 _ = cmd.Wait() 85 }() 86 87 // Put the process into a cgroup. 88 cm := newManager(t, containerConfig) 89 if err := cm.Apply(cmd.Process.Pid); err != nil { 90 t.Fatal(err) 91 } 92 // Check that we put the "container" into the "pod" cgroup. 93 if !strings.HasPrefix(cm.Path("devices"), pm.Path("devices")) { 94 t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q", 95 cm.Path("devices"), pm.Path("devices")) 96 } 97 if err := cm.Set(containerConfig.Resources); err != nil { 98 t.Fatal(err) 99 } 100 101 // Now update the pod a few times. 102 for i := 0; i < 42; i++ { 103 podConfig.Resources.PidsLimit++ 104 podConfig.Resources.Memory += 1024 * 1024 105 if err := pm.Set(podConfig.Resources); err != nil { 106 t.Fatal(err) 107 } 108 } 109 // Kill the "container". 110 if err := cmd.Process.Kill(); err != nil { 111 t.Fatal(err) 112 } 113 114 _ = cmd.Wait() 115 116 // "Container" stderr should be empty. 117 if stderr.Len() != 0 { 118 t.Fatalf("container stderr not empty: %s", stderr.String()) 119 } 120 } 121 122 func testSkipDevices(t *testing.T, skipDevices bool, expected []string) { 123 if !systemd.IsRunningSystemd() { 124 t.Skip("Test requires systemd.") 125 } 126 if os.Geteuid() != 0 { 127 t.Skip("Test requires root.") 128 } 129 // https://github.com/opencontainers/runc/issues/3743. 130 testutil.SkipOnCentOS(t, "Flaky (#3743)", 7) 131 132 podConfig := &configs.Cgroup{ 133 Parent: "system.slice", 134 Name: "system-runc_test_pods.slice", 135 Resources: &configs.Resources{ 136 SkipDevices: skipDevices, 137 }, 138 } 139 // Create "pods" cgroup (a systemd slice to hold containers). 140 pm := newManager(t, podConfig) 141 if err := pm.Apply(-1); err != nil { 142 t.Fatal(err) 143 } 144 if err := pm.Set(podConfig.Resources); err != nil { 145 t.Fatal(err) 146 } 147 148 config := &configs.Cgroup{ 149 Parent: "system-runc_test_pods.slice", 150 ScopePrefix: "test", 151 Name: "SkipDevices", 152 Resources: &configs.Resources{ 153 Devices: []*devices.Rule{ 154 // Allow access to /dev/full only. 155 { 156 Type: devices.CharDevice, 157 Major: 1, 158 Minor: 7, 159 Permissions: "rwm", 160 Allow: true, 161 }, 162 }, 163 }, 164 } 165 166 // Create a "container" within the "pods" cgroup. 167 // This is not a real container, just a process in the cgroup. 168 cmd := exec.Command("bash", "-c", "read; echo > /dev/full; cat /dev/null; true") 169 cmd.Env = append(os.Environ(), "LANG=C") 170 stdinR, stdinW, err := os.Pipe() 171 if err != nil { 172 t.Fatal(err) 173 } 174 cmd.Stdin = stdinR 175 var stderr bytes.Buffer 176 cmd.Stderr = &stderr 177 err = cmd.Start() 178 stdinR.Close() 179 defer stdinW.Close() 180 if err != nil { 181 t.Fatal(err) 182 } 183 // Make sure to not leave a zombie. 184 defer func() { 185 // These may fail, we don't care. 186 _, _ = stdinW.WriteString("hey\n") 187 _ = cmd.Wait() 188 }() 189 190 // Put the process into a cgroup. 191 m := newManager(t, config) 192 if err := m.Apply(cmd.Process.Pid); err != nil { 193 t.Fatal(err) 194 } 195 // Check that we put the "container" into the "pod" cgroup. 196 if !strings.HasPrefix(m.Path("devices"), pm.Path("devices")) { 197 t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q", 198 m.Path("devices"), pm.Path("devices")) 199 } 200 if err := m.Set(config.Resources); err != nil { 201 // failed to write "c 1:7 rwm": write /sys/fs/cgroup/devices/system.slice/system-runc_test_pods.slice/test-SkipDevices.scope/devices.allow: operation not permitted 202 if skipDevices == false && strings.HasSuffix(err.Error(), "/devices.allow: operation not permitted") { 203 // Cgroup v1 devices controller gives EPERM on trying 204 // to enable devices that are not enabled 205 // (skipDevices=false) in a parent cgroup. 206 // If this happens, test is passing. 207 return 208 } 209 t.Fatal(err) 210 } 211 212 // Check that we can access /dev/full but not /dev/zero. 213 if _, err := stdinW.WriteString("wow\n"); err != nil { 214 t.Fatal(err) 215 } 216 if err := cmd.Wait(); err != nil { 217 t.Fatal(err) 218 } 219 for _, exp := range expected { 220 if !strings.Contains(stderr.String(), exp) { 221 t.Errorf("expected %q, got: %s", exp, stderr.String()) 222 } 223 } 224 } 225 226 func TestSkipDevicesTrue(t *testing.T) { 227 testSkipDevices(t, true, []string{ 228 "echo: write error: No space left on device", 229 "cat: /dev/null: Operation not permitted", 230 }) 231 } 232 233 func TestSkipDevicesFalse(t *testing.T) { 234 // If SkipDevices is not set for the parent slice, access to both 235 // devices should fail. This is done to assess the test correctness. 236 // For cgroup v1, we check for m.Set returning EPERM. 237 // For cgroup v2, we check for the errors below. 238 testSkipDevices(t, false, []string{ 239 "/dev/full: Operation not permitted", 240 "cat: /dev/null: Operation not permitted", 241 }) 242 } 243 244 func testFindDeviceGroup() error { 245 const ( 246 major = 136 247 group = "char-pts" 248 ) 249 res, err := findDeviceGroup(devices.CharDevice, major) 250 if res != group || err != nil { 251 return fmt.Errorf("expected %v, nil, got %v, %w", group, res, err) 252 } 253 return nil 254 } 255 256 func TestFindDeviceGroup(t *testing.T) { 257 if err := testFindDeviceGroup(); err != nil { 258 t.Fatal(err) 259 } 260 } 261 262 func BenchmarkFindDeviceGroup(b *testing.B) { 263 for i := 0; i < b.N; i++ { 264 if err := testFindDeviceGroup(); err != nil { 265 b.Fatal(err) 266 } 267 } 268 } 269 270 func newManager(t *testing.T, config *configs.Cgroup) (m cgroups.Manager) { 271 t.Helper() 272 var err error 273 274 if cgroups.IsCgroup2UnifiedMode() { 275 m, err = systemd.NewUnifiedManager(config, "") 276 } else { 277 m, err = systemd.NewLegacyManager(config, nil) 278 } 279 if err != nil { 280 t.Fatal(err) 281 } 282 t.Cleanup(func() { _ = m.Destroy() }) 283 284 return m 285 }