mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/
synced 2026-11-10 20:48:12 +00:00
1,482,276 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
67b69b8a6e |
fhandle: decide the subtree check under mount_lock
may_decode_fh() lets a caller who is privileged over the mount namespace of @root decode handles below @root->dentry as long as the mount is mounted and no locked child covers something below it. The three parts are read one after the other without a lock: is_mounted() and capable_wrt_mount() read ->mnt_ns and has_locked_children() walks ->mnt_mounts under a mount_lock of its own. Today the gaps are harmless. A lazy umount in between clears ->mnt_ns, but the locked children stay attached to their unmounted parent, so the walk still finds them and the decode is refused either way. The following patches detach every unmounted mount from its parent. Then an umount between is_mounted() and the walk makes the walk come back empty and the caller decodes into what a locked child covered. So take mount_lock once and answer all three questions under it. is_mounted() is stable there, umount_tree() clears ->mnt_ns on the write side, and a mounted mount still has its locked children on its list. ns_capable() under the spinlock is fine, generic_permission() calls it in RCU walk already. has_locked_children() loses its locking wrapper, its other callers hold namespace_sem or mount_lock anyway. Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-13-dd44b89d44ce@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
39796254f4 |
selftests/filesystems: check that an automount below an overlay layer is refused
Make sure that we don't automount on top of internal things. Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-12-dd44b89d44ce@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
2244ca5662 |
selftests/filesystems: check the atime of the empty mount namespace root
No access time updates for immutable nullfs. Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-11-dd44b89d44ce@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
921c14fd00 |
selftests/filesystems: check that a lock lands on the right mount and stays
Check MNT_LOCKED: - A child in a user namespace of its own triggers the tracefs automount through a directory descriptor on the host's debugfs mount. The copy in the child's namespace has to be locked and the host's mount unlocked. - A child moves a bind of that automount beneath a locked covering mount, unmounts the covering mount and asks for the umount of an unlocked ancestor. This must not expire the mount which holds the lock now. - The host mounts and unmounts on a directory that a locked mount covers in a user namespace further down the propagation chain. That cover has to be locked afterwards as it was before. Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-10-dd44b89d44ce@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
d74d66aa70 |
namespace: keep the lock on a mount that a propagated copy is moved beneath
attach_recursive_mnt() transfers MNT_LOCKED from the top mount to the
mount that is moved beneath it with MOVE_MOUNT_BENEATH. This allows the
owner of a user namespace to replace its locked /proc or its root. The
mount beneath takes on the job of covering the underlying mountpoint
allowing the top mount to be unmounted.
Consider two mount namespaces:
(H) The host H has a shared mount P with a secret in P/d
(Z) Z is a user namespace made from M. Its copy of P receives
propagation from the host's P and its copy of M's cover on P/d is
locked Z's owner must not get to see P/d.
Now (H) mounts X on P/d. This propagates into (Z). The copy of X lands
beneath (Z)'s locked cover. The locked property is transfered from (Z)'s
cover to the copy of X propagated beneath it. The cover is now unlocked.
Now (H) unmounts X again. The copy of X in (Z) gets unmounted and the
covering mount is left unlocked on top of P/d. (Z) can now unmount it:
Z: umount2(P/d) = EINVAL /* the cover is locked */
H: mount X on P/d, umount X /* both propagate into Z /*
Z: umount2(P/d) = 0 /* the cover is now unlocked */
Z: read P/d/secret = "covered-by-root" /* secret revealed */
So only transfer the locked property to the mount beneath for mounts the
caller has placed. A propagated copy that lands beneath a locked mount
is locked as well so that the mount at the bottom of the stack carries a
lock the way every check expects. The mount on top of it remains locked
to ensure that it keeps covering even if the propagated mount is
unmounted again.
Fixes:
|
||
|
|
f78ddcc872 |
namespace: never expire a locked mount
Locked mounts are special. They protect the underlying files and directories from being revealed. do_umount() refuses to unmount locked mounts but shrink_submounts() doesn't. A shrinkable mount can become locked once the owner of a user namespace puts it beneath a locked mount with MOVE_MOUNT_BENEATH. The locked property now moves to the mount at the bottom making it possible to unmount the top mount. So umount() of an unlocked ancestor now expires the bottom mount first and the covered directory is revealed: move_mount(c -> x/hidden, MOVE_MOUNT_BENEATH) = 0 the cover after the lock moved: umount2(x/hidden) = 0 the holder of the lock: umount2(x/hidden) = EINVAL the root of the copy, busy: umount2(x) = EBUSY reads x/hidden/secret: "covered-by-root" So leave a locked mount alone as it dies together with its parent. A plain umount() of an unlocked mount with a locked one below it is EBUSY from now on. It's the same for any other locked child. A lazy umount still takes the whole tree. mark_mounts_for_expiry() never sees a locked mount. lock_mnt_tree() leaves a mount on an expiry list alone and nothing else puts a locked one on such a list. Add an assert for this. Fixes: |
||
|
|
caf16f9b4a |
nullfs: don't update the access time
There's no point in updating access times of the nullfs instance. Raise
S_IMMUTABLE.
Fixes:
|
||
|
|
33aadc7c72 |
namespace: handle mount locking for automounts correctly
If mounts are propagated across user namespaces, attach_recursive_mnt()
locks every copy of the source mount to protect overmounts from
vanishing and revealing the underlying files or directories.
The user namespace is taken from the caller's mount namespaces since
this is where the mounts end up. Except, that's not always true.
Automounts may legitimately get popped in by tasks located in a
different mount and user namespace during path lookup.
Then check doesn't make sense at that point. The copy in the namespace
of the parent - which may be the host's - is now locked and the host
cannot change the flags of its own mounts anymore.
Here's the reproducer:
- host has debugfs mounted nosuid,nodev,noexec and shared
- tracefs automount below it is not yet active
- hand a child process a directory descriptor on that mount
- child process enters auser and mount namespace
- child process' namespace now holds a locked copy of the host's debugfs
mount that receives propagation from it
- child process stats "tracing/." through the descriptor
- lookup runs on the host's mount so the automount lands below the host's mount
- propagation puts a copy below the child's copy
- both try to clear the flags on the mount they got, with a bind remount:
host, on its own automount: MS_REMOUNT|MS_BIND = EPERM
child, on the copy in its namespace: MS_REMOUNT|MS_BIND = 0
So the lock landed on the host's mount instead of the child's copy.
Congrats. So we need to compare with the owner of the namespace the
mount actually gets mounted on. For all regular cases that is the
caller's mount namespace and so nothing changes.
Detached trees in anonymous mount namespaces by be handed over via
SCM_RIGHTS or inherited in other ways on purpose so the attaching task's
mount namespace is authoritative, not the creator of the detached tree.
Fixes:
|
||
|
|
ba985ce084 |
namespace: refuse an automount below a mount that is in no namespace
It's possible to add autmounts even when the parent mount isn't in the
mount namespace of the caller. The only requirement we have is that the
parent mount namespace must not be NULL, i.e., unmounted.
Problem is that clone_private_mount() has MNT_NS_INTERNAL which makes
that trivially true. So that passes the test and attach_recursive_mnt()
accepts that as a mount point and funny enough, count_mounts()
dereferences MNT_NS_INTERNAL. The problem is it is an error pointer...
So we can reach this in userspace via fanotify. A filesystem mark on the
lower filesystem of an overlay reports paths on the layer clone and
reading the event hands out a descriptor on it. For example with debugfs
as the lower layer it goes kaboom:
openat(evfd, "tracing", O_DIRECTORY)
Oops: general protection fault
KASAN: null-ptr-deref in range [0x1d0-0x1d7]
RIP: 0010:count_mounts+0x35/0x200
attach_recursive_mnt
finish_automount
And since that sleeping beauty happens under namespace_sem held for
writing every mount operation on the system blocks from then on.
Congrats.
Use is_mounted() instead which rejects unmounted and internal mounts
alike. The open fails with EINVAL just as it did before
clone_private_mount() used MNT_NS_INTERNAL.
Fixes:
|
||
|
|
66cda7ec9a |
selftests/filesystems: check that an immutable inode takes no write hint
Check that an immutable inode takes no write hint. Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-4-dd44b89d44ce@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
b0d1022b9b |
fcntl: refuse F_SET_RW_HINT on an immutable inode
An immutable inode is never written to so write hints are pointless.
Refuse the hint on an immutable inode the way setattr(), fallocate() and
etxattr() refuse their changes.
Fixes:
|
||
|
|
8189ab688f |
namei: don't reveal overmounted entries in refwalk
In rcuwalk the dentry is validated before it is accepted. step_into()
step_into() rechecks d_seq and __follow_mount_rcu() rechecks mount_lock.
An entry that gets unlinked in between causes the lookup to retry and
miss.
A refwalk doesn't do this. lookup_fast() takes a reference on the hashed
dentry and simply accepts it. So an unlink that happens after the
reference was taken isn't seen by refwalk. That's fine for a simple
file. We just happened to open it before it was unlinked, no problem.
For a mountpoint and specifically a locked mountpoint it very much
isn't. The unlink detaches all mounts and then removes the name. Any
refwalk that hasn't traversed the mounts yet simply reveals the
underlying entry. It's a very narrow window but it can be hit:
reads of a covered file in 60 s, 3 walkers, ~15000 unlinks
no widening 27
that step + 200 us 3741
See the appended patch for a more reliable reproducer.
So check the entry after step_into(). unlink(), rmdir() and rename()
mark the dentry with dont_mount() before they detach the mounts and
remove the dentry.
So a dentry that is marked with DCACHE_CANT_MOUNT and is unhashed by the
time its mounts were looked at is a name that was unlinked under the
refwalk. DCACHE_CANT_MOUNT is read after the mounts were looked up and
it is set before they are detached by detach_mounts(). A refwalk that
missed the mounts will see DCACHE_CANT_MOUNT.
Plain d_unlinked() is fine. A rename takes the dentry off its hash chain
but ___d_drop() leaves d_hash.pprev set. So only __d_drop() and a rename
over the dentry unhash it. The only move that flips IS_ROOT splices in a
disconnected alias. That doesn't have DCACHE_CANT_MOUNT set.
Add the to step_into_slowpath(). ".." and LOOKUP_DOWN may legitimately
land on an unhashed directory. A refwalk that crossed onto a mount has
path.mnt different from nd->path.mnt. A dentry that a filesystem dropped
on its own (d_invalidate(), d_drop()) doesn't have the flag set and is
treated as before.
// SPDX-License-Identifier: GPL-2.0
/*
* unlink_covered: unlink a file that is a mountpoint in a detached copy of
* its mount, against walkers that open it through that copy.
*
* A is a tmpfs with the file f1 (content SECRET_F) and the plain file
* MARK_A. A' is an open_tree(OPEN_TREE_CLONE) copy of A with MARK_A bound
* on A'/f1, held through an O_PATH fd on its root once the tree fd is
* closed. Walkers open f1 through that fd while the driver unlinks A/f1,
* where nothing is mounted on it. A walker may read MARK_A or get ENOENT.
* A read of SECRET_F is a hit: the name was found after its mount was
* gone. The lockless walks use openat2(RESOLVE_CACHED).
*
* usage: unlink_covered [-t seconds] [-w walkers]
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <linux/openat2.h>
#ifndef __NR_open_tree
#define __NR_open_tree 428
#endif
#ifndef __NR_move_mount
#define __NR_move_mount 429
#endif
#ifndef __NR_openat2
#define __NR_openat2 437
#endif
#ifndef OPEN_TREE_CLONE
#define OPEN_TREE_CLONE 1
#endif
#ifndef OPEN_TREE_CLOEXEC
#define OPEN_TREE_CLOEXEC O_CLOEXEC
#endif
#ifndef MOVE_MOUNT_F_EMPTY_PATH
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
#endif
#define WORK "/tmp/uc"
#define ADIR WORK "/A"
static int duration = 60, nwalkers = 3;
static atomic_int stop, writer_waiting;
static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER;
static int cur_fd = -1; /* the root of A', -1 while there is none */
static atomic_long n_unlink, n_walk, n_mark, n_enoent, n_other, n_secret,
n_secret_cached;
static void die(const char *what)
{
fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
exit(2);
}
static void put_file(int dfd, const char *name, const char *content)
{
int fd = openat(dfd, name, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
0644);
if (fd < 0 || write(fd, content, strlen(content)) < 0)
die(name);
close(fd);
}
/* "plain/../" @n times, then f1: a longer walk that checks nothing on its way */
static char *longpath(int n)
{
char *p = malloc(n * 9 + 3), *q = p;
for (int i = 0; i < n; i++, q += 9)
memcpy(q, "plain/../", 9);
strcpy(q, "f1");
return p;
}
static void try_read(int dfd, const char *path, int cached)
{
struct open_how how = { .flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_CACHED };
char buf[32] = "";
long n;
int fd;
if (cached)
fd = syscall(__NR_openat2, dfd, path, &how, sizeof(how));
else
fd = openat(dfd, path, O_RDONLY | O_CLOEXEC);
atomic_fetch_add(&n_walk, 1);
if (fd < 0) {
if (errno == ENOENT)
atomic_fetch_add(&n_enoent, 1);
else if (!cached || errno != EAGAIN)
atomic_fetch_add(&n_other, 1);
return;
}
n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n >= 6 && !strncmp(buf, "SECRET", 6)) {
if (cached)
atomic_fetch_add(&n_secret_cached, 1);
if (!atomic_fetch_add(&n_secret, 1))
printf("HIT: read \"%s\" through %s (%s walk)\n", buf,
path, cached ? "lockless" : "any");
} else {
atomic_fetch_add(&n_mark, 1);
}
}
static void *walker(void *arg)
{
unsigned int r = (long)arg * 2654435761u;
while (!atomic_load(&stop)) {
char *p_long, *p_short;
int dfd;
while (atomic_load(&writer_waiting) && !atomic_load(&stop))
usleep(20);
pthread_rwlock_rdlock(&cur_lock);
dfd = cur_fd;
if (dfd < 0) {
pthread_rwlock_unlock(&cur_lock);
usleep(100);
continue;
}
r = r * 1103515245u + 12345u;
p_long = longpath(1 + (r >> 8) % 400);
p_short = longpath(0);
try_read(dfd, p_long, 0);
try_read(dfd, p_short, 0);
try_read(dfd, p_long, 1);
pthread_rwlock_unlock(&cur_lock);
free(p_long);
free(p_short);
}
return NULL;
}
/* hand the walkers a new A' (or none), close the old one */
static void publish(int fd)
{
int old;
atomic_fetch_add(&writer_waiting, 1);
pthread_rwlock_wrlock(&cur_lock);
old = cur_fd;
cur_fd = fd;
pthread_rwlock_unlock(&cur_lock);
atomic_fetch_sub(&writer_waiting, 1);
if (old >= 0)
close(old);
}
static void *driver(void *arg __attribute__((unused)))
{
int a;
if (mkdir(ADIR, 0755) && errno != EEXIST)
die("mkdir A");
if (mount("A", ADIR, "tmpfs", 0, "size=4M"))
die("mount A");
a = open(ADIR, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (a < 0 || mkdirat(a, "plain", 0755))
die("A/plain");
put_file(a, "MARK_A", "MARK_A");
while (!atomic_load(&stop)) {
int t, m, fd_a;
put_file(a, "f1", "SECRET_F");
t = syscall(__NR_open_tree, a, "",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH);
if (t < 0)
die("open_tree A");
m = syscall(__NR_open_tree, a, "MARK_A",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
if (m < 0)
die("open_tree MARK_A");
if (syscall(__NR_move_mount, m, "", t, "f1",
MOVE_MOUNT_F_EMPTY_PATH))
die("move_mount");
close(m);
fd_a = openat(t, ".", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (fd_a < 0)
die("open A'");
publish(fd_a);
usleep(200 + rand() % 1000);
close(t); /* A' is unmounted, fd_a holds it */
usleep(200 + rand() % 1000);
if (unlinkat(a, "f1", 0)) /* through A, a plain file there */
die("unlink f1");
atomic_fetch_add(&n_unlink, 1);
usleep(rand() % 300);
publish(-1);
}
close(a);
umount2(ADIR, MNT_DETACH);
return NULL;
}
int main(int argc, char **argv)
{
pthread_t d, *w;
int c, i;
setvbuf(stdout, NULL, _IOLBF, 0);
while ((c = getopt(argc, argv, "t:w:")) != -1) {
switch (c) {
case 't':
duration = atoi(optarg);
break;
case 'w':
nwalkers = atoi(optarg);
break;
default:
fprintf(stderr, "usage: unlink_covered [-t seconds] [-w walkers]\n");
return 2;
}
}
if (unshare(CLONE_NEWNS) ||
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
die("unshare");
if (mkdir(WORK, 0755) && errno != EEXIST)
die("mkdir");
w = calloc(nwalkers, sizeof(*w));
for (i = 0; i < nwalkers; i++)
if (pthread_create(&w[i], NULL, walker, (void *)(long)i))
die("pthread_create");
if (pthread_create(&d, NULL, driver, NULL))
die("pthread_create");
sleep(duration);
atomic_store(&stop, 1);
pthread_join(d, NULL);
for (i = 0; i < nwalkers; i++)
pthread_join(w[i], NULL);
printf("unlink_covered: %d s, %d walkers: unlinks %ld walks %ld mark %ld enoent %ld other %ld SECRET %ld (lockless %ld)\n",
duration, nwalkers, atomic_load(&n_unlink), atomic_load(&n_walk),
atomic_load(&n_mark), atomic_load(&n_enoent),
atomic_load(&n_other), atomic_load(&n_secret),
atomic_load(&n_secret_cached));
return atomic_load(&n_secret) ? 1 : 0;
}
diff --git a/fs/namei.c b/fs/namei.c
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -35,6 +35,7 @@
#include <linux/fcntl.h>
#include <linux/device_cgroup.h>
#include <linux/fs_struct.h>
+#include <linux/delay.h>
#include <linux/posix_acl.h>
#include <linux/hash.h>
#include <linux/bitops.h>
@@ -1205,9 +1206,26 @@ static int sysctl_protected_symlinks __read_mostly;
static int sysctl_protected_hardlinks __read_mostly;
static int sysctl_protected_fifos __read_mostly;
static int sysctl_protected_regular __read_mostly;
+/* debug: widen the two windows of the detach_mounts() race */
+static int sysctl_detach_race_walk_us __read_mostly;
+static int sysctl_detach_race_unlink_us __read_mostly;
#ifdef CONFIG_SYSCTL
static const struct ctl_table namei_sysctls[] = {
+ {
+ .procname = "detach_race_walk_us",
+ .data = &sysctl_detach_race_walk_us,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec,
+ },
+ {
+ .procname = "detach_race_unlink_us",
+ .data = &sysctl_detach_race_unlink_us,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec,
+ },
{
.procname = "protected_symlinks",
.data = &sysctl_protected_symlinks,
@@ -1878,6 +1896,10 @@ static struct dentry *lookup_fast(struct nameidata *nd)
dentry = __d_lookup(parent, &nd->last);
if (unlikely(!dentry))
return NULL;
+ /* debug: between finding a mountpoint and crossing its mounts */
+ if (unlikely(sysctl_detach_race_walk_us) && d_mountpoint(dentry))
+ usleep_range(sysctl_detach_race_walk_us,
+ sysctl_detach_race_walk_us + 10);
status = d_revalidate(nd->inode, &nd->last, dentry, nd->flags);
}
if (unlikely(status <= 0)) {
@@ -5693,6 +5715,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir,
if (!error) {
dont_mount(dentry);
detach_mounts(dentry);
+ /* debug: the mounts are gone, d_delete() is still to come */
+ if (unlikely(sysctl_detach_race_unlink_us))
+ usleep_range(sysctl_detach_race_unlink_us,
+ sysctl_detach_race_unlink_us + 10);
}
}
}
Fixes:
|
||
|
|
528b383dc3 |
namespace: unhash a dentry before detaching the mounts on it
unlink(), rmdir() and rename() remove the entry from the filesystem,
call detach_mounts() on the entry with the inode locked and then call
d_delete() once the inode is unlocked.
The thing is that between detach_mounts() and d_delete() the dentry is
still hashed and positive. But after detach_mounts() nothing covers the
dentry anymore. A lookup that finds this dentry in the dcache can
uncover what the mounts hid.
That's a problem when unlinking files or directories that are
mountpoints in other mount namespaces. Everybody who had the underlying
entry covered can race the detach_mount() call until d_delete() has run.
The race window isn't all that small. It encompasses namespace_unlock()
with a full synchronize_rcu_expedited() grace period and the
inode_unlock() of the entry.
In my experiments three walkers that kept trying read an overmounted
file in 11938 of the 16140 unlinks that removed its mountpoint from a
bind mount of the filesystem within a minute.
Fun fact, d_invalidate() has the same ordering problem but gets it
right. It unhashes the dentry first and detaches the mounts afterwards.
Let's do the same in __detach_mounts():
- A lookup that hasn't found the dentry yet misses it in the dcache and
waits for the directory lock that the caller holds until the name is
gone for good.
- A lockless lookup that found it already fails the mount_lock check
while the dentry still counts as a mountpoint and the d_seq check once
it doesn't, and retries.
This fixes the lockless path. We still need to fix the reference count
lookup in a follow-up patch.
So d_drop() the dentry. The dentry stays positive and held but can't be
found anymore. Then proceed with the detach and unlink.
Reproducer:
The reads were counted with the program below, three walkers for 60 s
in a VM with 4 CPUs. The race window is stretched with the debug patch
pasted here. fs.detach_race_walk_us sleeps in lookup_fast() once it
found a mountpoint dentry and before its mounts are crossed.
fs.detach_race_unlink_us sleeps in vfs_unlink() between detach_mounts()
and d_delete().
// SPDX-License-Identifier: GPL-2.0
/*
* unlink_covered: unlink a file that is a mountpoint in a detached copy of
* its mount, against lookups that open it through that copy.
*
* A is a tmpfs with the file f1 (content SECRET_F) and the plain file
* MARK_A. A' is an open_tree(OPEN_TREE_CLONE) copy of A with MARK_A bound
* on A'/f1, held through an O_PATH fd on its root once the tree fd is
* closed. Walkers open f1 through that fd while the driver unlinks A/f1,
* where nothing is mounted on it. A walker may read MARK_A or get ENOENT.
* A read of SECRET_F is a hit: the name was found after its mount was
* gone. The lockless walks use openat2(RESOLVE_CACHED).
*
* usage: unlink_covered [-t seconds] [-w walkers]
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <linux/openat2.h>
#ifndef __NR_open_tree
#define __NR_open_tree 428
#endif
#ifndef __NR_move_mount
#define __NR_move_mount 429
#endif
#ifndef __NR_openat2
#define __NR_openat2 437
#endif
#ifndef OPEN_TREE_CLONE
#define OPEN_TREE_CLONE 1
#endif
#ifndef OPEN_TREE_CLOEXEC
#define OPEN_TREE_CLOEXEC O_CLOEXEC
#endif
#ifndef MOVE_MOUNT_F_EMPTY_PATH
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
#endif
#define WORK "/tmp/uc"
#define ADIR WORK "/A"
static int duration = 60, nwalkers = 3;
static atomic_int stop, writer_waiting;
static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER;
static int cur_fd = -1; /* the root of A', -1 while there is none */
static atomic_long n_unlink, n_walk, n_mark, n_enoent, n_other, n_secret,
n_secret_cached;
static void die(const char *what)
{
fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
exit(2);
}
static void put_file(int dfd, const char *name, const char *content)
{
int fd = openat(dfd, name, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
0644);
if (fd < 0 || write(fd, content, strlen(content)) < 0)
die(name);
close(fd);
}
/* "plain/../" @n times, then f1: a longer walk that checks nothing on its way */
static char *longpath(int n)
{
char *p = malloc(n * 9 + 3), *q = p;
for (int i = 0; i < n; i++, q += 9)
memcpy(q, "plain/../", 9);
strcpy(q, "f1");
return p;
}
static void try_read(int dfd, const char *path, int cached)
{
struct open_how how = { .flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_CACHED };
char buf[32] = "";
long n;
int fd;
if (cached)
fd = syscall(__NR_openat2, dfd, path, &how, sizeof(how));
else
fd = openat(dfd, path, O_RDONLY | O_CLOEXEC);
atomic_fetch_add(&n_walk, 1);
if (fd < 0) {
if (errno == ENOENT)
atomic_fetch_add(&n_enoent, 1);
else if (!cached || errno != EAGAIN)
atomic_fetch_add(&n_other, 1);
return;
}
n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n >= 6 && !strncmp(buf, "SECRET", 6)) {
if (cached)
atomic_fetch_add(&n_secret_cached, 1);
if (!atomic_fetch_add(&n_secret, 1))
printf("HIT: read \"%s\" through %s (%s walk)\n", buf,
path, cached ? "lockless" : "any");
} else {
atomic_fetch_add(&n_mark, 1);
}
}
static void *walker(void *arg)
{
unsigned int r = (long)arg * 2654435761u;
while (!atomic_load(&stop)) {
char *p_long, *p_short;
int dfd;
while (atomic_load(&writer_waiting) && !atomic_load(&stop))
usleep(20);
pthread_rwlock_rdlock(&cur_lock);
dfd = cur_fd;
if (dfd < 0) {
pthread_rwlock_unlock(&cur_lock);
usleep(100);
continue;
}
r = r * 1103515245u + 12345u;
p_long = longpath(1 + (r >> 8) % 400);
p_short = longpath(0);
try_read(dfd, p_long, 0);
try_read(dfd, p_short, 0);
try_read(dfd, p_long, 1);
pthread_rwlock_unlock(&cur_lock);
free(p_long);
free(p_short);
}
return NULL;
}
/* hand the walkers a new A' (or none), close the old one */
static void publish(int fd)
{
int old;
atomic_fetch_add(&writer_waiting, 1);
pthread_rwlock_wrlock(&cur_lock);
old = cur_fd;
cur_fd = fd;
pthread_rwlock_unlock(&cur_lock);
atomic_fetch_sub(&writer_waiting, 1);
if (old >= 0)
close(old);
}
static void *driver(void *arg __attribute__((unused)))
{
int a;
if (mkdir(ADIR, 0755) && errno != EEXIST)
die("mkdir A");
if (mount("A", ADIR, "tmpfs", 0, "size=4M"))
die("mount A");
a = open(ADIR, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (a < 0 || mkdirat(a, "plain", 0755))
die("A/plain");
put_file(a, "MARK_A", "MARK_A");
while (!atomic_load(&stop)) {
int t, m, fd_a;
put_file(a, "f1", "SECRET_F");
t = syscall(__NR_open_tree, a, "",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH);
if (t < 0)
die("open_tree A");
m = syscall(__NR_open_tree, a, "MARK_A",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
if (m < 0)
die("open_tree MARK_A");
if (syscall(__NR_move_mount, m, "", t, "f1",
MOVE_MOUNT_F_EMPTY_PATH))
die("move_mount");
close(m);
fd_a = openat(t, ".", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (fd_a < 0)
die("open A'");
publish(fd_a);
usleep(200 + rand() % 1000);
close(t); /* A' is unmounted, fd_a holds it */
usleep(200 + rand() % 1000);
if (unlinkat(a, "f1", 0)) /* through A, a plain file there */
die("unlink f1");
atomic_fetch_add(&n_unlink, 1);
usleep(rand() % 300);
publish(-1);
}
close(a);
umount2(ADIR, MNT_DETACH);
return NULL;
}
int main(int argc, char **argv)
{
pthread_t d, *w;
int c, i;
setvbuf(stdout, NULL, _IOLBF, 0);
while ((c = getopt(argc, argv, "t:w:")) != -1) {
switch (c) {
case 't':
duration = atoi(optarg);
break;
case 'w':
nwalkers = atoi(optarg);
break;
default:
fprintf(stderr, "usage: unlink_covered [-t seconds] [-w walkers]\n");
return 2;
}
}
if (unshare(CLONE_NEWNS) ||
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
die("unshare");
if (mkdir(WORK, 0755) && errno != EEXIST)
die("mkdir");
w = calloc(nwalkers, sizeof(*w));
for (i = 0; i < nwalkers; i++)
if (pthread_create(&w[i], NULL, walker, (void *)(long)i))
die("pthread_create");
if (pthread_create(&d, NULL, driver, NULL))
die("pthread_create");
sleep(duration);
atomic_store(&stop, 1);
pthread_join(d, NULL);
for (i = 0; i < nwalkers; i++)
pthread_join(w[i], NULL);
printf("unlink_covered: %d s, %d walkers: unlinks %ld walks %ld mark %ld enoent %ld other %ld SECRET %ld (lockless %ld)\n",
duration, nwalkers, atomic_load(&n_unlink), atomic_load(&n_walk),
atomic_load(&n_mark), atomic_load(&n_enoent),
atomic_load(&n_other), atomic_load(&n_secret),
atomic_load(&n_secret_cached));
return atomic_load(&n_secret) ? 1 : 0;
}
diff --git a/fs/namei.c b/fs/namei.c
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -35,6 +35,7 @@
#include <linux/fcntl.h>
#include <linux/device_cgroup.h>
#include <linux/fs_struct.h>
+#include <linux/delay.h>
#include <linux/posix_acl.h>
#include <linux/hash.h>
#include <linux/bitops.h>
@@ -1205,9 +1206,26 @@ static int sysctl_protected_symlinks __read_mostly;
static int sysctl_protected_hardlinks __read_mostly;
static int sysctl_protected_fifos __read_mostly;
static int sysctl_protected_regular __read_mostly;
+/* debug: widen the two windows of the detach_mounts() race */
+static int sysctl_detach_race_walk_us __read_mostly;
+static int sysctl_detach_race_unlink_us __read_mostly;
#ifdef CONFIG_SYSCTL
static const struct ctl_table namei_sysctls[] = {
+ {
+ .procname = "detach_race_walk_us",
+ .data = &sysctl_detach_race_walk_us,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec,
+ },
+ {
+ .procname = "detach_race_unlink_us",
+ .data = &sysctl_detach_race_unlink_us,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec,
+ },
{
.procname = "protected_symlinks",
.data = &sysctl_protected_symlinks,
@@ -1878,6 +1896,10 @@ static struct dentry *lookup_fast(struct nameidata *nd)
dentry = __d_lookup(parent, &nd->last);
if (unlikely(!dentry))
return NULL;
+ /* debug: between finding a mountpoint and crossing its mounts */
+ if (unlikely(sysctl_detach_race_walk_us) && d_mountpoint(dentry))
+ usleep_range(sysctl_detach_race_walk_us,
+ sysctl_detach_race_walk_us + 10);
status = d_revalidate(nd->inode, &nd->last, dentry, nd->flags);
}
if (unlikely(status <= 0)) {
@@ -5693,6 +5715,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir,
if (!error) {
dont_mount(dentry);
detach_mounts(dentry);
+ /* debug: the mounts are gone, d_delete() is still to come */
+ if (unlikely(sysctl_detach_race_unlink_us))
+ usleep_range(sysctl_detach_race_unlink_us,
+ sysctl_detach_race_unlink_us + 10);
}
}
}
Fixes:
|
||
|
|
cbade031e2 |
Merge patch series "mount: more bugfixes, the Oprah edition"
Christian Brauner <brauner@kernel.org> says: Yet more bugfixes falling out of my recent work in this area: - don't let a pseudo dentry become the root of a mount - don't drop active namespace references that were never taken - don't put a mountpoint on a dentry that's being removed - remove the fsnotify marks of a mount namespace in process context - don't reconfigure internal superblocks via remount and umount - check the mounts before reading their parents in pivot_root() - look at the topmost mount for a mount namespace file - keep covered mounts covered in OPEN_TREE_NAMESPACE - check a recursive bind mount for mount namespace loops - check a submount for references right before unmounting it - queue a mount only once for mount notifications * patches from https://patch.msgid.link/20260930-work-mount-fixes-3-v1-0-be34c83956ae@kernel.org: namespace: don't let a pseudo dentry become the root of a mount unshare: don't drop active namespace references that were never taken dcache: don't put a mountpoint on a dentry that's being removed namespace: remove the fsnotify marks of a mount namespace in process context selftests/filesystems: check that the nullfs root can't be reconfigured namespace: don't reconfigure internal superblocks via remount and umount namespace: check the mounts before reading their parents in pivot_root() selftests/filesystems: check that a mount namespace file on top doesn't bury a mount namespace: look at the topmost mount for a mount namespace file selftests/filesystems: check that OPEN_TREE_NAMESPACE keeps mounts covered namespace: keep covered mounts covered in OPEN_TREE_NAMESPACE selftests/filesystems: check that a recursive bind mount can't pin the caller's mount namespace namespace: check a recursive bind mount for mount namespace loops selftests/filesystems: check that a busy submount survives a synchronous umount namespace: check a submount for references right before unmounting it namespace: queue a mount only once for mount notifications Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-0-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
b138e16fb1 |
namespace: don't let a pseudo dentry become the root of a mount
d_alloc_pseudo() hands out dentries for pipes, sockets and other files
that are never anyone's child or parent. They carry DCACHE_NORCU and
dentry_free() frees them right away because no lockless path walk can
ever reach them. That holds as long as such a dentry isn't the root of
a mount. But bind mounting /proc/self/fd/<fd> of such a file does
exactly that.
Most callers of alloc_file_pseudo() put their files on kernel-internal
mounts and may_copy_tree() refuses those. bpf_token_create() doesn't. It
places the token file on the bpffs mount the caller handed it and that
mount is in the caller's mount namespace so the clone goes through:
mount --bind /proc/self/fd/<token> <file>
open_tree(tokfd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE) + move_mount()
__follow_mount_rcu() then loads ->mnt_root of that mount, reads d_seq
and d_flags of the dentry and only then checks mount_lock. The dentry
can be gone by then. The final mntput() of an unmounted parent unhooks
a child that stayed attached to it under mount_lock alone. When the
child's holder does the final put right after that the root is dput()
and freed immediately while a walker that found the mount hashed still
looks at it.
Refuse to clone a mount with a DCACHE_NORCU dentry as its root. Nothing
sensible can be done with a bind mount of a bpf token anyway.
Fixes:
|
||
|
|
0b6f31a3c0 |
unshare: don't drop active namespace references that were never taken
Active references on the namespaces of an nsproxy are taken when the nsproxy is installed into a task in switch_task_namespaces() and copy_namespaces() and dropped again by put_nsproxy() through deactivate_nsproxy(). But ksys_unshare() calls put_nsproxy() on an nsproxy that was never installed when set_cred_ucounts() fails. The new namespaces of that nsproxy go from zero to minus one and the namespaces shared with the caller lose a reference that belongs to the nsproxy the caller keeps using: WARNING: kernel/nscommon.c:171 at __ns_ref_active_put+0x1cd/0x230 nsproxy_ns_active_put deactivate_nsproxy ksys_unshare Afterwards the namespaces the caller lives in aren't listed by listns() anymore. set_cred_ucounts() only fails when alloc_ucounts() can't allocate and it only allocates when the real uid of the caller differs from its effective uid. Commit |
||
|
|
75f4197f1e |
dcache: don't put a mountpoint on a dentry that's being removed
rmdir(), unlink() and rename() call dont_mount() on the victim and then
detach_mounts() with the victim's inode locked. do_lock_mount() takes
the inode lock of the mountpoint and checks cant_mount() so a mount
can't show up after detach_mounts().
But attach_recursive_mnt() makes a second mountpoint for the root of the
source mount so that the mounts already located at the destination can
be put on top of it. No inode is locked for that one and d_set_mounted()
only refuses a dentry that is unlinked. Between detach_mounts() and
d_delete() the victim is still hashed:
rmrace: b passed dont_mount() and detach_mounts(), sleeping
T2: move_mount(S, /tmp/plcant/x, BENEATH) = 0 errno 0 ()
T1: rmdir(/tmp/plcant/d/b) = 0 errno 0
109 107 0:61 /d/b//deleted /tmp/plcant/x rw,relatime - tmpfs tmpfs rw
108 109 0:63 / /tmp/plcant/x rw,relatime - tmpfs T rw
A bind mount of the directory that's being removed is moved beneath an
existing mount while the rmdir() is located between the two calls. The
mount ends up on the removed directory and nothing will ever detach it.
Check cant_mount() in d_set_mounted() as well. dont_mount() raises the
flag under d_lock before detach_mounts() runs so either the mountpoint
is set first and detach_mounts() finds the mount or the flag is seen and
the mount is refused.
Fixes:
|
||
|
|
1a116222ac |
namespace: remove the fsnotify marks of a mount namespace in process context
mnt_ns_release() drops the last passive reference of a mount namespace
and removes its fanotify marks via fsnotify_mntns_delete(). That takes
the mutex of every group with a mark on the namespace and the spinlock
of the connector. Fine from process context. But mnt_ns_tree_remove()
hands the reference the namespace was allocated with to call_rcu() and
so the marks are removed from the RCU softirq:
BUG: sleeping function called from invalid context at kernel/locking/mutex.c:623
in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 0, name: swapper/3
__mutex_lock+0x113/0x24b0
fsnotify_destroy_marks+0x11b/0x3d0
mnt_ns_release_rcu+0x57/0xa0
rcu_core+0x6b6/0x1e40
and lockdep complains about the connector lock being taken from softirq
context. A fanotify group with a FAN_MARK_MNTNS mark on the mount
namespace of another task is all that's needed. Root in a user
namespace can do that for a mount namespace it owns. The task exits and
the namespace is freed with the mark still on it.
Remove the marks in free_mnt_ns() before the namespace is handed to RCU.
That runs in process context once the last active reference is gone. A
mark is added through a file descriptor to the namespace which holds an
active reference so no mark can show up after that.
Fixes:
|
||
|
|
5e83e3a38b |
selftests/filesystems: check that the nullfs root can't be reconfigured
Add a test for the root of an empty mount namespace: - fspick() of the root fails with EINVAL - mount(MS_REMOUNT) of the root fails with EINVAL - umount() of the root fails and doesn't remount it read-only Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-12-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
d791606bb9 |
namespace: don't reconfigure internal superblocks via remount and umount
Commit |
||
|
|
6b7c001eb5 |
namespace: check the mounts before reading their parents in pivot_root()
path_pivot_root() reads the parents of new_root and of the caller's root
and checks whether they are shared before it checks that either mount is
in the caller's mount namespace. Only namespace_sem is held. That's fine
for a mount that is in the caller's mount namespace.
But new_root can be a file descriptor to a mount that has been unmounted
and that only the file descriptor keeps alive. If that mount stayed
attached to its parent when it was unmounted nothing pins the parent for
it. The final mntput() of the parent unhooks the children under
mount_lock alone and frees the parent afterwards:
pivot_root() close(fd), last ref on the parent
------------ ---------------------------------
ex_parent = new_mnt->mnt_parent
mntput_no_expire_slowpath()
__umount_mnt(new_mnt)
cleanup_mnt()
call_rcu()
IS_MNT_SHARED(ex_parent)
BUG: KASAN: slab-use-after-free in path_pivot_root+0xf1a/0x1840
Read of size 4 at addr ffff8881047382f8 by task pivot_widen/157
path_pivot_root+0xf1a/0x1840
__x64_sys_pivot_root+0x165/0x190
The same goes for the caller's root via chroot(). Both outcomes of the
check end in EINVAL so nothing but the read itself goes wrong. It's the
same thing commit
|
||
|
|
c518a195f6 |
selftests/filesystems: check that a mount namespace file on top doesn't bury a mount
Add a test for moving a mount with a mount namespace file on top of it: - S, a bind mount of a file, with a newer mount namespace's file on top - A shared with a slave B that has Q on B/file - S moved onto A/file, its copy lands on B/file below Q B/file keeps reading Q and once Q is unmounted it reads the copy. Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-9-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
8714b44ada |
namespace: look at the topmost mount for a mount namespace file
attach_recursive_mnt() preallocates a mountpoint for the root of the
topmost mount of the source so that a mount that already sits at the
destination can be put on top of the source or on top of one of its
propagated copies.
The copies are made without CL_COPY_MNT_NS_FILE so their chain of
overmounts is shorter than the source's. It ends below the first bind
mount of a mount namespace file. So while the loop walks up the chain of
the source it remembers the mountpoint of that mount in "shorter" and
the existing mount is put there for the copies.
But the loop ends at the topmost mount without ever looking at it. If
the topmost mount is the only mount namespace file in the chain
"shorter" stays NULL and mnt_change_mountpoint() attaches the existing
mount of the copy below the root of the mount namespace file. That's a
dentry of nsfs. No path walk in the copy's mount namespace ever gets
there. The mount is gone until its parent goes and the copy that took
its place can't be unmounted synchronously because it has a child:
S bind mount of a file
N bind mount of a mount namespace file on top of S
A shared mount, B a slave of A, Q mounted on B/file
move_mount(S, A/file)
cat B/file -> the content of S instead of Q
umount B/file -> EBUSY
Look at every mount of the chain including the topmost one.
Fixes:
|
||
|
|
fe187cbb48 |
selftests/filesystems: check that OPEN_TREE_NAMESPACE keeps mounts covered
Add a test for open_tree(OPEN_TREE_NAMESPACE) from a user namespace that doesn't own the mount namespace it copies from: - a file covered by a private or an unbindable tmpfs mount - the non-recursive copy of the parent mount fails with EINVAL - the recursive copy keeps the file covered in the new mount namespace - the owner of the mount namespace keeps the bind mount semantics Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-7-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
0febe278ec |
namespace: keep covered mounts covered in OPEN_TREE_NAMESPACE
open_tree(OPEN_TREE_NAMESPACE) creates a new mount namespace from a
copy of the tree at the given path. The caller needs to be privileged
over its current user namespace because the new mount namespace will be
owned by it. It doesn't need to be privileged over the mount namespace
the tree is copied from. An unprivileged user just needs to create a
user namespace first.
The copy follows the rules of a detached bind mount. Without
AT_RECURSIVE only the mount itself is cloned and its children are left
out. With AT_RECURSIVE unbindable mounts are skipped. Both is fine when
the caller has privileges over the source mount namespace because it
could unmount those mounts anyway. Not so for an unprivileged user in
that mount namespace. For them a mount namespace copy is what unshare()
does. copy_mnt_ns() copies everything including unbindable mounts and
lock_mnt_tree() makes sure nothing can be unmounted in the copy. Nothing
that was covered gets revealed.
create_new_namespace() only does the locking and so the covered content
is right there in the new mount namespace:
# mount -t tmpfs none /tmp/otn; mkdir /tmp/otn/covered
# echo hidden > /tmp/otn/covered/under.txt
# mount -t tmpfs none /tmp/otn/covered
uid 1000, after unshare(CLONE_NEWUSER):
fd = open_tree(AT_FDCWD, "/tmp/otn", OPEN_TREE_NAMESPACE);
setns(fd, CLONE_NEWNS);
open("/covered/under.txt", O_RDONLY) -> "hidden"
Covering paths with mounts is how container runtimes mask parts of
/proc and /sys and how admins hide things.
When the caller's user namespace doesn't own the source mount namespace
copy the way unshare() does and refuse a non-recursive copy of a mount
that has anything mounted below the requested directory and copy
unbindable mounts in a recursive copy. A caller that is privileged over
the source mount namespace sees no change.
Fixes:
|
||
|
|
7c07b18300 |
selftests/filesystems: check that a recursive bind mount can't pin the caller's mount namespace
Add a test for a recursive bind mount of a namespace file in another mount namespace: - a bind mount of the network namespace file, held through a descriptor - the caller's own mount namespace file stacked on top of it there - the recursive bind mount through the descriptor fails with EINVAL - a plain bind mount of the file still works The copy would pin the namespace it is put in otherwise. Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-5-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
c640a01a78 |
namespace: check a recursive bind mount for mount namespace loops
do_loopback() refuses to bind mount the file of a mount namespace that is as old as the caller's or older because a mount namespace that holds a mount of its own file, or of an ancestor's, would cause a cycle. But it only checks the dentry the bind mount starts from. With MS_REC everything below it is copied and do_loopback() passes CL_COPY_MNT_NS_FILE so mount namespace files below the source are copied. That's fine for a source in the caller's own mount namespace. Every mount namespace file in there passed the same check when it was bind-mounted. It isn't fine for a source in another mount namespace. may_copy_tree() accepts a bind mount of any nsfs or pidfs file no matter what mount namespace it lives in so that /proc/<pid>/ns/<ns> and pidfds can be bind mounted. If we create a bind-mount stack of mount namespaces file descriptors the kernel will copy them irrespective of their ancestoral relationship to the mount namespace in question: 151 146 0:7 net:[4026531833] /tmp/nrc/y rw - nsfs nsfs rw 152 151 0:7 mnt:[4026532293] /tmp/nrc/y rw - nsfs nsfs rw Mount 152 is a mount of the mount namespace file of mount namespace 4026532293 inside mount namespace 4026532293. That namespace and every mount in it are leaked... All it takes is a process in an older mount namespace that stacks the file and repeating it leaks without limit: after control: Shmem: 380 kB child: mount(/proc/self/fd/6, MS_BIND|MS_REC): ok after cycle: Shmem: 65916 kB do_move_mount() runs check_for_nsfs_mounts() over a detached tree for exactly that reason. Do the same for the copy before it is grafted. Fixes: |
||
|
|
520b5a15fc |
selftests/filesystems: check that a busy submount survives a synchronous umount
Add a test for the shrinkable submounts of a synchronous umount: - P shared, P1 a slave that is shared in turn, P2 its peer - B on P/options with copies on P1 and P2, R on top of the copy in P2 - P with B moved below P1, R is the working directory - umount(P1) slides R to where the copy of B is looked up The umount fails with EBUSY and R stays mounted. Needs the tracefs automount below debugfs for the shrinkable mounts. Link: https://patch.msgid.link/20260930-work-mount-fixes-3-v1-3-be34c83956ae@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
32bf924166 |
namespace: check a submount for references right before unmounting it
shrink_submounts() and mark_mounts_for_expiry() first collect all the
mounts they are allowed to unmount and then unmount them.
Whether a mount is busy is decided by propagate_mount_busy() on the
tree. But unmounting the first mount changes the tree that the second
one propagates into. propagate_umount()
moves a surviving overmount off a copy it unmounts and mounts it where
the copy was mounted. If that's where the propagated copy of the second
victim is looked up the overmount becomes a candidate of the second
umount_tree(). It's childless and so trim_one() commits it without
looking at its reference count.
So a synchronous umount pulls out a busy mount that is in use somewhere
else and marks it MNT_SYNC_UMOUNT while it has users:
umount2(/tmp/plshrink/p1, 0) = 0 errno 0
cwd is (unreachable)
The same two umounts requested one after the other fail with EBUSY.
This needs a mount with MNT_SHRINKABLE, i.e., automounted submounts of
NFS, AFS and CIFS or the tracefs mount below debugfs. Bind mounts
inherit the flag.
Check each mount right before it is unmounted under the same hold of
namespace_sem and mount_lock as the umount itself. Make sure that the
algorithm stays linear.
Fixes:
|
||
|
|
bd2feb7796 |
namespace: queue a mount only once for mount notifications
mnt_notify_add() puts a mount on notify_list. It doesn't check whether
the mount is on the list already. That's fine as long as every mount is
queued at most once per namespace_sem hold. It isn't.
propagate_umount() moves a surviving overmount off a stack of mounts
that are going away and queues it as moved. shrink_submounts() and
mark_mounts_for_expiry() call umount_tree() for several mounts under a
single namespace_sem hold. So the second umount_tree() can take down
exactly the mount the first one reparented, reparent it once more and so
end up queueing it a second time.
The second list_add_tail() cuts the mounts queued in between out of the
list while the head still points at the last of them. notify_mnt_list()
then only visits that one mount and the others are freed after the grace
period with notify_list still pointing at them. Every later mount
operation on the host walks freed memory:
BUG: KASAN: slab-use-after-free in __list_add_valid_or_report
Read of size 8 at addr ffff8881003d57e0 by task notify_dq/162
__list_add_valid_or_report+0x15c/0x1a0
mnt_add_to_ns+0x322/0x890
attach_recursive_mnt.isra.0+0xf2a/0x1a00
path_mount+0x139c/0x1d40
This needs a mount with MNT_SHRINKABLE to bind from. That's what
finish_automount() creates (submounts of NFS, AFS and CIFS and the
tracefs mount below debugfs). Bind mounts inherit the flag. A mount
namespace with a FAN_MARK_MNTNS mark does the rest and root in a user
namespace can have both.
Initialize to_notify in alloc_vfsmnt() and leave a mount alone that is
queued already. mnt_notify() looks at the state the mount has when the
list is drained so a single entry per mount is enough. The move event
for a mount that is unmounted under the same hold is lost. That's fine.
The detach is what matters.
Fixes:
|
||
|
|
b4698e50d4 |
Merge patch series "mount: a few additional bugfixes"
Christian Brauner <brauner@kernel.org> says: Here's a set of additional mount fixes that fell out of my recent look at the mount code: - Fix a UAF in mnt_change_mountpoint(). - Fix a UAF in statmount(). - FIX a UAF and various associated bugs in clone_mnt() - Prevent copying an unmounted mount tree. - Avoid pointless work in mount_setattr(). - Refuse fspick() on kernel internal superblocks. - Simplify dissolve_on_fput() and unshare(). * patches from https://patch.msgid.link/20260926-work-mount-fixes-2-v1-0-f357abf3d17b@kernel.org: selftests/filesystems: check that a dead mount's overmount isn't followed selftests/filesystems: check that an unmounted mount tree isn't walked namespace: drop the file's reference first in dissolve_on_fput() fs: put the old fs_struct before the old namespaces in unshare() fs: refuse fspick() on internal superblocks namespace: check the target before walking it in do_mount_setattr() namespace: don't copy an unmounted mount tree namespace: don't inherit MNT_UMOUNT in clone_mnt() statmount: read the parent of an unmounted mount under mount_lock namespace: reset the old parent's ->overmount in mnt_change_mountpoint() Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-0-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
7fbcc793f7 |
selftests/filesystems: check that a dead mount's overmount isn't followed
Add overmount_reparent_test: - a mount that a file descriptor keeps alive after propagate_umount() reparented its overmount and unmounted it must not be walked into the freed overmount by move_mount(MOVE_MOUNT_BENEATH) Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-10-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
07617913d0 |
selftests/filesystems: check that an unmounted mount tree isn't walked
Add unmounted_tree_test: - a detached nsfs or pidfs bind mount that a file descriptor keeps alive can't be copied recursively, a copy of the mount itself and a copy of a mounted one still work - a detached tree with a child that was vacated behind it can't be copied recursively either - a copy of a detached bind mount is an ordinary mount: unlinking its mountpoint from another mount namespace unmounts it - mount_setattr() refuses to change the propagation of a detached tree and still allows it for the root of an anonymous mount namespace Every case runs in a user and mount namespace of its own. Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-9-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
7253a6ff64 |
namespace: drop the file's reference first in dissolve_on_fput()
A detached mount tree is held alive by a file. dissolve_on_fput() unmounts the detached tree attached to that file. Currently dissolve_on_fput() leaves the file's reference to the root of the tree to the caller. So either __fput(), open_detached_copy(), or fsmount() drop the mount. That's backwards. The reference keeps the tree around and is dropped when the tree is already gone. Instead, let dissolve_on_fput() the mount and drop it after umount_tree() with namespace_sem still held. After umount_tree() the mntput() is never the last reference and so the root gets unmounted before its children. Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-8-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
57088a37b2 |
fs: put the old fs_struct before the old namespaces in unshare()
unshare(CLONE_NEWNS) always copies the fs_struct and copy_mnt_ns() points root and pwd of that copy into the new mount namespace. The old fs_struct pins the old root and pwd until ksys_unshare() frees it at the end and switch_task_namespaces() will already put the old mount namespace before. This causes pointless work in user namespaces because the copied mounts are locked and stay connected. Switch and free the fs_struct before switching the namespaces. The old root and pwd are wasted while their mount namespace is still alive which also keeps both puts on the mntput() fast path. setns() already does this right as it updates root and pwd before the namespaces are switched. Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-7-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
02587a4af8 |
fs: refuse fspick() on internal superblocks
There's a few instances where a internal superblock is reachable by
fspick(). Block that. graft_tree() and fsmount() already refuse them.
fanotify refuses mount and superblock marks on them for the same reason.
Fixes:
|
||
|
|
08f4288a37 |
namespace: check the target before walking it in do_mount_setattr()
When mount_setattr() changes the propagation type to MS_SHARED
do_mount_setattr() takes namespace_sem and calls invent_group_ids()
which walks the whole target tree with next_mnt() and allocates a peer
group id for every mount that doesn't have one.
Whether the caller may touch the target at all is only checked
afterwards under mount_lock. If it may not, cleanup_group_ids() walks
the tree a second time on the way out to free the ids again.
A detached tree that a file descriptor keeps alive, a mount in another
mount namespace or a mount below the root of an anonymous mount
namespace does a bunch of allocation work that is completely useless
because it will fail and revert right after.
Do the right thing and check the target right after taking namespace_sem
instead. A mount leaves its namespace only under namespace_sem so the
mount stays stable. The check under mount_lock stays for the case where
no propagation change is requested and namespace_sem isn't taken.
Callers get the same EINVAL as before.
Fixes:
|
||
|
|
8bacbf6be9 |
namespace: don't copy an unmounted mount tree
may_copy_tree() accepts a mount whose root is an nsfs or pidfs file before it checks whether that mount is mounted. This exception exists for nsfs and pidfs mounts. They are part of no mount namespace so neither check_mnt() nor is_mounted() would let them through and bind-mounting a nsfsfd or a pidfd wouldn't work. Problem is that exception also admits a bind mount of such a file when it was lazily unmounted and is only kept alive by a file descriptor: mount --bind /proc/self/ns/net /tmp/x exec 3< /tmp/x umount -l /tmp/x mount --rbind /proc/self/fd/3 /tmp/y With MS_REC or AT_RECURSIVE copy_tree() doesn't stop at the unmounted mount. It copies every mount that stayed attached to it as well. So if you have a bunch of overmounts over the nsfsfd or the pidfd the whole unmounted stack is copied. That's a bug. So refuse to walk an unmounted tree. Copying its root alone stays possible. So a bind mount of a namespace that is gone can still be recreated from a file descriptor on the old one. The file descriptor pins that mount and its root and __has_locked_children() only reads the direct children which don't leave the parent's list without namespace_sem. The internal mounts carry MNT_NS_INTERNAL instead of NULL and are still walked. Copying an unmounted tree recursively fails with EINVAL. Fixes: |
||
|
|
e2c6d32621 |
namespace: don't inherit MNT_UMOUNT in clone_mnt()
MNT_UMOUNT gets raised on a mount that umount_tree() or umount_one() has
taken out of its namespace. It's never cleared. __detach_mounts()
unhooks a mount that carries it instead of unmounting it.
disconnect_mount() lets an unmounted child stay attached only if its
parent carries it. propagate_umount() takes it as the sign that a mount
is going to be unmounted when it decides what else can be unmounted,
where a chain of locked mounts ends and where a for reparenting an
overmount.
clone_mnt() copies the flags of the source and only masks
MNT_INTERNAL_FLAGS which doesn't include MNT_UMOUNT. A mount that was
lazily unmounted but is kept alive by a file descriptor can still be
copied when its root is an nsfs or pidfs file because may_copy_tree()
accepts those before checking whether the source is mounted at all:
mount --bind /proc/self/ns/net /tmp/x
exec 3< /tmp/x
umount -l /tmp/x
mount --bind /proc/self/fd/3 /tmp/y
The mount on /tmp/y is attached to the caller's namespace with
MNT_UMOUNT already set and every copy of it inherits the flag again.
That causes a number of bugs:
- Unlink its mountpoint from a mount namespace where it isn't one.
__detach_mounts() takes it for a mount that was unmounted earlier,
unhooks it from its parent and drops the reference the namespace
holds on it. It stays in the namespace with one reference too few
and as its own parent, so it can't be unmounted through the file
descriptor anymore and the namespace never finds it again. When the
namespace goes away it's leaked and points to a freed struct
mnt_namespace that may_change_propagation() and anon_ns_root() read
on the next propagation change or mount_setattr() through the file
descriptor.
- Make it shared and unmount a peer. trace_transfers() follows the
flag from the peer into it and strips it from its peer group.
- Let a propagated umount reach it. gather_candidates() marks it with
T_UMOUNT_CANDIDATE but never puts it on the candidate list, so the
mark is never cleared and every later propagated umount skips it. If
it's the parent of a locked mount handle_locked() takes it as the
cutoff and commits the locked child that should have stayed and
disconnect_mount() leaves that child attached to it, unmounted under
a mounted parent, which is what disconnect_mount() exists to
prevent. When the namespace goes away umount_tree() visits the child
a second time. And once __detach_mounts() has made it its own parent
reparent() never finds the end of the chain of unmounted parents.
A clone is a new mount that has never been unmounted. Add MNT_UMOUNT
to MNT_INTERNAL_FLAGS so clone_mnt() never copies it. And warn when
umount_tree() reaches a mount that already has it set.
Fixes:
|
||
|
|
bb46094057 |
statmount: read the parent of an unmounted mount under mount_lock
statmount_mnt_basic() reads the ids of the parent through m->mnt_parent
with namespace_sem held for reading. That's fine for a mount that is
in a namespace. Everything above it is pinned by that namespace.
STATMOUNT_BY_FD also accepts a file descriptor on a mount that has
been unmounted on purpose and that only the file descriptor keeps alive.
If that mount stayed attached to its parent when it was unmounted the
parent is unmounted as well and nothing pins it for the child.
The final mntput() unmounts the children under mount_lock alone and
frees the parent afterwards. Concurrently, statmount_mnt_basic() reads
m->mnt_parent twice while that can happen and can easily dereference a
mount that's already gone.
Read the parent of an unmounted mount under mount_lock. The parent
can't be put while it is held and both ids come from the same mount. A
mounted mount's parent is pinned by the namespace and a mount can't
leave its namespace while namespace_sem is held, so the common case
takes no global lock.
Fixes:
|
||
|
|
476ef286a7 |
namespace: reset the old parent's ->overmount in mnt_change_mountpoint()
__umount_mnt() resets ->overmount of the parent when it unmounts the
mount that overmounts it. mnt_change_mountpoint() doesn't. It moves the
mount to its new parent and leaves the old one pointing at it.
For a mount that is moved beneath in attach_recursive_mnt() that doesn't
matter. The new mount has already reset ->overmount of the old parent by
the time the old top mount is moved onto it.
It matters for reparent() though. propagate_umount() moves a surviving
overmount onto the first ancestor that isn't going to be unmounted. The
mount it was taken from is unmounted immediately afterwards. If a file
descriptor keeps that unmounted mount alive ->overmount points at the
moved mount. That's a dangling pointer and UAF as the moved mount is
unmounted and freed.
do_lock_mount() follows ->overmount from the target path under
mount_lock. With a file descriptor on such a mount as the target of a
mount beneath topmost_overmount() walks into the freed mount and
do_lock_mount() takes references on whatever it finds there.
So reset ->overmount of the old parent in mnt_change_mountpoint() the
same way __umount_mnt() does. A mount's ->overmount is then always a
mount that is attached to it.
Fixes:
|
||
|
|
762a6411a5 |
Merge patch series "mount: a few gnarly fixes"
Christian Brauner <brauner@kernel.org> says: A short while ago I received a bug report about problematic behavior in the mount code and that let me to review and model a few corner cases including reference counting. The result is the following set of fixes: - Ensure that copies of unbindable mounts remain unbindable - Ensure that MOVE_MOUNT_SET_GROUP doesn't operate on unbindable mounts so we don't create an undefined state where a mount is both unbindable and a slave mount. - Ensure that we don't silently unmount busy mounts. This is nasty because it's an old bug from v4.13 where calculation of whether a synchronous (non-MNT_DETACH) umount would succeed drifted apart from the actual codepath that did the unmount. - Ensure that we don't silently unmount busy mounts. Same class, different mechanism. There's a race between the reference count check in propagate_mount_busy() (summing up per-cpu counters) and mntget(). Fixes and test for all. No test for the mntget() race because that needs aritifical delays. And fwiw, I think I another vomitorious series in the pipes. * patches from https://patch.msgid.link/20260923-work-mount-fixes-v1-0-f424cf8d3242@kernel.org: docs: update the unmount propagation rule fs: don't let a migrating task hide its reference from do_umount() selftests/filesystems: check that a busy propagated copy blocks a synchronous umount fs: don't silently unmount busy mounts selftests/move_mount_set_group: check that an unbindable target is refused mount: refuse MOVE_MOUNT_SET_GROUP on an unbindable mount selftests/filesystems: check that a copied mount namespace keeps unbindable mount: keep a copied mount unbindable Link: https://patch.msgid.link/20260923-work-mount-fixes-v1-0-f424cf8d3242@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
3f5dc09141 |
docs: update the unmount propagation rule
Section 5f still describes the rule propagate_umount() used before
commit
|
||
|
|
7eb84d54fa |
fs: don't let a migrating task hide its reference from do_umount()
propagate_mount_busy() uses mnt_get_count() via do_refcount_check() to figure out whether a synchronous umount may proceed or not by summing the per-CPU mnt_count counter under mount_lock. The fastpath for mntget() and mntput() doesn't require that lock. A task that already holds a reference and takes another one on a cpu the loop has summed can get migrated and drops that reference again on another cpu. The loop might not have reached that cpu yet. That ends up undercounting. The original reference of the task is missed and umount() ends up succeeding with the file still open. The mount is destroyed later when the task mntput()s so it degrades to a lazy umount. That's basically the umount(2) variant of commit |
||
|
|
bba9960c07 |
selftests/filesystems: check that a busy propagated copy blocks a synchronous umount
A slave namespace moves an open_tree() copy of the shared tree beneath the propagated copy of the victim and keeps the descriptor. Check that: - umount(2) of the victim fails with EBUSY while the descriptor is open - the moved tree is still attached in the slave namespace afterwards - the umount succeeds once the descriptor is closed The test needs CAP_SYS_ADMIN and skips otherwise. Link: https://patch.msgid.link/20260923-work-mount-fixes-v1-6-f424cf8d3242@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
6857e20901 |
fs: don't silently unmount busy mounts
The propagate_mount_busy() helper exists to decide whether a synchronous can suceed. It does that by looking at the copies of the victims in the mounts its parent propagates to. One of those functions from propagation hell. It skips a copy that has child mounts except for the case of a single child mount covering the copy's root. That algorithm used to be exact. It isn't anymore. Originally only childless copies were unmounted during propagation. And |
||
|
|
7541865c26 |
selftests/move_mount_set_group: check that an unbindable target is refused
MOVE_MOUNT_SET_GROUP must not accept an unbindable mount as the target. Cover both sources: - a slave, which used to leave the target unbindable and a slave at once - a shared mount, which used to silently drop the unbindable flag Check that the target is untouched after the refused call. Link: https://patch.msgid.link/20260923-work-mount-fixes-v1-4-f424cf8d3242@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
a2b42babc9 |
mount: refuse MOVE_MOUNT_SET_GROUP on an unbindable mount
do_set_group() only accepts mounts as targets that are neither shared
nor a slave. That encompasses undindable mounts.
If the source mount is a slave mount the target mount will end up with
source's master as its master. It keeps T_UNBINDABLE. That means we get
a mount that is both unbindable and a slave:
mount --bind /tmp/src /tmp/src
mount --make-shared /tmp/src
mount --bind /tmp/src /tmp/b
mount --make-slave /tmp/b
mount --bind /tmp/src /tmp/c
mount --make-unbindable /tmp/c
move_mount(b, "", c, "", MOVE_MOUNT_SET_GROUP)
grep /tmp/c /proc/self/mountinfo
... /tmp/c ... master:646 unbindable ...
This property cannot be produced any other way. change_mnt_propagation()
drops the master when it makes a mount unbindable (iow, it becomes
private) and doesn't touch an unbindable mount when it is supposed to
turned into a slave mount.
End-result is that the unbindable-slave mount receives everything
propagated from its master's peer group while it can't be bind mounted
itself. Not sure what that's supposed to do.
On the other side: if the source mount is shared, the target mount drops
T_UNBINDABLE because because set_mnt_shared() clears T_SHARED_MASK.
So teach do_set_group() to refuse an unbindable target mount the same
way a shared or slave target mount is refused.
The main user of MOVE_MOUNT_SET_GROUP is CRIU which restores sharing
first and applies MS_UNBINDABLE afterwards. It never hits this.
Link: https://patch.msgid.link/20260923-work-mount-fixes-v1-3-f424cf8d3242@kernel.org
Fixes:
|
||
|
|
0bab6b6d77 |
selftests/filesystems: check that a copied mount namespace keeps unbindable
Make a tmpfs unbindable, copy the mount namespace and check from the copy that: - a bind of the mount fails with EINVAL - a recursive bind fails as well - open_tree(OPEN_TREE_CLONE) of it fails - mountinfo still shows it as unbindable - a copy of the copy refuses the bind too The first case runs in the original namespace so the fixture stays honest about what it set up. Link: https://patch.msgid.link/20260923-work-mount-fixes-v1-2-f424cf8d3242@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> |
||
|
|
1ab8904ae6 |
mount: keep a copied mount unbindable
It's groundhog day. MNT_UNBINDABLE used to be set in in mnt->mnt.mnt_flags and was not part of MNT_INTERNAL_FLAGS. clone_mnt() copied it with all the other flags to the new mount. This guaranteed that the copy of an unbindable mount became unbindable as well. Unbindable copies that ended up as shared lost the unbindable property. But then commit |