Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ Trivial:

- Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too).
Requires kernel >= 5.12.
The same feature was later introduced in Docker v25 with a different syntax. nerdctl will support Docker v25 syntax too in the future.
The same feature was later introduced in Docker v25 with a different syntax: read-only mounts are now recursively read-only by default when supported,
and the behavior is customizable with `--mount type=bind,...,readonly,bind-recursive=<enabled|disabled|writable|readonly>`.
nerdctl now supports the Docker v25 syntax too, and the old `rro` syntax is deprecated.

## Similar tools

- [`ctr`](https://github.com/containerd/containerd/tree/main/cmd/ctr): incompatible with Docker CLI, and not friendly to users.
Expand Down
141 changes: 141 additions & 0 deletions cmd/nerdctl/container/container_run_mount_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package container
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand All @@ -31,6 +32,7 @@ import (
"github.com/containerd/nerdctl/mod/tigron/tig"

"github.com/containerd/nerdctl/v2/pkg/mountutil"
"github.com/containerd/nerdctl/v2/pkg/ociruntimeutil"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
Expand Down Expand Up @@ -744,6 +746,145 @@ func TestRunVolumeBindMode(t *testing.T) {
testCase.Run(t)
}

// requiresRRO requires that the kernel and the default OCI runtime support
// recursive read-only (RRO) mounts.
var requiresRRO = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
if err := ociruntimeutil.SupportsRecursivelyReadOnly(""); err != nil {
return false, fmt.Sprintf("recursive read-only mounts are not supported: %v", err)
}
return true, "recursive read-only mounts are supported"
},
}

// setupBindMountWithSubmount creates a temp directory ("top") containing a
// writable submount ("top/mnt"), for testing recursive read-only (RRO) mounts,
// and stores the path of the top directory in the "top" label.
func setupBindMountWithSubmount(data test.Data, helpers test.Helpers) {
top := data.Temp().Dir("top")
topMnt := data.Temp().Dir("top", "mnt")
sub := data.Temp().Dir("sub")
assert.NilError(helpers.T(), mobymount.Mount(sub, topMnt, "none", "bind"))
data.Labels().Set("top", top)
}

func cleanupBindMountWithSubmount(data test.Data, helpers test.Helpers) {
if top := data.Labels().Get("top"); top != "" {
topMnt := filepath.Join(top, "mnt")
if err := mobymount.Unmount(topMnt); err != nil {
helpers.T().Log(fmt.Sprintf("failed to unmount %q: %v", topMnt, err))
}
}
}

