Skip to content

Commit da780e4

Browse files
rpluem-vfkolyshkin
authored andcommitted
Fix bind mounts of filesystems with certain options set
Currently bind mounts of filesystems with nodev, nosuid, noexec, noatime, relatime, strictatime, nodiratime options set fail in rootless mode if the same options are not set for the bind mount. For ro filesystems this was resolved by #2570 by remounting again with ro set. Follow the same approach for nodev, nosuid, noexec, noatime, relatime, strictatime, nodiratime but allow to revert back to the old behaviour via the new `--no-mount-fallback` command line option. Add a testcase to verify that bind mounts of filesystems with nodev, nosuid, noexec, noatime options set work in rootless mode. Add a testcase that mounts a nodev, nosuid, noexec, noatime filesystem with a ro flag. Add two further testcases that ensure that the above testcases would fail if the `--no-mount-fallback` command line option is set. * contrib/completions/bash/runc: Add `--no-mount-fallback` command line option for bash completion. * create.go: Add `--no-mount-fallback` command line option. * restore.go: Add `--no-mount-fallback` command line option. * run.go: Add `--no-mount-fallback` command line option. * libcontainer/configs/config.go: Add `NoMountFallback` field to the `Config` struct to store the command line option value. * libcontainer/specconv/spec_linux.go: Add `NoMountFallback` field to the `CreateOpts` struct to store the command line option value and store it in the libcontainer config. * utils_linux.go: Store the command line option value in the `CreateOpts` struct. * libcontainer/rootfs_linux.go: In case that `--no-mount-fallback` is not set try to remount the bind filesystem again with the options nodev, nosuid, noexec, noatime, relatime, strictatime or nodiratime if they are set on the source filesystem. * tests/integration/mounts_sshfs.bats: Add testcases and rework sshfs setup to allow specifying different mount options depending on the test case. Signed-off-by: Ruediger Pluem <[email protected]>
1 parent 465cb34 commit da780e4

File tree

9 files changed

+119
-17
lines changed

9 files changed

+119
-17
lines changed

contrib/completions/bash/runc

