From c4ed0da4bee7b2f3dc86d41fae9fdb534414f0a2 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:16:58 -0700 Subject: [PATCH 1/2] mountutil: give an actionable error for unshared bind-propagation source Motivation: Bind-mounting with `bind-propagation=shared|rshared|slave|rslave` (e.g. `-v /:/host:rslave,ro`, as used by node_exporter's official compose file) requires the mount source to already be a "shared" or "slave" mount on the host, per the kernel's shared subtree rules. When it isn't (common on non-systemd hosts such as Alpine/OpenRC, where "/" is mounted private by default), nerdctl correctly rejects the request, but the error dumped the internal marker strings it checks for instead of telling the user what to do: mountpoint "/" doesn't have optional field neither of [shared: master:] This is not a bug in the propagation check itself: it mirrors the same kernel-level restriction moby's volume parser enforces, and a container runtime cannot make a private mount propagate without the host operator changing it first. Report: https://github.com/containerd/nerdctl/issues/4423 Approach: Reword the error raised by ensureMountOptionalValue() to name the requested propagation mode and suggest the actual fix, `mount --make-rshared ` on the host. --make-rshared satisfies both the "shared" and "slave" checks (a slave mount is accepted if its source has either a "shared:" or "master:" peer group), so the same suggestion is correct for both branches. No decision logic changes: the same condition still triggers the error, only the message improves. Validation: This package's tests live in a _linux.go-suffixed file, and this repo's own docs/testing/README.md states unit tests "must be run on a supported OS (linux, windows, or freebsd)" - this change was made on macOS, so `go test` cannot execute them here. What was actually run: - `go build ./...` (darwin) - passes - `GOOS=linux go build ./pkg/mountutil/...` and `GOOS=linux go vet ./pkg/mountutil/...` - pass - `GOOS=linux go test -c ./pkg/mountutil/` - the test binary, including the two updated cases in TestParseVolumeOptions, compiles cleanly (not executed, since it's a linux binary on a macOS host) - `gofmt -l` on both changed files - no output (already formatted) - Separately, in a scratch directory outside the repo, ran a standalone Go program reproducing ensureMountOptionalValue()'s exact logic against the real (OS-agnostic) mount.Info type: mounting "/" with an empty Optional field (the reported host state) reproduces the original opaque error and now yields the new actionable message for both "rslave" and "rshared", while mounting "/" with Optional="shared:1" succeeds with no error - confirming the fixed condition and message end-to-end, short of running it through this repo's own `go test`. This is a pure error-message improvement: user-visible behavior for valid propagation requests is unchanged, and the previously-failing case still fails, just with a clear next step instead of an internal implementation detail. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- pkg/mountutil/mountutil_linux.go | 8 +++---- pkg/mountutil/mountutil_linux_test.go | 30 ++++++++++++++++----------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 02da626213b..22e7b792c74 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -247,7 +247,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw, ociRuntime string, get if err != nil { return nil, nil, err } - if err := ensureMountOptionalValue(mi, "shared:"); err != nil { + if err := ensureMountOptionalValue(mi, got, "shared:"); err != nil { return nil, nil, err } @@ -276,7 +276,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw, ociRuntime string, get if err != nil { return nil, nil, err } - if err := ensureMountOptionalValue(mi, "shared:", "master:"); err != nil { + if err := ensureMountOptionalValue(mi, got, "shared:", "master:"); err != nil { return nil, nil, err } @@ -320,7 +320,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw, ociRuntime string, get // // For more details about "optional" field: // - https://github.com/moby/sys/blob/mountinfo/v0.4.1/mountinfo/mountinfo.go#L52-L56 -func ensureMountOptionalValue(mi mount.Info, vals ...string) error { +func ensureMountOptionalValue(mi mount.Info, propagation string, vals ...string) error { var hasValue bool for _, opt := range strings.Split(mi.Optional, " ") { for _, mark := range vals { @@ -330,7 +330,7 @@ func ensureMountOptionalValue(mi mount.Info, vals ...string) error { } } if !hasValue { - return fmt.Errorf("mountpoint %q doesn't have optional field neither of %+v", mi.Mountpoint, vals) + return fmt.Errorf("mountpoint %q is not a shared or slave mount, so it cannot be bind-mounted with propagation %q; try running `mount --make-rshared %s` on the host", mi.Mountpoint, propagation, mi.Mountpoint) } return nil } diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 5ee383d7065..6e9e2d11fc2 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -59,6 +59,7 @@ func TestParseVolumeOptions(t *testing.T) { wants []string wantRootfsPropagation string wantFail bool + wantErrContains string }{ { name: "unknown option is ignored (with warning)", @@ -176,12 +177,13 @@ func TestParseVolumeOptions(t *testing.T) { wants: []string{"ro", "rshared"}, }, { - name: "shared propagation is not allowed if the src is not shared", - vType: "bind", - src: "dummy", - optsRaw: "ro,shared", - srcOptional: nil, - wantFail: true, + name: "shared propagation is not allowed if the src is not shared", + vType: "bind", + src: "dummy", + optsRaw: "ro,shared", + srcOptional: nil, + wantFail: true, + wantErrContains: "mount --make-rshared dummy", }, { name: "make bind slave", @@ -203,12 +205,13 @@ func TestParseVolumeOptions(t *testing.T) { wants: []string{"ro", "slave"}, }, { - name: "slave propagation is not allowed if the src is not slave", - vType: "bind", - src: "dummy", - optsRaw: "ro,slave", - srcOptional: nil, - wantFail: true, + name: "slave propagation is not allowed if the src is not slave", + vType: "bind", + src: "dummy", + optsRaw: "ro,slave", + srcOptional: nil, + wantFail: true, + wantErrContains: "mount --make-rshared dummy", }, } for _, tt := range tests { @@ -222,6 +225,9 @@ func TestParseVolumeOptions(t *testing.T) { }) if err != nil { if tt.wantFail { + if tt.wantErrContains != "" { + assert.ErrorContains(t, err, tt.wantErrContains) + } return } t.Errorf("failed to parse option %q: %v", tt.optsRaw, err) From 209d95d23ee6a38acd89cb9ed9a67a0d017af4f5 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:37:44 -0700 Subject: [PATCH 2/2] mountutil: quote mountpoint in make-rshared hint per review nit Use %q instead of %s for the mountpoint in the make-rshared suggestion, matching the review suggestion. Update the two affected test expectations accordingly. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- pkg/mountutil/mountutil_linux.go | 2 +- pkg/mountutil/mountutil_linux_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 22e7b792c74..eb8ec64b866 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -330,7 +330,7 @@ func ensureMountOptionalValue(mi mount.Info, propagation string, vals ...string) } } if !hasValue { - return fmt.Errorf("mountpoint %q is not a shared or slave mount, so it cannot be bind-mounted with propagation %q; try running `mount --make-rshared %s` on the host", mi.Mountpoint, propagation, mi.Mountpoint) + return fmt.Errorf("mountpoint %q is not a shared or slave mount, so it cannot be bind-mounted with propagation %q; try running `mount --make-rshared %q` on the host", mi.Mountpoint, propagation, mi.Mountpoint) } return nil } diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 6e9e2d11fc2..4fb80140cd4 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -183,7 +183,7 @@ func TestParseVolumeOptions(t *testing.T) { optsRaw: "ro,shared", srcOptional: nil, wantFail: true, - wantErrContains: "mount --make-rshared dummy", + wantErrContains: `mount --make-rshared "dummy"`, }, { name: "make bind slave", @@ -211,7 +211,7 @@ func TestParseVolumeOptions(t *testing.T) { optsRaw: "ro,slave", srcOptional: nil, wantFail: true, - wantErrContains: "mount --make-rshared dummy", + wantErrContains: `mount --make-rshared "dummy"`, }, } for _, tt := range tests {