// TestRunBindMountRecursiveReadOnly tests that read-only bind mounts are
// recursively read-only when the kernel and the OCI runtime support it
// (Docker v25 behavior), and that the mode is customizable with the
// `bind-recursive` option of `--mount`.
func TestRunBindMountRecursiveReadOnly(t *testing.T) {
testCase := nerdtest.Setup()

// The test creates a bind mount on the host, in a mount namespace shared
// with the daemon. With the rootless harness, the test process runs on the
// host, while the daemon runs inside the mount namespace of RootlessKit,
// so the mount would not be visible to the daemon.
testCase.Require = require.All(
require.Not(nerdtest.Rootless),
requiresRRO,
)

testCase.Setup = setupBindMountWithSubmount

testCase.SubTests = []*test.Case{
{
Description: "-v :ro is recursively read-only by default",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"-v", data.Labels().Get("top")+":/mnt1:ro",
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "--mount readonly is recursively read-only by default",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly", data.Labels().Get("top")),
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "bind-recursive=writable keeps the submounts writable (Docker v24 behavior)",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly,bind-recursive=writable", data.Labels().Get("top")),
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/file && touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "bind-recursive=readonly forces the recursive read-only mount",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly,bind-propagation=rprivate,bind-recursive=readonly", data.Labels().Get("top")),
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
}

testCase.Cleanup = cleanupBindMountWithSubmount

testCase.Run(t)
}

// TestRunBindMountDeprecatedRRO tests the deprecated `rro` option of `-v` and
// `--mount`, which predates the `bind-recursive=readonly` option of Docker v25.
func TestRunBindMountDeprecatedRRO(t *testing.T) {
testCase := nerdtest.Setup()

// See TestRunBindMountRecursiveReadOnly for the rootless restriction.
testCase.Require = require.All(
require.Not(nerdtest.Docker),
require.Not(nerdtest.Rootless),
requiresRRO,
)

testCase.Setup = setupBindMountWithSubmount

testCase.SubTests = []*test.Case{
{
Description: "-v :rro",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"-v", data.Labels().Get("top")+":/mnt1:rro,rprivate",
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "--mount rro",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,rro,bind-propagation=rprivate", data.Labels().Get("top")),
testutil.AlpineImage,
"sh", "-euxc", "! touch /mnt1/mnt/file")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
}

testCase.Cleanup = cleanupBindMountWithSubmount

testCase.Run(t)
}

func TestRunBindMountPropagation(t *testing.T) {
testCase := nerdtest.Setup()
testCase.Setup = func(data test.Data, helpers test.Helpers) {
Expand Down
27 changes: 19 additions & 8 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,14 @@ Runtime flags:

Volume flags:

- :whale: `-v, --volume <SRC>:<DST>[:<OPT>]`: Bind mount a volume, e.g., `-v /mnt:/mnt:rro,rprivate`
- :whale: `-v, --volume <SRC>:<DST>[:<OPT>]`: Bind mount a volume, e.g., `-v /mnt:/mnt:ro`
- :whale: option `rw` : Read/Write (when writable)
- :whale: option `ro` : Non-recursive read-only
- :nerd_face: option `rro`: Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too.
Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)). With older runc, `rro` just works as `ro`.
- :whale: option `ro` : Read-only. Recursively read-only (e.g., making children such as `/mnt/usb` read-only, too) when the kernel and the OCI runtime support it
(kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6, as in Docker v25), otherwise non-recursive read-only.
Use `--mount type=bind,...,readonly,bind-recursive=<writable|readonly>` to control the recursive read-only mode explicitly.
- :nerd_face: option `rro`: **Deprecated** since the same feature was introduced in Docker v25 with a different syntax; use `--mount type=bind,...,readonly,bind-propagation=rprivate,bind-recursive=readonly` instead.
Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too.
Requires kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6; an error is raised when the recursive read-only mount is not supported.
- :whale: option `shared`, `slave`, `private`: Non-recursive "shared" / "slave" / "private" propagation
- :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation
- :nerd_face: option `bind`: Not-recursively bind-mounted
Expand All @@ -302,12 +305,20 @@ Volume flags:
- Common Options:
- :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind.
- :whale: `dst`, `destination`, `target`: Mount destination spec.
- :whale: `readonly`, `ro`: mount the filesystem read-only.
- :nerd_face: `rro`: mount the filesystem recursively read-only.
- :whale: `readonly`, `ro`: mount the filesystem read-only. Recursively read-only when the kernel and the OCI runtime support it
(kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6, as in Docker v25). See the `bind-recursive` option below to control the recursive read-only mode explicitly.
- :nerd_face: `rro`: **Deprecated** since the same feature was introduced in Docker v25 with a different syntax; use `readonly` with `bind-propagation=rprivate` and `bind-recursive=readonly` instead.
Mount the filesystem recursively read-only.
- Options specific to `bind`:
- :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default).
- :whale: `bind-recursive`: `enabled`(default) or `disabled`. If set to `disabled`, submounts are not recursively bind-mounted. This option is useful for readonly bind mount.
- :whale: `bind-nonrecursive`: `true` or `false`(default). Deprecated alias for `bind-recursive=disabled` / `bind-recursive=enabled`. If set to true, submounts are not recursively bind-mounted.
- :whale: `bind-recursive`: `enabled`(default), `disabled`, `writable`, or `readonly`.
- `enabled`: submounts are recursively bind-mounted, and a `readonly` mount is recursively read-only when the kernel and the OCI runtime support it.
- `disabled`: submounts are not recursively bind-mounted.
- `writable`: submounts of a `readonly` mount are kept writable (the default behavior of Docker until v24).
- `readonly`: a `readonly` mount is forced to be recursively read-only; an error is raised when the kernel or the OCI runtime does not support it.
Requires `bind-propagation=rprivate` to be specified in conjunction.
Whether the OCI runtime supports recursive read-only mounts is detected by running `$RUNTIME features`, and the result is cached in the XDG cache directory (e.g., `~/.cache/nerdctl/oci-runtime-features`).
- :whale: `bind-nonrecursive`: `true` or `false`(default). Deprecated alias for `bind-recursive=disabled` / `bind-recursive=enabled` (removed in Docker v29). If set to true, submounts are not recursively bind-mounted.
- unimplemented options: `consistency`
- Options specific to `tmpfs`:
- :whale: `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default.
Expand Down
7 changes: 7 additions & 0 deletions docs/dir.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,10 @@ and its networks definitions are private.

Files:
- `nerdctl-<NWNAME>.conflist`: CNI conf list created by nerdctl

## Cache

### `<XDG_CACHE_HOME>/.nerdctl/oci-runtime-features/<RUNTIMEPATHHASH>.json`
e.g., `~/.cache/nerdctl/oci-runtime-features/67865418f7d73228cb3df1357ba93456ab21567f3c1f1b5eca680b0b8cfbc04e.json`

A cached result of `<RUNTIME> features` (e.g., `runc features`).
4 changes: 2 additions & 2 deletions pkg/cmd/container/run_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr
var parsed []*mountutil.Processed //nolint:prealloc
for _, v := range strutil.DedupeStrSlice(options.Volume) {
// createDir=true for -v option to allow creation of directory on host if not found.
x, err := mountutil.ProcessFlagV(v, volStore, true)
x, err := mountutil.ProcessFlagV(v, volStore, true, options.Runtime)
if err != nil {
return nil, err
}
Expand All @@ -112,7 +112,7 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr
}

for _, v := range strutil.DedupeStrSlice(options.Mount) {
x, err := mountutil.ProcessFlagMount(v, volStore)
x, err := mountutil.ProcessFlagMount(v, volStore, options.Runtime)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/containerutil/cp_resolve_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func (res *resolver) getMount(path string) (*locator, string) {
if len(mnt.Destination) > len(loc.containerPath) {
loc.readonly = false
for _, option := range mnt.Options {
if option == "ro" {
if option == "ro" || option == "rro" {
loc.readonly = true
}
}
Expand Down
15 changes: 7 additions & 8 deletions pkg/mountutil/mountutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ type volumeSpec struct {
AnonymousVolume string
}

func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (*Processed, error) {
// ProcessFlagV processes the value of the `-v` flag.
// ociRuntime is the value of the `--runtime` flag, used for detecting whether the
// OCI runtime supports recursive read-only mounts.
func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool, ociRuntime string) (*Processed, error) {
var (
res *Processed
volSpec volumeSpec
Expand Down Expand Up @@ -119,7 +122,7 @@ func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (*

rawOpts := res.Mode

options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts)
options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts, ociRuntime)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -215,16 +218,12 @@ func handleNamedVolumes(source string, volStore volumestore.VolumeStore) (volume
return res, nil
}

func getVolumeOptions(src string, vType string, rawOpts string) ([]string, []oci.SpecOpts, error) {
func getVolumeOptions(src, vType, rawOpts, ociRuntime string) ([]string, []oci.SpecOpts, error) {
// always call parseVolumeOptions for bind mount to allow the parser to add some default options
var err error
var specOpts []oci.SpecOpts
options, specOpts, err := parseVolumeOptions(vType, src, rawOpts)
options, specOpts, err := parseVolumeOptions(vType, src, rawOpts, ociRuntime)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse volume options (%q, %q, %q): %w", vType, src, rawOpts, err)
}

specOpts = append(specOpts, specOpts...)
return options, specOpts, nil
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/mountutil/mountutil_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func UnprivilegedMountFlags(path string) ([]string, error) {

// parseVolumeOptions parses specified optsRaw with using information of
// the volume type and the src directory when necessary.
func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) {
var writeModeRawOpts []string
for _, opt := range strings.Split(optsRaw, ",") {
switch opt {
Expand All @@ -67,6 +67,6 @@ func ProcessFlagTmpfs(s string) (*Processed, error) {
return nil, errdefs.ErrNotImplemented
}

func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) {
func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) {
return nil, errdefs.ErrNotImplemented
}
4 changes: 2 additions & 2 deletions pkg/mountutil/mountutil_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func UnprivilegedMountFlags(path string) ([]string, error) {

// parseVolumeOptions parses specified optsRaw with using information of
// the volume type and the src directory when necessary.
func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) {
var writeModeRawOpts []string
for _, opt := range strings.Split(optsRaw, ",") {
switch opt {
Expand All @@ -68,6 +68,6 @@ func ProcessFlagTmpfs(s string) (*Processed, error) {
return nil, errdefs.ErrNotImplemented
}

func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) {
func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) {
return nil, errdefs.ErrNotImplemented
}
Loading
Loading