Merge patch series "namespace: a mount that copy_tree() leaves out can't take over a lock"

Christian Brauner <[email protected]> says:

Since commit c62a476693 ("move_mount: transfer MNT_LOCKED") a mount
can be moved beneath a locked mount. The lock moves from the old cover
to the new mount which takes over the job of hiding what's under it and
the old cover can be unmounted. That works as long as the new mount
stays. But the caller picks the new mount and copy_tree() leaves a
mount of a mount namespace file out of every copy made without
CL_COPY_MNT_NS_FILE, together with everything mounted on it, and never
looks at MNT_LOCKED. unshare(CLONE_NEWNS) and mount propagation make
such copies.

So a task that is root in its own user namespace gets rid of a locked
cover on a file without a race and without touching the filesystem
below it. It makes a child with a mount namespace of its own, binds the
child's /proc/<pid>/ns/mnt beneath the cover with MOVE_MOUNT_BENEATH,
which hands the lock to that bind, and calls unshare(CLONE_NEWNS). The
new mount namespace has nothing on the file. That's the usual way to
mask a single file, a bind of /dev/null or of an empty file. With a
bind of an ordinary file the file stays covered.

copy_tree() already refuses to leave out a locked unbindable mount with
-EPERM since commit df7342b240 ("mount: Don't allow copying
MNT_UNBINDABLE|MNT_LOCKED mounts"). Do the same for a locked mount of a
mount namespace file. That also covers a tree copied with
CL_COPY_MNT_NS_FILE and attached into a mount namespace of another user
namespace where lock_mnt_tree() locks it. Note that unshare(CLONE_NEWNS)
and clone(CLONE_NEWNS) fail with EPERM from such a mount namespace from
then on. The mount can't be left out and it can't be unmounted.

And refuse to move a mount of a mount namespace file beneath a locked
mount in the first place. Otherwise the task ends up with a mount it
can't unmount and an unshare() that fails for good.

* patches from https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-0-24035bd5d8e9@kernel.org:
  selftests/filesystems: test that a mntns file can't take over a lock
  namespace: a mount that copy_tree() leaves out can't take over a lock

Link: https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-0-24035bd5d8e9@kernel.org
Signed-off-by: Christian Brauner (Amutable) <[email protected]>
This commit is contained in:
Christian Brauner committed 2026-10-14 22:08:06 +02:00
commit 906f04fb1e
2 files changed
+139 -2

No files matched your search

+17 -2
View File
@@ -2252,6 +2252,11 @@ struct mount *copy_tree(struct mount *src_root, struct dentry *dentry,
}
if (!(flag & CL_COPY_MNT_NS_FILE) &&
is_mnt_ns_file(src_mnt->mnt.mnt_root)) {
if (src_mnt->mnt.mnt_flags & MNT_LOCKED) {
/* Both a mount namespace file and locked. */
dst_mnt = ERR_PTR(-EPERM);
goto out;
}
src_mnt = skip_mnt_tree(src_mnt);
continue;
}
@@ -3647,8 +3652,9 @@ static bool mount_is_ancestor(const struct mount *p1, const struct mount *p2)
* @mnt_to: mount under which to mount
* @mp: mountpoint of @mnt_to
*
* - Make sure that the caller can unmount the topmost mount ensuring
* that the caller could reveal the underlying mountpoint.
* - Make sure that @mnt_from can take over the lock of @mnt_to. A
* mount of a mount namespace file is left out of a copy of the tree
* and can't.
* - Ensure that nothing has been mounted on top of @mnt_from before we
* grabbed @namespace_sem to avoid creating pointless shadow mounts.
* - Prevent mounting beneath a mount if the propagation relationship
@@ -3664,6 +3670,15 @@ static int can_move_mount_beneath(const struct mount *mnt_from,
{
struct mount *parent_mnt_to = mnt_to->mnt_parent;
/*
* @mnt_from takes over the lock of @mnt_to. copy_tree() leaves a
* mount of a mount namespace file out of every copy made without
* CL_COPY_MNT_NS_FILE, together with what is mounted on it, so it
* can't keep anything hidden.
*/
if (IS_MNT_LOCKED(mnt_to) && is_mnt_ns_file(mnt_from->mnt.mnt_root))
return -EINVAL;
/* Avoid creating shadow mounts during mount propagation. */
if (mnt_from->overmount)
return -EINVAL;
@@ -9,6 +9,8 @@
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdbool.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -27,6 +29,12 @@
#ifndef MOVE_MOUNT_BENEATH
#define MOVE_MOUNT_BENEATH 0x00000200
#endif
#ifndef OPEN_TREE_CLONE
#define OPEN_TREE_CLONE 1
#endif
#ifndef OPEN_TREE_CLOEXEC
#define OPEN_TREE_CLOEXEC O_CLOEXEC
#endif
#define DIR_LEN 64
#define PATH_LEN 192
@@ -120,6 +128,35 @@ static bool covered(const char *path)
return fd < 0;
}
/* What @path reads, "" if it can't be read. */
static int read_word(const char *path, char *buf, size_t len)
{
ssize_t n = 0;
int fd;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
n = read(fd, buf, len - 1);
close(fd);
}
buf[n > 0 ? n : 0] = 0;
return fd < 0 ? -1 : 0;
}
/* Put a clone of the mount at @src beneath the top mount at @where. */
static int move_beneath(const char *src, const char *where)
{
int fd, ret;
fd = syscall(__NR_open_tree, AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
if (fd < 0)
return -1;
ret = syscall(__NR_move_mount, fd, "", AT_FDCWD, where,
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
close(fd);
return ret;
}
FIXTURE(locked_mount) {
char base[DIR_LEN];
bool tracing; /* debugfs with the tracefs automount is there */
@@ -429,4 +466,89 @@ TEST_F(locked_mount, propagated_copy_keeps_the_cover_locked)
EXPECT_EQ(ret, CHILD_OK);
}
/*
* A mount of a mount namespace file is left out of the copy that
* unshare(CLONE_NEWNS) makes, together with everything mounted on it, so
* it can't take over the lock of a cover it is moved beneath. The child
* tries just that with the namespace file of a grandchild and gets EINVAL.
* A bind of an ordinary file takes the lock over, and the file is still
* covered in the next copy of the mount namespace.
*/
static int ns_file_child(const char *base)
{
char secret[PATH_LEN], mine[PATH_LEN], nsfile[PATH_LEN], buf[32];
int ret = CHILD_OK, pfd[2];
pid_t pid;
snprintf(secret, sizeof(secret), "%s/P/secret", base);
snprintf(mine, sizeof(mine), "%s/mine", base);
if (enter_userns() || pipe(pfd))
return CHILD_SETUP;
/* the cover is locked */
if (!umount2(secret, 0) || errno != EINVAL)
return CHILD_STEP1;
/* a mount namespace newer than our own, so that a bind of it is allowed */
pid = fork();
if (pid < 0)
return CHILD_SETUP;
if (pid == 0) {
if (unshare(CLONE_NEWNS) || send_byte(pfd[1]))
_exit(1);
pause();
_exit(0);
}
if (wait_byte(pfd[0]))
return CHILD_SETUP;
snprintf(nsfile, sizeof(nsfile), "/proc/%d/ns/mnt", pid);
if (!move_beneath(nsfile, secret) || errno != EINVAL)
ret = CHILD_STEP2;
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
if (ret)
return ret;
/* an ordinary file beneath the cover takes the lock over */
if (move_beneath(mine, secret))
return CHILD_STEP3;
if (umount2(secret, 0))
return CHILD_STEP3;
if (!umount2(secret, 0) || errno != EINVAL)
return CHILD_STEP3;
/* and it is in the next copy of the namespace */
if (unshare(CLONE_NEWNS))
return CHILD_SETUP;
if (read_word(secret, buf, sizeof(buf)) || strcmp(buf, "mine"))
return CHILD_STEP4;
return CHILD_OK;
}
TEST_F(locked_mount, mount_namespace_file_cant_take_the_lock)
{
char p[PATH_LEN], q[PATH_LEN];
pid_t pid;
int ret;
snprintf(p, sizeof(p), "%s/P", self->base);
ASSERT_EQ(mkdir(p, 0755), 0);
ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
snprintf(p, sizeof(p), "%s/P/secret", self->base);
ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
snprintf(q, sizeof(q), "%s/P/empty", self->base);
ASSERT_EQ(create_file(q, ""), 0);
/* the cover: an empty file over the secret */
ASSERT_EQ(mount(q, p, NULL, MS_BIND, NULL), 0);
snprintf(q, sizeof(q), "%s/mine", self->base);
ASSERT_EQ(create_file(q, "mine"), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0)
_exit(ns_file_child(self->base));
ret = wait_child(pid);
TH_LOG("child exit code %d", ret);
EXPECT_EQ(ret, CHILD_OK);
}
TEST_HARNESS_MAIN