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  }