CVE-2026-99170: namespace: a mount that copy_tree() leaves out can't take over a lock
In the Linux kernel, the following vulnerability has been resolved:
namespace: a mount that copytree() leaves out can't take over a lock
Since commit c62a4766937e ("movemount: transfer MNTLOCKED") 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 copytree() leaves a mount of a mount namespace file out of every copy made without CLCOPYMNTNSFILE, together with everything mounted on it, and never looks at MNTLOCKED. unshare(CLONENEWNS) 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 MOVEMOUNTBENEATH, which hands the lock to that bind, and calls unshare(CLONENEWNS). 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.
copytree() already refuses to leave out a locked unbindable mount with -EPERM since commit df7342b24018 ("mount: Don't allow copying MNTUNBINDABLE|MNTLOCKED mounts"). Do the same for a locked mount of a mount namespace file. That also covers a tree copied with CLCOPYMNTNSFILE and attached into a mount namespace of another user namespace where lockmnttree() locks it. Note that unshare(CLONENEWNS) and clone(CLONENEWNS) 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 shrinksubmounts() 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 movemount() 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 MOVEMOUNTBENEATH. 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(CLONENEWNS) 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 AUTOMOUNTDIR names another automount point for "shrink". / #define GNUSOURCE #include <errno.h> #include <fcntl.h> #include <grp.h> #include <sched.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> ---truncated---