+3
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ _runc_run() {
461461
--no-subreaper
462462
--no-pivot
463463
--no-new-keyring
464+
--no-mount-fallback
464465
"
465466

466467
local options_with_args="
@@ -567,6 +568,7 @@ _runc_create() {
567568
--help
568569
--no-pivot
569570
--no-new-keyring
571+
--no-mount-fallback
570572
"
571573

572574
local options_with_args="
@@ -627,6 +629,7 @@ _runc_restore() {
627629
--no-pivot
628630
--auto-dedup
629631
--lazy-pages
632+
--no-mount-fallback
630633
"
631634

632635
local options_with_args="

create.go

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ command(s) that get executed on start, edit the args parameter of the spec. See
5151
Name: "preserve-fds",
5252
Usage: "Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total)",
5353
},
54+
cli.BoolFlag{
55+
Name: "no-mount-fallback",
56+
Usage: "Do not fallback when the specific configuration is not applicable (e.g., do not try to remount a bind mount again after the first attempt failed on source filesystems that have nodev, noexec, nosuid, noatime, relatime, strictatime, nodiratime set)",
57+
},
5458
},
5559
Action: func(context *cli.Context) error {
5660
if err := checkArgs(context, 1, exactArgs); err != nil {

libcontainer/configs/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ type Config struct {
212212
// RootlessCgroups is set when unlikely to have the full access to cgroups.
213213
// When RootlessCgroups is set, cgroups errors are ignored.
214214
RootlessCgroups bool `json:"rootless_cgroups,omitempty"`
215+
216+
// Do not try to remount a bind mount again after the first attempt failed on source
217+
// filesystems that have nodev, noexec, nosuid, noatime, relatime, strictatime, nodiratime set
218+
NoMountFallback bool `json:"no_mount_fallback,omitempty"`
215219
}
216220

217221
type (

libcontainer/rootfs_linux.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type mountConfig struct {
3535
cgroup2Path string
3636
rootlessCgroups bool
3737
cgroupns bool
38+
noMountFallback bool
3839
}
3940

4041
// mountEntry contains mount data specific to a mount point.
@@ -83,6 +84,7 @@ func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig, mountFds mountFds) (
8384
cgroup2Path: iConfig.Cgroup2Path,
8485
rootlessCgroups: iConfig.RootlessCgroups,
8586
cgroupns: config.Namespaces.Contains(configs.NEWCGROUP),
87+
noMountFallback: config.NoMountFallback,
8688
}
8789
for i, m := range config.Mounts {
8890
entry := mountEntry{Mount: m}
@@ -512,7 +514,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
512514
// first check that we have non-default options required before attempting a remount
513515
if m.Flags&^(unix.MS_REC|unix.MS_REMOUNT|unix.MS_BIND) != 0 {
514516
// only remount if unique mount options are set
515-
if err := remount(m, rootfs); err != nil {
517+
if err := remount(m, rootfs, c.noMountFallback); err != nil {
516518
return err
517519
}
518520
}
@@ -1101,24 +1103,33 @@ func writeSystemProperty(key, value string) error {
11011103
return os.WriteFile(path.Join("/proc/sys", keyPath), []byte(value), 0o644)
11021104
}
11031105

1104-
func remount(m mountEntry, rootfs string) error {
1106+
func remount(m mountEntry, rootfs string, noMountFallback bool) error {
11051107
return utils.WithProcfd(rootfs, m.Destination, func(dstFD string) error {
11061108
flags := uintptr(m.Flags | unix.MS_REMOUNT)
11071109
err := mountViaFDs(m.Source, m.srcFD, m.Destination, dstFD, m.Device, flags, "")
11081110
if err == nil {
11091111
return nil
11101112
}
1111-
// Check if the source has ro flag...
1113+
// Check if the source has flags set according to noMountFallback
11121114
src := m.src()
11131115
var s unix.Statfs_t
11141116
if err := unix.Statfs(src, &s); err != nil {
11151117
return &os.PathError{Op: "statfs", Path: src, Err: err}
11161118
}
1117-
if s.Flags&unix.MS_RDONLY != unix.MS_RDONLY {
1119+
var checkflags int
1120+
if noMountFallback {
1121+
// Check for ro only
1122+
checkflags = unix.MS_RDONLY
1123+
} else {
1124+
// Check for ro, nodev, noexec, nosuid, noatime, relatime, strictatime,
1125+
// nodiratime
1126+
checkflags = unix.MS_RDONLY | unix.MS_NODEV | unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NOATIME | unix.MS_RELATIME | unix.MS_STRICTATIME | unix.MS_NODIRATIME
1127+
}
1128+
if int(s.Flags)&checkflags == 0 {
11181129
return err
11191130
}
1120-
// ... and retry the mount with ro flag set.
1121-
flags |= unix.MS_RDONLY
1131+
// ... and retry the mount with flags found above.
1132+
flags |= uintptr(int(s.Flags) & checkflags)
11221133
return mountViaFDs(m.Source, m.srcFD, m.Destination, dstFD, m.Device, flags, "")
11231134
})
11241135
}

libcontainer/specconv/spec_linux.go

+2
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ type CreateOpts struct {
312312
Spec *specs.Spec
313313
RootlessEUID bool
314314
RootlessCgroups bool
315+
NoMountFallback bool
315316
}
316317

317318
// getwd is a wrapper similar to os.Getwd, except it always gets
@@ -358,6 +359,7 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
358359
NoNewKeyring: opts.NoNewKeyring,
359360
RootlessEUID: opts.RootlessEUID,
360361
RootlessCgroups: opts.RootlessCgroups,
362+
NoMountFallback: opts.NoMountFallback,
361363
}
362364

363365
for _, m := range spec.Mounts {

restore.go

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ using the runc checkpoint command.`,
9898
Value: "",
9999
Usage: "Specify an LSM mount context to be used during restore.",
100100
},
101+
cli.BoolFlag{
102+
Name: "no-mount-fallback",
103+
Usage: "Do not fallback when the specific configuration is not applicable (e.g., do not try to remount a bind mount again after the first attempt failed on source filesystems that have nodev, noexec, nosuid, noatime, relatime, strictatime, nodiratime set)",
104+
},
101105
},
102106
Action: func(context *cli.Context) error {
103107
if err := checkArgs(context, 1, exactArgs); err != nil {

run.go

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ command(s) that get executed on start, edit the args parameter of the spec. See
6464
Name: "preserve-fds",
6565
Usage: "Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total)",
6666
},
67+
cli.BoolFlag{
68+
Name: "no-mount-fallback",
69+
Usage: "Do not fallback when the specific configuration is not applicable (e.g., do not try to remount a bind mount again after the first attempt failed on source filesystems that have nodev, noexec, nosuid, noatime, relatime, strictatime, nodiratime set)",
70+
},
6771
},
6872
Action: func(context *cli.Context) error {
6973
if err := checkArgs(context, 1, exactArgs); err != nil {

tests/integration/mounts_sshfs.bats

+80-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@
33
load helpers
44

55
function setup() {
6-
# Create a ro fuse-sshfs mount; skip the test if it's not working.
6+
setup_busybox
7+
update_config '.process.args = ["/bin/echo", "Hello World"]'
8+
}
9+
10+
function teardown() {
11+
# Some distros do not have fusermount installed
12+
# as a dependency of fuse-sshfs, and good ol' umount works.
13+
fusermount -u "$DIR" || umount "$DIR"
14+
15+
teardown_bundle
16+
}
17+
18+
function setup_sshfs() {
19+
# Create a fuse-sshfs mount; skip the test if it's not working.
720
local sshfs="sshfs
821
-o UserKnownHostsFile=/dev/null
922
-o StrictHostKeyChecking=no
@@ -12,30 +25,86 @@ function setup() {
1225
DIR="$BATS_RUN_TMPDIR/fuse-sshfs"
1326
mkdir -p "$DIR"
1427

15-
if ! $sshfs -o ro rootless@localhost: "$DIR"; then
28+
if ! $sshfs -o "$1" rootless@localhost: "$DIR"; then
1629
skip "test requires working sshfs mounts"
1730
fi
31+
}
1832

19-
setup_busybox
20-
update_config '.process.args = ["/bin/echo", "Hello World"]'
33+
@test "runc run [rw bind mount of a ro fuse sshfs mount]" {
34+
setup_sshfs "ro"
35+
update_config ' .mounts += [{
36+
type: "bind",
37+
source: "'"$DIR"'",
38+
destination: "/mnt",
39+
options: ["rw", "rprivate", "nosuid", "nodev", "rbind"]
40+
}]'
41+
42+
runc run --no-mount-fallback test_busybox
43+
[ "$status" -eq 0 ]
2144
}
2245

23-
function teardown() {
24-
# New distros (Fedora 35) do not have fusermount installed
25-
# as a dependency of fuse-sshfs, and good ol' umount works.
26-
fusermount -u "$DIR" || umount "$DIR"
46+
@test "runc run [dev,exec,suid,atime bind mount of a nodev,nosuid,noexec,noatime fuse sshfs mount]" {
47+
setup_sshfs "nodev,nosuid,noexec,noatime"
48+
# The "sync" option is used to trigger a remount with the below options.
49+
# It serves no further purpose. Otherwise only a bind mount without
50+
# applying the below options will be done.
51+
update_config ' .mounts += [{
52+
type: "bind",
53+
source: "'"$DIR"'",
54+
destination: "/mnt",
55+
options: ["dev", "suid", "exec", "atime", "rprivate", "rbind", "sync"]
56+
}]'
2757

28-
teardown_bundle
58+
runc run test_busybox
59+
[ "$status" -eq 0 ]
2960
}
3061

31-
@test "runc run [rw bind mount of a ro fuse sshfs mount]" {
62+
@test "runc run [ro bind mount of a nodev,nosuid,noexec,noatime fuse sshfs mount]" {
63+
setup_sshfs "nodev,nosuid,noexec,noatime"
3264
update_config ' .mounts += [{
3365
type: "bind",
3466
source: "'"$DIR"'",
3567
destination: "/mnt",
36-
options: ["rw", "rprivate", "nosuid", "nodev", "rbind"]
68+
options: ["rbind", "ro"]
3769
}]'
3870

3971
runc run test_busybox
4072
[ "$status" -eq 0 ]
4173
}
74+
75+
@test "runc run [dev,exec,suid,atime bind mount of a nodev,nosuid,noexec,noatime fuse sshfs mount without fallback]" {
76+
setup_sshfs "nodev,nosuid,noexec,noatime"
77+
# The "sync" option is used to trigger a remount with the below options.
78+
# It serves no further purpose. Otherwise only a bind mount without
79+
# applying the below options will be done.
80+
update_config ' .mounts += [{
81+
type: "bind",
82+
source: "'"$DIR"'",
83+
destination: "/mnt",
84+
options: ["dev", "suid", "exec", "atime", "rprivate", "rbind", "sync"]
85+
}]'
86+
87+
runc run --no-mount-fallback test_busybox
88+
# The above will fail as we added --no-mount-fallback which causes us not to
89+
# try to remount a bind mount again after the first attempt failed on source
90+
# filesystems that have nodev, noexec, nosuid, noatime set.
91+
[ "$status" -ne 0 ]
92+
[[ "$output" == *"runc run failed: unable to start container process: error during container init: error mounting"*"operation not permitted"* ]]
93+
}
94+
95+
@test "runc run [ro bind mount of a nodev,nosuid,noexec,noatime fuse sshfs mount without fallback]" {
96+
setup_sshfs "nodev,nosuid,noexec,noatime"
97+
update_config ' .mounts += [{
98+
type: "bind",
99+
source: "'"$DIR"'",
100+
destination: "/mnt",
101+
options: ["rbind", "ro"]
102+
}]'
103+
104+
runc run --no-mount-fallback test_busybox
105+
# The above will fail as we added --no-mount-fallback which causes us not to
106+
# try to remount a bind mount again after the first attempt failed on source
107+
# filesystems that have nodev, noexec, nosuid, noatime set.
108+
[ "$status" -ne 0 ]
109+
[[ "$output" == *"runc run failed: unable to start container process: error during container init: error mounting"*"operation not permitted"* ]]
110+
}

utils_linux.go

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func createContainer(context *cli.Context, id string, spec *specs.Spec) (*libcon
175175
Spec: spec,
176176
RootlessEUID: os.Geteuid() != 0,
177177
RootlessCgroups: rootlessCg,
178+
NoMountFallback: context.Bool("no-mount-fallback"),
178179
})
179180
if err != nil {
180181
return nil, err

0 commit comments

Comments
 (0)