Skip to content

Commit 52d8967

Browse files
committed
libct: switch to mount_setattr(2) for vfs mount flags
Signed-off-by: lifubang <lifubang@acmcoder.com>
1 parent cc77bd8 commit 52d8967

4 files changed

Lines changed: 189 additions & 59 deletions

File tree

libcontainer/mount_linux.go

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -416,40 +416,51 @@ func fsconfigApplyOptions(fsfd int, opts string) error {
416416
return nil
417417
}
418418

419-
// msFlagsToMountAttr converts MS_* mount flags (as used by mount(2)) to
420-
// MOUNT_ATTR_* flags (as used by fsmount(2) and mount_setattr(2)).
421-
func msFlagsToMountAttr(flags int) int {
422-
attr := 0
423-
if flags&unix.MS_RDONLY != 0 {
424-
attr |= unix.MOUNT_ATTR_RDONLY
425-
}
426-
if flags&unix.MS_NOSUID != 0 {
427-
attr |= unix.MOUNT_ATTR_NOSUID
428-
}
429-
if flags&unix.MS_NODEV != 0 {
430-
attr |= unix.MOUNT_ATTR_NODEV
431-
}
432-
if flags&unix.MS_NOEXEC != 0 {
433-
attr |= unix.MOUNT_ATTR_NOEXEC
434-
}
435-
if flags&unix.MS_NOSYMFOLLOW != 0 {
436-
attr |= unix.MOUNT_ATTR_NOSYMFOLLOW
437-
}
438-
if flags&unix.MS_NODIRATIME != 0 {
439-
attr |= unix.MOUNT_ATTR_NODIRATIME
419+
// msFlagsToMountAttr converts mount(2) flags to mount attributes for
420+
// mount_setattr(2). It returns the set and clear bits for the mount attributes.
421+
func msFlagsToMountAttr(flags int) (set, cls uint64) {
422+
// Binary mount flags: if the flag is set, add it to "set"; otherwise add
423+
// it to "cls" so that mount_setattr(2) clears it.
424+
for _, f := range []struct {
425+
msFlag int
426+
mountAttr uint64
427+
}{
428+
{unix.MS_RDONLY, unix.MOUNT_ATTR_RDONLY},
429+
{unix.MS_NOSUID, unix.MOUNT_ATTR_NOSUID},
430+
{unix.MS_NODEV, unix.MOUNT_ATTR_NODEV},
431+
{unix.MS_NOEXEC, unix.MOUNT_ATTR_NOEXEC},
432+
{unix.MS_NODIRATIME, unix.MOUNT_ATTR_NODIRATIME},
433+
{unix.MS_NOSYMFOLLOW, unix.MOUNT_ATTR_NOSYMFOLLOW},
434+
} {
435+
if flags&f.msFlag != 0 {
436+
set |= f.mountAttr
437+
} else {
438+
cls |= f.mountAttr
439+
}
440440
}
441-
// The atime mode flags are mutually exclusive and map to a multi-bit
442-
// field (bits 4-6) in MOUNT_ATTR. MS_RELATIME maps to 0 (default).
443-
switch flags & (unix.MS_NOATIME | unix.MS_RELATIME | unix.MS_STRICTATIME) {
444-
case unix.MS_NOATIME:
445-
attr |= unix.MOUNT_ATTR_NOATIME
446-
case unix.MS_STRICTATIME:
447-
attr |= unix.MOUNT_ATTR_STRICTATIME
441+
// Unlike the binary flags above, the atime behaviour is a multi-valued
442+
// field, not an independent on/off bit. MOUNT_ATTR__ATIME (0x70) is the
443+
// bitmask that covers this 3-bit field, with these possible values:
444+
// - MOUNT_ATTR_RELATIME (0x00) — the default; update atime relative to mtime/ctime
445+
// - MOUNT_ATTR_NOATIME (0x10) — do not update access times
446+
// - MOUNT_ATTR_STRICTATIME (0x20) — always update access times
447+
switch {
448+
case flags&unix.MS_NOATIME != 0:
449+
set |= unix.MOUNT_ATTR_NOATIME
450+
case flags&unix.MS_STRICTATIME != 0:
451+
set |= unix.MOUNT_ATTR_STRICTATIME
452+
case flags&unix.MS_RELATIME != 0:
453+
set |= unix.MOUNT_ATTR_RELATIME
448454
}
449455
if flags&unix.MS_LAZYTIME != 0 {
450-
logrus.Warnf("MS_LAZYTIME mount flag specified, but kernel does not support MOUNT_ATTR_LAZYTIME")
456+
logrus.Warnf("MS_LAZYTIME mount flag specified, but kernel does not support MOUNT_ATTR_LAZYTIME; ignoring")
451457
}
452-
return attr
458+
// Always clear the atime mask in attr_clr: whether an atime option was
459+
// specified or not, mount_setattr(2) needs the mask to modify this
460+
// multi-valued field -- either to set a new value or reset to default.
461+
cls |= unix.MOUNT_ATTR__ATIME
462+
463+
return set, cls
453464
}
454465

455466
// createDetachedFsMount creates a detached filesystem mount using fsopen(2),
@@ -479,7 +490,8 @@ func createDetachedFsMount(m *configs.Mount, flags int, data string) (_ *mountSo
479490
return nil, &os.PathError{Op: "fsconfig", Path: fstype, Err: err}
480491
}
481492

482-
fd, err := unix.Fsmount(fsfd, unix.FSMOUNT_CLOEXEC, msFlagsToMountAttr(flags))
493+
mountAttrs, _ := msFlagsToMountAttr(flags)
494+
fd, err := unix.Fsmount(fsfd, unix.FSMOUNT_CLOEXEC, int(mountAttrs))
483495
if err != nil {
484496
return nil, &os.PathError{Op: "fsmount", Path: fstype, Err: err}
485497
}

libcontainer/mount_linux_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,126 @@ func TestStringifyMountFlags(t *testing.T) {
5252
}
5353
}
5454
}
55+
56+
func TestMsFlagsToMountAttr(t *testing.T) {
57+
// allBinaryAttrs is the bitmask of all binary (non-atime) mount attributes.
58+
allBinaryAttrs := uint64(unix.MOUNT_ATTR_RDONLY | unix.MOUNT_ATTR_NOSUID |
59+
unix.MOUNT_ATTR_NODEV | unix.MOUNT_ATTR_NOEXEC |
60+
unix.MOUNT_ATTR_NODIRATIME | unix.MOUNT_ATTR_NOSYMFOLLOW)
61+
62+
// Helper to compute the expected "cls" when only a subset of binary attrs
63+
// is set: the set attrs are removed from cls, the rest stay, and the atime
64+
// field is always cleared.
65+
expectCls := func(setAttrs uint64) uint64 {
66+
return (allBinaryAttrs &^ setAttrs) | unix.MOUNT_ATTR__ATIME
67+
}
68+
69+
for _, test := range []struct {
70+
name string
71+
flags int
72+
wantSet uint64
73+
wantCls uint64
74+
}{
75+
// No flags: all binary attrs cleared, atime defaults to RELATIME.
76+
{"empty", 0, 0, allBinaryAttrs | unix.MOUNT_ATTR__ATIME},
77+
78+
// --- Individual binary flags ---
79+
// Each set flag moves its attr from "cls" to "set"; the rest stay in "cls".
80+
{
81+
"rdonly", unix.MS_RDONLY,
82+
unix.MOUNT_ATTR_RDONLY, expectCls(unix.MOUNT_ATTR_RDONLY),
83+
},
84+
{
85+
"nosuid", unix.MS_NOSUID,
86+
unix.MOUNT_ATTR_NOSUID, expectCls(unix.MOUNT_ATTR_NOSUID),
87+
},
88+
{
89+
"nodev", unix.MS_NODEV,
90+
unix.MOUNT_ATTR_NODEV, expectCls(unix.MOUNT_ATTR_NODEV),
91+
},
92+
{
93+
"noexec", unix.MS_NOEXEC,
94+
unix.MOUNT_ATTR_NOEXEC, expectCls(unix.MOUNT_ATTR_NOEXEC),
95+
},
96+
{
97+
"nodiratime", unix.MS_NODIRATIME,
98+
unix.MOUNT_ATTR_NODIRATIME, expectCls(unix.MOUNT_ATTR_NODIRATIME),
99+
},
100+
{
101+
"nosymfollow", unix.MS_NOSYMFOLLOW,
102+
unix.MOUNT_ATTR_NOSYMFOLLOW, expectCls(unix.MOUNT_ATTR_NOSYMFOLLOW),
103+
},
104+
105+
// --- Individual atime modes ---
106+
// MOUNT_ATTR__ATIME is always in "cls" to clear the atime field first.
107+
{
108+
"noatime", unix.MS_NOATIME,
109+
unix.MOUNT_ATTR_NOATIME, expectCls(0),
110+
},
111+
{
112+
"strictatime", unix.MS_STRICTATIME,
113+
unix.MOUNT_ATTR_STRICTATIME, expectCls(0),
114+
},
115+
{
116+
"relatime", unix.MS_RELATIME,
117+
0, // MOUNT_ATTR_RELATIME is 0x00, clearing the field is sufficient
118+
expectCls(0),
119+
},
120+
121+
// --- Combinations ---
122+
{
123+
"all-binary-flags",
124+
unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
125+
unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW,
126+
allBinaryAttrs, unix.MOUNT_ATTR__ATIME,
127+
},
128+
129+
{
130+
"all-binary-plus-noatime",
131+
unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
132+
unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_NOATIME,
133+
allBinaryAttrs | unix.MOUNT_ATTR_NOATIME, unix.MOUNT_ATTR__ATIME,
134+
},
135+
136+
{
137+
"all-binary-plus-strictatime",
138+
unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
139+
unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_STRICTATIME,
140+
allBinaryAttrs | unix.MOUNT_ATTR_STRICTATIME, unix.MOUNT_ATTR__ATIME,
141+
},
142+
143+
{
144+
"all-binary-plus-relatime",
145+
unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
146+
unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_RELATIME,
147+
allBinaryAttrs, // RELATIME is 0x00, so nothing extra in "set"
148+
unix.MOUNT_ATTR__ATIME,
149+
},
150+
151+
{
152+
"rdonly-plus-noatime",
153+
unix.MS_RDONLY | unix.MS_NOATIME,
154+
unix.MOUNT_ATTR_RDONLY | unix.MOUNT_ATTR_NOATIME,
155+
expectCls(unix.MOUNT_ATTR_RDONLY),
156+
},
157+
158+
// Multiple atime flags set (should not happen in practice, but the
159+
// switch picks the first: NOATIME > STRICTATIME > RELATIME).
160+
{
161+
"noatime-plus-strictatime",
162+
unix.MS_NOATIME | unix.MS_STRICTATIME,
163+
unix.MOUNT_ATTR_NOATIME, expectCls(0),
164+
},
165+
{
166+
"strictatime-plus-relatime",
167+
unix.MS_STRICTATIME | unix.MS_RELATIME,
168+
unix.MOUNT_ATTR_STRICTATIME, expectCls(0),
169+
},
170+
} {
171+
gotSet, gotCls := msFlagsToMountAttr(test.flags)
172+
if gotSet != test.wantSet || gotCls != test.wantCls {
173+
t.Errorf("%s: msFlagsToMountAttr(0x%x) = (set=0x%x, cls=0x%x), want (set=0x%x, cls=0x%x)",
174+
test.name, test.flags, gotSet, gotCls, test.wantSet, test.wantCls)
175+
}
176+
}
177+
}

libcontainer/rootfs_linux.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ func mountToRootfs(c *mountConfig, m *mountEntry) error {
568568
}
569569

570570
// The initial MS_BIND won't change the mount options, we need to do a
571-
// separate MS_BIND|MS_REMOUNT to apply the mount options. We skip
571+
// separate mount_setattr(2) to apply the mount options. We skip
572572
// doing this if the user has not specified any mount flags at all
573573
// (including cleared flags) -- in which case we just keep the original
574574
// mount flags.
@@ -577,6 +577,28 @@ func mountToRootfs(c *mountConfig, m *mountEntry) error {
577577
// contrast to mount(8)'s current behaviour, but is what users probably
578578
// expect. See <https://github.com/util-linux/util-linux/issues/2433>.
579579
if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 {
580+
// The kernel does not yet support MOUNT_ATTR_LAZYTIME.
581+
if m.Flags&unix.MS_LAZYTIME == 0 {
582+
// Try to use mount_setattr(2) with AT_EMPTY_PATH to apply the flags
583+
// directly to the detached mount fd. This is more secure and efficient
584+
// than the old MS_BIND|MS_REMOUNT approach.
585+
attrSet, attrCls := msFlagsToMountAttr(m.Flags)
586+
attr := unix.MountAttr{
587+
Attr_clr: attrCls,
588+
}
589+
if attrSet != 0 {
590+
attr.Attr_set = attrSet
591+
}
592+
mountErr := unix.MountSetattr(int(m.dstFile.Fd()), "", unix.AT_EMPTY_PATH, &attr)
593+
if mountErr == nil {
594+
return setRecAttr(m)
595+
}
596+
if !errors.Is(mountErr, unix.ENOSYS) && !errors.Is(mountErr, unix.EOPNOTSUPP) && !errors.Is(mountErr, unix.EPERM) {
597+
return mountErr
598+
}
599+
// fallback to old school MS_BIND|MS_REMOUNT.
600+
logrus.Debugf("mount_setattr(2) failed for bind-mount %s, falling back to MS_BIND|MS_REMOUNT: %v", m.Destination, mountErr)
601+
}
580602
if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
581603
flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT
582604
// The runtime-spec says we SHOULD map to the relevant mount(8)

tests/integration/mounts_sshfs.bats

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,6 @@ function fail_sshfs_bind_flags() {
133133
run -0 grep -wq rw <<<"$mnt_flags"
134134
run ! grep -wq noexec <<<"$mnt_flags"
135135
run ! grep -wq nosymfollow <<<"$mnt_flags"
136-
# FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
137-
# way will be ignored!
138-
run -0 grep -wq nodiratime <<<"$mnt_flags"
139136

140137
# Now try with a user namespace.
141138
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -212,9 +209,6 @@ function fail_sshfs_bind_flags() {
212209
run ! grep -wq nosuid <<<"$mnt_flags"
213210
run ! grep -wq nodev <<<"$mnt_flags"
214211
run ! grep -wq noexec <<<"$mnt_flags"
215-
# FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
216-
# way will be ignored!
217-
run -0 grep -wq noatime <<<"$mnt_flags"
218212

219213
# Now try with a user namespace.
220214
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -300,8 +294,6 @@ function fail_sshfs_bind_flags() {
300294
# Unspecified flags must be cleared.
301295
run ! grep -wq nosuid <<<"$mnt_flags"
302296
run ! grep -wq nodev <<<"$mnt_flags"
303-
# FIXME: As with mount(8), runc keeps the old atime setting by default.
304-
run -0 grep -wq noatime <<<"$mnt_flags"
305297

306298
# Now try with a user namespace.
307299
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -329,22 +321,6 @@ function fail_sshfs_bind_flags() {
329321
run ! grep -wq nodiratime <<<"${1:-$mnt_flags}"
330322
}
331323

332-
# FIXME: As with mount(8), runc keeps the old atime setting by default.
333-
pass_sshfs_bind_flags "noatime" "bind"
334-
run -0 grep -wq noatime <<<"$mnt_flags"
335-
run ! grep -wq relatime <<<"$mnt_flags"
336-
337-
# FIXME: As with mount(8), runc keeps the old atime setting by default.
338-
pass_sshfs_bind_flags "noatime" "bind,norelatime"
339-
run -0 grep -wq noatime <<<"$mnt_flags"
340-
run ! grep -wq relatime <<<"$mnt_flags"
341-
342-
# FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
343-
# way will be ignored!
344-
pass_sshfs_bind_flags "noatime" "bind,atime"
345-
run -0 grep -wq noatime <<<"$mnt_flags"
346-
run ! grep -wq relatime <<<"$mnt_flags"
347-
348324
# ... but explicitly setting a different flag works.
349325
pass_sshfs_bind_flags "noatime" "bind,relatime"
350326
run ! grep -wq noatime <<<"$mnt_flags"
@@ -370,11 +346,8 @@ function fail_sshfs_bind_flags() {
370346
run ! grep -wq noatime <<<"$mnt_flags"
371347
run -0 grep -wq nodiratime <<<"$mnt_flags"
372348
run ! grep -wq relatime <<<"$mnt_flags"
373-
# FIXME FIXME: relatime should not be set in this case.
374349
pass_sshfs_bind_flags "noatime" "bind,nodiratime,norelatime"
375-
run ! grep -wq noatime <<<"$mnt_flags"
376350
run -0 grep -wq nodiratime <<<"$mnt_flags"
377-
run -0 grep -wq relatime <<<"$mnt_flags"
378351

379352
# Now try with a user namespace.
380353
update_config ' .linux.namespaces += [{"type": "user"}]

0 commit comments

Comments
 (0)