CVE-2026-99170
NoneCVSS
—
EPSS
0.2%
Published
Oct 28, 2026
CISA due date
—
Cloudanix analysis
A Linux kernel bug lets an attacker inside an isolated environment remove a protective cover from a hidden file, potentially exposing data that should have stayed inaccessible.
- Exploitation
- Local Privilege
- Attacker needs
- Low Privilege
- Complexity
- Moderate
- Impact
- Data Exposure
- Component
- OS kernel
- Class
- Privilege Escalation
- Remediation
- Upgrade
- Effort
- Medium
- Patch risk
- Low
What it actually does
The flaw permits a root process inside its own user namespace to bypass a locked mount cover protecting a file by binding a mount namespace file beneath it via MOVE_MOUNT_BENEATH, transferring the lock, then calling unshare(CLONE_NEWNS) so copy_tree() omits the locked mount-ns file and drops the cover without touching the underlying filesystem.
Why it matters to the business
Attackers with limited privileges in container or namespace-isolated workloads can access files that administrators intended to keep masked, increasing risk of credential or secret exposure on systems that rely on bind-mount hiding or mount locking for isolation.
How an attack unfolds
An attacker enters a new user namespace, creates a child process whose mount namespace file is bound beneath the locked cover using MOVE_MOUNT_BENEATH, then performs unshare(CLONE_NEWNS); copy_tree() omits the mount-ns file so the lock disappears and the previously covered file becomes readable.
How to fix it
Apply the upstream kernel patch that refuses to copy locked mounts of mount namespace files and refuses MOVE_MOUNT_BENEATH of such mounts beneath locked parents; rebuild and deploy updated kernels to all affected hosts.
How to confirm the fix worked
Confirm the patched kernel rejects the move_mount and unshare sequence with EPERM or EINVAL; re-run the published reproducer to verify the cover remains in place and the secret file stays unreadable.
Typical attack path
- Enter a new user namespace with a new mount namespace
- Bind the child process mount namespace file beneath the locked cover using MOVE_MOUNT_BENEATH
- Call unshare(CLONE_NEWNS) to produce a copy that omits the locked mount-ns file
- Read the file that was previously hidden by the cover
How to detect it
Review auditd or eBPF logs for sequences of move_mount beneath locked mounts followed immediately by unshare(CLONE_NEWNS) from processes inside user namespaces; correlate with subsequent file-open events on previously covered paths.
What your security team should do
- Expedite kernel updates on any system exposing user namespaces or mount propagation to untrusted workloads
- Audit container runtime configurations that allow unprivileged users to create mount namespaces
- Add monitoring for unusual unshare(CLONE_NEWNS) and move_mount activity originating from user namespaces
The record
In the Linux kernel, the following vulnerability has been resolved: namespace: a mount that copy_tree() leaves out can't take over a lock 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/<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 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 <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---
Affected packages
| Ecosystem | Package | Introduced | Fixed |
|---|