github.com/rumpl/bof@v23.0.0-rc.2+incompatible/integration/container/ipcmode_linux_test.go (about)

     1  package container // import "github.com/docker/docker/integration/container"
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	containertypes "github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/api/types/versions"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/docker/integration/internal/container"
    16  	"github.com/docker/docker/testutil/daemon"
    17  	"github.com/docker/docker/testutil/request"
    18  	"gotest.tools/v3/assert"
    19  	is "gotest.tools/v3/assert/cmp"
    20  	"gotest.tools/v3/fs"
    21  	"gotest.tools/v3/skip"
    22  )
    23  
    24  // testIpcCheckDevExists checks whether a given mount (identified by its
    25  // major:minor pair from /proc/self/mountinfo) exists on the host system.
    26  //
    27  // The format of /proc/self/mountinfo is like:
    28  //
    29  //	29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw
    30  //	^^^^\
    31  //	     - this is the minor:major we look for
    32  func testIpcCheckDevExists(mm string) (bool, error) {
    33  	f, err := os.Open("/proc/self/mountinfo")
    34  	if err != nil {
    35  		return false, err
    36  	}
    37  	defer f.Close()
    38  
    39  	s := bufio.NewScanner(f)
    40  	for s.Scan() {
    41  		fields := strings.Fields(s.Text())
    42  		if len(fields) < 7 {
    43  			continue
    44  		}
    45  		if fields[2] == mm {
    46  			return true, nil
    47  		}
    48  	}
    49  
    50  	return false, s.Err()
    51  }
    52  
    53  // testIpcNonePrivateShareable is a helper function to test "none",
    54  // "private" and "shareable" modes.
    55  func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool, mustBeShared bool) {
    56  	defer setupTest(t)()
    57  
    58  	cfg := containertypes.Config{
    59  		Image: "busybox",
    60  		Cmd:   []string{"top"},
    61  	}
    62  	hostCfg := containertypes.HostConfig{
    63  		IpcMode: containertypes.IpcMode(mode),
    64  	}
    65  	client := testEnv.APIClient()
    66  	ctx := context.Background()
    67  
    68  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
    69  	assert.NilError(t, err)
    70  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
    71  
    72  	err = client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
    73  	assert.NilError(t, err)
    74  
    75  	// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
    76  	cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
    77  	result, err := container.Exec(ctx, client, resp.ID, []string{"sh", "-c", cmd})
    78  	assert.NilError(t, err)
    79  	mm := result.Combined()
    80  	if !mustBeMounted {
    81  		assert.Check(t, is.Equal(mm, ""))
    82  		// no more checks to perform
    83  		return
    84  	}
    85  	assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
    86  
    87  	shared, err := testIpcCheckDevExists(mm)
    88  	assert.NilError(t, err)
    89  	t.Logf("[testIpcPrivateShareable] ipcmode: %v, ipcdev: %v, shared: %v, mustBeShared: %v\n", mode, mm, shared, mustBeShared)
    90  	assert.Check(t, is.Equal(shared, mustBeShared))
    91  }
    92  
    93  // TestIpcModeNone checks the container "none" IPC mode
    94  // (--ipc none) works as expected. It makes sure there is no
    95  // /dev/shm mount inside the container.
    96  func TestIpcModeNone(t *testing.T) {
    97  	skip.If(t, testEnv.IsRemoteDaemon)
    98  
    99  	testIpcNonePrivateShareable(t, "none", false, false)
   100  }
   101  
   102  // TestAPIIpcModePrivate checks the container private IPC mode
   103  // (--ipc private) works as expected. It gets the minor:major pair
   104  // of /dev/shm mount from the container, and makes sure there is no
   105  // such pair on the host.
   106  func TestIpcModePrivate(t *testing.T) {
   107  	skip.If(t, testEnv.IsRemoteDaemon)
   108  
   109  	testIpcNonePrivateShareable(t, "private", true, false)
   110  }
   111  
   112  // TestAPIIpcModeShareable checks the container shareable IPC mode
   113  // (--ipc shareable) works as expected. It gets the minor:major pair
   114  // of /dev/shm mount from the container, and makes sure such pair
   115  // also exists on the host.
   116  func TestIpcModeShareable(t *testing.T) {
   117  	skip.If(t, testEnv.IsRemoteDaemon)
   118  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   119  
   120  	testIpcNonePrivateShareable(t, "shareable", true, true)
   121  }
   122  
   123  // testIpcContainer is a helper function to test --ipc container:NNN mode in various scenarios
   124  func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
   125  	t.Helper()
   126  
   127  	defer setupTest(t)()
   128  
   129  	cfg := containertypes.Config{
   130  		Image: "busybox",
   131  		Cmd:   []string{"top"},
   132  	}
   133  	hostCfg := containertypes.HostConfig{
   134  		IpcMode: containertypes.IpcMode(donorMode),
   135  	}
   136  	ctx := context.Background()
   137  	client := testEnv.APIClient()
   138  
   139  	// create and start the "donor" container
   140  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
   141  	assert.NilError(t, err)
   142  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   143  	name1 := resp.ID
   144  
   145  	err = client.ContainerStart(ctx, name1, types.ContainerStartOptions{})
   146  	assert.NilError(t, err)
   147  
   148  	// create and start the second container
   149  	hostCfg.IpcMode = containertypes.IpcMode("container:" + name1)
   150  	resp, err = client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
   151  	assert.NilError(t, err)
   152  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   153  	name2 := resp.ID
   154  
   155  	err = client.ContainerStart(ctx, name2, types.ContainerStartOptions{})
   156  	if !mustWork {
   157  		// start should fail with a specific error
   158  		assert.Check(t, is.ErrorContains(err, "non-shareable IPC"))
   159  		// no more checks to perform here
   160  		return
   161  	}
   162  
   163  	// start should succeed
   164  	assert.NilError(t, err)
   165  
   166  	// check that IPC is shared
   167  	// 1. create a file in the first container
   168  	_, err = container.Exec(ctx, client, name1, []string{"sh", "-c", "printf covfefe > /dev/shm/bar"})
   169  	assert.NilError(t, err)
   170  	// 2. check it's the same file in the second one
   171  	result, err := container.Exec(ctx, client, name2, []string{"cat", "/dev/shm/bar"})
   172  	assert.NilError(t, err)
   173  	out := result.Combined()
   174  	assert.Check(t, is.Equal(true, regexp.MustCompile("^covfefe$").MatchString(out)))
   175  }
   176  
   177  // TestAPIIpcModeShareableAndPrivate checks that
   178  // 1) a container created with --ipc container:ID can use IPC of another shareable container.
   179  // 2) a container created with --ipc container:ID can NOT use IPC of another private container.
   180  func TestAPIIpcModeShareableAndContainer(t *testing.T) {
   181  	skip.If(t, testEnv.IsRemoteDaemon)
   182  
   183  	testIpcContainer(t, "shareable", true)
   184  
   185  	testIpcContainer(t, "private", false)
   186  }
   187  
   188  /* TestAPIIpcModeHost checks that a container created with --ipc host
   189   * can use IPC of the host system.
   190   */
   191  func TestAPIIpcModeHost(t *testing.T) {
   192  	skip.If(t, testEnv.IsRemoteDaemon)
   193  	skip.If(t, testEnv.IsUserNamespace)
   194  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   195  
   196  	cfg := containertypes.Config{
   197  		Image: "busybox",
   198  		Cmd:   []string{"top"},
   199  	}
   200  	hostCfg := containertypes.HostConfig{
   201  		IpcMode: containertypes.IPCModeHost,
   202  	}
   203  	ctx := context.Background()
   204  
   205  	client := testEnv.APIClient()
   206  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
   207  	assert.NilError(t, err)
   208  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   209  	name := resp.ID
   210  
   211  	err = client.ContainerStart(ctx, name, types.ContainerStartOptions{})
   212  	assert.NilError(t, err)
   213  
   214  	// check that IPC is shared
   215  	// 1. create a file inside container
   216  	_, err = container.Exec(ctx, client, name, []string{"sh", "-c", "printf covfefe > /dev/shm/." + name})
   217  	assert.NilError(t, err)
   218  	// 2. check it's the same on the host
   219  	bytes, err := os.ReadFile("/dev/shm/." + name)
   220  	assert.NilError(t, err)
   221  	assert.Check(t, is.Equal("covfefe", string(bytes)))
   222  	// 3. clean up
   223  	_, err = container.Exec(ctx, client, name, []string{"rm", "-f", "/dev/shm/." + name})
   224  	assert.NilError(t, err)
   225  }
   226  
   227  // testDaemonIpcPrivateShareable is a helper function to test "private" and "shareable" daemon default ipc modes.
   228  func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...string) {
   229  	defer setupTest(t)()
   230  
   231  	d := daemon.New(t)
   232  	d.StartWithBusybox(t, arg...)
   233  	defer d.Stop(t)
   234  
   235  	c := d.NewClientT(t)
   236  
   237  	cfg := containertypes.Config{
   238  		Image: "busybox",
   239  		Cmd:   []string{"top"},
   240  	}
   241  	ctx := context.Background()
   242  
   243  	resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "")
   244  	assert.NilError(t, err)
   245  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   246  
   247  	err = c.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
   248  	assert.NilError(t, err)
   249  
   250  	// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
   251  	cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
   252  	result, err := container.Exec(ctx, c, resp.ID, []string{"sh", "-c", cmd})
   253  	assert.NilError(t, err)
   254  	mm := result.Combined()
   255  	assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
   256  
   257  	shared, err := testIpcCheckDevExists(mm)
   258  	assert.NilError(t, err)
   259  	t.Logf("[testDaemonIpcPrivateShareable] ipcdev: %v, shared: %v, mustBeShared: %v\n", mm, shared, mustBeShared)
   260  	assert.Check(t, is.Equal(shared, mustBeShared))
   261  }
   262  
   263  // TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended.
   264  func TestDaemonIpcModeShareable(t *testing.T) {
   265  	skip.If(t, testEnv.IsRemoteDaemon)
   266  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   267  
   268  	testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable")
   269  }
   270  
   271  // TestDaemonIpcModePrivate checks that --default-ipc-mode private works as intended.
   272  func TestDaemonIpcModePrivate(t *testing.T) {
   273  	skip.If(t, testEnv.IsRemoteDaemon)
   274  
   275  	testDaemonIpcPrivateShareable(t, false, "--default-ipc-mode", "private")
   276  }
   277  
   278  // used to check if an IpcMode given in config works as intended
   279  func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) {
   280  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   281  	config := `{"default-ipc-mode": "` + mode + `"}`
   282  	file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config))
   283  	defer file.Remove()
   284  
   285  	testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path())
   286  }
   287  
   288  // TestDaemonIpcModePrivateFromConfig checks that "default-ipc-mode: private" config works as intended.
   289  func TestDaemonIpcModePrivateFromConfig(t *testing.T) {
   290  	skip.If(t, testEnv.IsRemoteDaemon)
   291  
   292  	testDaemonIpcFromConfig(t, "private", false)
   293  }
   294  
   295  // TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended.
   296  func TestDaemonIpcModeShareableFromConfig(t *testing.T) {
   297  	skip.If(t, testEnv.IsRemoteDaemon)
   298  
   299  	testDaemonIpcFromConfig(t, "shareable", true)
   300  }
   301  
   302  // TestIpcModeOlderClient checks that older client gets shareable IPC mode
   303  // by default, even when the daemon default is private.
   304  func TestIpcModeOlderClient(t *testing.T) {
   305  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "requires a daemon with DefaultIpcMode: private")
   306  	c := testEnv.APIClient()
   307  	skip.If(t, versions.LessThan(c.ClientVersion(), "1.40"), "requires client API >= 1.40")
   308  
   309  	t.Parallel()
   310  
   311  	ctx := context.Background()
   312  
   313  	// pre-check: default ipc mode in daemon is private
   314  	cID := container.Create(ctx, t, c, container.WithAutoRemove)
   315  
   316  	inspect, err := c.ContainerInspect(ctx, cID)
   317  	assert.NilError(t, err)
   318  	assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "private"))
   319  
   320  	// main check: using older client creates "shareable" container
   321  	c = request.NewAPIClient(t, client.WithVersion("1.39"))
   322  	cID = container.Create(ctx, t, c, container.WithAutoRemove)
   323  
   324  	inspect, err = c.ContainerInspect(ctx, cID)
   325  	assert.NilError(t, err)
   326  	assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable"))
   327  }