summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-09 23:37:44 +0200
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-09 23:37:44 +0200
commitf99005fbed4e8071110faa350cc5fa453afa1455 (patch)
tree2e5e41e0cadba5b7c961a82a7c17d00439bf0751
parent9515f634dbed2bb8928fbb3b01102dd4e64f4aeb (diff)
parent3d399224425573875b6f6f1181bd8cf9a28cc7d1 (diff)
downloadlinux-next-f99005fbed4e8071110faa350cc5fa453afa1455.tar.gz
linux-next-f99005fbed4e8071110faa350cc5fa453afa1455.zip
Merge tag 'vfs-7.3-rc7.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs
Pull vfs fixes from Christian Brauner: "This contains fixes for the current development cycle. All of them came out of a review of the mount code that started with a bug report. The review modeled the corner cases of mount propagation, unmounting and mount reference counting and turned up a lot of bugs. Most of them years old. Most fixes come with a selftest. - Rework connected mounts. A mount that is unmounted together with its parent can stay attached to the parent to keep its mountpoint covered. That happens when the mountpoint is removed with rmdir(), unlink() or rename(), when a detached tree is dissolved, and for locked mounts in any umount that isn't synchronous, including the teardown of their mount namespace. The parent then owns the child and drops it on its own final mntput(). So any reference from the child's superblock back to one of its ancestors becomes a cycle that is never freed. A loop device backed by an image on a tmpfs and mounted on that same tmpfs is enough. Remove the directory the tmpfs is mounted on from the host, let the container's mount namespace exit, and the loop device, the tmpfs and the filesystem on the loop device are leaked for good. The same works with autofs, zram, ecryptfs, binfmt_misc, fuse passthrough, zloop, a mass storage gadget and md, and the selftests have reproducers for them. This has been possible since v4.1. It is also why "put_mnt_ns(): leave mounts connected" was reverted in -rc5. Keeping every mount of a dying mount namespace connected made these cycles trivial to create. Every unmounted mount is now detached from its parent. Where the mountpoint has to stay covered the mount leaves a cover on the parent instead, allocated together with the mount. A lookup on the unmounted parent that hits a cover finds an empty immutable directory or file on the private nullfs instance. Nothing leads from a cover to another mount, so no unmounted mount owns another one and no cycle can form. This is visible to userspace. A formerly connected mount can no longer be reached through its unmounted parent and ".." inside it leads nowhere, as for every other lazily unmounted mount. With that the private nullfs instance becomes reachable from userspace, so it now refuses mounts on top, is mounted read-only, and refuses fsnotify marks and file locks. Its inodes are shared by every holder and a watch or a lock would otherwise reach across users. may_decode_fh() now also decides its subtree check under a single mount_lock hold, as a racing umount could otherwise let it decode into what a locked child covered. - umount: * Don't silently unmount busy mounts. Since v4.13 propagate_umount() takes down propagated copies of the victim with children as long as each child is an overmount or another copy of the victim, but propagate_mount_busy() only ever checked copies without children or with just an overmount. A container that moved a tree beneath its copy of a host mount lost that tree from under its open file descriptor to a plain umount() on the host. propagate_mount_busy() now applies the same rules, walking each chain of copies once. * Don't let a migrating task hide its reference from umount(). mnt_get_count() sums the per-cpu counters under mount_lock but the mntget() and mntput() fast paths don't take it. A task that takes a reference on a cpu the sum has already passed and drops it after migrating to one the sum hasn't reached yet hides the reference it held to begin with, and umount() succeeds with the file still open. Gets and puts now live in separate per-cpu counters and all puts are summed before all gets with a full barrier in between, the way srcu_readers_active_idx_check() does it. mntget() is unchanged and mntput() gains an smp_wmb(). * Check each submount for references right before unmounting it. shrink_submounts() and mark_mounts_for_expiry() checked all their victims up front. Unmounting the first could move a busy overmount to where the next victim's propagated copy is looked up and it was then unmounted without a check. * Never expire a locked mount. A shrinkable mount moved beneath a locked mount with MOVE_MOUNT_BENEATH takes over the lock, and umount() of an unlocked ancestor expired it and revealed what it covered. That umount() now fails with EBUSY as it does for any other locked child. A lazy umount still takes the whole tree. - Overmounts and locked mounts: * Unhash a dentry before detaching the mounts on it. unlink(), rmdir() and rename() detach the mounts on the victim but only d_delete() it once its inode is unlocked, a window that includes an expedited RCU grace period. In between, a lookup from a mount namespace in which the dentry is a mountpoint found it hashed, positive and uncovered. Drop the dentry first, as d_invalidate() already does. * Don't reveal overmounted entries in refwalk. A refwalk that had grabbed the dentry before the unlink never rechecked it the way rcuwalk does with d_seq and mount_lock. Without any artificial widening three walkers read the covered file 27 times in a minute. step_into() now fails an unhashed dentry marked DCACHE_CANT_MOUNT with -ESTALE and the walk is retried. * Keep covered mounts covered in OPEN_TREE_NAMESPACE. Creating such a mount namespace only takes a user namespace and the copy followed bind mount rules: no children without AT_RECURSIVE and no unbindable mounts with it. An unprivileged user could see what mounts covered in the source, such as the parts of /proc and /sys that container runtimes mask. If the caller doesn't own the source mount namespace a non-recursive copy of a mount with something mounted below the requested directory is now refused and a recursive copy includes unbindable mounts, the way unshare() copies. * Keep the lock on a mount that a propagated copy is moved beneath. MNT_LOCKED moved to any mount that ended up beneath a locked mount, propagated copies included. A host mount and umount on a directory covered by a locked mount in a less privileged mount namespace left that cover unlocked for the namespace's owner to remove. Only mounts the caller places beneath take over the lock now. * Handle mount locking for automounts correctly. Which copies to lock was decided by the mount namespace of the task that triggered the automount. A task in a user namespace that triggered one on a host mount through a file descriptor got the host's own automount locked while its own copy stayed unlocked and could have nosuid, nodev and noexec cleared. Use the owner of the mount namespace the mount lands in. - Use-after-free and crashes: * Refuse an automount below a mount that is in no namespace. The private clones overlayfs uses for its layers have the MNT_NS_INTERNAL error pointer as their namespace, which finish_automount() let through and count_mounts() dereferenced. A fanotify filesystem mark on an overlayfs lower layer hands out file descriptors on such a clone. With debugfs as the lower layer opening "tracing" oopses with namespace_sem held for writing and every mount operation on the system blocks from then on. * Reset the old parent's ->overmount in mnt_change_mountpoint(). When propagate_umount() moved an overmount off a mount that a file descriptor kept alive, MOVE_MOUNT_BENEATH through that descriptor later followed the stale pointer into the freed overmount. * statmount() with STATMOUNT_BY_FD and pivot_root() read the parent of a mount that may be unmounted and only held by a file descriptor, while the parent's final mntput() can free it. statmount() now reads it under mount_lock and pivot_root() first checks that both mounts are in the caller's mount namespace. * Queue a mount only once for mount notifications. A mount reparented by one umount_tree() and taken down by the next under the same namespace_sem hold, as in shrink_submounts(), was queued twice. That cut the mounts queued in between out of notify_list while it still pointed at them, and once they were freed every later mount operation walked freed memory. * Don't let a pseudo dentry become the root of a mount. A bind mount of a bpf token file did that with a DCACHE_NORCU dentry, which is freed without an RCU grace period while lockless path walks may still look at it. Refuse to clone such a mount. * Don't inherit MNT_UMOUNT in clone_mnt(). A bind mount of a lazily unmounted nsfs or pidfs mount through its file descriptor started out flagged as unmounted. Among other things __detach_mounts() then dropped the namespace's reference on it, the mount outlived its namespace and mount_setattr() through the descriptor read the freed namespace. A recursive bind mount of such a mount also copied the unmounted stack still attached to it. That now fails with EINVAL, copying the mount itself still works. * Remove the fsnotify marks of a mount namespace in free_mnt_ns() instead of the RCU callback that frees the namespace, where taking the group mutexes meant sleeping in softirq context. - Propagation and copies: * Keep a copied mount unbindable. Since v6.17 clone_mnt() didn't copy the unbindable flag, so every mount namespace created with CLONE_NEWNS had bindable copies of all unbindable mounts. This had been fixed once before. * Refuse MOVE_MOUNT_SET_GROUP on an unbindable mount. It made the mount an unbindable slave, a state nothing else can produce, or silently dropped the unbindable flag. CRIU applies MS_UNBINDABLE after restoring sharing and isn't affected. * Check a recursive bind mount for mount namespace loops. Recursively bind mounting a tree from another mount namespace could put a mount of a namespace's file inside that same namespace, which then pins itself and all its mounts. Repeating it leaks without limit, the reproducer took Shmem from 380 kB to 65916 kB. The copy is now checked with check_for_nsfs_mounts() before it is grafted, as move_mount() does. * Look at the topmost mount for a mount namespace file. attach_recursive_mnt() never looked at the topmost mount of the source's chain of overmounts. If that was the chain's only mount namespace file an existing mount at a propagated destination got buried below the root of the nsfs file where no path walk reaches it. * Don't put a mountpoint on a dentry that's being removed. attach_recursive_mnt() makes a mountpoint of the source's root without its inode lock, so a racing rmdir() of that directory could leave a mount on it that nothing ever detaches. d_set_mounted() now checks cant_mount() as well. - nullfs: * Take no inode lock for readdir of an immutable directory. The root of every empty mount namespace is the same nullfs directory and iterate_dir() held its i_rwsem across ->iterate_shared(). A reader whose buffer faults on a FUSE mount of its own holds it for as long as its server wants, and with an exclusive locker queued behind it every lookup that misses the dcache, every create and every mount in that directory waits. One user of an empty mount namespace stalls all others. Directories with the new FOP_IMMUTABLE flag skip the lock. * Refuse to reconfigure internal superblocks through fspick(), MS_REMOUNT or the read-only remount that a synchronous umount() of the root does. For nullfs only root in the initial user namespace could do it, but the superblock is shared by every mount namespace and the flags showed up in statfs() for all of them. * Don't update the access time on nullfs and refuse F_SET_RW_HINT on an immutable inode. - unshare: Free an nsproxy that was never installed with nsproxy_free() when set_cred_ucounts() fails. put_nsproxy() dropped active references that were never taken, which triggered a warning and hid the caller's own namespaces from listns(). - Smaller changes: mount_setattr() checks the target before it walks the tree to allocate peer group ids, unshare() puts the old fs_struct before the old namespaces, dissolve_on_fput() drops the file's reference to the tree itself, disconnect_mount() is simplified and the documentation of the propagated unmount rule is brought up to date" * tag 'vfs-7.3-rc7.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: (58 commits) namespace: simplify disconnect_mount() selftests/filesystems: test covered mounts namespace: rework connected mounts nullfs: add an empty immutable regular file selftests/filesystems: check that reading the root of an empty mount namespace stalls nobody selftests/filesystems: add a helper that holds a readdir in a page fault readdir: take no inode lock on an immutable directory nullfs: refuse file locks fsnotify: let a filesystem refuse marks on its objects namespace: nothing is mounted on or written through knullfs namespace: keep the private nullfs instance in knullfs fhandle: decide the subtree check under mount_lock selftests/filesystems: check that an automount below an overlay layer is refused selftests/filesystems: check the atime of the empty mount namespace root selftests/filesystems: check that a lock lands on the right mount and stays namespace: keep the lock on a mount that a propagated copy is moved beneath namespace: never expire a locked mount nullfs: don't update the access time namespace: handle mount locking for automounts correctly namespace: refuse an automount below a mount that is in no namespace ...
-rw-r--r--Documentation/filesystems/propagate_umount.txt12
-rw-r--r--Documentation/filesystems/sharedsubtree.rst20
-rw-r--r--fs/dcache.c4
-rw-r--r--fs/fcntl.c3
-rw-r--r--fs/fhandle.c20
-rw-r--r--fs/file_table.c3
-rw-r--r--fs/fs_context.c4
-rw-r--r--fs/mount.h18
-rw-r--r--fs/namei.c29
-rw-r--r--fs/namespace.c461
-rw-r--r--fs/notify/mark.c4
-rw-r--r--fs/nullfs.c123
-rw-r--r--fs/pnode.c112
-rw-r--r--fs/readdir.c13
-rw-r--r--include/linux/fs.h3
-rw-r--r--include/linux/mount.h2
-rw-r--r--include/linux/nsproxy.h1
-rw-r--r--kernel/fork.c12
-rw-r--r--kernel/nsproxy.c2
-rw-r--r--tools/testing/selftests/Makefile3
-rw-r--r--tools/testing/selftests/filesystems/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/.gitignore3
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/Makefile5
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c108
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c129
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c45
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/Makefile6
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c227
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/.gitignore8
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/Makefile12
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/config39
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c377
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c1542
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c579
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c193
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c190
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c181
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/settings1
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c484
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c195
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/Makefile1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c174
-rw-r--r--tools/testing/selftests/filesystems/readdir_hold.h224
-rw-r--r--tools/testing/selftests/filesystems/rw_hint_test.c129
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/Makefile6
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c432
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c211
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c226
-rw-r--r--tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c74
53 files changed, 6462 insertions, 195 deletions
diff --git a/Documentation/filesystems/propagate_umount.txt b/Documentation/filesystems/propagate_umount.txt
index 9a7eb96df300..de58ce817582 100644
--- a/Documentation/filesystems/propagate_umount.txt
+++ b/Documentation/filesystems/propagate_umount.txt
@@ -32,12 +32,12 @@ of set. They can be reparented to the place where the bottom of
stack is attached to a mount that will survive. NOTE: doing that
will violate a constraint on having no more than one mount with
the same parent/mountpoint pair; however, the caller (umount_tree())
-will immediately remedy that - it may keep unmounted element attached
-to parent, but only if the parent itself is unmounted. Since all
-conflicts created by reparenting have common parent *not* in the
-set and one side of the conflict (bottom of the stack of overmounts)
-is in the set, it will be resolved. However, we rely upon umount_tree()
-doing that pretty much immediately after the call of propagate_umount().
+will immediately remedy that - it detaches every element of the set
+from its parent. Since all conflicts created by reparenting have
+common parent *not* in the set and one side of the conflict (bottom
+of the stack of overmounts) is in the set, it will be resolved.
+However, we rely upon umount_tree() doing that pretty much immediately
+after the call of propagate_umount().
Algorithm is based on two statements:
1) for any set S, there is a maximal non-shifting subset of S
diff --git a/Documentation/filesystems/sharedsubtree.rst b/Documentation/filesystems/sharedsubtree.rst
index 8b7dc9159083..8bae6d9a7e04 100644
--- a/Documentation/filesystems/sharedsubtree.rst
+++ b/Documentation/filesystems/sharedsubtree.rst
@@ -564,8 +564,8 @@ f) Unmount semantics
where 'A' is a mount mounted on mount 'B' at dentry 'b'.
If mount 'B' is shared, then all most-recently-mounted mounts at dentry
- 'b' on mounts that receive propagation from mount 'B' and does not have
- sub-mounts within them are unmounted.
+ 'b' on mounts that receive propagation from mount 'B' are unmounted as
+ well, if every mount below them is also unmounted.
Example: Let's say 'B1', 'B2', 'B3' are shared mounts that propagate to
each other.
@@ -584,10 +584,18 @@ f) Unmount semantics
So all 'C1', 'C2' and 'C3' should be unmounted.
- If any of 'C2' or 'C3' has some child mounts, then that mount is not
- unmounted, but all other mounts are unmounted. However if 'C1' is told
- to be unmounted and 'C1' has some sub-mounts, the umount operation is
- failed entirely.
+ If any of 'C2' or 'C3' has a child mount that cannot be unmounted
+ then that mount is not unmounted. But all other mounts are unmounted.
+ A child mount that is itself unmounted by the same unmount
+ propagation does not keep its parent mounted. However if 'C1' is
+ supposed to be unmounted and 'C1' has some sub-mounts, the unmount
+ fails.
+
+ A lazy umount (MNT_DETACH) takes a whole tree. Every mount of the
+ tree then propagates its unmount from its own parent as described
+ above, so the mounts that receive propagation lose the corresponding
+ trees as well. Documentation/filesystems/propagate_umount.txt has the
+ precise rules, including the ones for locked mounts.
g) Clone Namespace
diff --git a/fs/dcache.c b/fs/dcache.c
index a66be85f9d01..f44c7b39c3b3 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -1583,6 +1583,8 @@ EXPORT_SYMBOL(path_has_submounts);
*
* Only one of d_invalidate() and d_set_mounted() must succeed. For
* this reason take rename_lock and d_lock on dentry and ancestors.
+ * Likewise for dont_mount() which marks a dentry that is being removed
+ * under d_lock.
*/
int d_set_mounted(struct dentry *dentry)
{
@@ -1599,7 +1601,7 @@ int d_set_mounted(struct dentry *dentry)
spin_unlock(&p->d_lock);
}
spin_lock(&dentry->d_lock);
- if (!d_unlinked(dentry)) {
+ if (!d_unlinked(dentry) && !cant_mount(dentry)) {
ret = -EBUSY;
if (!d_mountpoint(dentry)) {
dentry->d_flags |= DCACHE_MOUNTED;
diff --git a/fs/fcntl.c b/fs/fcntl.c
index c158f082f1da..9b71bc9d71d1 100644
--- a/fs/fcntl.c
+++ b/fs/fcntl.c
@@ -372,6 +372,9 @@ static long fcntl_set_rw_hint(struct file *file, unsigned long arg)
u64 __user *argp = (u64 __user *)arg;
u64 hint;
+ /* nothing is ever written to it */
+ if (IS_IMMUTABLE(inode))
+ return -EPERM;
if (!inode_owner_or_capable(file_mnt_idmap(file), inode))
return -EPERM;
diff --git a/fs/fhandle.c b/fs/fhandle.c
index f8829231e3d7..d22f2e065677 100644
--- a/fs/fhandle.c
+++ b/fs/fhandle.c
@@ -298,6 +298,22 @@ static bool capable_wrt_mount(struct mount *mount)
return mnt_ns && ns_capable(mnt_ns->user_ns, CAP_SYS_ADMIN);
}
+/*
+ * Does the caller have an unobstructed way to everything below @root? Only
+ * if the mount is mounted, the caller is privileged over its mount namespace
+ * and no locked child covers something below @root->dentry. One answer from
+ * under mount_lock: an umount in between clears ->mnt_ns and takes the
+ * children off the mount, the locked ones too.
+ */
+static bool subtree_unobstructed(const struct path *root)
+{
+ struct mount *mnt = real_mount(root->mnt);
+
+ guard(mount_locked_reader)();
+ return is_mounted(root->mnt) && capable_wrt_mount(mnt) &&
+ !has_locked_children(mnt, root->dentry);
+}
+
static inline int may_decode_fh(struct handle_to_path_ctx *ctx,
unsigned int o_flags)
{
@@ -332,9 +348,7 @@ static inline int may_decode_fh(struct handle_to_path_ctx *ctx,
if (ns_capable(root->mnt->mnt_sb->s_user_ns, CAP_SYS_ADMIN))
ctx->flags = HANDLE_CHECK_PERMS;
- else if (is_mounted(root->mnt) &&
- capable_wrt_mount(real_mount(root->mnt)) &&
- !has_locked_children(real_mount(root->mnt), root->dentry))
+ else if (subtree_unobstructed(root))
ctx->flags = HANDLE_CHECK_PERMS | HANDLE_CHECK_SUBTREE;
else
return -EPERM;
diff --git a/fs/file_table.c b/fs/file_table.c
index c68b8c0a4097..e2665eaa6f8c 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -520,7 +520,8 @@ static void __fput(struct file *file)
dput(dentry);
if (unlikely(mode & FMODE_NEED_UNMOUNT))
dissolve_on_fput(mnt);
- mntput(mnt);
+ else
+ mntput(mnt);
out:
file_free(file);
}
diff --git a/fs/fs_context.c b/fs/fs_context.c
index 23ad66cd94e1..a50584df97be 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -315,6 +315,10 @@ struct fs_context *fs_context_for_reconfigure(struct dentry *dentry,
unsigned int sb_flags,
unsigned int sb_flags_mask)
{
+ /* kernel-internal superblocks are nobody's to reconfigure */
+ if (dentry->d_sb->s_flags & SB_NOUSER)
+ return ERR_PTR(-EINVAL);
+
return alloc_fs_context(dentry->d_sb->s_type, dentry, sb_flags,
sb_flags_mask, FS_CONTEXT_FOR_RECONFIGURE);
}
diff --git a/fs/mount.h b/fs/mount.h
index 94fcc306d21e..4d23cade9f6c 100644
--- a/fs/mount.h
+++ b/fs/mount.h
@@ -6,6 +6,8 @@
#include <linux/fs_pin.h>
extern struct file_system_type nullfs_fs_type;
+extern struct dentry *nullfs_new_file(struct super_block *sb);
+extern struct vfsmount *knullfs;
extern struct list_head notify_list;
struct mnt_namespace {
@@ -33,16 +35,20 @@ struct mnt_namespace {
} __randomize_layout;
struct mnt_pcp {
- int mnt_count;
+ unsigned int mnt_gets;
+ unsigned int mnt_puts;
int mnt_writers;
};
struct mountpoint {
struct hlist_node m_hash;
struct dentry *m_dentry;
- struct hlist_head m_list;
+ struct hlist_head m_list; /* mounts on it and pins */
+ struct hlist_head m_covers; /* covers of unmounted parents */
};
+struct mnt_cover;
+
struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
@@ -84,7 +90,7 @@ struct mount {
struct mountpoint *mnt_mp; /* where is it mounted */
union {
struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */
- struct hlist_node mnt_umount;
+ struct hlist_node mnt_umount; /* on the unmounted list */
};
#ifdef CONFIG_FSNOTIFY
struct fsnotify_mark_connector __rcu *mnt_fsnotify_marks;
@@ -98,7 +104,8 @@ struct mount {
int mnt_group_id; /* peer group identifier */
int mnt_expiry_mark; /* true if marked for expiry */
struct hlist_head mnt_pins;
- struct hlist_head mnt_stuck_children;
+ struct mnt_cover *mnt_cover; /* the one it may leave behind */
+ struct hlist_head mnt_covers; /* left behind by its unmounted children */
struct hlist_node mnt_ns_visible; /* link in ns->mnt_visible_mounts */
struct mount *overmount; /* mounted on ->mnt_root */
} __randomize_layout;
@@ -233,6 +240,9 @@ static inline struct mnt_namespace *to_mnt_ns(struct ns_common *ns)
#ifdef CONFIG_FSNOTIFY
static inline void mnt_notify_add(struct mount *m)
{
+ /* queued already under this namespace_sem hold */
+ if (!list_empty(&m->to_notify))
+ return;
/* Optimize the case where there are no watches */
if ((m->mnt_ns && m->mnt_ns->n_fsnotify_marks) ||
(m->prev_ns && m->prev_ns->n_fsnotify_marks))
diff --git a/fs/namei.c b/fs/namei.c
index 20a6534ea3ef..c87c14ee1a25 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -2087,6 +2087,33 @@ all_done: // pure jump
}
/*
+ * Be careful in case the dentry is unlinked or renamed. Any mounts
+ * stacked on top of it are going away. We need to make sure that we
+ * don't reveal the underyling dentry during refwalk. In rcuwalk we
+ * catch this via d_seq and another lookup for the name. Give the same
+ * guarantee in refwalk.
+ */
+static bool unlink_may_reveal(struct nameidata *nd, int flags,
+ struct path *path, struct dentry *dentry)
+{
+ /* ".." and LOOKUP_DOWN may land on an unhashed directory */
+ if (flags & WALK_NOFOLLOW)
+ return false;
+ if (nd->flags & LOOKUP_REVAL)
+ return false;
+ /* We crossed onto a mount and the name led us here while it still existed */
+ if (path->mnt != nd->path.mnt)
+ return false;
+ /* only a name on its way out is flagged */
+ if (likely(!cant_mount(dentry)))
+ return false;
+ if (!d_unlinked(dentry))
+ return false;
+ dput(no_free_ptr(path->dentry));
+ return true;
+}
+
+/*
* Do we need to follow links? We _really_ want to be able
* to do this check without having to look at inode->i_op,
* so we keep a cache of "no, this doesn't need follow_link"
@@ -2115,6 +2142,8 @@ static noinline const char *step_into_slowpath(struct nameidata *nd, int flags,
if (unlikely(!inode))
return ERR_PTR(-ENOENT);
} else {
+ if (unlikely(unlink_may_reveal(nd, flags, &path, dentry)))
+ return ERR_PTR(-ESTALE);
dput(nd->path.dentry);
if (nd->path.mnt != path.mnt)
mntput(nd->path.mnt);
diff --git a/fs/namespace.c b/fs/namespace.c
index 580877e46b1a..3ed3766f3a18 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -80,6 +80,8 @@ static u64 mnt_id_ctr = MNT_UNIQUE_ID_OFFSET;
static struct hlist_head *mount_hashtable __ro_after_init;
static struct hlist_head *mountpoint_hashtable __ro_after_init;
static struct kmem_cache *mnt_cache __ro_after_init;
+struct vfsmount *knullfs __ro_after_init; /* private nullfs instance */
+static struct vfsmount *knullfs_file __ro_after_init; /* its regular file */
static DECLARE_RWSEM(namespace_sem);
static HLIST_HEAD(unmounted); /* protected by namespace_sem */
static LIST_HEAD(ex_mountpoints); /* protected by namespace_sem */
@@ -130,7 +132,6 @@ static void mnt_ns_release(struct mnt_namespace *ns)
{
/* keep alive for {list,stat}mount() */
if (ns && refcount_dec_and_test(&ns->passive)) {
- fsnotify_mntns_delete(ns);
put_user_ns(ns->user_ns);
kfree(ns);
}
@@ -210,6 +211,19 @@ static inline struct hlist_head *mp_hash(struct dentry *dentry)
return &mountpoint_hashtable[tmp & mp_hash_mask];
}
+/*
+ * What an unmounted mount leaves behind at its unmounted parent instead of
+ * staying attached to it. A lookup on the parent at the mountpoint finds
+ * a stand-in for as long as the cover is there. The parent frees it.
+ */
+struct mnt_cover {
+ struct hlist_node node; /* parent->mnt_covers, RCU */
+ struct hlist_node pin; /* mp->m_covers, keeps the mountpoint */
+ struct dentry *dentry;
+ struct mountpoint *mp;
+ struct rcu_head rcu;
+};
+
static int mnt_alloc_id(struct mount *mnt)
{
int res;
@@ -249,16 +263,24 @@ void mnt_release_group_id(struct mount *mnt)
mnt->mnt_group_id = 0;
}
-/*
- * vfsmount lock must be held for read
- */
-static inline void mnt_add_count(struct mount *mnt, int n)
+static inline void mnt_inc_count(struct mount *mnt)
+{
+#ifdef CONFIG_SMP
+ this_cpu_inc(mnt->mnt_pcp->mnt_gets);
+#else
+ preempt_disable();
+ mnt->mnt_count++;
+ preempt_enable();
+#endif
+}
+
+static inline void mnt_dec_count(struct mount *mnt)
{
#ifdef CONFIG_SMP
- this_cpu_add(mnt->mnt_pcp->mnt_count, n);
+ this_cpu_inc(mnt->mnt_pcp->mnt_puts);
#else
preempt_disable();
- mnt->mnt_count += n;
+ mnt->mnt_count--;
preempt_enable();
#endif
}
@@ -269,14 +291,17 @@ static inline void mnt_add_count(struct mount *mnt, int n)
int mnt_get_count(struct mount *mnt)
{
#ifdef CONFIG_SMP
- int count = 0;
+ unsigned int gets = 0, puts = 0;
int cpu;
- for_each_possible_cpu(cpu) {
- count += per_cpu_ptr(mnt->mnt_pcp, cpu)->mnt_count;
- }
+ /* puts first, so a put counted here has its get counted below */
+ for_each_possible_cpu(cpu)
+ puts += per_cpu_ptr(mnt->mnt_pcp, cpu)->mnt_puts;
+ smp_mb(); /* pairs with the smp_wmb() in mntput_no_expire() */
+ for_each_possible_cpu(cpu)
+ gets += per_cpu_ptr(mnt->mnt_pcp, cpu)->mnt_gets;
- return count;
+ return gets - puts;
#else
return mnt->mnt_count;
#endif
@@ -288,6 +313,10 @@ static struct mount *alloc_vfsmnt(const char *name)
if (mnt) {
int err;
+ mnt->mnt_cover = kzalloc_obj(struct mnt_cover, GFP_KERNEL_ACCOUNT);
+ if (!mnt->mnt_cover)
+ goto out_free_cache;
+
err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;
@@ -305,7 +334,7 @@ static struct mount *alloc_vfsmnt(const char *name)
if (!mnt->mnt_pcp)
goto out_free_devname;
- this_cpu_add(mnt->mnt_pcp->mnt_count, 1);
+ this_cpu_inc(mnt->mnt_pcp->mnt_gets);
#else
mnt->mnt_count = 1;
mnt->mnt_writers = 0;
@@ -320,8 +349,11 @@ static struct mount *alloc_vfsmnt(const char *name)
INIT_HLIST_HEAD(&mnt->mnt_slave_list);
INIT_HLIST_NODE(&mnt->mnt_slave);
INIT_HLIST_NODE(&mnt->mnt_mp_list);
- INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
+ INIT_HLIST_HEAD(&mnt->mnt_covers);
INIT_HLIST_NODE(&mnt->mnt_ns_visible);
+#ifdef CONFIG_FSNOTIFY
+ INIT_LIST_HEAD(&mnt->to_notify);
+#endif
RB_CLEAR_NODE(&mnt->mnt_node);
mnt->mnt.mnt_idmap = &nop_mnt_idmap;
}
@@ -334,6 +366,7 @@ out_free_devname:
out_free_id:
mnt_free_id(mnt);
out_free_cache:
+ kfree(mnt->mnt_cover);
kmem_cache_free(mnt_cache, mnt);
return NULL;
}
@@ -725,6 +758,8 @@ int sb_prepare_remount_readonly(struct super_block *sb)
static void free_vfsmnt(struct mount *mnt)
{
mnt_idmap_put(mnt_idmap(&mnt->mnt));
+ /* NULL if it left it behind */
+ kfree(mnt->mnt_cover);
kfree_const(mnt->mnt_devname);
#ifdef CONFIG_SMP
free_percpu(mnt->mnt_pcp);
@@ -746,13 +781,13 @@ int __legitimize_mnt(struct vfsmount *bastard, unsigned seq)
if (bastard == NULL)
return 0;
mnt = real_mount(bastard);
- mnt_add_count(mnt, 1);
- smp_mb(); // see mntput_no_expire() and do_umount()
+ mnt_inc_count(mnt);
+ smp_mb(); /* see mntput_no_expire_slowpath() and do_umount() */
if (likely(!read_seqretry(&mount_lock, seq)))
return 0;
lock_mount_hash();
if (unlikely(bastard->mnt_flags & (MNT_SYNC_UMOUNT | MNT_DOOMED))) {
- mnt_add_count(mnt, -1);
+ mnt_dec_count(mnt);
unlock_mount_hash();
return 1;
}
@@ -781,20 +816,31 @@ static bool legitimize_mnt(struct vfsmount *bastard, unsigned seq)
* @dentry: dentry of mountpoint
*
* If @mnt has a child mount @c mounted on @dentry find and return it.
+ * If @mnt is unmounted and a child that was unmounted with it left its
+ * cover behind at @dentry, return the stand-in for it instead: knullfs
+ * for a directory, its regular file for anything else.
* Caller must either hold the spinlock component of @mount_lock or
* hold rcu_read_lock(), sample the seqcount component before the call
* and recheck it afterwards.
*
- * Return: The child of @mnt mounted on @dentry or %NULL.
+ * Return: The child of @mnt mounted on @dentry, a stand-in or %NULL.
*/
struct mount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry)
{
struct hlist_head *head = m_hash(mnt, dentry);
+ struct mnt_cover *cover;
struct mount *p;
hlist_for_each_entry_rcu(p, head, mnt_hash)
if (&p->mnt_parent->mnt == mnt && p->mnt_mountpoint == dentry)
return p;
+ /* an unmounted mount keeps the covers its unmounted children left */
+ /* a lockless caller rechecks mount_lock after a miss, a stale flag is harmless */
+ if (unlikely(data_race(mnt->mnt_flags) & MNT_UMOUNT)) {
+ hlist_for_each_entry_rcu(cover, &real_mount(mnt)->mnt_covers, node)
+ if (cover->dentry == dentry)
+ return real_mount(d_is_dir(dentry) ? knullfs : knullfs_file);
+ }
return NULL;
}
@@ -910,6 +956,7 @@ mountpoint:
mp->m_dentry = dget(dentry);
hlist_add_head(&mp->m_hash, mp_hash(dentry));
INIT_HLIST_HEAD(&mp->m_list);
+ INIT_HLIST_HEAD(&mp->m_covers);
hlist_add_head(&m->node, &mp->m_list);
m->mp = no_free_ptr(mp);
read_sequnlock_excl(&mount_lock);
@@ -922,7 +969,7 @@ mountpoint:
*/
static void maybe_free_mountpoint(struct mountpoint *mp, struct list_head *list)
{
- if (hlist_empty(&mp->m_list)) {
+ if (hlist_empty(&mp->m_list) && hlist_empty(&mp->m_covers)) {
struct dentry *dentry = mp->m_dentry;
spin_lock(&dentry->d_lock);
dentry->d_flags &= ~DCACHE_MOUNTED;
@@ -1010,6 +1057,36 @@ static void umount_mnt(struct mount *mnt)
}
/*
+ * @mnt is unmounted together with its parent and would have stayed attached
+ * to it. Leave its cover behind before it is detached so that a lookup on the
+ * parent at the mountpoint keeps finding a mount instead of what @mnt covered.
+ *
+ * locks: mount_lock[write_seqlock]
+ */
+static void leave_cover(struct mount *mnt)
+{
+ struct mnt_cover *cover = mnt->mnt_cover;
+
+ mnt->mnt_cover = NULL;
+ cover->dentry = mnt->mnt_mountpoint;
+ cover->mp = mnt->mnt_mp;
+ /* keeps the mountpoint once @mnt has let go of it */
+ hlist_add_head(&cover->pin, &cover->mp->m_covers);
+ hlist_add_head_rcu(&cover->node, &mnt->mnt_parent->mnt_covers);
+}
+
+/*
+ * locks: mount_lock[write_seqlock]
+ */
+static void drop_cover(struct mnt_cover *cover, struct list_head *shrink_list)
+{
+ hlist_del_rcu(&cover->node);
+ hlist_del(&cover->pin);
+ maybe_free_mountpoint(cover->mp, shrink_list);
+ kfree_rcu(cover, rcu);
+}
+
+/*
* vfsmount lock must be held for write
*/
void mnt_set_mountpoint(struct mount *mnt,
@@ -1058,7 +1135,10 @@ static void attach_mnt(struct mount *mnt, struct mount *parent,
void mnt_change_mountpoint(struct mount *parent, struct mountpoint *mp, struct mount *mnt)
{
struct mountpoint *old_mp = mnt->mnt_mp;
+ struct mount *old_parent = mnt->mnt_parent;
+ if (old_parent->overmount == mnt)
+ old_parent->overmount = NULL;
list_del_init(&mnt->mnt_child);
hlist_del_init(&mnt->mnt_mp_list);
hlist_del_init_rcu(&mnt->mnt_hash);
@@ -1254,6 +1334,7 @@ static struct mount *clone_mnt(struct mount *old, struct dentry *root,
mnt->mnt.mnt_flags = READ_ONCE(old->mnt.mnt_flags) &
~MNT_INTERNAL_FLAGS;
+ mnt->mnt_t_flags = old->mnt_t_flags & T_UNBINDABLE;
if (flag & (CL_SLAVE | CL_PRIVATE))
mnt->mnt_group_id = 0; /* not a peer of original */
@@ -1296,8 +1377,6 @@ static struct mount *clone_mnt(struct mount *old, struct dentry *root,
static void cleanup_mnt(struct mount *mnt)
{
- struct hlist_node *p;
- struct mount *m;
/*
* The warning here probably indicates that somebody messed
* up a mnt_want/drop_write() pair. If this happens, the
@@ -1308,10 +1387,6 @@ static void cleanup_mnt(struct mount *mnt)
WARN_ON(mnt_get_writers(mnt));
if (unlikely(mnt->mnt_pins.first))
mnt_pin_kill(mnt);
- hlist_for_each_entry_safe(m, p, &mnt->mnt_stuck_children, mnt_umount) {
- hlist_del(&m->mnt_umount);
- mntput(&m->mnt);
- }
fsnotify_vfsmount_delete(&mnt->mnt);
dput(mnt->mnt.mnt_root);
deactivate_super(mnt->mnt.mnt_sb);
@@ -1337,6 +1412,8 @@ static DECLARE_DELAYED_WORK(delayed_mntput_work, delayed_mntput);
static void noinline mntput_no_expire_slowpath(struct mount *mnt)
{
+ struct mnt_cover *cover;
+ struct hlist_node *n;
LIST_HEAD(list);
int count;
@@ -1347,7 +1424,7 @@ static void noinline mntput_no_expire_slowpath(struct mount *mnt)
* mount_lock, we'll see their refcount increment here.
*/
smp_mb();
- mnt_add_count(mnt, -1);
+ mnt_dec_count(mnt);
count = mnt_get_count(mnt);
if (count != 0) {
WARN_ON(count < 0);
@@ -1367,13 +1444,10 @@ static void noinline mntput_no_expire_slowpath(struct mount *mnt)
if (unlikely(!list_empty(&mnt->mnt_expire)))
list_del(&mnt->mnt_expire);
- if (unlikely(!list_empty(&mnt->mnt_mounts))) {
- struct mount *p, *tmp;
- list_for_each_entry_safe(p, tmp, &mnt->mnt_mounts, mnt_child) {
- __umount_mnt(p, &list);
- hlist_add_head(&p->mnt_umount, &mnt->mnt_stuck_children);
- }
- }
+ /* nothing stays attached to an unmounted mount */
+ VFS_WARN_ON_ONCE(!list_empty(&mnt->mnt_mounts));
+ hlist_for_each_entry_safe(cover, n, &mnt->mnt_covers, node)
+ drop_cover(cover, &list);
unlock_mount_hash();
shrink_dentry_list(&list);
@@ -1404,7 +1478,8 @@ static void mntput_no_expire(struct mount *mnt)
* non-NULL under rcu_read_lock(), the reference
* we are dropping is not the final one.
*/
- mnt_add_count(mnt, -1);
+ smp_wmb(); /* pairs with the smp_mb() in mnt_get_count() */
+ mnt_dec_count(mnt);
rcu_read_unlock();
return;
}
@@ -1426,7 +1501,7 @@ EXPORT_SYMBOL(mntput);
struct vfsmount *mntget(struct vfsmount *mnt)
{
if (mnt)
- mnt_add_count(real_mount(mnt), 1);
+ mnt_inc_count(real_mount(mnt));
return mnt;
}
EXPORT_SYMBOL(mntget);
@@ -1737,36 +1812,17 @@ static inline void namespace_lock(void)
enum umount_tree_flags {
UMOUNT_SYNC = 1,
UMOUNT_PROPAGATE = 2,
- UMOUNT_CONNECTED = 4,
+ UMOUNT_COVER = 4,
};
-static bool disconnect_mount(struct mount *mnt, enum umount_tree_flags how)
+/* Do we need to leave the mountpoint on the parent covered? */
+static bool needs_cover(struct mount *mnt, enum umount_tree_flags how)
{
- /* Leaving mounts connected is only valid for lazy umounts */
if (how & UMOUNT_SYNC)
- return true;
-
- /* A mount without a parent has nothing to be connected to */
- if (!mnt_has_parent(mnt))
- return true;
-
- /* Because the reference counting rules change when mounts are
- * unmounted and connected, umounted mounts may not be
- * connected to mounted mounts.
- */
- if (!(mnt->mnt_parent->mnt.mnt_flags & MNT_UMOUNT))
- return true;
-
- /* Has it been requested that the mount remain connected? */
- if (how & UMOUNT_CONNECTED)
return false;
-
- /* Is the mount locked such that it needs to remain connected? */
- if (IS_MNT_LOCKED(mnt))
+ if (!(mnt->mnt_parent->mnt.mnt_flags & MNT_UMOUNT))
return false;
-
- /* By default disconnect the mount */
- return true;
+ return (how & UMOUNT_COVER) || IS_MNT_LOCKED(mnt);
}
/*
@@ -1783,6 +1839,8 @@ static void umount_tree(struct mount *mnt, enum umount_tree_flags how)
/* Gather the mounts to umount */
for (p = mnt; p; p = next_mnt(p, mnt)) {
+ /* A mount is unmounted once. */
+ VFS_WARN_ON_ONCE(p->mnt.mnt_flags & MNT_UMOUNT);
p->mnt.mnt_flags |= MNT_UMOUNT;
if (mnt_ns_attached(p))
move_from_ns(p);
@@ -1802,7 +1860,6 @@ static void umount_tree(struct mount *mnt, enum umount_tree_flags how)
while (!list_empty(&tmp_list)) {
struct mnt_namespace *ns;
- bool disconnect;
p = list_first_entry(&tmp_list, struct mount, mnt_list);
list_del_init(&p->mnt_expire);
list_del_init(&p->mnt_list);
@@ -1815,17 +1872,12 @@ static void umount_tree(struct mount *mnt, enum umount_tree_flags how)
if (how & UMOUNT_SYNC)
p->mnt.mnt_flags |= MNT_SYNC_UMOUNT;
- disconnect = disconnect_mount(p, how);
if (mnt_has_parent(p)) {
- if (!disconnect) {
- /* Don't forget about p */
- list_add_tail(&p->mnt_child, &p->mnt_parent->mnt_mounts);
- } else {
- umount_mnt(p);
- }
+ if (needs_cover(p, how))
+ leave_cover(p);
+ umount_mnt(p);
}
- if (disconnect)
- hlist_add_head(&p->mnt_umount, &unmounted);
+ hlist_add_head(&p->mnt_umount, &unmounted);
/*
* At this point p->mnt_ns is NULL, notification will be queued
@@ -1976,11 +2028,16 @@ out:
* detach_mounts allows lazily unmounting those mounts instead of
* leaking them.
*
+ * The dentry is unhashed before the mounts go so that no lookup finds
+ * what they covered. The caller removes it for good afterwards.
+ *
* The caller may hold dentry->d_inode->i_rwsem.
*/
void __detach_mounts(struct dentry *dentry)
{
struct pinned_mountpoint mp = {};
+ struct mnt_cover *cover;
+ struct hlist_node *n;
struct mount *mnt;
guard(namespace_excl)();
@@ -1989,15 +2046,16 @@ void __detach_mounts(struct dentry *dentry)
if (!lookup_mountpoint(dentry, &mp))
return;
+ /* the name goes first, what covered it goes second */
+ d_drop(dentry);
event++;
while (mp.node.next) {
mnt = hlist_entry(mp.node.next, struct mount, mnt_mp_list);
- if (mnt->mnt.mnt_flags & MNT_UMOUNT) {
- umount_mnt(mnt);
- hlist_add_head(&mnt->mnt_umount, &unmounted);
- }
- else umount_tree(mnt, UMOUNT_CONNECTED);
+ umount_tree(mnt, UMOUNT_COVER);
}
+ /* the dentry goes away, so do the covers left behind on it */
+ hlist_for_each_entry_safe(cover, n, &mp.mp->m_covers, pin)
+ drop_cover(cover, &ex_mountpoints);
unpin_mountpoint(&mp);
}
@@ -2294,9 +2352,11 @@ void drop_collected_paths(const struct path *paths, const struct path *prealloc)
static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *, bool);
+/* Consumes the caller's reference to @mnt. */
void dissolve_on_fput(struct vfsmount *mnt)
{
- struct mount *m = real_mount(mnt);
+ struct vfsmount *p __free(mntput) = mnt;
+ struct mount *m = real_mount(p);
/*
* m used to be the root of anon namespace; if it still is one,
@@ -2319,13 +2379,14 @@ void dissolve_on_fput(struct vfsmount *mnt)
emptied_ns = m->mnt_ns;
lock_mount_hash();
- umount_tree(m, UMOUNT_CONNECTED);
+ umount_tree(m, UMOUNT_COVER);
unlock_mount_hash();
+ mntput(no_free_ptr(p));
}
}
/* locks: namespace_shared && pinned(mnt) || mount_locked_reader */
-static bool __has_locked_children(struct mount *mnt, struct dentry *dentry)
+bool has_locked_children(struct mount *mnt, struct dentry *dentry)
{
struct mount *child;
@@ -2339,10 +2400,16 @@ static bool __has_locked_children(struct mount *mnt, struct dentry *dentry)
return false;
}
-bool has_locked_children(struct mount *mnt, struct dentry *dentry)
+/* locks: namespace_shared && pinned(mnt) || mount_locked_reader */
+static bool __has_children(struct mount *mnt, struct dentry *dentry)
{
- guard(mount_locked_reader)();
- return __has_locked_children(mnt, dentry);
+ struct mount *child;
+
+ list_for_each_entry(child, &mnt->mnt_mounts, mnt_child) {
+ if (is_subdir(child->mnt_mountpoint, dentry))
+ return true;
+ }
+ return false;
}
/*
@@ -2401,7 +2468,7 @@ struct vfsmount *clone_private_mount(const struct path *path)
if (!ns_capable(old_mnt->mnt_ns->user_ns, CAP_SYS_ADMIN))
return ERR_PTR(-EPERM);
- if (__has_locked_children(old_mnt, path->dentry))
+ if (has_locked_children(old_mnt, path->dentry))
return ERR_PTR(-EINVAL);
new_mnt = clone_mnt(old_mnt, path->dentry, CL_PRIVATE);
@@ -2564,11 +2631,11 @@ enum mnt_tree_flags_t {
static int attach_recursive_mnt(struct mount *source_mnt,
const struct pinned_mountpoint *dest)
{
- struct user_namespace *user_ns = current->nsproxy->mnt_ns->user_ns;
struct mount *dest_mnt = dest->parent;
struct mountpoint *dest_mp = dest->mp;
HLIST_HEAD(tree_list);
struct mnt_namespace *ns = dest_mnt->mnt_ns;
+ struct user_namespace *user_ns = ns->user_ns;
struct pinned_mountpoint root = {};
struct mountpoint *shorter = NULL;
struct mount *child, *p;
@@ -2578,12 +2645,30 @@ static int attach_recursive_mnt(struct mount *source_mnt,
bool moving = mnt_has_parent(source_mnt);
/*
+ * A caller in an unprivileged mount namespaces may trigger an
+ * automount and propagate locked mounts into privileged mount
+ * namespaces. Take ownership from the target mount namespace.
+ * It's equivalent for everything but the automount case.
+ *
+ * Detached trees in anonymous mount namespaces by be handed
+ * over via SCM_RIGHTS or inherited in other ways on purpose
+ * the attaching task's mount namespace is authoritative, not
+ * the creator of the detached tree.
+ */
+ if (is_anon_ns(ns))
+ user_ns = current->nsproxy->mnt_ns->user_ns;
+ else
+ user_ns = ns->user_ns;
+
+ /*
* Preallocate a mountpoint in case the new mounts need to be
* mounted beneath mounts on the same mountpoint.
*/
- for (top = source_mnt; unlikely(top->overmount); top = top->overmount) {
+ for (top = source_mnt; ; top = top->overmount) {
if (!shorter && is_mnt_ns_file(top->mnt.mnt_root))
shorter = top->mnt_mp;
+ if (likely(!top->overmount))
+ break;
}
err = get_mountpoint(top->mnt.mnt_root, &root);
if (err)
@@ -2654,15 +2739,17 @@ static int attach_recursive_mnt(struct mount *source_mnt,
/*
* If @q was locked it was meant to hide
* whatever was under it. Let @child take over
- * that job and lock it, then we can unlock @q.
- * That'll allow another namespace to shed @q
- * and reveal @child. Clearly, that mounter
- * consented to this by not severing the mount
- * relationship. Otherwise, what's the point.
+ * that job and lock it. If @child is the mount
+ * the caller placed we can then unlock @q:
+ * nothing another namespace does removes it
+ * again. A propagated copy goes away when the
+ * mounter of the original unmounts it, so @q
+ * keeps its lock.
*/
if (IS_MNT_LOCKED(q)) {
child->mnt.mnt_flags |= MNT_LOCKED;
- q->mnt.mnt_flags &= ~MNT_LOCKED;
+ if (child == source_mnt)
+ q->mnt.mnt_flags &= ~MNT_LOCKED;
}
mnt_change_mountpoint(r, mp, q);
}
@@ -2759,6 +2846,11 @@ static void do_lock_mount(const struct path *path,
scoped_guard(mount_locked_reader) {
m = where_to_mount(path, &dentry, beneath);
+ /* sticky, so it takes no locks to refuse it */
+ if (unlikely(cant_mount(dentry))) {
+ res->parent = ERR_PTR(-ENOENT);
+ return;
+ }
if (&m->mnt != path->mnt) {
mntget(&m->mnt);
dget(dentry);
@@ -2989,7 +3081,14 @@ static struct mount *__do_loopback(const struct path *old_path,
if (!may_copy_tree(old_path))
return ERR_PTR(-EINVAL);
- if (!recurse && __has_locked_children(old, old_path->dentry))
+ /* a pseudo dentry is freed without an RCU delay, no walk may find it */
+ if (old_path->dentry->d_flags & DCACHE_NORCU)
+ return ERR_PTR(-EINVAL);
+
+ if (recurse && !old->mnt_ns)
+ return ERR_PTR(-EINVAL);
+
+ if (!recurse && has_locked_children(old, old_path->dentry))
return ERR_PTR(-EINVAL);
if (recurse)
@@ -3028,7 +3127,11 @@ static int do_loopback(const struct path *path, const char *old_name,
if (IS_ERR(mnt))
return PTR_ERR(mnt);
- err = graft_tree(mnt, &mp);
+ /* the copy may carry mount namespace files from below the source */
+ if (recurse && !check_for_nsfs_mounts(mnt))
+ err = -EINVAL;
+ else
+ err = graft_tree(mnt, &mp);
if (err) {
lock_mount_hash();
umount_tree(mnt, UMOUNT_SYNC);
@@ -3088,7 +3191,7 @@ static struct file *open_detached_copy(struct path *path, unsigned int flags)
path->mnt = mntget(&ns->root->mnt);
file = dentry_open(path, O_PATH, current_cred());
if (IS_ERR(file))
- dissolve_on_fput(path->mnt);
+ dissolve_on_fput(no_free_ptr(path->mnt));
else
file->f_mode |= FMODE_NEED_UNMOUNT;
return file;
@@ -3110,12 +3213,18 @@ static struct mnt_namespace *create_new_namespace(struct path *path,
struct mount *mnt;
unsigned int copy_flags = 0;
bool locked = false, recurse = flags & MOUNT_COPY_RECURSIVE;
+ bool foreign = user_ns != ns->user_ns;
if (unlikely(!d_can_lookup(path->dentry)))
return ERR_PTR(-ENOTDIR);
- if (user_ns != ns->user_ns)
- copy_flags |= CL_SLAVE;
+ /*
+ * Without privileges over the mount namespace the copy is made from
+ * nothing mounted below @path may be left out. It would reveal what
+ * it covers. That's what unshare() gives such a caller as well.
+ */
+ if (foreign)
+ copy_flags |= CL_SLAVE | CL_COPY_UNBINDABLE;
new_ns = alloc_mnt_ns(user_ns, false);
if (IS_ERR(new_ns))
@@ -3149,10 +3258,14 @@ static struct mnt_namespace *create_new_namespace(struct path *path,
/*
* We don't emulate unshare()ing a mount namespace. We stick to
* the restrictions of creating detached bind-mounts. It has a
- * lot saner and simpler semantics.
+ * lot saner and simpler semantics. A caller without privileges
+ * over the mount namespace can't leave out any child though.
*/
if (flags & MOUNT_COPY_NEW)
mnt = clone_mnt(real_mount(path->mnt), path->dentry, copy_flags);
+ else if (foreign && !recurse &&
+ __has_children(real_mount(path->mnt), path->dentry))
+ mnt = ERR_PTR(-EINVAL);
else
mnt = __do_loopback(path, recurse, copy_flags);
scoped_guard(mount_writer) {
@@ -3169,7 +3282,7 @@ static struct mnt_namespace *create_new_namespace(struct path *path,
* of the real rootfs we created.
*/
attach_mnt(mnt, new_ns_root, mp.mp);
- if (user_ns != ns->user_ns)
+ if (foreign)
lock_mnt_tree(new_ns_root);
}
@@ -3463,11 +3576,11 @@ static int do_set_group(const struct path *from_path, const struct path *to_path
return -EINVAL;
/* From mount should not have locked children in place of To's root */
- if (__has_locked_children(from, to->mnt.mnt_root))
+ if (has_locked_children(from, to->mnt.mnt_root))
return -EINVAL;
/* Setting sharing groups is only allowed on private mounts */
- if (IS_MNT_SHARED(to) || IS_MNT_SLAVE(to))
+ if (IS_MNT_SHARED(to) || IS_MNT_SLAVE(to) || IS_MNT_UNBINDABLE(to))
return -EINVAL;
/* From should not be private */
@@ -3743,7 +3856,7 @@ static int do_add_mount(struct mount *newmnt, const struct pinned_mountpoint *mp
if (!(mnt_flags & MNT_SHRINKABLE))
return -EINVAL;
/* ... and for those we'd better have mountpoint still alive */
- if (!parent->mnt_ns)
+ if (!is_mounted(&parent->mnt))
return -EINVAL;
}
@@ -3953,6 +4066,8 @@ void mark_mounts_for_expiry(struct list_head *mounts)
list_for_each_entry_safe(mnt, next, mounts, mnt_expire) {
if (!is_mounted(&mnt->mnt))
continue;
+ /* lock_mnt_tree() leaves expirable mounts alone */
+ VFS_WARN_ON_ONCE(IS_MNT_LOCKED(mnt));
if (!xchg(&mnt->mnt_expiry_mark, 1) ||
propagate_mount_busy(mnt, 1))
continue;
@@ -3960,6 +4075,11 @@ void mark_mounts_for_expiry(struct list_head *mounts)
}
while (!list_empty(&graveyard)) {
mnt = list_first_entry(&graveyard, struct mount, mnt_expire);
+ /* an earlier umount_tree() may have moved a busy mount here */
+ if (propagate_mount_busy(mnt, 1)) {
+ list_move(&mnt->mnt_expire, mounts);
+ continue;
+ }
touch_mnt_namespace(mnt->mnt_ns);
umount_tree(mnt, UMOUNT_PROPAGATE|UMOUNT_SYNC);
}
@@ -3968,16 +4088,37 @@ void mark_mounts_for_expiry(struct list_head *mounts)
EXPORT_SYMBOL_GPL(mark_mounts_for_expiry);
/*
+ * Unmount @mnt if it's a shrinkable mount without children that nobody uses.
+ *
+ * mount_lock must be held for write
+ */
+static bool shrink_submount(struct mount *mnt)
+{
+ /* not the kernel's to remove either */
+ if (IS_MNT_LOCKED(mnt) || propagate_mount_busy(mnt, 1))
+ return false;
+ touch_mnt_namespace(mnt->mnt_ns);
+ umount_tree(mnt, UMOUNT_PROPAGATE|UMOUNT_SYNC);
+ return true;
+}
+
+/*
* Ripoff of 'select_parent()'
*
- * search the list of submounts for a given mountpoint, and move any
- * shrinkable submounts to the 'graveyard' list.
+ * unmount the shrinkable submounts of @parent that aren't busy, children
+ * before their parent, and say whether anything went
+ *
+ * The cursor into the children of @this_parent survives the umount of a
+ * child mount without child mounts. The mounts that get umounted together with
+ * it are located under receiving mounts of @this_parent and never under
+ * @this_parent itself. The one exception is @this_parent getting unmounted
+ * then the walk starts over.
*/
-static int select_submounts(struct mount *parent, struct list_head *graveyard)
+static bool __shrink_submounts(struct mount *parent)
{
struct mount *this_parent = parent;
struct list_head *next;
- int found = 0;
+ bool shrunk = false;
repeat:
next = this_parent->mnt_mounts.next;
@@ -3996,42 +4137,45 @@ resume:
this_parent = mnt;
goto repeat;
}
-
- if (!propagate_mount_busy(mnt, 1)) {
- list_move_tail(&mnt->mnt_expire, graveyard);
- found++;
- }
+ if (!shrink_submount(mnt))
+ continue;
+ shrunk = true;
+ if (unlikely(this_parent->mnt.mnt_flags & MNT_UMOUNT))
+ return true;
}
/*
* All done at this level ... ascend and resume the search
*/
if (this_parent != parent) {
- next = this_parent->mnt_child.next;
- this_parent = this_parent->mnt_parent;
+ struct mount *mnt = this_parent;
+
+ next = mnt->mnt_child.next;
+ this_parent = mnt->mnt_parent;
+ /* its children are gone, maybe it can go as well */
+ if (shrink_submount(mnt)) {
+ shrunk = true;
+ if (unlikely(this_parent->mnt.mnt_flags & MNT_UMOUNT))
+ return true;
+ }
goto resume;
}
- return found;
+ return shrunk;
}
/*
- * process a list of expirable mountpoints with the intent of discarding any
- * submounts of a specific parent mountpoint
+ * unmount the shrinkable submounts of @mnt that aren't busy
+ *
+ * The busy check and the umount of a mount are adjacent. An umount can
+ * still empty or move a mount in a part of the tree that was walked
+ * already, so walk again until nothing goes.
*
* mount_lock must be held for write
*/
static void shrink_submounts(struct mount *mnt)
{
- LIST_HEAD(graveyard);
- struct mount *m;
-
- /* extract submounts of 'mountpoint' from the expiration list */
- while (select_submounts(mnt, &graveyard)) {
- while (!list_empty(&graveyard)) {
- m = list_first_entry(&graveyard, struct mount,
- mnt_expire);
- touch_mnt_namespace(m->mnt_ns);
- umount_tree(m, UMOUNT_PROPAGATE|UMOUNT_SYNC);
- }
+ for (;;) {
+ if (!__shrink_submounts(mnt))
+ break;
}
}
@@ -4196,6 +4340,8 @@ static void free_mnt_ns(struct mnt_namespace *ns)
if (!is_anon_ns(ns))
ns_common_free(ns);
dec_mnt_namespaces(ns->ucounts);
+ /* the last active reference is gone, no mark can show up anymore */
+ fsnotify_mntns_delete(ns);
mnt_ns_tree_remove(ns);
}
@@ -4545,7 +4691,7 @@ SYSCALL_DEFINE3(fsmount, int, fs_fd, unsigned int, flags,
FD_PREPARE(fdf, (flags & FSMOUNT_CLOEXEC) ? O_CLOEXEC : 0,
dentry_open(&new_path, O_PATH, fc->cred));
if (fdf.err) {
- dissolve_on_fput(new_path.mnt);
+ dissolve_on_fput(no_free_ptr(new_path.mnt));
return fdf.err;
}
@@ -4697,14 +4843,15 @@ int path_pivot_root(struct path *new, struct path *old)
new_mnt = real_mount(new->mnt);
root_mnt = real_mount(root.mnt);
+ /* only a mounted mount has a parent that namespace_sem pins */
+ if (!check_mnt(root_mnt) || !check_mnt(new_mnt))
+ return -EINVAL;
ex_parent = new_mnt->mnt_parent;
root_parent = root_mnt->mnt_parent;
if (IS_MNT_SHARED(old_mnt) ||
IS_MNT_SHARED(ex_parent) ||
IS_MNT_SHARED(root_parent))
return -EINVAL;
- if (!check_mnt(root_mnt) || !check_mnt(new_mnt))
- return -EINVAL;
if (new_mnt->mnt.mnt_flags & MNT_LOCKED)
return -EINVAL;
if (d_unlinked(new->dentry))
@@ -4955,6 +5102,11 @@ static int do_mount_setattr(const struct path *path, struct mount_kattr *kattr)
* propagation.
*/
namespace_lock();
+ /* invent_group_ids() walks the tree, only walk a mounted one */
+ if (!anon_ns_root(mnt) && !check_mnt(mnt)) {
+ namespace_unlock();
+ return -EINVAL;
+ }
if (kattr->propagation == MS_SHARED) {
err = invent_group_ids(mnt, kattr->kflags & MOUNT_KATTR_RECURSE);
if (err) {
@@ -4967,6 +5119,7 @@ static int do_mount_setattr(const struct path *path, struct mount_kattr *kattr)
err = -EINVAL;
lock_mount_hash();
+ /* Checked under namespace_sem already if the propagation changes. */
if (!anon_ns_root(mnt) && !check_mnt(mnt))
goto out;
@@ -5319,15 +5472,25 @@ static void statmount_sb_basic(struct kstatmount *s)
s->sm.sb_flags = sb->s_flags & (SB_RDONLY|SB_SYNCHRONOUS|SB_DIRSYNC|SB_LAZYTIME);
}
+static void statmount_mnt_parent(struct kstatmount *s, const struct mount *m)
+{
+ s->sm.mnt_parent_id = m->mnt_parent->mnt_id_unique;
+ s->sm.mnt_parent_id_old = m->mnt_parent->mnt_id;
+}
+
static void statmount_mnt_basic(struct kstatmount *s)
{
struct mount *m = real_mount(s->mnt);
s->sm.mask |= STATMOUNT_MNT_BASIC;
s->sm.mnt_id = m->mnt_id_unique;
- s->sm.mnt_parent_id = m->mnt_parent->mnt_id_unique;
s->sm.mnt_id_old = m->mnt_id;
- s->sm.mnt_parent_id_old = m->mnt_parent->mnt_id;
+ /* An unmounted mount is cut loose from its parent under mount_lock alone. */
+ if (likely(is_mounted(s->mnt)))
+ statmount_mnt_parent(s, m);
+ else
+ scoped_guard(mount_locked_reader)
+ statmount_mnt_parent(s, m);
s->sm.mnt_attr = mnt_to_attr_flags(&m->mnt);
s->sm.mnt_propagation = mnt_to_propagation_flags(m);
s->sm.mnt_peer_group = m->mnt_group_id;
@@ -6199,6 +6362,25 @@ static void __init mount_rootfs_on_nullfs(struct vfsmount *mnt,
attach_mnt(real_mount(mnt), mp.parent, mp.mp);
}
+static struct vfsmount *__init knullfs_file_mount(void)
+{
+ struct dentry *file;
+ struct mount *mnt;
+
+ file = nullfs_new_file(knullfs->mnt_sb);
+ if (IS_ERR(file))
+ return ERR_CAST(file);
+ mnt = clone_mnt(real_mount(knullfs), file, CL_PRIVATE);
+ dput(file);
+ if (IS_ERR(mnt))
+ return ERR_CAST(mnt);
+ mnt->mnt_ns = MNT_NS_INTERNAL;
+ mnt->mnt.mnt_flags |= MNT_INTERNAL;
+ mnt->mnt.mnt_flags &= ~MNT_READONLY;
+ dont_mount(mnt->mnt.mnt_root);
+ return &mnt->mnt;
+}
+
static void __init init_mount_tree(void)
{
struct vfsmount *mnt, *nullfs_mnt;
@@ -6210,10 +6392,14 @@ static void __init init_mount_tree(void)
*
* (1) nullfs with mount id 1
* (2) mutable rootfs with mount id 2
- * (3) private nullfs for kthreads (SB_KERNMOUNT)
+ * (3) private nullfs for kthreads (SB_KERNMOUNT), kept in knullfs
+ * (4) a second mount of (3) rooted on a regular file, kept in
+ * knullfs_file
*
* with (2) mounted on top of (1). The init_task's root and pwd
* are pointed at (3) so all kthreads start isolated in nullfs.
+ * A lookup at the cover an unmounted mount left behind finds (3)
+ * or (4), see __lookup_mnt().
*/
nullfs_mnt = vfs_kern_mount(&nullfs_fs_type, 0, "nullfs", NULL);
if (IS_ERR(nullfs_mnt))
@@ -6245,11 +6431,18 @@ static void __init init_mount_tree(void)
init_mnt_ns.nr_mounts++;
}
- nullfs_mnt = kern_mount(&nullfs_fs_type);
- if (IS_ERR(nullfs_mnt))
+ knullfs = kern_mount(&nullfs_fs_type);
+ if (IS_ERR(knullfs))
panic("VFS: Failed to create private nullfs instance");
- root.mnt = nullfs_mnt;
- root.dentry = nullfs_mnt->mnt_root;
+ /* nothing is ever mounted on the root of a kernel thread */
+ dont_mount(knullfs->mnt_root);
+ /* and nothing is ever written through it */
+ knullfs->mnt_flags |= MNT_READONLY;
+ knullfs_file = knullfs_file_mount();
+ if (IS_ERR(knullfs_file))
+ panic("VFS: Failed to create the nullfs file stand-in");
+ root.mnt = knullfs;
+ root.dentry = knullfs->mnt_root;
init_task.nsproxy->mnt_ns = &init_mnt_ns;
get_mnt_ns(&init_mnt_ns);
diff --git a/fs/notify/mark.c b/fs/notify/mark.c
index b2640d836a71..d17628580a57 100644
--- a/fs/notify/mark.c
+++ b/fs/notify/mark.c
@@ -903,6 +903,10 @@ static int fsnotify_add_mark_list(struct fsnotify_mark *mark, void *obj,
if (WARN_ON(!fsnotify_valid_obj_type(obj_type)))
return -EINVAL;
+ /* the filesystem doesn't want its objects watched */
+ if (sb && (sb->s_type->fs_flags & FS_DISALLOW_NOTIFY))
+ return -EINVAL;
+
/*
* Attach the sb info before attaching a connector to any object on sb.
* The sb info will remain attached as long as sb lives.
diff --git a/fs/nullfs.c b/fs/nullfs.c
index e06352c7b2cc..e7221bc8aedb 100644
--- a/fs/nullfs.c
+++ b/fs/nullfs.c
@@ -3,6 +3,8 @@
#include <linux/fs/super_types.h>
#include <linux/fs_context.h>
#include <linux/magic.h>
+#include <linux/splice.h>
+#include <linux/uio.h>
#include "mount.h"
@@ -10,6 +12,121 @@ static const struct super_operations nullfs_super_operations = {
.statfs = simple_statfs,
};
+static loff_t nullfs_dir_llseek(struct file *file, loff_t offset, int whence)
+{
+ /* an empty directory has two entries . and .. at offsets 0 and 1 */
+ return generic_file_llseek_size(file, offset, whence, 2, 2);
+}
+
+static int nullfs_dir_readdir(struct file *file, struct dir_context *ctx)
+{
+ dir_emit_dots(file, ctx);
+ return 0;
+}
+
+/* the one inode of nullfs is shared by every holder, so no locks on it */
+static int nullfs_nolock(struct file *file, int cmd, struct file_lock *fl)
+{
+ return -ENOLCK;
+}
+
+/* what libfs gives an empty directory, plus the refusal of file locks */
+static const struct file_operations nullfs_dir_operations = {
+ .llseek = nullfs_dir_llseek,
+ .read = generic_read_dir,
+ .iterate_shared = nullfs_dir_readdir,
+ .fsync = noop_fsync,
+ .lock = nullfs_nolock,
+ .flock = nullfs_nolock,
+ .fop_flags = FOP_IMMUTABLE,
+};
+
+static ssize_t nullfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
+{
+ return 0;
+}
+
+static ssize_t nullfs_file_splice_read(struct file *in, loff_t *ppos,
+ struct pipe_inode_info *pipe,
+ size_t len, unsigned int flags)
+{
+ return 0;
+}
+
+static ssize_t nullfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
+{
+ size_t count = iov_iter_count(from);
+
+ iov_iter_advance(from, count);
+ return count;
+}
+
+static int nullfs_pipe_to_null(struct pipe_inode_info *pipe,
+ struct pipe_buffer *buf, struct splice_desc *sd)
+{
+ return sd->len;
+}
+
+static ssize_t nullfs_file_splice_write(struct pipe_inode_info *pipe,
+ struct file *out, loff_t *ppos,
+ size_t len, unsigned int flags)
+{
+ return splice_from_pipe(pipe, out, ppos, len, flags, nullfs_pipe_to_null);
+}
+
+static int nullfs_file_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
+ struct iattr *attr)
+{
+ if (attr->ia_valid & (ATTR_MODE | ATTR_UID | ATTR_GID))
+ return -EPERM;
+ return 0;
+}
+
+static const struct inode_operations nullfs_file_inode_operations = {
+ .setattr = nullfs_file_setattr,
+};
+
+static const struct file_operations nullfs_file_operations = {
+ .llseek = generic_file_llseek,
+ .read_iter = nullfs_file_read_iter,
+ .write_iter = nullfs_file_write_iter,
+ .splice_read = nullfs_file_splice_read,
+ .splice_write = nullfs_file_splice_write,
+ .fsync = noop_fsync,
+ .lock = nullfs_nolock,
+ .flock = nullfs_nolock,
+};
+
+/*
+ * An empty regular file on @sb as a dentry of its own, never hashed under
+ * the root so no lookup finds it.
+ */
+struct dentry *nullfs_new_file(struct super_block *sb)
+{
+ struct dentry *dentry;
+ struct inode *inode;
+
+ inode = new_inode(sb);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ /* the root directory is 1 */
+ inode->i_ino = 2;
+ inode->i_mode = S_IFREG | 0666;
+ inode->i_op = &nullfs_file_inode_operations;
+ inode->i_fop = &nullfs_file_operations;
+ simple_inode_init_ts(inode);
+ inode->i_flags |= S_NOATIME;
+
+ dentry = d_alloc_anon(sb);
+ if (!dentry) {
+ iput(inode);
+ return ERR_PTR(-ENOMEM);
+ }
+ d_instantiate(dentry, inode);
+ return dentry;
+}
+
static int nullfs_fs_fill_super(struct super_block *s, struct fs_context *fc)
{
struct inode *inode;
@@ -30,10 +147,11 @@ static int nullfs_fs_fill_super(struct super_block *s, struct fs_context *fc)
/* nullfs is permanently empty... */
make_empty_dir_inode(inode);
+ inode->i_fop = &nullfs_dir_operations;
simple_inode_init_ts(inode);
inode->i_ino = 1;
- /* ... and immutable. */
- inode->i_flags |= S_IMMUTABLE;
+ /* ... and immutable, reading it leaves no trace either. */
+ inode->i_flags |= S_IMMUTABLE | S_NOATIME;
s->s_root = d_make_root(inode);
if (!s->s_root)
@@ -61,6 +179,7 @@ static int nullfs_init_fs_context(struct fs_context *fc)
struct file_system_type nullfs_fs_type = {
.name = "nullfs",
+ .fs_flags = FS_DISALLOW_NOTIFY,
.init_fs_context = nullfs_init_fs_context,
.kill_sb = kill_anon_super,
};
diff --git a/fs/pnode.c b/fs/pnode.c
index 5d91c3e58d2a..5f07fa14099b 100644
--- a/fs/pnode.c
+++ b/fs/pnode.c
@@ -410,19 +410,99 @@ bool propagation_would_overmount(const struct mount *from,
return false;
}
+/* Does @m receive propagation from @parent? */
+static bool receives_from(struct mount *m, struct mount *parent)
+{
+ if (m == parent)
+ return false;
+ for (; m; m = m->mnt_master)
+ if (m == parent || peers(m, parent))
+ return true;
+ return false;
+}
+
+/*
+ * Does @m receive propagation from the victim's parent as well? If so, then
+ * the mount at the victim's mountpoint inside of @m is a umount candidate as
+ * well. So it's the next candidate in the chain. Otherwise the chain ends at
+ * @m.
+ */
+static struct mount *next_candidate(struct mount *m, struct mount *victim)
+{
+ if (!receives_from(m, victim->mnt_parent))
+ return NULL;
+ return __lookup_mnt(&m->mnt, victim->mnt_mountpoint);
+}
+
+/*
+ * Would propagate_umount() pull out a mount of the chain of candidates that
+ * starts at @c, and does that mount have references beyond its own?
+ *
+ * This mirrors how trim_one(), trim_ancestors() and handle_locked() handle a
+ * synchronous umount:
+ *
+ * - single victim
+ * - without children
+ * - with MNT_LOCKED already cleared on every candidate by propagate_mount_unlock()
+ *
+ * A copy of the victim gets unmounted when each of its children is
+ * the next candidate in the chain or its overmount, unless the next
+ * unmount candidate is not its overmount and some unmount candidate further
+ * down has a child outside the chain. Keep this in sync with
+ * Documentation/filesystems/propagate_umount.txt.
+ */
+static bool chain_busy(struct mount *c, struct mount *victim)
+{
+ struct mount *m, *n, *next, *deepest = NULL;
+ bool above;
+
+ /* the deepest candidate with a child outside the chain */
+ for (m = c; m; m = next) {
+ next = next_candidate(m, victim);
+ list_for_each_entry(n, &m->mnt_mounts, mnt_child) {
+ if (n != next && n != victim) {
+ deepest = m;
+ break;
+ }
+ }
+ }
+
+ above = deepest != NULL; /* @deepest is at or below @m */
+ for (m = c; m; m = next) {
+ bool goes = true;
+
+ next = next_candidate(m, victim);
+ list_for_each_entry(n, &m->mnt_mounts, mnt_child) {
+ if (n != next && n != m->overmount && n != victim) {
+ goes = false;
+ break;
+ }
+ }
+ if (goes && next && next != m->overmount && above && m != deepest)
+ goes = false;
+ if (m == deepest)
+ above = false;
+ if (goes && do_refcount_check(m, 1))
+ return true;
+ }
+ return false;
+}
+
/*
* check if the mount 'mnt' can be unmounted successfully.
* @mnt: the mount to be checked for unmount
* NOTE: unmounting 'mnt' would naturally propagate to all
* other mounts its parent propagates to.
- * Check if any of these mounts that **do not have submounts**
- * have more references than 'refcnt'. If so return busy.
+ * Check if any of the mounts that propagate_umount() would pull out
+ * along with it have more references than their own. If so return busy.
*
* vfsmount lock must be held for write
*/
int propagate_mount_busy(struct mount *mnt, int refcnt)
{
struct mount *parent = mnt->mnt_parent;
+ struct dentry *mp = mnt->mnt_mountpoint;
+ struct mount *m;
/*
* quickly check if the current mount can be unmounted.
@@ -435,24 +515,16 @@ int propagate_mount_busy(struct mount *mnt, int refcnt)
if (mnt == parent)
return 0;
- for (struct mount *m = propagation_next(parent, parent); m;
- m = propagation_next(m, parent)) {
- struct list_head *head;
- struct mount *child = __lookup_mnt(&m->mnt, mnt->mnt_mountpoint);
+ /* the candidates are the mounts at @mp below the receivers */
+ for (m = propagation_next(parent, parent); m;
+ m = propagation_next(m, parent)) {
+ struct mount *c = __lookup_mnt(&m->mnt, mp);
- if (!child)
+ /* each chain once, from its top: skip receivers that are candidates */
+ if (!c || (mnt_has_parent(m) && m->mnt_mountpoint == mp &&
+ receives_from(m->mnt_parent, parent)))
continue;
-
- head = &child->mnt_mounts;
- if (!list_empty(head)) {
- /*
- * a mount that covers child completely wouldn't prevent
- * it being pulled out; any other would.
- */
- if (!list_is_singular(head) || !child->overmount)
- continue;
- }
- if (do_refcount_check(child, 1))
+ if (chain_busy(c, mnt))
return 1;
}
return 0;
@@ -625,8 +697,8 @@ static void handle_locked(struct mount *m, struct list_head *to_umount)
* the same parent and mountpoint; that will be remedied as soon as we
* return from propagate_umount() - its caller (umount_tree()) will detach
* the stack from the parent it (and now @m) is attached to. umount_tree()
- * might choose to keep unmounted pieces stuck to each other, but it always
- * detaches them from the mounts that remain in the tree.
+ * detaches every unmounted mount from its parent, whether that parent
+ * remains in the tree or not.
*/
static void reparent(struct mount *m)
{
diff --git a/fs/readdir.c b/fs/readdir.c
index 76bb1ae3a450..f2288841337a 100644
--- a/fs/readdir.c
+++ b/fs/readdir.c
@@ -87,6 +87,8 @@ EXPORT_SYMBOL(wrap_directory_iterator);
int iterate_dir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
+ /* never an entry, never removed: nothing for the lock to keep still */
+ bool locked = !(file->f_op->fop_flags & FOP_IMMUTABLE);
int res = -ENOTDIR;
if (!file->f_op->iterate_shared)
@@ -100,9 +102,11 @@ int iterate_dir(struct file *file, struct dir_context *ctx)
if (res)
goto out;
- res = down_read_killable(&inode->i_rwsem);
- if (res)
- goto out;
+ if (locked) {
+ res = down_read_killable(&inode->i_rwsem);
+ if (res)
+ goto out;
+ }
res = -ENOENT;
if (!IS_DEADDIR(inode)) {
@@ -112,7 +116,8 @@ int iterate_dir(struct file *file, struct dir_context *ctx)
fsnotify_access(file);
file_accessed(file);
}
- inode_unlock_shared(inode);
+ if (locked)
+ inode_unlock_shared(inode);
out:
return res;
}
diff --git a/include/linux/fs.h b/include/linux/fs.h
index f9d1e05e8ae6..deb411e86661 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -1978,6 +1978,8 @@ struct file_operations {
#define FOP_ASYNC_LOCK ((__force fop_flags_t)(1 << 6))
/* File system supports uncached read/write buffered IO */
#define FOP_DONTCACHE ((__force fop_flags_t)(1 << 7))
+/* Never changes and is never removed, readdir of a directory takes no lock */
+#define FOP_IMMUTABLE ((__force fop_flags_t)(1 << 8))
/* Wrap a directory iterator that needs exclusive inode access */
int wrap_directory_iterator(struct file *, struct dir_context *,
@@ -2296,6 +2298,7 @@ struct file_system_type {
#define FS_POWER_FREEZE 256 /* Always freeze on suspend/hibernate */
#define FS_USERNS_MOUNT_RESTRICTED 512 /* Restrict mount in userns if not already visible */
#define FS_USERNS_DELEGATABLE 1024 /* Can be mounted inside userns from outside */
+#define FS_DISALLOW_NOTIFY 2048 /* No fsnotify marks on its objects */
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_spec *parameters;
diff --git a/include/linux/mount.h b/include/linux/mount.h
index acfe7ef86a1b..b16ba5d45605 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -52,7 +52,7 @@ enum mount_flags {
MNT_ATIME_MASK = MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME,
MNT_INTERNAL_FLAGS = MNT_INTERNAL | MNT_DOOMED |
- MNT_SYNC_UMOUNT | MNT_LOCKED
+ MNT_SYNC_UMOUNT | MNT_LOCKED | MNT_UMOUNT
};
struct vfsmount {
diff --git a/include/linux/nsproxy.h b/include/linux/nsproxy.h
index 5a67648721c7..dc2447f5f092 100644
--- a/include/linux/nsproxy.h
+++ b/include/linux/nsproxy.h
@@ -100,6 +100,7 @@ void exit_cred_namespaces(struct task_struct *tsk);
void switch_task_namespaces(struct task_struct *tsk, struct nsproxy *new);
int exec_task_namespaces(void);
void deactivate_nsproxy(struct nsproxy *ns);
+void nsproxy_free(struct nsproxy *ns);
int unshare_nsproxy_namespaces(unsigned long, struct nsproxy **,
struct cred *, struct fs_struct *);
int __init nsproxy_cache_init(void);
diff --git a/kernel/fork.c b/kernel/fork.c
index 10f2d05d816a..eb198c8dfdc7 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -3317,14 +3317,17 @@ int ksys_unshare(unsigned long unshare_flags)
shm_init_task(current);
}
+ if (new_fs) {
+ new_fs = switch_fs_struct(new_fs);
+ if (new_fs)
+ free_fs_struct(no_free_ptr(new_fs));
+ }
+
if (new_nsproxy) {
switch_task_namespaces(current, new_nsproxy);
new_nsproxy = NULL;
}
- if (new_fs)
- new_fs = switch_fs_struct(new_fs);
-
if (new_fd) {
guard(task_lock)(current);
swap(current->files, new_fd);
@@ -3340,8 +3343,9 @@ int ksys_unshare(unsigned long unshare_flags)
perf_event_namespaces(current);
bad_unshare_cleanup_nsproxy:
+ /* never installed, so no active references to drop */
if (new_nsproxy)
- put_nsproxy(new_nsproxy);
+ nsproxy_free(new_nsproxy);
bad_unshare_cleanup_cred:
if (new_cred)
put_cred(new_cred);
diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c
index d9d3d5973bf5..3fb1595a4a59 100644
--- a/kernel/nsproxy.c
+++ b/kernel/nsproxy.c
@@ -61,7 +61,7 @@ static inline struct nsproxy *create_nsproxy(void)
return nsproxy;
}
-static inline void nsproxy_free(struct nsproxy *ns)
+void nsproxy_free(struct nsproxy *ns)
{
put_mnt_ns(ns->mnt_ns);
put_uts_ns(ns->uts_ns);
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 273853937c25..bc4fdb6f5337 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -43,6 +43,7 @@ TARGETS += filesystems/open_tree_ns
TARGETS += filesystems/overlayfs
TARGETS += filesystems/statmount
TARGETS += filesystems/mount-notify
+TARGETS += filesystems/mount_cycle
TARGETS += filesystems/nsfs
TARGETS += filesystems/fuse
TARGETS += filesystems/move_mount
@@ -50,6 +51,8 @@ TARGETS += filesystems/empty_mntns
TARGETS += filesystems/fsmount_ns
TARGETS += filesystems/fscontext_ns
TARGETS += filesystems/xattr
+TARGETS += filesystems/mntns_unbindable
+TARGETS += filesystems/umount_propagation
TARGETS += firmware
TARGETS += fpu
TARGETS += ftrace
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore
index 9eb185fb2f9d..62f7b1c46649 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -7,3 +7,4 @@ anon_inode_test
kernfs_test
idmapped_tmpfile
ustat_test
+rw_hint_test
diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile
index 03be337c1f35..93e2cc9123b0 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
-TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test
+TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test rw_hint_test
TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
diff --git a/tools/testing/selftests/filesystems/empty_mntns/.gitignore b/tools/testing/selftests/filesystems/empty_mntns/.gitignore
index 99f89d329db2..27bcbcaeb1d1 100644
--- a/tools/testing/selftests/filesystems/empty_mntns/.gitignore
+++ b/tools/testing/selftests/filesystems/empty_mntns/.gitignore
@@ -2,3 +2,6 @@
clone3_empty_mntns_test
empty_mntns_test
overmount_chroot_test
+internal_sb_reconfigure_test
+nullfs_atime_test
+root_readdir_test
diff --git a/tools/testing/selftests/filesystems/empty_mntns/Makefile b/tools/testing/selftests/filesystems/empty_mntns/Makefile
index 22e3fb915e81..22af2164c54e 100644
--- a/tools/testing/selftests/filesystems/empty_mntns/Makefile
+++ b/tools/testing/selftests/filesystems/empty_mntns/Makefile
@@ -4,9 +4,14 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := empty_mntns_test overmount_chroot_test clone3_empty_mntns_test
+TEST_GEN_PROGS += internal_sb_reconfigure_test nullfs_atime_test root_readdir_test
+
+LOCAL_HDRS += ../readdir_hold.h
include ../../lib.mk
$(OUTPUT)/empty_mntns_test: ../utils.c
$(OUTPUT)/overmount_chroot_test: ../utils.c
$(OUTPUT)/clone3_empty_mntns_test: ../utils.c
+$(OUTPUT)/internal_sb_reconfigure_test: ../utils.c
+$(OUTPUT)/root_readdir_test: LDLIBS += -pthread
diff --git a/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c
new file mode 100644
index 000000000000..cb645d1e5a9a
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * The root of an empty mount namespace is a nullfs mount. Its superblock is
+ * kernel-internal and shared by every mount namespace. It can't be
+ * reconfigured, neither through fspick() nor through mount(MS_REMOUNT) nor
+ * through umount() of the root which remounts it read-only.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/statfs.h>
+#include <sys/statvfs.h>
+#include <sys/syscall.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../utils.h"
+#include "../wrappers.h"
+#include "empty_mntns.h"
+#include "kselftest_harness.h"
+
+#ifndef __NR_fspick
+#define __NR_fspick 433
+#endif
+
+static int sys_fspick(int dfd, const char *path, unsigned int flags)
+{
+ return syscall(__NR_fspick, dfd, path, flags);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_USERNS, /* could not create the user namespace */
+ CHILD_UNSHARE, /* could not create the empty mount namespace */
+ CHILD_FSPICK, /* fspick() of the root was not refused with EINVAL */
+ CHILD_REMOUNT, /* mount(MS_REMOUNT) was not refused with EINVAL */
+ CHILD_UMOUNT, /* umount() of the root succeeded */
+ CHILD_STATFS, /* statfs() of the root failed */
+ CHILD_RDONLY, /* the root ended up read-only */
+};
+
+static int empty_mntns_child(void)
+{
+ struct statfs st;
+
+ if (enter_userns())
+ return CHILD_USERNS;
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ return CHILD_UNSHARE;
+
+ if (sys_fspick(AT_FDCWD, "/", 0) >= 0 || errno != EINVAL)
+ return CHILD_FSPICK;
+ if (!mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL) ||
+ errno != EINVAL)
+ return CHILD_REMOUNT;
+ if (!umount2("/", 0))
+ return CHILD_UMOUNT;
+ if (statfs("/", &st))
+ return CHILD_STATFS;
+ if (st.f_flags & ST_RDONLY)
+ return CHILD_RDONLY;
+ return CHILD_OK;
+}
+
+FIXTURE(internal_sb_reconfigure) {};
+
+FIXTURE_SETUP(internal_sb_reconfigure)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ if (enter_userns())
+ _exit(1);
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ _exit(1);
+ _exit(0);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ if (!WIFEXITED(status) || WEXITSTATUS(status))
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+}
+
+FIXTURE_TEARDOWN(internal_sb_reconfigure) {}
+
+TEST_F(internal_sb_reconfigure, nullfs_root)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(empty_mntns_child());
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c
new file mode 100644
index 000000000000..51e34f3c4f54
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * The root of every empty mount namespace is the same nullfs inode. A read
+ * of it by one user must not change the access time another user sees.
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef UNSHARE_EMPTY_MNTNS
+#define UNSHARE_EMPTY_MNTNS 0x00100000
+#endif
+
+enum {
+ CHILD_OK,
+ CHILD_UNSUPPORTED,
+ CHILD_SETUP,
+ CHILD_CHANGED,
+};
+
+static int wait_byte(int fd)
+{
+ char c;
+
+ return read(fd, &c, 1) == 1 ? 0 : -1;
+}
+
+static int send_byte(int fd)
+{
+ return write(fd, "x", 1) == 1 ? 0 : -1;
+}
+
+static int empty_mntns(void)
+{
+ if (!unshare(UNSHARE_EMPTY_MNTNS))
+ return 0;
+ return errno == EINVAL ? CHILD_UNSUPPORTED : CHILD_SETUP;
+}
+
+/* the watcher: stats its root before and after the reader read its own */
+static int watcher(int to_reader, int from_reader)
+{
+ struct stat before, after;
+ int ret;
+
+ ret = empty_mntns();
+ if (ret)
+ return ret;
+ if (stat("/", &before))
+ return CHILD_SETUP;
+ if (send_byte(to_reader) || wait_byte(from_reader))
+ return CHILD_SETUP;
+ if (stat("/", &after))
+ return CHILD_SETUP;
+ if (before.st_atim.tv_sec != after.st_atim.tv_sec ||
+ before.st_atim.tv_nsec != after.st_atim.tv_nsec)
+ return CHILD_CHANGED;
+ return CHILD_OK;
+}
+
+/* the reader: lists its own root, which is the same inode */
+static int reader(int to_watcher, int from_watcher)
+{
+ struct dirent *de;
+ DIR *d;
+ int ret;
+
+ ret = empty_mntns();
+ if (ret)
+ return ret;
+ if (wait_byte(from_watcher))
+ return CHILD_SETUP;
+ d = opendir("/");
+ if (!d)
+ return CHILD_SETUP;
+ while ((de = readdir(d)))
+ ;
+ closedir(d);
+ return send_byte(to_watcher) ? CHILD_SETUP : CHILD_OK;
+}
+
+static int wait_child(pid_t pid)
+{
+ int status;
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+TEST(empty_mntns_root_atime)
+{
+ int to_reader[2], to_watcher[2], w, r;
+ pid_t watcher_pid, reader_pid;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+ ASSERT_EQ(pipe(to_reader), 0);
+ ASSERT_EQ(pipe(to_watcher), 0);
+
+ watcher_pid = fork();
+ ASSERT_GE(watcher_pid, 0);
+ if (watcher_pid == 0)
+ _exit(watcher(to_reader[1], to_watcher[0]));
+ reader_pid = fork();
+ ASSERT_GE(reader_pid, 0);
+ if (reader_pid == 0)
+ _exit(reader(to_watcher[1], to_reader[0]));
+
+ r = wait_child(reader_pid);
+ w = wait_child(watcher_pid);
+ if (r == CHILD_UNSUPPORTED || w == CHILD_UNSUPPORTED)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ EXPECT_EQ(r, CHILD_OK);
+ EXPECT_EQ(w, CHILD_OK)
+ TH_LOG("the access time of the root changed while this namespace did nothing");
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c
new file mode 100644
index 000000000000..0ff544247027
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Reading the root of an empty mount namespace holds nothing that anybody
+ * else waits for: the directory never has an entry, so a readdir stuck in
+ * the page fault of its buffer stalls neither a create nor a lookup in it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+#include "../readdir_hold.h"
+#include "empty_mntns.h"
+
+TEST(readdir_blocks_nobody)
+{
+ struct readdir_hold hold;
+ bool stalled;
+ int dfd;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+ if (readdir_hold_init(&hold))
+ SKIP(return, "test requires userfaultfd");
+ if (unshare(UNSHARE_EMPTY_MNTNS)) {
+ readdir_hold_destroy(&hold);
+ if (errno == EINVAL)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ ASSERT_TRUE(false)
+ TH_LOG("unshare(UNSHARE_EMPTY_MNTNS): %m");
+ }
+ dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(dfd, 0);
+ ASSERT_EQ(readdir_hold_check(&hold, dfd, &stalled), 0);
+ /* the lookup came back while the readdir was stuck in its fault */
+ EXPECT_FALSE(stalled);
+ close(dfd);
+ readdir_hold_destroy(&hold);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mntns_unbindable/Makefile b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile
new file mode 100644
index 000000000000..33a311c5bd72
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := mntns_unbindable_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c
new file mode 100644
index 000000000000..9aebc37cf74d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An unbindable mount stays unbindable in a cloned mount namespace.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+
+static int sys_open_tree(int dfd, const char *filename, unsigned int flags)
+{
+ return syscall(__NR_open_tree, dfd, filename, flags);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK, /* the operation failed with EINVAL as it must */
+ CHILD_ALLOWED, /* the operation succeeded: the flag was lost */
+ CHILD_UNSHARE, /* unshare(CLONE_NEWNS) failed */
+ CHILD_ERRNO, /* the operation failed with some other errno */
+ CHILD_MOUNTINFO, /* the mount was not found in mountinfo */
+};
+
+FIXTURE(mntns_unbindable)
+{
+ char base[64];
+ char src[80];
+ char dst[80];
+ bool mounted;
+};
+
+FIXTURE_SETUP(mntns_unbindable)
+{
+ self->mounted = false;
+
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/mntns_unbindable.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+ self->mounted = true;
+
+ snprintf(self->src, sizeof(self->src), "%s/src", self->base);
+ snprintf(self->dst, sizeof(self->dst), "%s/dst", self->base);
+ ASSERT_EQ(mkdir(self->src, 0755), 0);
+ ASSERT_EQ(mkdir(self->dst, 0755), 0);
+
+ ASSERT_EQ(mount("tmpfs", self->src, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, self->src, NULL, MS_UNBINDABLE, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(mntns_unbindable)
+{
+ if (self->mounted)
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static int classify(int ret, int err)
+{
+ if (ret >= 0)
+ return CHILD_ALLOWED;
+ return err == EINVAL ? CHILD_OK : CHILD_ERRNO;
+}
+
+/* Is the mount on @mountpoint marked unbindable in /proc/self/mountinfo? */
+static int mountinfo_unbindable(const char *mountpoint)
+{
+ char line[4096];
+ FILE *f;
+ int ret = CHILD_MOUNTINFO;
+
+ f = fopen("/proc/self/mountinfo", "re");
+ if (!f)
+ return CHILD_ERRNO;
+
+ while (fgets(line, sizeof(line), f)) {
+ char *fields[6], *p = line, *opt;
+ int i;
+
+ for (i = 0; i < 6; i++) {
+ fields[i] = strsep(&p, " ");
+ if (!fields[i])
+ break;
+ }
+ if (i < 6 || strcmp(fields[4], mountpoint))
+ continue;
+
+ /* the optional fields, up to the "-" separator */
+ ret = CHILD_ALLOWED;
+ while ((opt = strsep(&p, " ")) && strcmp(opt, "-")) {
+ if (!strcmp(opt, "unbindable"))
+ ret = CHILD_OK;
+ }
+ break;
+ }
+ fclose(f);
+ return ret;
+}
+
+static int run_in_child(int (*fn)(const char *src, const char *dst),
+ const char *src, const char *dst)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (pid == 0)
+ _exit(fn(src, dst));
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+static int bind_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND, NULL);
+ return classify(ret, errno);
+}
+
+static int rbind_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND | MS_REC, NULL);
+ return classify(ret, errno);
+}
+
+static int open_tree_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = sys_open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ return classify(ret, errno);
+}
+
+static int mountinfo_after_clone(const char *src, const char *dst)
+{
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ return mountinfo_unbindable(src);
+}
+
+static int bind_after_two_clones(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND, NULL);
+ return classify(ret, errno);
+}
+
+/* The namespace the mount was made unbindable in. */
+TEST_F(mntns_unbindable, refuses_bind)
+{
+ int ret = mount(self->src, self->dst, NULL, MS_BIND, NULL);
+
+ ASSERT_EQ(classify(ret, errno), CHILD_OK);
+ ASSERT_EQ(mountinfo_unbindable(self->src), CHILD_OK);
+}
+
+/* A copy of the namespace must not turn the mount bindable. */
+TEST_F(mntns_unbindable, refuses_bind_after_clone)
+{
+ ASSERT_EQ(run_in_child(bind_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("bind of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_rbind_after_clone)
+{
+ ASSERT_EQ(run_in_child(rbind_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("rbind of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_open_tree_after_clone)
+{
+ ASSERT_EQ(run_in_child(open_tree_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("OPEN_TREE_CLONE of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, mountinfo_after_clone)
+{
+ ASSERT_EQ(run_in_child(mountinfo_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("mountinfo does not show the mount as unbindable in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_bind_after_two_clones)
+{
+ ASSERT_EQ(run_in_child(bind_after_two_clones, self->src, self->dst), CHILD_OK)
+ TH_LOG("bind of an unbindable mount allowed two mount namespace copies down");
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/.gitignore b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
new file mode 100644
index 000000000000..30f7dd071bcb
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+loop_cycle_test
+unmounted_tree_test
+overmount_reparent_test
+mount_cover_test
+locked_handle_test
+nsfs_rbind_loop_test
+overmount_ns_file_test
diff --git a/tools/testing/selftests/filesystems/mount_cycle/Makefile b/tools/testing/selftests/filesystems/mount_cycle/Makefile
new file mode 100644
index 000000000000..fbe8c5e19d42
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/Makefile
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := loop_cycle_test unmounted_tree_test overmount_reparent_test mount_cover_test locked_handle_test
+TEST_GEN_PROGS += nsfs_rbind_loop_test overmount_ns_file_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+LOCAL_HDRS += ../readdir_hold.h
+
+include ../../lib.mk
+
+$(OUTPUT)/locked_handle_test: LDLIBS += -pthread
+$(OUTPUT)/mount_cover_test: LDLIBS += -pthread
diff --git a/tools/testing/selftests/filesystems/mount_cycle/config b/tools/testing/selftests/filesystems/mount_cycle/config
new file mode 100644
index 000000000000..3bd5ce46af74
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/config
@@ -0,0 +1,39 @@
+CONFIG_USER_NS=y
+CONFIG_TMPFS=y
+CONFIG_BLK_DEV_LOOP=y
+CONFIG_VFAT_FS=y
+CONFIG_MSDOS_FS=y
+CONFIG_NLS_CODEPAGE_437=y
+CONFIG_NLS_ISO8859_1=y
+CONFIG_MINIX_FS=y
+CONFIG_AUTOFS_FS=y
+CONFIG_ZRAM=y
+CONFIG_ZRAM_WRITEBACK=y
+CONFIG_KEYS=y
+CONFIG_ECRYPT_FS=y
+CONFIG_BINFMT_MISC=y
+CONFIG_FUSE_FS=y
+CONFIG_FUSE_PASSTHROUGH=y
+CONFIG_BLK_DEV_ZONED=y
+CONFIG_BLK_DEV_ZONED_LOOP=y
+CONFIG_CONFIGFS_FS=y
+CONFIG_USB_SUPPORT=y
+CONFIG_USB=y
+CONFIG_USB_STORAGE=y
+CONFIG_SCSI=y
+CONFIG_BLK_DEV_SD=y
+CONFIG_USB_GADGET=y
+CONFIG_USB_DUMMY_HCD=y
+CONFIG_USB_CONFIGFS=y
+CONFIG_USB_CONFIGFS_MASS_STORAGE=y
+CONFIG_MD=y
+CONFIG_BLK_DEV_MD=y
+CONFIG_MD_RAID1=y
+CONFIG_MD_BITMAP=y
+CONFIG_MD_BITMAP_FILE=y
+CONFIG_INOTIFY_USER=y
+CONFIG_DNOTIFY=y
+CONFIG_FANOTIFY=y
+CONFIG_FILE_LOCKING=y
+CONFIG_CRYPTO_AES=y
+CONFIG_USERFAULTFD=y
diff --git a/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c
new file mode 100644
index 000000000000..b21f43471e49
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * may_decode_fh() refuses a file handle below a mount with locked children
+ * to root in a user namespace. That has to hold while the mount is lazily
+ * unmounted and its children, the locked ones too, are taken off it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE 0x8000
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+#define ROUNDS 100 /* lazy umounts raced per test */
+#define RACERS 3 /* threads in open_by_handle_at() */
+#define EXTRA_MOUNTS 64 /* make umount_tree() hold mount_lock longer */
+#define MAX_DELAY_US 3000 /* before the umount */
+#define TAIL_US 2000 /* after it */
+
+struct handle {
+ struct file_handle fh;
+ unsigned char buf[MAX_HANDLE_SZ];
+};
+
+/* exit codes of the child */
+enum {
+ CHILD_OK,
+ CHILD_SETUP,
+ CHILD_DECODED, /* decoded past a locked child */
+ CHILD_MOUNTED, /* not refused while mounted */
+ CHILD_UNMOUNTED, /* not refused once unmounted */
+ CHILD_ALLOWED, /* refused where nothing is locked */
+ CHILD_NOUSERNS, /* no user namespace to be had */
+};
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ prctl(PR_SET_DUMPABLE, 1);
+ /* EINVAL: no USER_NS, ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM */
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return errno == EINVAL || errno == ENOSPC || errno == EPERM ?
+ CHILD_NOUSERNS : CHILD_SETUP;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return CHILD_SETUP;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return CHILD_SETUP;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return CHILD_SETUP;
+ if (setgid(0) || setuid(0))
+ return CHILD_SETUP;
+ if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return CHILD_SETUP;
+ return CHILD_OK;
+}
+
+static int get_handle(const char *path, struct handle *h)
+{
+ int mntid;
+
+ h->fh.handle_bytes = MAX_HANDLE_SZ;
+ return name_to_handle_at(AT_FDCWD, path, &h->fh, &mntid, 0);
+}
+
+static int decode(int dfd, struct handle *h)
+{
+ return open_by_handle_at(dfd, &h->fh, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+}
+
+struct race {
+ int dfd;
+ struct handle *h;
+ atomic_int go;
+ atomic_int stop;
+ atomic_int won; /* the first decoded descriptor */
+};
+
+static void *racer(void *arg)
+{
+ struct race *r = arg;
+
+ while (!atomic_load(&r->go))
+ ;
+ while (!atomic_load(&r->stop)) {
+ int none = -1;
+ int fd;
+
+ fd = decode(r->dfd, r->h);
+ if (fd < 0)
+ continue;
+ if (!atomic_compare_exchange_strong(&r->won, &none, fd))
+ close(fd);
+ }
+ return NULL;
+}
+
+/* stop the @n racers started so far and wait for them, they spin on @r */
+static void stop_racers(struct race *r, pthread_t *th, int n)
+{
+ int i;
+
+ atomic_store(&r->stop, 1);
+ atomic_store(&r->go, 1); /* one still waiting for the start sees stop next */
+ for (i = 0; i < n; i++)
+ pthread_join(th[i], NULL);
+}
+
+/*
+ * Bind @h recursively at @w, or take a detached copy of it, and race
+ * open_by_handle_at() of @inner against the lazy umount. The decoded
+ * descriptor, if there was one, is left in @won.
+ */
+static int race_round(const char *h, const char *w, struct handle *inner,
+ bool dissolve, int *won)
+{
+ struct race r = { .h = inner, .won = -1 };
+ pthread_t th[RACERS];
+ int treefd = -1, fd, i;
+
+ if (dissolve) {
+ treefd = syscall(__NR_open_tree, AT_FDCWD, h,
+ OPEN_TREE_CLONE | AT_RECURSIVE | OPEN_TREE_CLOEXEC);
+ if (treefd < 0)
+ return CHILD_SETUP;
+ /* an ordinary descriptor on the copy, treefd's close dissolves it */
+ r.dfd = openat(treefd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ } else {
+ if (mount(h, w, NULL, MS_BIND | MS_REC, NULL))
+ return CHILD_SETUP;
+ r.dfd = open(w, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ }
+ if (r.dfd < 0)
+ return CHILD_SETUP;
+
+ fd = decode(r.dfd, inner);
+ if (fd >= 0 || errno != EPERM) {
+ if (fd >= 0)
+ close(fd);
+ return CHILD_MOUNTED;
+ }
+
+ for (i = 0; i < RACERS; i++) {
+ if (pthread_create(&th[i], NULL, racer, &r)) {
+ stop_racers(&r, th, i);
+ return CHILD_SETUP;
+ }
+ }
+ atomic_store(&r.go, 1);
+ usleep(rand() % MAX_DELAY_US);
+ if (dissolve)
+ close(treefd);
+ else if (umount2(w, MNT_DETACH)) {
+ stop_racers(&r, th, RACERS);
+ return CHILD_SETUP;
+ }
+ usleep(TAIL_US);
+ stop_racers(&r, th, RACERS);
+
+ fd = decode(r.dfd, inner);
+ if (fd >= 0) {
+ close(fd);
+ return CHILD_UNMOUNTED;
+ }
+ close(r.dfd);
+ *won = atomic_load(&r.won);
+ return CHILD_OK;
+}
+
+static int race_child(const char *h, const char *w, struct handle *inner,
+ bool dissolve)
+{
+ int ret, won, i;
+
+ srand(getpid());
+ ret = enter_userns();
+ if (ret)
+ return ret;
+ for (i = 0; i < ROUNDS; i++) {
+ ret = race_round(h, w, inner, dissolve, &won);
+ if (ret)
+ return ret;
+ if (won >= 0)
+ return CHILD_DECODED;
+ }
+ return CHILD_OK;
+}
+
+/* a bind without locked children decodes while mounted and not after */
+static int allowed_child(const char *plain, const char *w, struct handle *h)
+{
+ int dfd, fd, ret;
+
+ ret = enter_userns();
+ if (ret)
+ return ret;
+ if (mount(plain, w, NULL, MS_BIND | MS_REC, NULL))
+ return CHILD_SETUP;
+ dfd = open(w, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dfd < 0)
+ return CHILD_SETUP;
+ fd = decode(dfd, h);
+ if (fd < 0)
+ return CHILD_ALLOWED;
+ close(fd);
+ if (umount2(w, MNT_DETACH))
+ return CHILD_SETUP;
+ fd = decode(dfd, h);
+ if (fd >= 0) {
+ close(fd);
+ return CHILD_UNMOUNTED;
+ }
+ return errno == EPERM ? CHILD_OK : CHILD_UNMOUNTED;
+}
+
+FIXTURE(locked_handle) {
+ char base[DIR_LEN];
+ char h[PATH_LEN]; /* the tree with the covered directory */
+ char plain[PATH_LEN]; /* a tree with no mount in it */
+ char w[PATH_LEN]; /* where the child binds either */
+ struct handle inner; /* h/top/secret/inner, covered */
+ struct handle uncovered; /* plain/inner */
+};
+
+FIXTURE_SETUP(locked_handle)
+{
+ char p[PATH_LEN], e[PATH_LEN];
+ int i;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ snprintf(self->base, sizeof(self->base), "/tmp/locked_handle.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+
+ snprintf(self->h, sizeof(self->h), "%s/h", self->base);
+ snprintf(self->plain, sizeof(self->plain), "%s/plain", self->base);
+ snprintf(self->w, sizeof(self->w), "%s/w", self->base);
+ ASSERT_EQ(mkdir(self->h, 0755), 0);
+ ASSERT_EQ(mkdir(self->plain, 0755), 0);
+ ASSERT_EQ(mkdir(self->w, 0755), 0);
+ snprintf(p, sizeof(p), "%s/plain/inner", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(get_handle(p, &self->uncovered), 0);
+
+ /* the handle is for this filesystem */
+ ASSERT_EQ(mount("tmpfs", self->h, "tmpfs", 0, NULL), 0);
+ snprintf(p, sizeof(p), "%s/h/top", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ snprintf(p, sizeof(p), "%s/h/top/secret", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ snprintf(p, sizeof(p), "%s/h/top/secret/inner", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(get_handle(p, &self->inner), 0);
+ snprintf(e, sizeof(e), "%s/h/empty", self->base);
+ ASSERT_EQ(mkdir(e, 0755), 0);
+
+ /* cover it, and some more so the umount takes longer */
+ snprintf(p, sizeof(p), "%s/h/top/secret", self->base);
+ ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, NULL), 0);
+ for (i = 0; i < EXTRA_MOUNTS; i++) {
+ snprintf(p, sizeof(p), "%s/h/c%d", self->base, i);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(mount(e, p, NULL, MS_BIND, NULL), 0);
+ }
+}
+
+FIXTURE_TEARDOWN(locked_handle)
+{
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static int wait_child(pid_t pid)
+{
+ int status;
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+TEST_F(locked_handle, lazy_umount)
+{
+ pid_t pid;
+ int ret;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(race_child(self->h, self->w, &self->inner, false));
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ if (ret == CHILD_NOUSERNS)
+ SKIP(return, "no user namespaces");
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+TEST_F(locked_handle, dissolve)
+{
+ pid_t pid;
+ int ret;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(race_child(self->h, self->w, &self->inner, true));
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ if (ret == CHILD_NOUSERNS)
+ SKIP(return, "no user namespaces");
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+TEST_F(locked_handle, allowed_use)
+{
+ pid_t pid;
+ int ret;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(allowed_child(self->plain, self->w, &self->uncovered));
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ if (ret == CHILD_NOUSERNS)
+ SKIP(return, "no user namespaces");
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c b/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c
new file mode 100644
index 000000000000..6b4f5304c2e4
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c
@@ -0,0 +1,1542 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A mount that another namespace's rmdir detached, or that went down with
+ * the detached tree it was in when the tree's last fd was closed, keeps
+ * its submounts connected, and a connected submount is put by its
+ * parent's final mntput(). A submount whose filesystem keeps a file open
+ * on the parent then holds the parent's count above zero for good:
+ * nothing in userspace refers to either mount any more and nothing can
+ * release them. A loop device is the simplest such filesystem, its
+ * backing file sits on the parent.
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <stdbool.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/fuse.h>
+#include <linux/keyctl.h>
+#include <linux/major.h>
+#include <linux/raid/md_u.h>
+#include <linux/raid/md_p.h>
+#include <linux/loop.h>
+#include <linux/magic.h>
+#include <sys/syscall.h>
+#include <sys/sysmacros.h>
+#include <sys/uio.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#define IMAGE_SIZE (1440 * 1024)
+#define SECTOR 512
+
+/*
+ * The tmpfs of the fixture. A directory of its own from mkdtemp(), so that
+ * the test needs no writable root directory and two instances don't take
+ * each other's mounts down.
+ */
+static char base[64];
+
+/* The configfs directory of the gadget, named after the test process. */
+static char gadget[96];
+
+/* A path below the base directory. Eight of them can be in use at a time. */
+static const char *at(const char *rel)
+{
+ static char buf[8][PATH_MAX];
+ static unsigned int next;
+ char *p = buf[next++ % 8];
+
+ snprintf(p, PATH_MAX, "%s/%s", base, rel);
+ return p;
+}
+
+/* A path below the gadget's configfs directory. */
+static const char *gat(const char *rel)
+{
+ static char buf[4][PATH_MAX];
+ static unsigned int next;
+ char *p = buf[next++ % 4];
+
+ snprintf(p, PATH_MAX, "%s%s", gadget, rel);
+ return p;
+}
+
+/* The loop devices this process has bound, to release them on every path. */
+#define MAX_LOOPS 4
+static int bound[MAX_LOOPS];
+static int nr_bound;
+
+/* What a child tells its parent: the loop devices it has bound, -1 for none. */
+struct report {
+ int n[2];
+};
+
+/* A blank FAT12 floppy image: boot sector, two FATs, an empty root directory. */
+static int write_fat12(int fd)
+{
+ struct stat st;
+ unsigned char sector[SECTOR] = {
+ 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1',
+ [11] = 0x00, 0x02, /* bytes per sector: 512 */
+ [13] = 1, /* sectors per cluster */
+ [14] = 1, 0, /* reserved sectors */
+ [16] = 2, /* FATs */
+ [17] = 0xe0, 0x00, /* root directory entries: 224 */
+ [19] = 0x40, 0x0b, /* total sectors: 2880 */
+ [21] = 0xf0, /* media descriptor */
+ [22] = 9, 0, /* sectors per FAT */
+ [24] = 18, 0, /* sectors per track */
+ [26] = 2, 0, /* heads */
+ [38] = 0x29, /* extended boot signature */
+ [39] = 0x12, 0x34, 0x56, 0x78,
+ [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ',
+ [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ',
+ [510] = 0x55, 0xaa,
+ };
+ unsigned char fat[SECTOR] = { 0xf0, 0xff, 0xff };
+
+ if (pwrite(fd, sector, SECTOR, 0) != SECTOR)
+ return -1;
+ /* the first FAT and the second one, one sector each is enough */
+ if (pwrite(fd, fat, SECTOR, 1 * SECTOR) != SECTOR ||
+ pwrite(fd, fat, SECTOR, 10 * SECTOR) != SECTOR)
+ return -1;
+ if (fstat(fd, &st) || S_ISBLK(st.st_mode))
+ return 0;
+ return ftruncate(fd, IMAGE_SIZE);
+}
+
+/*
+ * A blank FAT12 image with 4 KiB sectors and @sectors of them, for a device
+ * with 4 KiB logical blocks (zram) or for a bigger image than a floppy.
+ */
+static int write_fat12_4k(int fd, unsigned int sectors)
+{
+ unsigned int fat_sectors = (sectors * 3 / 2 + 4095) / 4096;
+ unsigned char sector[4096] = {
+ 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1',
+ [11] = 0x00, 0x10, /* bytes per sector: 4096 */
+ [13] = 1, /* sectors per cluster */
+ [14] = 1, 0, /* reserved sectors */
+ [16] = 2, /* FATs */
+ [17] = 128, 0, /* root directory entries: one sector */
+ [19] = sectors & 0xff, sectors >> 8,
+ [21] = 0xf8, /* media descriptor */
+ [22] = fat_sectors, 0,
+ [24] = 63, 0, /* sectors per track */
+ [26] = 255, 0, /* heads */
+ [38] = 0x29, /* extended boot signature */
+ [39] = 0x12, 0x34, 0x56, 0x78,
+ [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ',
+ [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ',
+ [510] = 0x55, 0xaa,
+ };
+ unsigned char fat[4096] = { 0xf8, 0xff, 0xff };
+ struct stat st;
+
+ if (pwrite(fd, sector, sizeof(sector), 0) != sizeof(sector))
+ return -1;
+ if (pwrite(fd, fat, sizeof(fat), 1 * 4096) != sizeof(fat) ||
+ pwrite(fd, fat, sizeof(fat), (1 + fat_sectors) * 4096) != sizeof(fat))
+ return -1;
+ if (fstat(fd, &st) || S_ISBLK(st.st_mode))
+ return 0;
+ return ftruncate(fd, (off_t)sectors * 4096);
+}
+
+#define MINIX_BLOCK 1024
+#define MINIX_BLOCKS 4096 /* a 4 MiB image */
+#define MINIX_INODES 512
+#define MINIX_ITABLE (MINIX_INODES * 32 / MINIX_BLOCK)
+#define MINIX_FIRSTDATA (2 + 1 + 1 + MINIX_ITABLE) /* boot, super, imap, zmap, inodes */
+
+/*
+ * A blank minix v1 image, for the holders that need a FIFO or a device
+ * node on the dying mount, which vfat can't hold. Superblock in block 1,
+ * one block each for the inode and zone bitmaps, the inode table, and
+ * the root directory in the first data zone.
+ */
+static int write_minix(int fd)
+{
+ struct {
+ __u16 s_ninodes, s_nzones, s_imap_blocks, s_zmap_blocks;
+ __u16 s_firstdatazone, s_log_zone_size;
+ __u32 s_max_size;
+ __u16 s_magic, s_state;
+ } sb = {
+ .s_ninodes = MINIX_INODES,
+ .s_nzones = MINIX_BLOCKS,
+ .s_imap_blocks = 1,
+ .s_zmap_blocks = 1,
+ .s_firstdatazone = MINIX_FIRSTDATA,
+ .s_max_size = (7 + 512 + 512 * 512) * MINIX_BLOCK,
+ .s_magic = MINIX_SUPER_MAGIC,
+ .s_state = 1, /* MINIX_VALID_FS */
+ };
+ struct {
+ __u16 i_mode, i_uid;
+ __u32 i_size, i_time;
+ __u8 i_gid, i_nlinks;
+ __u16 i_zone[9];
+ } root = {
+ .i_mode = S_IFDIR | 0755,
+ .i_size = 2 * 16,
+ .i_nlinks = 2,
+ .i_zone = { MINIX_FIRSTDATA },
+ };
+ unsigned char imap[MINIX_BLOCK], zmap[MINIX_BLOCK], dir[MINIX_BLOCK] = {};
+ int i;
+
+ /* bit 0 is reserved in both maps, the root inode and its zone are in use */
+ memset(imap, 0xff, sizeof(imap));
+ for (i = 2; i <= MINIX_INODES; i++)
+ imap[i / 8] &= ~(1 << (i % 8));
+ memset(zmap, 0xff, sizeof(zmap));
+ for (i = 2; i <= MINIX_BLOCKS - MINIX_FIRSTDATA; i++)
+ zmap[i / 8] &= ~(1 << (i % 8));
+ dir[0] = 1;
+ dir[2] = '.';
+ dir[16] = 1;
+ dir[18] = '.';
+ dir[19] = '.';
+
+ if (pwrite(fd, &sb, sizeof(sb), 1 * MINIX_BLOCK) != sizeof(sb) ||
+ pwrite(fd, imap, sizeof(imap), 2 * MINIX_BLOCK) != sizeof(imap) ||
+ pwrite(fd, zmap, sizeof(zmap), 3 * MINIX_BLOCK) != sizeof(zmap) ||
+ pwrite(fd, &root, sizeof(root), 4 * MINIX_BLOCK) != sizeof(root) ||
+ pwrite(fd, dir, sizeof(dir), MINIX_FIRSTDATA * MINIX_BLOCK) != sizeof(dir))
+ return -1;
+ return ftruncate(fd, (off_t)MINIX_BLOCKS * MINIX_BLOCK);
+}
+
+static int read_sysfs(const char *path, char *buf, size_t size)
+{
+ ssize_t n;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, size - 1);
+ close(fd);
+ if (n < 0)
+ return -1;
+ buf[n] = '\0';
+ return 0;
+}
+
+/* Is @dev the mount source of this line of mountinfo? loop1 is not loop10. */
+static bool line_has_source(const char *line, const char *dev)
+{
+ const char *sep, *src, *end;
+
+ sep = strstr(line, " - ");
+ if (!sep)
+ return false;
+ src = strchr(sep + 3, ' '); /* skip the filesystem type */
+ if (!src)
+ return false;
+ src++;
+ end = strchr(src, ' ');
+ if (!end)
+ return false;
+ return (size_t)(end - src) == strlen(dev) && !strncmp(src, dev, end - src);
+}
+
+/*
+ * Does a mount namespace of a process that /proc shows have a mount of @dev?
+ * The mounts under test are in no namespace at this point on any kernel, so
+ * this only checks that the test got as far as it thinks.
+ */
+static bool mounted_anywhere(const char *dev)
+{
+ char path[PATH_MAX], line[4096];
+ struct dirent *de;
+ bool found = false;
+ DIR *proc;
+ FILE *f;
+
+ proc = opendir("/proc");
+ if (!proc)
+ return false;
+ while (!found && (de = readdir(proc))) {
+ if (de->d_name[0] < '0' || de->d_name[0] > '9')
+ continue;
+ snprintf(path, sizeof(path), "/proc/%s/mountinfo", de->d_name);
+ f = fopen(path, "re");
+ if (!f)
+ continue;
+ while (fgets(line, sizeof(line), f)) {
+ if (line_has_source(line, dev)) {
+ found = true;
+ break;
+ }
+ }
+ fclose(f);
+ }
+ closedir(proc);
+ return found;
+}
+
+/* Wait up to @ms milliseconds for the loop device to give up its backing file. */
+static bool loop_released(const char *sysfs, int ms)
+{
+ char buf[PATH_MAX];
+
+ for (; ms > 0; ms -= 100) {
+ if (read_sysfs(sysfs, buf, sizeof(buf)) < 0)
+ return errno == ENOENT;
+ usleep(100000);
+ }
+ return read_sysfs(sysfs, buf, sizeof(buf)) < 0 && errno == ENOENT;
+}
+
+/* Tell the loop device to give its file up, now or when its last user is gone. */
+static void loop_clear(int n)
+{
+ char dev[32];
+ int lfd;
+
+ if (n < 0)
+ return;
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ lfd = open(dev, O_RDWR);
+ if (lfd < 0)
+ return;
+ ioctl(lfd, LOOP_CLR_FD);
+ close(lfd);
+}
+
+FIXTURE(loop_cycle) {
+ char dev[32]; /* the loop device the child set up */
+ char sysfs[64]; /* its backing_file attribute */
+ int loops[MAX_LOOPS]; /* every loop device the case has bound */
+ int nr_loops;
+ bool gadget; /* the gadget's configfs directory exists */
+};
+
+static void remember_loop(FIXTURE_DATA(loop_cycle) *self, int n)
+{
+ if (n >= 0 && self->nr_loops < MAX_LOOPS)
+ self->loops[self->nr_loops++] = n;
+}
+
+FIXTURE_SETUP(loop_cycle)
+{
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+ if (access("/dev/loop-control", R_OK | W_OK))
+ SKIP(return, "test requires loop devices");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(base, sizeof(base), "/tmp/loop_cycle.XXXXXX");
+ ASSERT_NE(mkdtemp(base), NULL);
+ /* a failed assertion in the setup does not run the teardown */
+ ASSERT_EQ(mount("tmpfs", base, "tmpfs", 0, NULL), 0)
+ rmdir(base);
+ snprintf(gadget, sizeof(gadget),
+ "/sys/kernel/config/usb_gadget/kselftest_cycle_%d", getpid());
+ self->dev[0] = '\0';
+ self->nr_loops = 0;
+ self->gadget = false;
+}
+
+static int mount_configfs(void);
+static void gadget_remove(void);
+
+FIXTURE_TEARDOWN(loop_cycle)
+{
+ if (self->gadget)
+ gadget_remove();
+ /* whatever a failed assertion has left bound */
+ for (int i = 0; i < self->nr_loops; i++)
+ loop_clear(self->loops[i]);
+ umount2(base, MNT_DETACH);
+ rmdir(base);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_NS, /* could not set up the namespace or the tmpfs */
+ CHILD_IMAGE, /* could not write the image */
+ CHILD_LOOP, /* could not set up the loop device */
+ CHILD_MOUNT, /* could not mount it (vfat and msdos both refused) */
+ CHILD_PIPE, /* the parent went away */
+ CHILD_HOLDER, /* could not set the holder up below the mount */
+ CHILD_SKIP, /* the kernel lacks what the holder needs */
+ CHILD_NOFS, /* the kernel lacks the filesystem of the image */
+};
+
+/* Bind the loop device that LOOP_CTL_GET_FREE names to @ifd; the device number. */
+static int loop_bind_free(int ifd)
+{
+ int cfd, lfd, n;
+ char dev[32];
+
+ cfd = open("/dev/loop-control", O_RDWR);
+ if (cfd < 0)
+ return -1;
+ n = ioctl(cfd, LOOP_CTL_GET_FREE);
+ close(cfd);
+ if (n < 0)
+ return -1;
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ lfd = open(dev, O_RDWR);
+ if (lfd < 0)
+ return -1;
+ if (ioctl(lfd, LOOP_SET_FD, ifd))
+ n = -1;
+ close(lfd);
+ return n;
+}
+
+/*
+ * Bind a free loop device to the open image @ifd; the device number. The
+ * device is free when LOOP_CTL_GET_FREE names it and may be somebody else's
+ * a moment later, so try again when it is busy.
+ */
+static int loop_bind(int ifd)
+{
+ int n = -1;
+
+ for (int i = 0; i < 64 && n < 0; i++) {
+ n = loop_bind_free(ifd);
+ if (n < 0 && errno != EBUSY)
+ return -1;
+ }
+ if (n >= 0 && nr_bound < MAX_LOOPS)
+ bound[nr_bound++] = n;
+ return n;
+}
+
+/* Mount a FAT image, as vfat or as msdos; -1 with ENODEV if the kernel has neither. */
+static int mount_fat(const char *dev, const char *mp)
+{
+ int err;
+
+ if (!mount(dev, mp, "vfat", 0, NULL))
+ return 0;
+ err = errno;
+ if (!mount(dev, mp, "msdos", 0, NULL))
+ return 0;
+ if (err != ENODEV)
+ errno = err;
+ return -1;
+}
+
+/*
+ * A child that gives up has to take down what it has set up: nobody else
+ * knows about it. Unmount, then tell the loop devices to let go. The mounts
+ * are released after this process is gone and the devices follow them.
+ */
+static int child_fails(int ret)
+{
+ umount2(at("vol"), MNT_DETACH);
+ umount2(at("vol2"), MNT_DETACH);
+ umount2(at("p"), MNT_DETACH);
+ umount2(at("img"), MNT_DETACH);
+ for (int i = 0; i < nr_bound; i++)
+ loop_clear(bound[i]);
+ return ret;
+}
+
+/* Write an image to @img, bind a loop device to it and mount that at @mp; the device number. */
+static int loop_mount(const char *img, const char *mp)
+{
+ char dev[32];
+ int ifd, n;
+
+ ifd = open(img, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (ifd < 0 || write_fat12(ifd))
+ return -CHILD_IMAGE;
+ n = loop_bind(ifd);
+ close(ifd); /* the loop device holds the file from now on */
+ if (n < 0)
+ return -CHILD_LOOP;
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ if (mkdir(mp, 0755))
+ return -CHILD_MOUNT;
+ if (mount_fat(dev, mp))
+ return errno == ENODEV ? -CHILD_NOFS : -CHILD_MOUNT;
+ return n;
+}
+
+/*
+ * Two tmpfs mounts, each carrying the image of the loop mount below the
+ * other: the loop mount below vol has its image on vol2 and the other way
+ * round.
+ */
+static int crossed_child(int to_parent, int from_parent)
+{
+ struct report r;
+ char c;
+
+ if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return CHILD_NS;
+ if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL) ||
+ mkdir(at("vol2"), 0755) || mount("tmpfs", at("vol2"), "tmpfs", 0, NULL))
+ return child_fails(CHILD_NS);
+ r.n[0] = loop_mount(at("vol2/img"), at("vol/mnt"));
+ if (r.n[0] < 0)
+ return child_fails(-r.n[0]);
+ r.n[1] = loop_mount(at("vol/img"), at("vol2/mnt"));
+ if (r.n[1] < 0)
+ return child_fails(-r.n[1]);
+ if (write(to_parent, &r, sizeof(r)) != sizeof(r))
+ return child_fails(CHILD_PIPE);
+ if (read(from_parent, &c, 1) != 1)
+ return child_fails(CHILD_PIPE);
+ return CHILD_OK;
+}
+
+/*
+ * In its own mount namespace the child mounts a tmpfs on vol,
+ * puts a filesystem image on it, binds a loop device to the image and
+ * mounts that loop device below. The loop device's backing file is a
+ * reference on the mount the image is on, held by the loop device, held
+ * by the mounted filesystem, held by the mount below.
+ */
+static int loop_child(int to_parent, int from_parent)
+{
+ struct report r = { { -1, -1 } };
+ char c;
+
+ if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return CHILD_NS;
+ if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL))
+ return child_fails(CHILD_NS);
+ r.n[0] = loop_mount(at("vol/img"), at("vol/mnt"));
+ if (r.n[0] < 0)
+ return child_fails(-r.n[0]);
+
+ if (write(to_parent, &r, sizeof(r)) != sizeof(r))
+ return child_fails(CHILD_PIPE);
+ /* keep the namespace alive while the parent removes the directory */
+ if (read(from_parent, &c, 1) != 1)
+ return child_fails(CHILD_PIPE);
+ return CHILD_OK;
+}
+
+/*
+ * The child did not report. Skip if the kernel lacks something, fail with
+ * what the child said otherwise. A child that a signal killed has failed.
+ */
+#define CHILD_GAVE_UP(pid) do { \
+ int __status; \
+ \
+ ASSERT_EQ(waitpid(pid, &__status, 0), pid); \
+ if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_NOFS) \
+ SKIP(return, "test requires the filesystem of the image (FAT or minix)"); \
+ if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_SKIP) \
+ SKIP(return, "the kernel lacks what this holder needs"); \
+ ASSERT_TRUE(false) \
+ TH_LOG("child failed to set up: %s %d", \
+ WIFEXITED(__status) ? "exit status" : "signal", \
+ WIFEXITED(__status) ? WEXITSTATUS(__status) : WTERMSIG(__status)); \
+} while (0)
+
+/* The child has to leave by itself and with nothing to complain about. */
+#define CHILD_LEFT(pid) do { \
+ int __status; \
+ \
+ ASSERT_EQ(waitpid(pid, &__status, 0), pid); \
+ ASSERT_TRUE(WIFEXITED(__status)); \
+ ASSERT_EQ(WEXITSTATUS(__status), CHILD_OK); \
+} while (0)
+
+/*
+ * An exclusive open of the device fails while a filesystem holds it, and
+ * the only filesystem that ever did is the one mounted below the dead
+ * mount. Give a release in flight a moment. Then the device must clear
+ * right away rather than only be marked for autoclear.
+ */
+static void assert_loop_released(struct __test_metadata *_metadata,
+ FIXTURE_DATA(loop_cycle) *self)
+{
+ int lfd;
+
+ for (int i = 0; i < 20; i++) {
+ lfd = open(self->dev, O_RDONLY | O_EXCL);
+ if (lfd >= 0)
+ break;
+ usleep(100000);
+ }
+ EXPECT_GE(lfd, 0)
+ TH_LOG("%s is still held by the loop mount below the dead mount: nothing refers to either mount and nothing can release them",
+ self->dev);
+ if (lfd >= 0)
+ close(lfd);
+
+ lfd = open(self->dev, O_RDWR);
+ ASSERT_GE(lfd, 0);
+ ASSERT_EQ(ioctl(lfd, LOOP_CLR_FD), 0);
+ close(lfd);
+ ASSERT_TRUE(loop_released(self->sysfs, 5000))
+ TH_LOG("%s kept its backing file after LOOP_CLR_FD: the filesystem on it is still mounted somewhere nobody can reach",
+ self->dev);
+ /* somebody else may bind it from now on, so the teardown leaves it alone */
+ for (int i = 0; i < self->nr_loops; i++) {
+ char dev[32];
+
+ snprintf(dev, sizeof(dev), "/dev/loop%d", self->loops[i]);
+ if (!strcmp(dev, self->dev))
+ self->loops[i] = -1;
+ }
+}
+
+/*
+ * rmdir of vol from here, where it is not a mountpoint, detaches
+ * the child's tmpfs with the loop mount connected below it. Once the child
+ * is gone nothing refers to either mount. The loop device must then be
+ * free to give up its backing file, which only happens when the mounted
+ * filesystem below the detached tmpfs has been released.
+ */
+TEST_F(loop_cycle, detached_loop_mount_released)
+{
+ int to_parent[2], to_child[2];
+ struct report r = { { -1, -1 } };
+ char buf[PATH_MAX];
+ pid_t pid;
+
+ ASSERT_EQ(pipe(to_parent), 0);
+ ASSERT_EQ(pipe(to_child), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(to_parent[0]);
+ close(to_child[1]);
+ _exit(loop_child(to_parent[1], to_child[0]));
+ }
+ close(to_parent[1]);
+ close(to_child[0]);
+
+ if (read(to_parent[0], &r, sizeof(r)) != sizeof(r))
+ CHILD_GAVE_UP(pid);
+ remember_loop(self, r.n[0]);
+ snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]);
+ snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]);
+ ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0);
+ ASSERT_NE(strstr(buf, "/vol/img"), NULL);
+
+ /* not a mountpoint in this namespace, so the directory can go */
+ ASSERT_EQ(rmdir(at("vol")), 0);
+
+ /* the child leaves: its namespace and every reference it held are gone */
+ ASSERT_EQ(write(to_child[1], "", 1), 1);
+ CHILD_LEFT(pid);
+ close(to_parent[0]);
+ close(to_child[1]);
+
+ /* nothing can reach the two mounts any more */
+ ASSERT_EQ(access(at("vol"), F_OK), -1);
+ ASSERT_FALSE(mounted_anywhere(self->dev));
+
+ assert_loop_released(_metadata, self);
+}
+
+/*
+ * The same two mounts in a detached tree: a clone of vol from
+ * open_tree(), the image opened through the clone so that the loop device
+ * holds the clone, and the loop mount moved below the clone. The last
+ * close of the tree's fd dissolves the tree with the loop mount left
+ * connected below the dead clone, and nothing refers to either afterwards.
+ */
+TEST_F(loop_cycle, dissolved_tree_loop_mount_released)
+{
+ int tfd, ifd, fsfd, mfd, n;
+ char buf[PATH_MAX];
+
+ fsfd = sys_fsopen("vfat", 0);
+ if (fsfd < 0)
+ fsfd = sys_fsopen("msdos", 0);
+ if (fsfd < 0)
+ SKIP(return, "test requires a FAT filesystem");
+
+ ASSERT_EQ(mkdir(at("vol"), 0755), 0);
+ ASSERT_EQ(mount("tmpfs", at("vol"), "tmpfs", 0, NULL), 0);
+ tfd = sys_open_tree(AT_FDCWD, at("vol"), OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+
+ /* the image, opened through the clone: the loop device holds the clone */
+ ifd = openat(tfd, "img", O_RDWR | O_CREAT | O_EXCL, 0600);
+ ASSERT_GE(ifd, 0);
+ ASSERT_EQ(write_fat12(ifd), 0);
+ n = loop_bind(ifd);
+ close(ifd);
+ ASSERT_GE(n, 0);
+ remember_loop(self, n);
+ snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", n);
+ snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", n);
+
+ /* the loop mount, moved below the clone */
+ ASSERT_EQ(mkdirat(tfd, "mnt", 0755), 0);
+ ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", self->dev, 0), 0);
+ ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
+ mfd = sys_fsmount(fsfd, 0, 0);
+ ASSERT_GE(mfd, 0);
+ close(fsfd);
+ ASSERT_EQ(sys_move_mount(mfd, "", tfd, "mnt", MOVE_MOUNT_F_EMPTY_PATH), 0);
+ close(mfd);
+
+ ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0);
+ ASSERT_NE(strstr(buf, "img"), NULL);
+
+ /* the last fd of the tree: both mounts die, the loop mount connected */
+ close(tfd);
+
+ /* nothing can reach the two mounts any more */
+ ASSERT_FALSE(mounted_anywhere(self->dev));
+
+ assert_loop_released(_metadata, self);
+}
+
+/*
+ * The cycle in two steps: rmdir of vol leaves the loop mount below it
+ * connected while its image's mount, vol2, is alive; then rmdir of vol2
+ * takes that one with the loop mount whose image is on the dead vol. Each
+ * dead mount now owns a loop mount whose filesystem pins the other.
+ */
+TEST_F(loop_cycle, crossed_images_released)
+{
+ int to_parent[2], to_child[2];
+ struct report r = { { -1, -1 } };
+ char sysfs[2][64], dev[2][32];
+ pid_t pid;
+
+ ASSERT_EQ(pipe(to_parent), 0);
+ ASSERT_EQ(pipe(to_child), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(to_parent[0]);
+ close(to_child[1]);
+ _exit(crossed_child(to_parent[1], to_child[0]));
+ }
+ close(to_parent[1]);
+ close(to_child[0]);
+
+ if (read(to_parent[0], &r, sizeof(r)) != sizeof(r))
+ CHILD_GAVE_UP(pid);
+ for (int i = 0; i < 2; i++) {
+ remember_loop(self, r.n[i]);
+ snprintf(dev[i], sizeof(dev[i]), "/dev/loop%d", r.n[i]);
+ snprintf(sysfs[i], sizeof(sysfs[i]), "/sys/block/loop%d/loop/backing_file", r.n[i]);
+ }
+
+ /* step one: the mount with the first loop mount below it goes */
+ ASSERT_EQ(rmdir(at("vol")), 0);
+
+ /* step two: the other one, with the loop mount whose image is on the first */
+ ASSERT_EQ(rmdir(at("vol2")), 0);
+
+ /* the child leaves: its namespace and every reference it held are gone */
+ ASSERT_EQ(write(to_child[1], "", 1), 1);
+ CHILD_LEFT(pid);
+ close(to_parent[0]);
+ close(to_child[1]);
+
+ /* nothing can reach the four mounts any more */
+ for (int i = 0; i < 2; i++) {
+ ASSERT_FALSE(mounted_anywhere(dev[i]));
+ strcpy(self->dev, dev[i]);
+ strcpy(self->sysfs, sysfs[i]);
+ assert_loop_released(_metadata, self);
+ }
+}
+
+/*
+ * The holders. Each keeps a file or a path on a mount P for as long as
+ * its own filesystem or device lives, and each has that filesystem or
+ * device mounted at C below P. Once another namespace's rmdir has
+ * detached P with C connected below it and the child is gone, P is owned
+ * by nobody, C by P, and P is kept by whatever the holder still holds.
+ *
+ * P is a loop mount so that its death can be observed: the loop device
+ * gives its backing file up when P's superblock goes. The image sits on
+ * a tmpfs next to P, not above it, so the loop device's own reference is
+ * not part of the picture.
+ */
+enum holder {
+ HOLDER_AUTOFS, /* a FIFO on P as the daemon's pipe */
+ HOLDER_ZRAM, /* a device node on P as the writeback device */
+ HOLDER_ECRYPTFS, /* a directory on P as the lower directory */
+ HOLDER_BINFMT_MISC, /* an executable on P as an 'F' interpreter */
+ HOLDER_FUSE, /* a file on P as a passthrough backing file */
+ HOLDER_ZLOOP, /* a directory on P for the zone files */
+ HOLDER_GADGET, /* a file on P as a mass storage LUN, over dummy_hcd */
+ HOLDER_MD, /* a file on P as an array's bitmap file */
+};
+
+#define HOLDER_IMG at("img/p.img")
+#define HOLDER_MNT at("p")
+#define HOLDER_BELOW at("p/c")
+
+static int write_file(const char *path, const char *s)
+{
+ int fd = open(path, O_WRONLY);
+ ssize_t n;
+
+ if (fd < 0)
+ return -1;
+ n = write(fd, s, strlen(s));
+ close(fd);
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Bind a free loop device to @img; the device number. */
+static int loop_attach(const char *img)
+{
+ int ifd, n;
+
+ ifd = open(img, O_RDWR);
+ if (ifd < 0)
+ return -1;
+ n = loop_bind(ifd);
+ close(ifd);
+ return n;
+}
+
+static int holder_autofs(void)
+{
+ char opts[64];
+ int pfd;
+
+ if (mkfifo(at("p/pipe"), 0600))
+ return CHILD_HOLDER;
+ pfd = open(at("p/pipe"), O_RDWR);
+ if (pfd < 0)
+ return CHILD_HOLDER;
+ snprintf(opts, sizeof(opts), "fd=%d,minproto=5,maxproto=5", pfd);
+ if (mount("autofs", HOLDER_BELOW, "autofs", 0, opts))
+ return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER;
+ close(pfd); /* the mount keeps its own */
+ return CHILD_OK;
+}
+
+/*
+ * zram's writeback device has to be a block device node, and that node has
+ * to be on P. Point it at a second loop device.
+ */
+static void zram_reset(void);
+
+static int holder_zram(void)
+{
+ char dev[32], buf[64];
+ struct stat st;
+ int fd, n;
+
+ if (access("/sys/block/zram0/backing_dev", W_OK))
+ return CHILD_SKIP;
+ /* somebody else's device, leave it alone */
+ if (read_sysfs("/sys/block/zram0/initstate", buf, sizeof(buf)) || buf[0] != '0')
+ return CHILD_SKIP;
+ fd = open(at("img/wb.img"), O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd < 0 || ftruncate(fd, IMAGE_SIZE))
+ return CHILD_HOLDER;
+ close(fd);
+ n = loop_attach(at("img/wb.img"));
+ if (n < 0)
+ return CHILD_HOLDER;
+ /* the minor is not the number of the device when loop has partitions */
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ if (stat(dev, &st) || mknod(at("p/wbdev"), S_IFBLK | 0600, st.st_rdev))
+ return CHILD_HOLDER;
+ if (write_file("/sys/block/zram0/backing_dev", at("p/wbdev")))
+ return CHILD_HOLDER;
+ snprintf(buf, sizeof(buf), "%d", 4 * 1024 * 1024);
+ if (write_file("/sys/block/zram0/disksize", buf))
+ goto undo;
+ fd = open("/dev/zram0", O_RDWR);
+ if (fd < 0)
+ goto undo;
+ n = write_fat12_4k(fd, 1024); /* zram has 4 KiB blocks */
+ close(fd); /* the reset in undo is refused while the device is open */
+ if (n)
+ goto undo;
+ if (mount_fat("/dev/zram0", HOLDER_BELOW)) {
+ n = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER;
+ zram_reset();
+ return n;
+ }
+ return CHILD_OK;
+undo:
+ zram_reset();
+ return CHILD_HOLDER;
+}
+
+/* Is @alg in /proc/crypto? A module is listed once a request has loaded it. */
+static bool crypto_has(const char *alg)
+{
+ char line[256], name[64];
+ bool found = false;
+ FILE *f;
+
+ f = fopen("/proc/crypto", "r");
+ if (!f)
+ return true; /* no way to tell, assume it is */
+ while (!found && fgets(line, sizeof(line), f))
+ found = sscanf(line, "name : %63s", name) == 1 && !strcmp(name, alg);
+ fclose(f);
+ return found;
+}
+
+/* The kernel's auth token layout, which userspace has to match byte for byte. */
+struct ecryptfs_auth_tok {
+ __u16 version;
+ __u16 token_type;
+ __u32 flags;
+ struct {
+ __u32 flags, encrypted_key_size, decrypted_key_size;
+ __u8 encrypted_key[512], decrypted_key[64];
+ } session_key;
+ __u8 reserved[32];
+ struct {
+ __u32 password_bytes;
+ __s32 hash_algo;
+ __u32 hash_iterations, session_key_encryption_key_bytes, flags;
+ __u8 session_key_encryption_key[64];
+ __u8 signature[17];
+ __u8 salt[8];
+ } password;
+} __attribute__((packed));
+
+#define ECRYPTFS_SIG "0123456789abcdef"
+
+static int holder_ecryptfs(void)
+{
+ struct ecryptfs_auth_tok tok = {
+ .version = 0x0004,
+ .token_type = 0, /* ECRYPTFS_PASSWORD */
+ .password.session_key_encryption_key_bytes = 16,
+ .password.flags = 0x02, /* ECRYPTFS_SESSION_KEY_ENCRYPTION_KEY_SET */
+ .password.signature = ECRYPTFS_SIG,
+ };
+
+ /* a session keyring of this child's own, so that the key goes with it */
+ if (syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0)
+ return errno == ENOSYS ? CHILD_SKIP : CHILD_HOLDER;
+ if (syscall(__NR_add_key, "user", ECRYPTFS_SIG, &tok, sizeof(tok),
+ KEY_SPEC_SESSION_KEYRING) < 0)
+ return CHILD_HOLDER;
+ if (mkdir(at("p/lower"), 0755))
+ return CHILD_HOLDER;
+ if (mount(at("p/lower"), HOLDER_BELOW, "ecryptfs", 0,
+ "ecryptfs_sig=" ECRYPTFS_SIG ",ecryptfs_cipher=aes,ecryptfs_key_bytes=16")) {
+ /* EINVAL without the cipher; a module is loaded by the attempt */
+ if (errno == ENODEV || (errno == EINVAL && !crypto_has("aes")))
+ return CHILD_SKIP;
+ return CHILD_HOLDER;
+ }
+ return CHILD_OK;
+}
+
+/*
+ * binfmt_misc instances are per user namespace, so the mount below P is
+ * made from a new one, which gets a copy of P.
+ */
+static int holder_binfmt_misc(void)
+{
+ static const char interp[] = "#!/bin/true\n";
+ char reg[PATH_MAX];
+ int out;
+
+ /* 'F' opens the interpreter at registration, nothing runs it here */
+ out = open(at("p/interp"), O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (out < 0 || write(out, interp, sizeof(interp) - 1) != sizeof(interp) - 1)
+ return CHILD_HOLDER;
+ close(out);
+
+ /* ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM says no */
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) {
+ if (errno == EINVAL || errno == ENOSPC || errno == EPERM)
+ return CHILD_SKIP;
+ return CHILD_HOLDER;
+ }
+ if (write_file("/proc/self/setgroups", "deny") ||
+ write_file("/proc/self/uid_map", "0 0 1") ||
+ write_file("/proc/self/gid_map", "0 0 1"))
+ return CHILD_HOLDER;
+ if (mount("binfmt_misc", HOLDER_BELOW, "binfmt_misc", 0, NULL))
+ return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER;
+ snprintf(reg, sizeof(reg), ":cycle:E::cyc::%s:F", at("p/interp"));
+ if (write_file(at("p/c/register"), reg))
+ return CHILD_HOLDER;
+ return CHILD_OK;
+}
+
+/*
+ * A fuse server that only ever answers FUSE_INIT, with passthrough on, and
+ * then registers a file on P as a backing file. The registration alone
+ * makes the fuse superblock hold the file.
+ */
+static int holder_fuse(void)
+{
+ struct fuse_backing_map map = {};
+ struct fuse_in_header *ih;
+ struct fuse_init_out init = {
+ .major = FUSE_KERNEL_VERSION,
+ .minor = FUSE_KERNEL_MINOR_VERSION,
+ .flags = FUSE_INIT_EXT,
+ .flags2 = FUSE_PASSTHROUGH >> 32,
+ .max_write = 4096,
+ .max_stack_depth = 1,
+ };
+ struct fuse_out_header oh = { .len = sizeof(oh) + sizeof(init) };
+ struct iovec iov[2] = { { &oh, sizeof(oh) }, { &init, sizeof(init) } };
+ char opts[64], buf[8192];
+ int ffd, bfd;
+ ssize_t n;
+
+ bfd = open(at("p/backing"), O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (bfd < 0)
+ return CHILD_HOLDER;
+ ffd = open("/dev/fuse", O_RDWR);
+ if (ffd < 0)
+ return CHILD_SKIP;
+ snprintf(opts, sizeof(opts), "fd=%d,rootmode=40000,user_id=0,group_id=0", ffd);
+ if (mount("fuse", HOLDER_BELOW, "fuse", 0, opts))
+ return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER;
+
+ n = read(ffd, buf, sizeof(buf));
+ ih = (void *)buf;
+ if (n < (ssize_t)sizeof(*ih) || ih->opcode != FUSE_INIT)
+ return CHILD_HOLDER;
+ oh.unique = ih->unique;
+ if (writev(ffd, iov, 2) != (ssize_t)oh.len)
+ return CHILD_HOLDER;
+
+ map.fd = bfd;
+ if (ioctl(ffd, FUSE_DEV_IOC_BACKING_OPEN, &map) < 0) {
+ /* EOPNOTSUPP: no FUSE_PASSTHROUGH, ENOTTY: no such ioctl */
+ if (errno == EPERM || errno == EOPNOTSUPP || errno == ENOTTY)
+ return CHILD_SKIP;
+ return CHILD_HOLDER;
+ }
+ close(bfd); /* the connection keeps its own */
+ return CHILD_OK; /* ffd stays open until the child exits */
+}
+
+/* Wait for a device node the kernel is about to create. */
+static int open_when_there(const char *dev, int flags, int ms)
+{
+ int fd;
+
+ for (; ms > 0; ms -= 100) {
+ fd = open(dev, flags);
+ if (fd >= 0)
+ return fd;
+ usleep(100000);
+ }
+ return -1;
+}
+
+/*
+ * zloop keeps every zone file open. One conventional zone is enough for a
+ * FAT image, the sequential one stays empty.
+ */
+static int holder_zloop(void)
+{
+ char cmd[PATH_MAX];
+ int fd, ret = CHILD_HOLDER;
+
+ if (access("/dev/zloop-control", W_OK))
+ return CHILD_SKIP;
+ /* somebody else's device, leave it alone */
+ if (!access("/sys/block/zloop0", F_OK))
+ return CHILD_SKIP;
+ if (mkdir(at("p/zl"), 0755) || mkdir(at("p/zl/0"), 0755))
+ return CHILD_HOLDER;
+ snprintf(cmd, sizeof(cmd),
+ "add id=0,capacity_mb=8,zone_size_mb=4,conv_zones=1,base_dir=%s",
+ at("p/zl"));
+ if (write_file("/dev/zloop-control", cmd))
+ return CHILD_HOLDER;
+ fd = open_when_there("/dev/zloop0", O_RDWR, 5000);
+ if (fd < 0 || write_fat12_4k(fd, 1024)) /* 4 KiB blocks, like P */
+ goto undo;
+ close(fd);
+ fd = -1;
+ if (mount_fat("/dev/zloop0", HOLDER_BELOW)) {
+ ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER;
+ goto undo;
+ }
+ return CHILD_OK;
+undo:
+ if (fd >= 0)
+ close(fd);
+ write_file("/dev/zloop-control", "remove id=0");
+ return ret;
+}
+
+static int mount_configfs(void)
+{
+ if (access("/sys/kernel/config", F_OK))
+ return -1;
+ if (mount("configfs", "/sys/kernel/config", "configfs", 0, NULL) && errno != EBUSY)
+ return -1;
+ return 0;
+}
+
+/* Take the gadget out of configfs again, in the reverse order of its creation. */
+static void gadget_remove(void)
+{
+ if (mount_configfs())
+ return;
+ write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n");
+ write_file(gat("/UDC"), "\n");
+ unlink(gat("/configs/c.1/mass_storage.0"));
+ rmdir(gat("/configs/c.1/strings/0x409"));
+ rmdir(gat("/configs/c.1"));
+ rmdir(gat("/functions/mass_storage.0"));
+ rmdir(gat("/strings/0x409"));
+ rmdir(gat(""));
+}
+
+/* The disk usb-storage created for the gadget, by the LUN's inquiry string. */
+static int find_gadget_disk(char *dev, size_t len, int ms)
+{
+ char path[PATH_MAX], model[64];
+ struct dirent *de;
+ DIR *d;
+
+ for (; ms > 0; ms -= 100, usleep(100000)) {
+ d = opendir("/sys/block");
+ if (!d)
+ return -1;
+ while ((de = readdir(d))) {
+ if (strncmp(de->d_name, "sd", 2))
+ continue;
+ snprintf(path, sizeof(path), "/sys/block/%s/device/model", de->d_name);
+ if (read_sysfs(path, model, sizeof(model)) ||
+ strncmp(model, "File-Stor Gadget", 16))
+ continue;
+ snprintf(dev, len, "/dev/%s", de->d_name);
+ closedir(d);
+ return 0;
+ }
+ closedir(d);
+ }
+ return -1;
+}
+
+/*
+ * A mass storage gadget bound to the dummy UDC, so that this kernel is
+ * also the USB host that sees the LUN as a SCSI disk. sd locks the
+ * medium on open, which is what keeps the LUN's file from being ejected.
+ */
+#define GADGET_STEP(x) do { \
+ if (x) { \
+ fprintf(stderr, "gadget: %s failed: %s\n", #x, strerror(errno)); \
+ gadget_remove(); \
+ return CHILD_HOLDER; \
+ } \
+} while (0)
+
+static int holder_gadget(void)
+{
+ char dev[PATH_MAX];
+ int fd;
+
+ if (mount_configfs())
+ return CHILD_SKIP;
+ if (access("/sys/kernel/config/usb_gadget", F_OK) ||
+ access("/sys/class/udc/dummy_udc.0", F_OK))
+ return CHILD_SKIP;
+
+ fd = open(at("p/lun.img"), O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd < 0 || write_fat12(fd))
+ return CHILD_HOLDER;
+ close(fd);
+
+ GADGET_STEP(mkdir(gat(""), 0755));
+ GADGET_STEP(write_file(gat("/idVendor"), "0x1d6b"));
+ GADGET_STEP(write_file(gat("/idProduct"), "0x0104"));
+ GADGET_STEP(mkdir(gat("/strings/0x409"), 0755));
+ GADGET_STEP(write_file(gat("/strings/0x409/serialnumber"), "1"));
+ GADGET_STEP(write_file(gat("/strings/0x409/manufacturer"), "kselftest"));
+ GADGET_STEP(write_file(gat("/strings/0x409/product"), "cycle"));
+ GADGET_STEP(mkdir(gat("/configs/c.1"), 0755));
+ GADGET_STEP(mkdir(gat("/configs/c.1/strings/0x409"), 0755));
+ GADGET_STEP(write_file(gat("/configs/c.1/strings/0x409/configuration"), "c"));
+ GADGET_STEP(mkdir(gat("/functions/mass_storage.0"), 0755));
+ GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/removable"), "1"));
+ GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/file"), at("p/lun.img")));
+ GADGET_STEP(symlink(gat("/functions/mass_storage.0"), gat("/configs/c.1/mass_storage.0")));
+ /* EBUSY: the controller is somebody else's, leave it alone */
+ if (write_file(gat("/UDC"), "dummy_udc.0")) {
+ fd = errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER;
+ gadget_remove();
+ return fd;
+ }
+
+ /* usb-storage waits a second before it scans the device */
+ if (find_gadget_disk(dev, sizeof(dev), 15000)) {
+ /* without usb-storage and sd the LUN never shows up as a disk */
+ fd = (access("/sys/bus/usb/drivers/usb-storage", F_OK) ||
+ access("/sys/bus/scsi/drivers/sd", F_OK)) ? CHILD_SKIP : CHILD_HOLDER;
+ gadget_remove();
+ return fd;
+ }
+ fd = open_when_there(dev, O_RDONLY, 5000);
+ GADGET_STEP(fd < 0);
+ close(fd);
+ if (mount_fat(dev, HOLDER_BELOW)) {
+ fd = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER;
+ gadget_remove();
+ return fd;
+ }
+ return CHILD_OK;
+}
+
+#define BITMAP_MAGIC 0x6d746962
+
+/*
+ * A RAID1 of one loop device, not persistent, with its bitmap in a file
+ * on P. The bitmap file needs a superblock the kernel accepts; sync_size,
+ * uuid and events are not looked at for a non-persistent array.
+ */
+static int holder_md(void)
+{
+ struct {
+ __u32 magic, version;
+ __u8 uuid[16];
+ __u64 events, events_cleared, sync_size;
+ __u32 state, chunksize, daemon_sleep, write_behind;
+ } bsb = {
+ .magic = BITMAP_MAGIC,
+ .version = 4,
+ .chunksize = 64 * 1024,
+ .daemon_sleep = 5,
+ };
+ mdu_array_info_t info = {
+ .level = 1,
+ .raid_disks = 1,
+ .size = 8 * 1024, /* KiB */
+ .not_persistent = 1,
+ };
+ mdu_disk_info_t disk = {
+ .major = 7,
+ .state = (1 << MD_DISK_ACTIVE) | (1 << MD_DISK_SYNC),
+ };
+ int fd, mdfd, bfd, n, ret = CHILD_HOLDER;
+ char dev[32], buf[4096];
+ struct stat st;
+
+ fd = open(at("img/md.img"), O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd < 0 || ftruncate(fd, 8 * 1024 * 1024))
+ return CHILD_HOLDER;
+ close(fd);
+ n = loop_attach(at("img/md.img"));
+ if (n < 0)
+ return CHILD_HOLDER;
+ /* the minor is not the number of the device when loop has partitions */
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ if (stat(dev, &st))
+ return CHILD_HOLDER;
+ disk.major = major(st.st_rdev);
+ disk.minor = minor(st.st_rdev);
+
+ bfd = open(at("p/bitmap"), O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (bfd < 0 || write(bfd, &bsb, sizeof(bsb)) != sizeof(bsb) || ftruncate(bfd, 4096))
+ return CHILD_HOLDER;
+
+ if (read_sysfs("/proc/mdstat", buf, sizeof(buf))) {
+ fprintf(stderr, "md: no /proc/mdstat\n");
+ return CHILD_SKIP;
+ }
+ if (!strstr(buf, "[raid1]")) {
+ fprintf(stderr, "md: no raid1 personality: %s\n", buf);
+ return CHILD_SKIP;
+ }
+ /* a node of our own: /dev may not have one and is not ours to change */
+ if (mknod(at("img/md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0)))
+ return CHILD_HOLDER;
+ mdfd = open(at("img/md0"), O_RDWR);
+ if (mdfd < 0)
+ return CHILD_SKIP;
+ /* somebody else's array: SET_ARRAY_INFO would say EINVAL, not EBUSY */
+ if (!read_sysfs("/sys/block/md0/md/array_state", buf, sizeof(buf)) &&
+ strncmp(buf, "clear", 5)) {
+ fprintf(stderr, "md: md0 is in use: %s", buf);
+ close(mdfd);
+ return CHILD_SKIP;
+ }
+ /* the bitmap ops are only installed once a bitmap type is chosen */
+ write_file("/sys/block/md0/md/bitmap_type", "bitmap");
+ /* EBUSY: somebody else's array, leave it alone */
+ if (ioctl(mdfd, SET_ARRAY_INFO, &info))
+ return errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER;
+ /* the array is ours from here on and has to be stopped on every path */
+ if (ioctl(mdfd, ADD_NEW_DISK, &disk))
+ goto undo;
+ /* attach the bitmap file before the array runs, the way mdadm does */
+ if (ioctl(mdfd, SET_BITMAP_FILE, bfd)) {
+ fprintf(stderr, "md: SET_BITMAP_FILE: %s\n", strerror(errno));
+ if (errno == EINVAL)
+ ret = CHILD_SKIP;
+ goto undo;
+ }
+ close(bfd); /* the array keeps its own */
+ if (ioctl(mdfd, RUN_ARRAY, NULL)) {
+ fprintf(stderr, "md: RUN_ARRAY: %s\n", strerror(errno));
+ goto undo;
+ }
+ if (write_fat12(mdfd))
+ goto undo;
+ close(mdfd);
+ if (mount_fat(at("img/md0"), HOLDER_BELOW)) {
+ ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER;
+ mdfd = open(at("img/md0"), O_RDWR);
+ goto undo;
+ }
+ return CHILD_OK;
+undo:
+ if (mdfd >= 0) {
+ ioctl(mdfd, STOP_ARRAY);
+ close(mdfd);
+ }
+ return ret;
+}
+
+/*
+ * A device keeps its file for as long as it is configured, so once nothing
+ * below the dead mount is left the device has to be told to let go. With
+ * the cycle unbroken C's filesystem still holds the device and every one
+ * of these refuses.
+ */
+static int holder_let_go_once(enum holder holder)
+{
+ int fd, ret;
+
+ switch (holder) {
+ case HOLDER_ZRAM:
+ return write_file("/sys/block/zram0/reset", "1");
+ case HOLDER_ZLOOP:
+ return write_file("/dev/zloop-control", "remove id=0");
+ case HOLDER_GADGET:
+ if (mount_configfs())
+ return -1;
+ /* a zero-length write is a no-op for configfs; a newline ejects */
+ return write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n");
+ case HOLDER_MD:
+ fd = open(at("img/md0"), O_RDONLY);
+ if (fd < 0) {
+ /* the child's node went with its tmpfs */
+ mknod(at("md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0));
+ fd = open(at("md0"), O_RDONLY);
+ }
+ if (fd < 0)
+ return -1;
+ ret = ioctl(fd, STOP_ARRAY);
+ close(fd);
+ return ret;
+ default:
+ return 0;
+ }
+}
+
+static void zram_reset(void)
+{
+ holder_let_go_once(HOLDER_ZRAM);
+}
+
+/*
+ * In its own mount namespace the child puts the image on a tmpfs next to
+ * P, mounts P from a loop device, and sets the holder up with its own
+ * filesystem or device mounted at C below P.
+ */
+static int holder_child(int to_parent, int from_parent, enum holder holder)
+{
+ bool fifo = holder == HOLDER_AUTOFS || holder == HOLDER_ZRAM;
+ /* room for a 4 MiB zone file or a floppy image on P */
+ bool big = holder == HOLDER_ZLOOP || holder == HOLDER_GADGET;
+ struct report r = { { -1, -1 } };
+ char dev[32], c;
+ int ifd, n, ret;
+
+ if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return CHILD_NS;
+ if (mkdir(at("img"), 0755) || mount("tmpfs", at("img"), "tmpfs", 0, NULL))
+ return child_fails(CHILD_NS);
+
+ ifd = open(HOLDER_IMG, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (ifd < 0 || (fifo ? write_minix(ifd) :
+ big ? write_fat12_4k(ifd, 3072) : write_fat12(ifd)))
+ return child_fails(CHILD_IMAGE);
+ close(ifd);
+ n = loop_attach(HOLDER_IMG);
+ if (n < 0)
+ return child_fails(CHILD_LOOP);
+ snprintf(dev, sizeof(dev), "/dev/loop%d", n);
+ if (mkdir(HOLDER_MNT, 0755))
+ return child_fails(CHILD_MOUNT);
+ if (fifo ? mount(dev, HOLDER_MNT, "minix", 0, NULL) : mount_fat(dev, HOLDER_MNT))
+ return child_fails(errno == ENODEV ? CHILD_NOFS : CHILD_MOUNT);
+ if (mkdir(HOLDER_BELOW, 0755))
+ return child_fails(CHILD_MOUNT);
+
+ switch (holder) {
+ case HOLDER_AUTOFS:
+ ret = holder_autofs();
+ break;
+ case HOLDER_ZRAM:
+ ret = holder_zram();
+ break;
+ case HOLDER_ECRYPTFS:
+ ret = holder_ecryptfs();
+ break;
+ case HOLDER_BINFMT_MISC:
+ ret = holder_binfmt_misc();
+ break;
+ case HOLDER_FUSE:
+ ret = holder_fuse();
+ break;
+ case HOLDER_ZLOOP:
+ ret = holder_zloop();
+ break;
+ case HOLDER_GADGET:
+ ret = holder_gadget();
+ break;
+ case HOLDER_MD:
+ ret = holder_md();
+ break;
+ default:
+ ret = CHILD_HOLDER;
+ }
+ if (ret != CHILD_OK)
+ return child_fails(ret);
+
+ /* P's device first, then the one the holder has bound for itself */
+ r.n[0] = n;
+ if (nr_bound > 1)
+ r.n[1] = bound[1];
+ if (write(to_parent, &r, sizeof(r)) != sizeof(r) ||
+ read(from_parent, &c, 1) != 1) {
+ /* the parent went away and will not tell the devices to let go */
+ umount2(HOLDER_BELOW, MNT_DETACH);
+ for (int i = 0; i < 50 && holder_let_go_once(holder); i++)
+ usleep(100000);
+ if (holder == HOLDER_GADGET)
+ gadget_remove();
+ return child_fails(CHILD_PIPE);
+ }
+ return CHILD_OK;
+}
+
+/*
+ * The release of C's filesystem may still be in flight when the child is
+ * gone, and a device that is still held refuses to let go, so try for a
+ * while. With the cycle unbroken it refuses for good.
+ */
+static void holder_let_go(enum holder holder)
+{
+ for (int i = 0; i < 50; i++) {
+ if (!holder_let_go_once(holder))
+ return;
+ usleep(100000);
+ }
+}
+
+/*
+ * rmdir of p from here, where it is a plain directory, detaches
+ * P in the child's namespace with C connected below it. Once the child is
+ * gone the holder's file on P is the only thing left that refers to P,
+ * and it is dropped only when C's filesystem dies, which waits for P.
+ * The loop device backing P tells whether that resolved.
+ */
+static void holder_cycle(struct __test_metadata *_metadata,
+ FIXTURE_DATA(loop_cycle) *self, enum holder holder)
+{
+ struct report r = { { -1, -1 } };
+ int to_parent[2], from_parent[2];
+ pid_t pid;
+
+ ASSERT_EQ(pipe(to_parent), 0);
+ ASSERT_EQ(pipe(from_parent), 0);
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(to_parent[0]);
+ close(from_parent[1]);
+ _exit(holder_child(to_parent[1], from_parent[0], holder));
+ }
+ close(to_parent[1]);
+ close(from_parent[0]);
+
+ if (read(to_parent[0], &r, sizeof(r)) != sizeof(r))
+ CHILD_GAVE_UP(pid);
+ self->gadget = holder == HOLDER_GADGET;
+ remember_loop(self, r.n[0]);
+ remember_loop(self, r.n[1]);
+ snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]);
+ snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]);
+
+ ASSERT_EQ(rmdir(HOLDER_MNT), 0);
+ ASSERT_EQ(write(from_parent[1], "x", 1), 1);
+ CHILD_LEFT(pid);
+ close(to_parent[0]);
+ close(from_parent[1]);
+
+ holder_let_go(holder);
+ assert_loop_released(_metadata, self);
+ /* the teardown takes the gadget and the holder's own loop device away */
+}
+
+TEST_F(loop_cycle, autofs_pipe_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_AUTOFS);
+}
+
+TEST_F(loop_cycle, zram_writeback_node_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_ZRAM);
+}
+
+TEST_F(loop_cycle, ecryptfs_lower_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_ECRYPTFS);
+}
+
+TEST_F(loop_cycle, binfmt_misc_interpreter_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_BINFMT_MISC);
+}
+
+TEST_F(loop_cycle, fuse_backing_file_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_FUSE);
+}
+
+TEST_F(loop_cycle, zloop_zone_files_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_ZLOOP);
+}
+
+/* up to 15 s for the disk to show up and 12 s for a device that is held */
+TEST_F_TIMEOUT(loop_cycle, mass_storage_lun_on_dead_mount_released, 120)
+{
+ holder_cycle(_metadata, self, HOLDER_GADGET);
+}
+
+TEST_F(loop_cycle, md_bitmap_file_on_dead_mount_released)
+{
+ holder_cycle(_metadata, self, HOLDER_MD);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c b/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c
new file mode 100644
index 000000000000..0a0b0c037691
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c
@@ -0,0 +1,579 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An unmounted mount that would have stayed attached to its unmounted parent
+ * leaves a cover behind instead. A lookup on the parent at the mountpoint
+ * finds knullfs: an empty read-only directory that is shared by every cover
+ * and every kernel thread, so it can't be watched or locked and nothing can
+ * be mounted on it. The mount itself is a root from then on. Where a file
+ * was mounted, the stand-in is an empty regular file of the same instance.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/fanotify.h>
+#include <sys/file.h>
+#include <sys/inotify.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/syscall.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+#include "../readdir_hold.h"
+
+#ifndef NULL_FS_MAGIC
+#define NULL_FS_MAGIC 0x4E554C4C
+#endif
+
+#ifndef TMPFS_MAGIC
+#define TMPFS_MAGIC 0x01021994
+#endif
+
+#ifndef F_SETDELEG
+#define F_SETDELEG (F_SETLEASE + 16)
+#endif
+
+/* struct delegation of <linux/fcntl.h>, which doesn't mix with <fcntl.h> */
+struct delegation_req {
+ uint32_t d_flags;
+ uint16_t d_type;
+ uint16_t __pad;
+};
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+/* what the parent asks the child to do */
+#define CMD_RMDIR 'r'
+#define CMD_CLOSE_T 't'
+#define CMD_QUIT 'q'
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int touch(const char *path)
+{
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+ if (fd < 0)
+ return -1;
+ close(fd);
+ return 0;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ prctl(PR_SET_DUMPABLE, 1);
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ if (setgid(0) || setuid(0))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+/*
+ * The child mounts P on @base/p, C on P/covered and the file P/src on P/file
+ * in a mount namespace of its own, binds P a second time at @base/q and hands
+ * out descriptors on P and on C. rmdir() of @base/p from here unmounts P
+ * together with C and the file bind. P and C are held by the descriptors and
+ * the unmounted children leave their covers behind. On request the child
+ * removes C's mountpoint through the bind.
+ *
+ * It also mounts T on @base/t with a child on T/covered, binds T at @base/u
+ * with a second child on the same dentry and hands out descriptors on T and
+ * U. rmdir() of both from here leaves two covers on one mountpoint. On request
+ * the child lets go of T.
+ */
+static int cover_child(const char *base, int to_parent, int from_parent)
+{
+ char p[PATH_LEN], c[PATH_LEN], q[PATH_LEN], qc[PATH_LEN];
+ char f[PATH_LEN], src[PATH_LEN], t[PATH_LEN], u[PATH_LEN], tc[PATH_LEN];
+ int fds[4], ret;
+ char cmd;
+
+ if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return 1;
+ snprintf(p, sizeof(p), "%s/p", base);
+ if (mkdir(p, 0755) || mount("tmpfs", p, "tmpfs", 0, NULL))
+ return 2;
+ snprintf(c, sizeof(c), "%s/p/covered", base);
+ if (mkdir(c, 0755) || mount("tmpfs", c, "tmpfs", 0, NULL))
+ return 3;
+ snprintf(f, sizeof(f), "%s/p/file", base);
+ snprintf(src, sizeof(src), "%s/p/src", base);
+ if (touch(src) || touch(f) || mount(src, f, NULL, MS_BIND, NULL))
+ return 4;
+ snprintf(q, sizeof(q), "%s/q", base);
+ if (mkdir(q, 0755) || mount(p, q, NULL, MS_BIND, NULL))
+ return 5;
+ snprintf(t, sizeof(t), "%s/t", base);
+ if (mkdir(t, 0755) || mount("tmpfs", t, "tmpfs", 0, NULL))
+ return 6;
+ snprintf(tc, sizeof(tc), "%s/t/covered", base);
+ if (mkdir(tc, 0755) || mount("tmpfs", tc, "tmpfs", 0, NULL))
+ return 7;
+ snprintf(u, sizeof(u), "%s/u", base);
+ if (mkdir(u, 0755) || mount(t, u, NULL, MS_BIND, NULL))
+ return 8;
+ snprintf(tc, sizeof(tc), "%s/u/covered", base);
+ if (mount("tmpfs", tc, "tmpfs", 0, NULL))
+ return 9;
+ fds[0] = open(p, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ fds[1] = open(c, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ fds[2] = open(t, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ fds[3] = open(u, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (fds[0] < 0 || fds[1] < 0 || fds[2] < 0 || fds[3] < 0)
+ return 10;
+ if (write(to_parent, fds, sizeof(fds)) != sizeof(fds))
+ return 11;
+
+ snprintf(qc, sizeof(qc), "%s/q/covered", base);
+ for (;;) {
+ if (read(from_parent, &cmd, 1) != 1)
+ return 12;
+ switch (cmd) {
+ case CMD_RMDIR:
+ ret = rmdir(qc) ? errno : 0;
+ if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret))
+ return 13;
+ break;
+ case CMD_CLOSE_T:
+ ret = close(fds[2]) ? errno : 0;
+ if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret))
+ return 13;
+ break;
+ case CMD_QUIT:
+ return 0;
+ default:
+ return 14;
+ }
+ }
+}
+
+FIXTURE(mount_cover) {
+ char base[DIR_LEN];
+ pid_t child;
+ int to_child;
+ int from_child;
+ int dfd; /* P, unmounted, held */
+ int cfd; /* C, unmounted, held */
+ int fd; /* what a lookup on P finds at C's mountpoint */
+ int tfd; /* T, unmounted, held */
+ int ufd; /* U, a bind of T, unmounted, held */
+ int fan; /* fanotify group from before the user namespace, or -1 */
+ struct readdir_hold hold; /* likewise from before, uffd -1 without */
+};
+
+/*
+ * Everything the setup has made. The harness does not run the teardown
+ * when an assertion of the setup fails, so the setup calls this itself.
+ */
+static void cover_cleanup(FIXTURE_DATA(mount_cover) *self)
+{
+ char cmd = CMD_QUIT;
+ int status;
+
+ if (self->fd >= 0)
+ close(self->fd);
+ if (self->cfd >= 0)
+ close(self->cfd);
+ if (self->dfd >= 0)
+ close(self->dfd);
+ if (self->tfd >= 0)
+ close(self->tfd);
+ if (self->ufd >= 0)
+ close(self->ufd);
+ if (self->fan >= 0)
+ close(self->fan);
+ readdir_hold_destroy(&self->hold);
+ if (self->child > 0) {
+ if (write(self->to_child, &cmd, 1) != 1)
+ kill(self->child, SIGKILL);
+ waitpid(self->child, &status, 0);
+ }
+ if (self->to_child >= 0)
+ close(self->to_child);
+ if (self->from_child >= 0)
+ close(self->from_child);
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static int same_file(int fd1, int fd2)
+{
+ struct stat st1, st2;
+
+ if (fstat(fd1, &st1) || fstat(fd2, &st2))
+ return 0;
+ return st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino;
+}
+
+FIXTURE_SETUP(mount_cover)
+{
+ int to_parent[2], to_child[2], fds[4], pidfd, status;
+ char dir[PATH_LEN];
+ struct statfs sf;
+
+ self->child = 0;
+ self->to_child = self->from_child = -1;
+ self->dfd = self->cfd = self->fd = self->tfd = self->ufd = -1;
+
+ /*
+ * A group for plain events takes CAP_SYS_ADMIN in the initial user
+ * namespace, so get one before that is gone. An inode mark can be
+ * added to it from anywhere.
+ */
+ self->fan = fanotify_init(FAN_CLASS_NOTIF | FAN_CLOEXEC, O_RDONLY);
+ /* same for the userfaultfd that holds a readdir in its fault */
+ readdir_hold_init(&self->hold);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/mount_cover.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ if (enter_userns()) {
+ cover_cleanup(self);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0)
+ cover_cleanup(self);
+
+ snprintf(dir, sizeof(dir), "%s/p", self->base);
+ ASSERT_EQ(pipe(to_parent), 0)
+ cover_cleanup(self);
+ ASSERT_EQ(pipe(to_child), 0)
+ cover_cleanup(self);
+ self->child = fork();
+ ASSERT_GE(self->child, 0)
+ cover_cleanup(self);
+ if (self->child == 0) {
+ close(to_parent[0]);
+ close(to_child[1]);
+ _exit(cover_child(self->base, to_parent[1], to_child[0]));
+ }
+ close(to_parent[1]);
+ close(to_child[0]);
+ self->to_child = to_child[1];
+ self->from_child = to_parent[0];
+ if (read(self->from_child, fds, sizeof(fds)) != sizeof(fds)) {
+ pid_t pid = self->child;
+
+ waitpid(pid, &status, 0);
+ self->child = 0;
+ cover_cleanup(self);
+ ASSERT_TRUE(false)
+ TH_LOG("child failed to set up: %s %d",
+ WIFEXITED(status) ? "step" : "signal",
+ WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status));
+ }
+
+ pidfd = syscall(__NR_pidfd_open, self->child, 0);
+ ASSERT_GE(pidfd, 0)
+ cover_cleanup(self);
+ self->dfd = syscall(__NR_pidfd_getfd, pidfd, fds[0], 0);
+ self->cfd = syscall(__NR_pidfd_getfd, pidfd, fds[1], 0);
+ self->tfd = syscall(__NR_pidfd_getfd, pidfd, fds[2], 0);
+ self->ufd = syscall(__NR_pidfd_getfd, pidfd, fds[3], 0);
+ close(pidfd);
+ ASSERT_GE(self->dfd, 0)
+ cover_cleanup(self);
+ ASSERT_GE(self->cfd, 0)
+ cover_cleanup(self);
+ ASSERT_GE(self->tfd, 0)
+ cover_cleanup(self);
+ ASSERT_GE(self->ufd, 0)
+ cover_cleanup(self);
+
+ /* unmounts P and C, C leaves its cover behind */
+ ASSERT_EQ(rmdir(dir), 0)
+ cover_cleanup(self);
+ /* unmounts T and U with their children, two covers on one mountpoint */
+ snprintf(dir, sizeof(dir), "%s/t", self->base);
+ ASSERT_EQ(rmdir(dir), 0)
+ cover_cleanup(self);
+ snprintf(dir, sizeof(dir), "%s/u", self->base);
+ ASSERT_EQ(rmdir(dir), 0)
+ cover_cleanup(self);
+
+ self->fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(self->fd, 0)
+ cover_cleanup(self);
+ ASSERT_EQ(fstatfs(self->fd, &sf), 0)
+ cover_cleanup(self);
+ ASSERT_EQ(sf.f_type, NULL_FS_MAGIC)
+ cover_cleanup(self);
+}
+
+FIXTURE_TEARDOWN(mount_cover)
+{
+ cover_cleanup(self);
+}
+
+TEST_F(mount_cover, not_watchable)
+{
+ char p[PATH_LEN];
+ int ifd;
+
+ snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd);
+
+ ifd = inotify_init1(IN_CLOEXEC);
+ if (ifd < 0 && errno == ENOSYS) {
+ TH_LOG("no inotify in this kernel, skipping that part");
+ } else {
+ ASSERT_GE(ifd, 0);
+ EXPECT_EQ(inotify_add_watch(ifd, p, IN_OPEN), -1);
+ EXPECT_EQ(errno, EINVAL);
+ close(ifd);
+ }
+
+ /* dnotify ends up at the same place */
+ EXPECT_EQ(fcntl(self->fd, F_NOTIFY, DN_ACCESS), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ if (self->fan < 0) {
+ TH_LOG("no fanotify group without CAP_SYS_ADMIN in the initial user namespace, skipping the fanotify part");
+ return;
+ }
+ EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD, FAN_OPEN, self->fd, NULL), -1);
+ EXPECT_EQ(errno, EINVAL);
+}
+
+TEST_F(mount_cover, not_lockable)
+{
+ struct flock fl = {
+ .l_type = F_RDLCK,
+ .l_whence = SEEK_SET,
+ };
+
+ EXPECT_EQ(flock(self->fd, LOCK_EX | LOCK_NB), -1);
+ EXPECT_EQ(errno, ENOLCK);
+ EXPECT_EQ(fcntl(self->fd, F_SETLK, &fl), -1);
+ EXPECT_EQ(errno, ENOLCK);
+ EXPECT_EQ(fcntl(self->fd, F_GETLK, &fl), -1);
+ EXPECT_EQ(errno, ENOLCK);
+}
+
+/* a lease is refused for the owner and for everybody else alike */
+TEST_F(mount_cover, not_leasable)
+{
+ struct delegation_req deleg = {
+ .d_type = F_RDLCK,
+ };
+
+ EXPECT_EQ(fcntl(self->fd, F_SETLEASE, F_RDLCK), -1);
+ EXPECT_TRUE(errno == EINVAL || errno == EACCES);
+ EXPECT_EQ(fcntl(self->fd, F_SETDELEG, &deleg), -1);
+ EXPECT_TRUE(errno == EINVAL || errno == EACCES);
+}
+
+TEST_F(mount_cover, not_mountable)
+{
+ char p[PATH_LEN];
+
+ snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd);
+ EXPECT_EQ(mount("tmpfs", p, "tmpfs", 0, NULL), -1);
+ EXPECT_EQ(errno, ENOENT);
+}
+
+TEST_F(mount_cover, read_only)
+{
+ struct statvfs sv;
+
+ EXPECT_EQ(mkdirat(self->fd, "x", 0755), -1);
+ EXPECT_EQ(errno, ENOENT);
+ EXPECT_EQ(fchmod(self->fd, 0777), -1);
+ EXPECT_EQ(errno, EROFS);
+ /* the immutable inode is checked before the read-only mount */
+ EXPECT_EQ(faccessat(self->fd, ".", W_OK, 0), -1);
+ EXPECT_EQ(errno, EPERM);
+ ASSERT_EQ(fstatvfs(self->fd, &sv), 0);
+ EXPECT_TRUE(sv.f_flag & ST_RDONLY);
+}
+
+/* the stand-in is a root of its own, ".." stays put */
+TEST_F(mount_cover, island)
+{
+ int fd;
+
+ fd = openat(self->fd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_TRUE(same_file(fd, self->fd));
+ close(fd);
+}
+
+/*
+ * C is alive for as long as the child holds it, but it can't be reached
+ * through P anymore and it's a root of its own as well.
+ */
+TEST_F(mount_cover, held_child_detached)
+{
+ struct statfs sf;
+ int fd;
+
+ ASSERT_EQ(fstatfs(self->cfd, &sf), 0);
+ EXPECT_EQ(sf.f_type, TMPFS_MAGIC);
+
+ fd = openat(self->cfd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_TRUE(same_file(fd, self->cfd));
+ close(fd);
+
+ fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstatfs(fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+ close(fd);
+}
+
+/*
+ * The cover goes with its mountpoint: once the child has removed C's
+ * mountpoint through the bind of P, the name is gone from P as well.
+ * What was opened through the cover before stays open.
+ */
+TEST_F(mount_cover, cover_goes_with_mountpoint)
+{
+ char cmd = CMD_RMDIR;
+ struct statfs sf;
+ int ret;
+
+ ASSERT_EQ(write(self->to_child, &cmd, 1), 1);
+ ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret));
+ ASSERT_EQ(ret, 0);
+
+ EXPECT_EQ(openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1);
+ EXPECT_EQ(errno, ENOENT);
+ ASSERT_EQ(fstatfs(self->fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+}
+
+/*
+ * A readdir of the stand-in holds nothing that others wait for: one holder
+ * sticks in the page fault of its buffer, and a create and a lookup of
+ * another come back meanwhile.
+ */
+TEST_F(mount_cover, readdir_blocks_nobody)
+{
+ bool stalled;
+
+ if (self->hold.uffd < 0)
+ SKIP(return, "test requires userfaultfd");
+ ASSERT_EQ(readdir_hold_check(&self->hold, self->fd, &stalled), 0);
+ EXPECT_FALSE(stalled);
+}
+
+/* where a file was mounted, the stand-in is an empty regular file */
+TEST_F(mount_cover, file_stand_in)
+{
+ char src[PATH_LEN], p[PATH_LEN];
+ struct statfs sf;
+ struct stat st;
+ char c;
+ int fd;
+
+ fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_TRUE(S_ISREG(st.st_mode));
+ ASSERT_EQ(fstatfs(fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+ /* the two stand-ins are two inodes */
+ EXPECT_FALSE(same_file(fd, self->fd));
+ EXPECT_EQ(read(fd, &c, 1), 0);
+ EXPECT_EQ(flock(fd, LOCK_EX | LOCK_NB), -1);
+ EXPECT_EQ(errno, ENOLCK);
+
+ snprintf(src, sizeof(src), "%s/src", self->base);
+ ASSERT_EQ(touch(src), 0);
+ snprintf(p, sizeof(p), "/proc/self/fd/%d", fd);
+ EXPECT_EQ(mount(src, p, NULL, MS_BIND, NULL), -1);
+ EXPECT_EQ(errno, ENOENT);
+ close(fd);
+
+ /* writes and truncates go nowhere, like /dev/null */
+ fd = openat(self->dfd, "file", O_WRONLY | O_TRUNC | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(write(fd, "xy", 2), 2);
+ EXPECT_EQ(ftruncate(fd, 4), 0);
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_size, 0);
+ /* the shared inode keeps its identity */
+ EXPECT_EQ(fchmod(fd, 0600), -1);
+ EXPECT_EQ(errno, EPERM);
+ close(fd);
+ fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(read(fd, &c, 1), 0);
+ close(fd);
+
+ EXPECT_EQ(openat(self->dfd, "file", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1);
+ EXPECT_EQ(errno, ENOTDIR);
+}
+
+/*
+ * Two unmounted parents left covers on the same dentry. A lookup on
+ * either finds a stand-in, and U's cover stays when T goes.
+ */
+TEST_F(mount_cover, shared_mountpoint)
+{
+ char cmd = CMD_CLOSE_T;
+ struct statfs sf;
+ int fd, ret;
+
+ fd = openat(self->tfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstatfs(fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+ close(fd);
+
+ fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstatfs(fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+ close(fd);
+
+ /* the last references to T go, with T its cover */
+ ASSERT_EQ(write(self->to_child, &cmd, 1), 1);
+ ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret));
+ ASSERT_EQ(ret, 0);
+ close(self->tfd);
+ self->tfd = -1;
+
+ fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstatfs(fd, &sf), 0);
+ EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c b/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c
new file mode 100644
index 000000000000..0928a584eddc
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A recursive bind mount of a namespace file that lives in another mount
+ * namespace copies whatever is stacked on top of it there. If that includes
+ * the file of the caller's own mount namespace, or of an older one, the copy
+ * would pin the namespace it is put in forever. The bind mount has to be
+ * refused, a plain bind mount of the file itself still works.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_UNSHARE, /* could not create the newer mount namespace */
+ CHILD_PIPE, /* the parent went away */
+ CHILD_TMPFS, /* could not mount the tmpfs in the new namespace */
+ CHILD_REC_ALLOWED, /* the recursive bind mount was not refused */
+ CHILD_REC_ERRNO, /* it was refused with the wrong error */
+ CHILD_PLAIN_REFUSED, /* the plain bind mount of the file was refused */
+};
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int create_file(const char *path)
+{
+ int fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
+
+ if (fd < 0)
+ return -1;
+ close(fd);
+ return 0;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ if (setgid(0) || setuid(0))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+static int send_msg(int fd, char msg)
+{
+ return write(fd, &msg, 1) == 1 ? 0 : -1;
+}
+
+static char recv_msg(int fd)
+{
+ char msg;
+
+ if (read(fd, &msg, 1) != 1)
+ return 0;
+ return msg;
+}
+
+FIXTURE(nsfs_rbind_loop) {
+ char dir[DIR_LEN];
+ char x[PATH_LEN];
+};
+
+FIXTURE_SETUP(nsfs_rbind_loop)
+{
+ snprintf(self->dir, sizeof(self->dir), "/tmp/nsfs_rbind_loop.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns()) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+ snprintf(self->x, sizeof(self->x), "%s/x", self->dir);
+ ASSERT_EQ(create_file(self->x), 0);
+}
+
+FIXTURE_TEARDOWN(nsfs_rbind_loop)
+{
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/*
+ * The child in the newer mount namespace binds the network namespace file
+ * mount of the parent through @fd. Recursively that would copy the mount of
+ * its own mount namespace file that the parent stacked on top.
+ */
+static int newer_ns_child(const char *dir, int fd, int to_parent, int from_parent)
+{
+ char src[32], y[PATH_LEN];
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ if (send_msg(to_parent, 'r') || recv_msg(from_parent) != 'g')
+ return CHILD_PIPE;
+
+ snprintf(y, sizeof(y), "%s/y", dir);
+ if (mount("tmpfs", y, "tmpfs", 0, NULL))
+ return CHILD_TMPFS;
+ snprintf(src, sizeof(src), "/proc/self/fd/%d", fd);
+ snprintf(y, sizeof(y), "%s/y/f", dir);
+ if (create_file(y))
+ return CHILD_TMPFS;
+
+ if (!mount(src, y, NULL, MS_BIND | MS_REC, NULL))
+ return CHILD_REC_ALLOWED;
+ if (errno != EINVAL)
+ return CHILD_REC_ERRNO;
+ if (mount(src, y, NULL, MS_BIND, NULL))
+ return CHILD_PLAIN_REFUSED;
+ umount2(y, MNT_DETACH);
+ return CHILD_OK;
+}
+
+TEST_F(nsfs_rbind_loop, own_ns_file_below_foreign_source)
+{
+ int to_child[2], to_parent[2], fd, status;
+ char p[PATH_LEN];
+ pid_t pid;
+
+ snprintf(p, sizeof(p), "%s/y", self->dir);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+
+ /* M, a mount of our network namespace file, held by a descriptor */
+ ASSERT_EQ(mount("/proc/self/ns/net", self->x, NULL, MS_BIND, NULL), 0);
+ fd = open(self->x, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(pipe(to_child), 0);
+ ASSERT_EQ(pipe(to_parent), 0);
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(to_child[1]);
+ close(to_parent[0]);
+ _exit(newer_ns_child(self->dir, fd, to_parent[1], to_child[0]));
+ }
+ close(to_child[0]);
+ close(to_parent[1]);
+ ASSERT_EQ(recv_msg(to_parent[0]), 'r');
+
+ /* the child's mount namespace file on top of M */
+ snprintf(p, sizeof(p), "/proc/%d/ns/mnt", pid);
+ ASSERT_EQ(mount(p, self->x, NULL, MS_BIND, NULL), 0);
+
+ ASSERT_EQ(send_msg(to_child[1], 'g'), 0);
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+
+ close(fd);
+ ASSERT_EQ(umount2(self->x, MNT_DETACH), 0);
+ ASSERT_EQ(umount2(self->x, MNT_DETACH), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c
new file mode 100644
index 000000000000..ea1b33fa0bf8
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A mount namespace file bind-mounted on top of the mount that is moved
+ * onto a shared mount isn't copied to the peers and slaves. The mount that
+ * already sits at the destination in a slave has to end up on top of the
+ * propagated copy, not below the root of the mount namespace file where no
+ * path walk ever finds it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.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 <sys/wait.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int create_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* the first bytes of the file at @path, "" if it can't be read */
+static const char *read_file(const char *path, char *buf, size_t len)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = read(fd, buf, len - 1);
+ close(fd);
+ }
+ buf[n > 0 ? n : 0] = '\0';
+ return buf;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ if (setgid(0) || setuid(0))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+FIXTURE(overmount_ns_file) {
+ char dir[DIR_LEN];
+ pid_t child;
+};
+
+FIXTURE_SETUP(overmount_ns_file)
+{
+ self->child = -1;
+ snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_ns_file.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns()) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(overmount_ns_file)
+{
+ if (self->child > 0) {
+ kill(self->child, SIGKILL);
+ waitpid(self->child, NULL, 0);
+ }
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/*
+ * A is a shared tmpfs and B its slave with Q, a bind mount of a file, on
+ * B/file. S is a bind mount of a file with N, a bind mount of a newer mount
+ * namespace's file, on top of it. S is moved onto A/file. Its copy S' lands
+ * on B/file below Q, without N. B/file keeps reading Q and once Q is
+ * unmounted it reads S'.
+ */
+TEST_F(overmount_ns_file, existing_mount_stays_on_top)
+{
+ char a[PATH_LEN], b[PATH_LEN], s[PATH_LEN], p[PATH_LEN], buf[16];
+ int fd, pfd[2];
+ char c;
+
+ snprintf(a, sizeof(a), "%s/A", self->dir);
+ snprintf(b, sizeof(b), "%s/B", self->dir);
+ snprintf(s, sizeof(s), "%s/s", self->dir);
+ ASSERT_EQ(mkdir(a, 0755), 0);
+ ASSERT_EQ(mkdir(b, 0755), 0);
+ ASSERT_EQ(mkdir(s, 0755), 0);
+
+ /* A shared, B its slave, Q on B/file */
+ ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0);
+ snprintf(p, sizeof(p), "%s/A/file", self->dir);
+ ASSERT_EQ(create_file(p, "A"), 0);
+ ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, b, NULL, MS_SLAVE, NULL), 0);
+ snprintf(p, sizeof(p), "%s/Q", self->dir);
+ ASSERT_EQ(create_file(p, "Q"), 0);
+ snprintf(b, sizeof(b), "%s/B/file", self->dir);
+ ASSERT_EQ(mount(p, b, NULL, MS_BIND, NULL), 0);
+
+ /* S on s/f, pinned by a file descriptor before N goes on top */
+ ASSERT_EQ(mount("tmpfs", s, "tmpfs", 0, NULL), 0);
+ snprintf(p, sizeof(p), "%s/s/f", self->dir);
+ ASSERT_EQ(create_file(p, "f"), 0);
+ snprintf(s, sizeof(s), "%s/s/S", self->dir);
+ ASSERT_EQ(create_file(s, "S"), 0);
+ ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0);
+ fd = open(p, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ /* a newer mount namespace whose file can be bound */
+ ASSERT_EQ(pipe(pfd), 0);
+ self->child = fork();
+ ASSERT_GE(self->child, 0);
+ if (self->child == 0) {
+ close(pfd[0]);
+ if (unshare(CLONE_NEWNS) || write(pfd[1], "r", 1) != 1)
+ _exit(1);
+ pause();
+ _exit(0);
+ }
+ close(pfd[1]);
+ ASSERT_EQ(read(pfd[0], &c, 1), 1);
+ close(pfd[0]);
+ snprintf(s, sizeof(s), "/proc/%d/ns/mnt", self->child);
+ ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0);
+
+ ASSERT_STREQ(read_file(b, buf, sizeof(buf)), "Q");
+
+ snprintf(a, sizeof(a), "%s/A/file", self->dir);
+ ASSERT_EQ(sys_move_mount(fd, "", AT_FDCWD, a, MOVE_MOUNT_F_EMPTY_PATH), 0);
+ close(fd);
+
+ /* Q is still on top of the copy in B and can be unmounted */
+ EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "Q");
+ EXPECT_EQ(umount2(b, 0), 0);
+ EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "S");
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c
new file mode 100644
index 000000000000..46a56b722f3e
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * propagate_umount() slides a surviving overmount off a mount that is being
+ * unmounted and unmounts that mount right after. If a file descriptor keeps
+ * the unmounted mount alive its ->overmount is left pointing at the moved
+ * mount, which is freed on its own schedule. move_mount(MOVE_MOUNT_BENEATH)
+ * with such a file descriptor as the target walks ->overmount in
+ * topmost_overmount() before it checks that the target is still mounted and
+ * must not step into the freed mount.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.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 "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#ifndef MOVE_MOUNT_BENEATH
+#define MOVE_MOUNT_BENEATH 0x00000200
+#endif
+
+#ifndef FSCONFIG_CMD_CREATE
+#define FSCONFIG_CMD_CREATE 6
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ if (setgid(0) || setuid(0))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+/* A detached tmpfs mount to serve as the source of a move. */
+static int detached_tmpfs(void)
+{
+ int sfd, mfd;
+
+ sfd = sys_fsopen("tmpfs", 0);
+ if (sfd < 0)
+ return -1;
+ if (sys_fsconfig(sfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) {
+ close(sfd);
+ return -1;
+ }
+ mfd = sys_fsmount(sfd, 0, 0);
+ close(sfd);
+ return mfd;
+}
+
+FIXTURE(overmount_reparent) {
+ char dir[DIR_LEN];
+};
+
+FIXTURE_SETUP(overmount_reparent)
+{
+ snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_reparent.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns()) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+}
+
+FIXTURE_TEARDOWN(overmount_reparent)
+{
+ char p[PATH_LEN];
+
+ snprintf(p, sizeof(p), "%s/b", self->dir);
+ umount2(p, MNT_DETACH);
+ snprintf(p, sizeof(p), "%s/a", self->dir);
+ umount2(p, MNT_DETACH);
+}
+
+/*
+ * A shared mount base_a is bind-mounted to base_b as its peer. A mount X on
+ * base_a/mp propagates a copy X_b onto base_b/mp. A file descriptor pins X_b,
+ * then X_b is made private and an overmount is stacked on its root.
+ *
+ * A lazy unmount of X propagates to X_b: X_b is committed to the unmount while
+ * its overmount survives, so propagate_umount() reparents the overmount onto
+ * base_b and unmounts X_b, which the file descriptor keeps alive. Unmounting
+ * the reparented overmount frees it. X_b->overmount now dangles unless
+ * mnt_change_mountpoint() reset it.
+ *
+ * move_mount(MOVE_MOUNT_BENEATH) through the file descriptor makes
+ * do_lock_mount() follow X_b->overmount in topmost_overmount() before it
+ * notices that X_b is no longer mounted. With the pointer reset the move fails
+ * cleanly with ENOENT; otherwise it reads the freed overmount.
+ */
+TEST_F(overmount_reparent, dead_overmount_holder_not_followed)
+{
+ char a[PATH_LEN], b[PATH_LEN], amp[PATH_LEN], bmp[PATH_LEN];
+ int fd, mfd, ret;
+
+ snprintf(a, sizeof(a), "%s/a", self->dir);
+ snprintf(b, sizeof(b), "%s/b", self->dir);
+ ASSERT_EQ(mkdir(a, 0755), 0);
+ ASSERT_EQ(mkdir(b, 0755), 0);
+
+ ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0);
+ ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0);
+
+ snprintf(amp, sizeof(amp), "%s/a/mp", self->dir);
+ snprintf(bmp, sizeof(bmp), "%s/b/mp", self->dir);
+ /* base_a and base_b share one superblock, so this dir is in both. */
+ ASSERT_EQ(mkdir(amp, 0755), 0);
+
+ /* X on base_a/mp propagates a copy X_b onto base_b/mp. */
+ ASSERT_EQ(mount("tmpfs", amp, "tmpfs", 0, NULL), 0);
+
+ /* Pin X_b before anything is stacked on top of it. */
+ fd = open(bmp, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ /* Keep the overmount local to X_b. */
+ ASSERT_EQ(mount(NULL, bmp, NULL, MS_PRIVATE, NULL), 0);
+
+ /* The overmount on X_b's root: X_b->overmount points at it. */
+ ASSERT_EQ(mount("tmpfs", bmp, "tmpfs", 0, NULL), 0);
+
+ /* Reparents the overmount onto base_b and unmounts X_b. */
+ ASSERT_EQ(umount2(amp, MNT_DETACH), 0);
+
+ /* Free the reparented overmount. */
+ ASSERT_EQ(umount2(bmp, MNT_DETACH), 0);
+ usleep(100000);
+
+ mfd = detached_tmpfs();
+ ASSERT_GE(mfd, 0);
+
+ ret = sys_move_mount(mfd, "", fd, "",
+ MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH |
+ MOVE_MOUNT_BENEATH);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, ENOENT);
+
+ close(mfd);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/settings b/tools/testing/selftests/filesystems/mount_cycle/settings
new file mode 100644
index 000000000000..694d70710ff0
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c b/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c
new file mode 100644
index 000000000000..233f8ea92efc
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c
@@ -0,0 +1,484 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An unmounted mount tree that a file descriptor keeps alive is put and
+ * vacated behind the root's back, so nothing may walk it under
+ * namespace_sem: it can't be copied recursively and its propagation can't
+ * be changed. The mount at its root alone can still be copied and that copy
+ * is an ordinary mount.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/mount.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#ifndef __NR_mount_setattr
+#define __NR_mount_setattr 442
+#endif
+
+#ifndef __NR_pidfd_open
+#define __NR_pidfd_open 434
+#endif
+
+#define PATH_LEN 64
+
+static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags,
+ struct mount_attr *attr, size_t size)
+{
+ return syscall(__NR_mount_setattr, dfd, path, flags, attr, size);
+}
+
+static inline int sys_pidfd_open(pid_t pid, unsigned int flags)
+{
+ return syscall(__NR_pidfd_open, pid, flags);
+}
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int touch(const char *path)
+{
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+ if (fd < 0)
+ return -1;
+ close(fd);
+ return 0;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ if (setgid(0) || setuid(0))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+/* A private mount namespace of our own, all mounts private. */
+static int own_mntns(void)
+{
+ if (unshare(CLONE_NEWNS))
+ return -1;
+ return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
+}
+
+FIXTURE(unmounted_tree) {
+ char dir[PATH_LEN];
+};
+
+FIXTURE_SETUP(unmounted_tree)
+{
+ snprintf(self->dir, sizeof(self->dir), "/tmp/unmounted_tree.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns()) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(unmounted_tree)
+{
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* A recursive copy of the mount @fd refers to must fail with EINVAL. */
+static void assert_not_walked(struct __test_metadata *_metadata, int fd,
+ const char *target)
+{
+ char link[PATH_LEN];
+ int tfd;
+
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+ EXPECT_EQ(mount(link, target, NULL, MS_BIND | MS_REC, NULL), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_CLONE |
+ OPEN_TREE_CLOEXEC);
+ EXPECT_LT(tfd, 0);
+ EXPECT_EQ(errno, EINVAL);
+ if (tfd >= 0)
+ close(tfd);
+}
+
+/* The mount @fd refers to can be copied on its own and mounted on @target. */
+static void assert_root_copied(struct __test_metadata *_metadata, int fd,
+ const char *target)
+{
+ int tfd;
+
+ tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+ ASSERT_EQ(sys_move_mount(tfd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH), 0);
+ close(tfd);
+ ASSERT_EQ(umount2(target, MNT_DETACH), 0);
+}
+
+/*
+ * A lazily unmounted bind mount of an nsfs file, kept alive by an fd, can't
+ * be copied recursively anymore. Copying the mount itself still works and
+ * so does copying a mounted bind mount of the same file.
+ */
+TEST_F(unmounted_tree, detached_nsfs_bind_not_walked)
+{
+ char x[PATH_LEN], y[PATH_LEN], z[PATH_LEN], link[PATH_LEN];
+ int fd, tfd;
+
+ snprintf(x, sizeof(x), "%s/x", self->dir);
+ snprintf(y, sizeof(y), "%s/y", self->dir);
+ snprintf(z, sizeof(z), "%s/z", self->dir);
+ ASSERT_EQ(touch(x), 0);
+ ASSERT_EQ(touch(y), 0);
+ ASSERT_EQ(touch(z), 0);
+
+ ASSERT_EQ(mount("/proc/self/ns/net", x, NULL, MS_BIND, NULL), 0);
+ fd = open(x, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(umount2(x, MNT_DETACH), 0);
+
+ assert_not_walked(_metadata, fd, y);
+
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+ ASSERT_EQ(mount(link, y, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(umount2(y, MNT_DETACH), 0);
+ assert_root_copied(_metadata, fd, y);
+ /* opening it is fine too */
+ tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+ close(tfd);
+ close(fd);
+
+ ASSERT_EQ(mount("/proc/self/ns/net", y, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(y, z, NULL, MS_BIND | MS_REC, NULL), 0);
+ tfd = sys_open_tree(AT_FDCWD, y, AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+ close(tfd);
+}
+
+/* Bind-mount the pidfd @pidfd on @target the way pidfd_bind_mount does. */
+static int bind_pidfd(int pidfd, const char *target)
+{
+ int tfd, ret;
+
+ tfd = sys_open_tree(pidfd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (tfd < 0)
+ return -1;
+ ret = sys_move_mount(tfd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH);
+ close(tfd);
+ return ret;
+}
+
+/* The same for a bind mount of a pidfd. */
+TEST_F(unmounted_tree, detached_pidfs_bind_not_walked)
+{
+ char x[PATH_LEN], y[PATH_LEN];
+ int pidfd, fd, tfd;
+
+ snprintf(x, sizeof(x), "%s/x", self->dir);
+ snprintf(y, sizeof(y), "%s/y", self->dir);
+ ASSERT_EQ(touch(x), 0);
+ ASSERT_EQ(touch(y), 0);
+
+ pidfd = sys_pidfd_open(getpid(), 0);
+ ASSERT_GE(pidfd, 0);
+ ASSERT_EQ(bind_pidfd(pidfd, x), 0);
+ fd = open(x, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(umount2(x, MNT_DETACH), 0);
+
+ assert_not_walked(_metadata, fd, y);
+ assert_root_copied(_metadata, fd, y);
+ close(fd);
+
+ ASSERT_EQ(bind_pidfd(pidfd, y), 0);
+ tfd = sys_open_tree(AT_FDCWD, y, AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+ close(tfd);
+ close(pidfd);
+}
+
+/* What a child in its own mount namespace reports back. */
+enum {
+ CHILD_OK,
+ CHILD_NS, /* could not set up the mount namespace */
+ CHILD_MOUNT, /* could not set up the mounts */
+ CHILD_PIPE, /* the parent went away */
+ CHILD_WALKED, /* the detached tree was copied recursively */
+ CHILD_ROOT_REFUSED, /* the mount at its root wasn't copied */
+ CHILD_STILL_MOUNTED, /* the copy survived the unlink of its mountpoint */
+ CHILD_ERRNO, /* refused, but not with EINVAL */
+};
+
+static const char *child_reason(int code)
+{
+ static const char *const reasons[] = {
+ [CHILD_OK] = "ok",
+ [CHILD_NS] = "could not set up the mount namespace",
+ [CHILD_MOUNT] = "could not set up the mounts",
+ [CHILD_PIPE] = "the parent went away",
+ [CHILD_WALKED] = "the detached tree was copied recursively",
+ [CHILD_ROOT_REFUSED] = "the mount at its root wasn't copied",
+ [CHILD_STILL_MOUNTED] = "the copy survived the unlink of its mountpoint",
+ [CHILD_ERRNO] = "refused, but not with EINVAL",
+ };
+
+ if (code < 0 || code >= (int)(sizeof(reasons) / sizeof(reasons[0])))
+ return "child died";
+ return reasons[code];
+}
+
+struct child_args {
+ const char *x; /* the nsfs bind mount goes here */
+ const char *s; /* a file to bind on top of it */
+ const char *y; /* a target to copy to */
+};
+
+/*
+ * Run @fn in a child in a mount namespace of its own. Once the child
+ * reports that its mounts are in place, unlink @victim here, where it is a
+ * plain file, and let the child carry on. Returns what the child reported.
+ */
+static int run_child(struct __test_metadata *_metadata,
+ int (*fn)(const struct child_args *, int, int),
+ const struct child_args *a, const char *victim)
+{
+ int to_parent[2], to_child[2], status;
+ pid_t pid;
+ char c;
+
+ if (pipe(to_parent) || pipe(to_child))
+ return -1;
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (pid == 0) {
+ close(to_parent[0]);
+ close(to_child[1]);
+ _exit(fn(a, to_parent[1], to_child[0]));
+ }
+ close(to_parent[1]);
+ close(to_child[0]);
+ if (read(to_parent[0], &c, 1) == 1) {
+ /* not a mountpoint in this namespace, so the file can go */
+ EXPECT_EQ(unlink(victim), 0);
+ EXPECT_EQ(write(to_child[1], "", 1), 1);
+ }
+ close(to_parent[0]);
+ close(to_child[1]);
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/*
+ * An nsfs bind mount on @x, an fd on it and a bind mount of @s on top. Once
+ * the parent has unlinked @x underneath, the first mount is detached and the
+ * second one, which stayed attached to it, has been vacated. The tree may
+ * not be walked, the mount at its root may still be copied.
+ */
+static int vacant_child(const struct child_args *a, int to_parent, int from_parent)
+{
+ char link[PATH_LEN];
+ int fd, tfd;
+ char c;
+
+ if (own_mntns())
+ return CHILD_NS;
+ if (mount("/proc/self/ns/net", a->x, NULL, MS_BIND, NULL))
+ return CHILD_MOUNT;
+ fd = open(a->x, O_PATH | O_CLOEXEC);
+ if (fd < 0 || mount(a->s, a->x, NULL, MS_BIND, NULL))
+ return CHILD_MOUNT;
+
+ if (write(to_parent, "", 1) != 1 || read(from_parent, &c, 1) != 1)
+ return CHILD_PIPE;
+
+ tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_CLONE |
+ OPEN_TREE_CLOEXEC);
+ if (tfd >= 0) {
+ close(tfd);
+ return CHILD_WALKED;
+ }
+ if (errno != EINVAL)
+ return CHILD_ERRNO;
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+ if (!mount(link, a->y, NULL, MS_BIND | MS_REC, NULL))
+ return CHILD_WALKED;
+ if (errno != EINVAL)
+ return CHILD_ERRNO;
+
+ tfd = sys_open_tree(fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (tfd < 0)
+ return CHILD_ROOT_REFUSED;
+ close(tfd);
+ /* the last reference on the detached mount collects the vacant one */
+ close(fd);
+ return CHILD_OK;
+}
+
+/*
+ * unlink() of the child's mountpoint from here, where it is a plain file,
+ * detaches the child's nsfs bind mount with the mount on top left attached
+ * to it. The mount on top loses its last reference right there and is
+ * vacated. The child must not be able to walk that tree.
+ */
+TEST_F(unmounted_tree, vacant_child_not_copied)
+{
+ char x[PATH_LEN], s[PATH_LEN], y[PATH_LEN];
+ struct child_args a = { .x = x, .s = s, .y = y };
+ int ret;
+
+ snprintf(x, sizeof(x), "%s/x", self->dir);
+ snprintf(s, sizeof(s), "%s/s", self->dir);
+ snprintf(y, sizeof(y), "%s/y", self->dir);
+ ASSERT_EQ(touch(x), 0);
+ ASSERT_EQ(touch(s), 0);
+ ASSERT_EQ(touch(y), 0);
+
+ ret = run_child(_metadata, vacant_child, &a, x);
+ ASSERT_EQ(ret, CHILD_OK)
+ TH_LOG("child: %s", child_reason(ret));
+}
+
+/*
+ * An nsfs bind mount on @x, lazily unmounted but kept alive by an fd, and a
+ * copy of it on @y. That copy is a mount like any other: once the parent has
+ * unlinked @y underneath, it is unmounted and its attributes can't be changed
+ * anymore. A copy that took the flag along is only unhooked, stays in the
+ * namespace and still takes the change.
+ */
+static int ordinary_copy(const struct child_args *a, int to_parent, int from_parent)
+{
+ struct mount_attr attr = {
+ .attr_set = MOUNT_ATTR_NOSUID,
+ };
+ char link[PATH_LEN];
+ int fd, fdy;
+ char c;
+
+ if (own_mntns())
+ return CHILD_NS;
+ if (mount("/proc/self/ns/net", a->x, NULL, MS_BIND, NULL))
+ return CHILD_MOUNT;
+ fd = open(a->x, O_PATH | O_CLOEXEC);
+ if (fd < 0 || umount2(a->x, MNT_DETACH))
+ return CHILD_MOUNT;
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+ if (mount(link, a->y, NULL, MS_BIND, NULL))
+ return CHILD_MOUNT;
+ fdy = open(a->y, O_PATH | O_CLOEXEC);
+ if (fdy < 0)
+ return CHILD_MOUNT;
+
+ if (write(to_parent, "", 1) != 1 || read(from_parent, &c, 1) != 1)
+ return CHILD_PIPE;
+
+ if (!sys_mount_setattr(fdy, "", AT_EMPTY_PATH, &attr, sizeof(attr)))
+ return CHILD_STILL_MOUNTED;
+ if (errno != EINVAL)
+ return CHILD_ERRNO;
+ close(fdy);
+ close(fd);
+ return CHILD_OK;
+}
+
+/*
+ * A copy of a detached nsfs bind mount must not take after its source and
+ * read as unmounted. unlink() of its mountpoint from here unmounts it like
+ * any other mount, so the child can't change its attributes anymore.
+ */
+TEST_F(unmounted_tree, copy_of_detached_bind_is_ordinary)
+{
+ char x[PATH_LEN], y[PATH_LEN];
+ struct child_args a = { .x = x, .y = y };
+ int ret;
+
+ snprintf(x, sizeof(x), "%s/x", self->dir);
+ snprintf(y, sizeof(y), "%s/y", self->dir);
+ ASSERT_EQ(touch(x), 0);
+ ASSERT_EQ(touch(y), 0);
+
+ ret = run_child(_metadata, ordinary_copy, &a, y);
+ ASSERT_EQ(ret, CHILD_OK)
+ TH_LOG("child: %s", child_reason(ret));
+}
+
+/*
+ * mount_setattr() may change the propagation of a detached tree while that
+ * tree is still the root of an anonymous mount namespace, but not once the
+ * tree has been dissolved and only a second fd keeps its root alive.
+ */
+TEST_F(unmounted_tree, detached_tree_setattr_refused)
+{
+ struct mount_attr attr = {
+ .propagation = MS_SHARED,
+ };
+ char a[PATH_LEN], b[PATH_LEN];
+ int tfd, fd;
+
+ snprintf(a, sizeof(a), "%s/a", self->dir);
+ snprintf(b, sizeof(b), "%s/a/b", self->dir);
+ ASSERT_EQ(mkdir(a, 0755), 0);
+ ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mkdir(b, 0755), 0);
+ ASSERT_EQ(mount("tmpfs", b, "tmpfs", 0, NULL), 0);
+
+ tfd = sys_open_tree(AT_FDCWD, self->dir, AT_RECURSIVE | OPEN_TREE_CLONE |
+ OPEN_TREE_CLOEXEC);
+ ASSERT_GE(tfd, 0);
+ ASSERT_EQ(sys_mount_setattr(tfd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), 0);
+ attr.propagation = MS_PRIVATE;
+ ASSERT_EQ(sys_mount_setattr(tfd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), 0);
+
+ /* a second reference on the root, then dissolve the tree */
+ fd = sys_open_tree(tfd, "", AT_EMPTY_PATH | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ close(tfd);
+
+ attr.propagation = MS_SHARED;
+ ASSERT_EQ(sys_mount_setattr(fd, "", AT_EMPTY_PATH | AT_RECURSIVE, &attr, sizeof(attr)), -1);
+ ASSERT_EQ(errno, EINVAL);
+ attr.propagation = MS_PRIVATE;
+ ASSERT_EQ(sys_mount_setattr(fd, "", AT_EMPTY_PATH, &attr, sizeof(attr)), -1);
+ ASSERT_EQ(errno, EINVAL);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
index fb12b93fbcaa..76f95c0ae5ef 100644
--- a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
+++ b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
@@ -1 +1,2 @@
open_tree_ns_test
+open_tree_ns_covered_test
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/Makefile b/tools/testing/selftests/filesystems/open_tree_ns/Makefile
index 4976ed1d7d4a..fb2aa77b6edb 100644
--- a/tools/testing/selftests/filesystems/open_tree_ns/Makefile
+++ b/tools/testing/selftests/filesystems/open_tree_ns/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
-TEST_GEN_PROGS := open_tree_ns_test
+TEST_GEN_PROGS := open_tree_ns_test open_tree_ns_covered_test
CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS := -lcap
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c
new file mode 100644
index 000000000000..27f5cddfb9ef
--- /dev/null
+++ b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * open_tree(OPEN_TREE_NAMESPACE) by a caller that isn't privileged over the
+ * mount namespace it copies from must not reveal what the mounts below the
+ * copied mount cover. Without AT_RECURSIVE the copy is refused when there's
+ * anything mounted below the requested directory. With AT_RECURSIVE
+ * unbindable mounts are copied as well.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_NAMESPACE
+#define OPEN_TREE_NAMESPACE (1 << 1)
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_USERNS, /* could not create the child user namespace */
+ CHILD_NONREC_ALLOWED, /* the non-recursive copy was not refused */
+ CHILD_NONREC_ERRNO, /* it was refused with the wrong error */
+ CHILD_REC_REFUSED, /* the recursive copy failed */
+ CHILD_SETNS, /* could not enter the new mount namespace */
+ CHILD_NO_COVER, /* the covering mount is missing in the copy */
+ CHILD_REVEALED, /* the covered file is visible in the copy */
+};
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int create_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Become root in a new user namespace, the uid @uid is mapped to 0. */
+static int enter_userns(uid_t uid, gid_t gid)
+{
+ char map[32];
+
+ if (unshare(CLONE_NEWUSER))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ return setgid(0) || setuid(0) ? -1 : 0;
+}
+
+FIXTURE(open_tree_ns_covered) {
+ char dir[DIR_LEN];
+ char cover[PATH_LEN];
+};
+
+FIXTURE_VARIANT(open_tree_ns_covered) {
+ int propagation;
+};
+
+FIXTURE_VARIANT_ADD(open_tree_ns_covered, private_cover) {
+ .propagation = MS_PRIVATE,
+};
+
+FIXTURE_VARIANT_ADD(open_tree_ns_covered, unbindable_cover) {
+ .propagation = MS_UNBINDABLE,
+};
+
+/*
+ * Root in a user namespace owns a private mount namespace with a tmpfs
+ * on @dir and a second tmpfs covering @dir/covered/under.txt.
+ */
+FIXTURE_SETUP(open_tree_ns_covered)
+{
+ char p[PATH_LEN];
+ int fd;
+
+ snprintf(self->dir, sizeof(self->dir), "/tmp/open_tree_ns_covered.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns(getuid(), getgid()) || unshare(CLONE_NEWNS)) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+
+ /* the flag has to exist before its rules can be checked */
+ fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+ if (fd < 0 && (errno == EINVAL || errno == ENOSYS)) {
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+ SKIP(return, "OPEN_TREE_NAMESPACE not supported");
+ }
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ snprintf(self->cover, sizeof(self->cover), "%s/covered", self->dir);
+ ASSERT_EQ(mkdir(self->cover, 0755), 0);
+ snprintf(p, sizeof(p), "%s/covered/under.txt", self->dir);
+ ASSERT_EQ(create_file(p, "hidden"), 0);
+ ASSERT_EQ(mount("tmpfs", self->cover, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, self->cover, NULL, variant->propagation, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(open_tree_ns_covered)
+{
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* A caller in a new user namespace that doesn't own the mount namespace. */
+static int foreign_child(const char *dir)
+{
+ struct stat st;
+ int fd;
+
+ if (enter_userns(0, 0))
+ return CHILD_USERNS;
+
+ fd = sys_open_tree(AT_FDCWD, dir, OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+ if (fd >= 0)
+ return CHILD_NONREC_ALLOWED;
+ if (errno != EINVAL)
+ return CHILD_NONREC_ERRNO;
+
+ fd = sys_open_tree(AT_FDCWD, dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+ if (fd < 0)
+ return CHILD_REC_REFUSED;
+ if (setns(fd, CLONE_NEWNS))
+ return CHILD_SETNS;
+ if (stat("/covered", &st))
+ return CHILD_NO_COVER;
+ if (!access("/covered/under.txt", F_OK) || errno != ENOENT)
+ return CHILD_REVEALED;
+ return CHILD_OK;
+}
+
+TEST_F(open_tree_ns_covered, foreign_user_namespace)
+{
+ int status, fd;
+ pid_t pid;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(foreign_child(self->dir));
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+
+ /* the owner of the mount namespace keeps bind mount semantics */
+ fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/overlayfs/.gitignore b/tools/testing/selftests/filesystems/overlayfs/.gitignore
index 077f7a128168..b343cc430051 100644
--- a/tools/testing/selftests/filesystems/overlayfs/.gitignore
+++ b/tools/testing/selftests/filesystems/overlayfs/.gitignore
@@ -2,3 +2,4 @@
dev_in_maps
set_layers_via_fds
idmapped_mounts
+automount_in_layer
diff --git a/tools/testing/selftests/filesystems/overlayfs/Makefile b/tools/testing/selftests/filesystems/overlayfs/Makefile
index b3185f684add..382a59bda5e7 100644
--- a/tools/testing/selftests/filesystems/overlayfs/Makefile
+++ b/tools/testing/selftests/filesystems/overlayfs/Makefile
@@ -9,6 +9,7 @@ LOCAL_HDRS += ../wrappers.h log.h
TEST_GEN_PROGS := dev_in_maps
TEST_GEN_PROGS += set_layers_via_fds
TEST_GEN_PROGS += idmapped_mounts
+TEST_GEN_PROGS += automount_in_layer
include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c b/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c
new file mode 100644
index 000000000000..0476c4582bdf
--- /dev/null
+++ b/tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A layer of an overlay is a private clone of the mount it was given and
+ * belongs to no mount namespace. fanotify hands out descriptors on it. An
+ * automount triggered through one has no namespace to go into: the open has
+ * to fail, not oops with namespace_sem held.
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/magic.h>
+#include <sys/fanotify.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/vfs.h>
+
+#include "../../kselftest_harness.h"
+
+#define DIR_LEN 64
+#define PATH_LEN 192
+
+static bool have_fs(const char *name)
+{
+ char line[128];
+ bool found = false;
+ FILE *f;
+
+ f = fopen("/proc/filesystems", "re");
+ if (!f)
+ return false;
+ while (fgets(line, sizeof(line), f)) {
+ char *nl = strchr(line, '\n');
+ char *tab = strchr(line, '\t');
+
+ if (nl)
+ *nl = 0;
+ if (tab && !strcmp(tab + 1, name))
+ found = true;
+ }
+ fclose(f);
+ return found;
+}
+
+static int mnt_id_of(int fd)
+{
+ char path[64], buf[4096], *p;
+ ssize_t n;
+ int info;
+
+ snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd);
+ info = open(path, O_RDONLY | O_CLOEXEC);
+ if (info < 0)
+ return -1;
+ n = read(info, buf, sizeof(buf) - 1);
+ close(info);
+ if (n <= 0)
+ return -1;
+ buf[n] = 0;
+ p = strstr(buf, "mnt_id:");
+ return p ? atoi(p + strlen("mnt_id:")) : -1;
+}
+
+static bool on_debugfs(int fd)
+{
+ struct statfs sf;
+
+ return !fstatfs(fd, &sf) && sf.f_type == DEBUGFS_MAGIC;
+}
+
+FIXTURE(layer) {
+ char base[DIR_LEN];
+ int fan;
+ int evfd; /* on the layer clone of the lower debugfs */
+};
+
+FIXTURE_SETUP(layer)
+{
+ char lower[PATH_LEN], other[PATH_LEN], ovl[PATH_LEN], opts[2 * PATH_LEN + 16];
+ struct fanotify_event_metadata *ev;
+ struct pollfd pfd;
+ char buf[4096];
+ int lfd, lower_id;
+ ssize_t n;
+ DIR *d;
+
+ self->fan = -1;
+ self->evfd = -1;
+ if (geteuid())
+ SKIP(return, "test requires root");
+ if (!have_fs("debugfs") || !have_fs("tracefs"))
+ SKIP(return, "test requires debugfs with the tracefs automount");
+ if (!have_fs("overlay"))
+ SKIP(return, "test requires overlayfs");
+
+ snprintf(self->base, sizeof(self->base), "/tmp/layer.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, "mode=0755"), 0);
+ snprintf(lower, sizeof(lower), "%s/lower", self->base);
+ snprintf(other, sizeof(other), "%s/other", self->base);
+ snprintf(ovl, sizeof(ovl), "%s/ovl", self->base);
+ ASSERT_EQ(mkdir(lower, 0755), 0);
+ ASSERT_EQ(mkdir(other, 0755), 0);
+ ASSERT_EQ(mkdir(ovl, 0755), 0);
+ ASSERT_EQ(mount("debugfs", lower, "debugfs", 0, NULL), 0);
+ snprintf(opts, sizeof(opts), "lowerdir=%s:%s", lower, other);
+ ASSERT_EQ(mount("overlay", ovl, "overlay", MS_RDONLY, opts), 0);
+
+ self->fan = fanotify_init(FAN_CLASS_NOTIF | FAN_NONBLOCK | FAN_CLOEXEC,
+ O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(self->fan, 0);
+ ASSERT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
+ FAN_OPEN | FAN_ONDIR, AT_FDCWD, lower), 0);
+ lfd = open(lower, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(lfd, 0);
+ lower_id = mnt_id_of(lfd);
+ close(lfd);
+
+ /* the overlay opens its lower directory through the layer clone */
+ d = opendir(ovl);
+ ASSERT_NE(d, NULL);
+ closedir(d);
+ pfd.fd = self->fan;
+ pfd.events = POLLIN;
+ ASSERT_EQ(poll(&pfd, 1, 5000), 1);
+ n = read(self->fan, buf, sizeof(buf));
+ ASSERT_GT(n, 0);
+ for (ev = (void *)buf; FAN_EVENT_OK(ev, n); ev = FAN_EVENT_NEXT(ev, n)) {
+ if (ev->fd < 0)
+ continue;
+ if (self->evfd < 0 && on_debugfs(ev->fd) &&
+ mnt_id_of(ev->fd) != lower_id)
+ self->evfd = ev->fd;
+ else
+ close(ev->fd);
+ }
+ ASSERT_GE(self->evfd, 0)
+ TH_LOG("no event on the layer clone");
+}
+
+FIXTURE_TEARDOWN(layer)
+{
+ if (self->evfd >= 0)
+ close(self->evfd);
+ if (self->fan >= 0)
+ close(self->fan);
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+TEST_F(layer, automount_below_the_clone_is_refused)
+{
+ struct stat st;
+ int fd;
+
+ /* the automount point is there */
+ ASSERT_EQ(fstatat(self->evfd, "tracing", &st, AT_NO_AUTOMOUNT), 0);
+ /* the clone is in no namespace, so the automount has nowhere to go */
+ fd = openat(self->evfd, "tracing", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ EXPECT_LT(fd, 0);
+ EXPECT_EQ(errno, EINVAL);
+ if (fd >= 0)
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/readdir_hold.h b/tools/testing/selftests/filesystems/readdir_hold.h
new file mode 100644
index 000000000000..57eee1bef470
--- /dev/null
+++ b/tools/testing/selftests/filesystems/readdir_hold.h
@@ -0,0 +1,224 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Hold a readdir of a directory in the page fault of its buffer and see
+ * whether a create and a lookup in that directory wait for it. For a
+ * directory that is permanently empty they must not.
+ */
+#ifndef __SELFTESTS_READDIR_HOLD_H
+#define __SELFTESTS_READDIR_HOLD_H
+
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/userfaultfd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+
+#define HOLD_FAULT_MS 5000 /* for the reader to reach the fault */
+#define HOLD_QUEUE_MS 2000 /* for the create to queue up behind it */
+#define HOLD_LOOKUP_MS 5000 /* for the lookup to come back */
+
+struct readdir_hold {
+ int uffd;
+ int taskdir; /* /proc/self/task, from before any namespace change */
+ long page_size;
+ char *page; /* faults until released */
+ int dfd;
+ pid_t creator_tid;
+ int created;
+ int done[2]; /* the finder writes a byte when it is back */
+};
+
+/*
+ * The fault happens in the kernel, so the userfaultfd needs CAP_SYS_PTRACE
+ * in the initial user namespace or vm.unprivileged_userfaultfd. Call this
+ * before entering a user namespace.
+ */
+static inline int readdir_hold_init(struct readdir_hold *h)
+{
+ struct uffdio_api api = { .api = UFFD_API };
+
+ h->page = MAP_FAILED;
+ h->page_size = sysconf(_SC_PAGESIZE);
+ h->taskdir = open("/proc/self/task", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ h->uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+ if (h->uffd < 0 || h->taskdir < 0 || ioctl(h->uffd, UFFDIO_API, &api)) {
+ if (h->uffd >= 0)
+ close(h->uffd);
+ if (h->taskdir >= 0)
+ close(h->taskdir);
+ h->uffd = h->taskdir = -1;
+ return -1;
+ }
+ return 0;
+}
+
+static inline void readdir_hold_destroy(struct readdir_hold *h)
+{
+ if (h->uffd >= 0)
+ close(h->uffd);
+ if (h->taskdir >= 0)
+ close(h->taskdir);
+ h->uffd = h->taskdir = -1;
+}
+
+static inline void *readdir_hold_reader(void *arg)
+{
+ struct readdir_hold *h = arg;
+
+ /* the first byte written to the buffer faults until released */
+ syscall(__NR_getdents64, h->dfd, h->page, h->page_size);
+ return NULL;
+}
+
+static inline void *readdir_hold_creator(void *arg)
+{
+ struct readdir_hold *h = arg;
+
+ h->creator_tid = syscall(__NR_gettid);
+ /* takes the directory lock exclusive before it fails */
+ mkdirat(h->dfd, "x", 0755);
+ __atomic_store_n(&h->created, 1, __ATOMIC_RELEASE);
+ return NULL;
+}
+
+static inline void *readdir_hold_finder(void *arg)
+{
+ struct readdir_hold *h = arg;
+ int fd;
+
+ /* a lookup that misses the dcache takes the lock shared */
+ fd = openat(h->dfd, "no_such_name", O_RDONLY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+ if (write(h->done[1], "x", 1) != 1)
+ perror("readdir_hold: finder");
+ return NULL;
+}
+
+/* the creator is back, or waits in the kernel for the lock */
+static inline bool readdir_hold_creator_settled(struct readdir_hold *h)
+{
+ char path[32], buf[256], *p;
+ ssize_t n;
+ int fd;
+
+ if (__atomic_load_n(&h->created, __ATOMIC_ACQUIRE))
+ return true;
+ if (!h->creator_tid)
+ return false;
+ snprintf(path, sizeof(path), "%d/stat", h->creator_tid);
+ fd = openat(h->taskdir, path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return false;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return false;
+ buf[n] = 0;
+ /* "pid (comm) state ..." */
+ p = strrchr(buf, ')');
+ return p && p[1] == ' ' && p[2] == 'D';
+}
+
+static inline bool readdir_hold_faulted(struct readdir_hold *h)
+{
+ struct pollfd pfd = { .fd = h->uffd, .events = POLLIN };
+ struct uffd_msg msg;
+
+ if (poll(&pfd, 1, HOLD_FAULT_MS) != 1)
+ return false;
+ if (read(h->uffd, &msg, sizeof(msg)) != sizeof(msg))
+ return false;
+ return msg.event == UFFD_EVENT_PAGEFAULT;
+}
+
+/* let the reader go on */
+static inline void readdir_hold_release(struct readdir_hold *h)
+{
+ struct uffdio_copy cp = {
+ .dst = (unsigned long)h->page,
+ .len = h->page_size,
+ };
+ void *zero;
+
+ zero = calloc(1, h->page_size);
+ if (!zero)
+ return;
+ cp.src = (unsigned long)zero;
+ if (ioctl(h->uffd, UFFDIO_COPY, &cp) && errno != EEXIST)
+ perror("readdir_hold: UFFDIO_COPY");
+ free(zero);
+}
+
+/*
+ * A readdir of @dfd that sticks in the fault of its buffer, then a create
+ * and a lookup in @dfd. Whether the lookup came back while the readdir was
+ * still stuck goes to @stalled. Returns -1 when that could not be found
+ * out.
+ */
+static inline int readdir_hold_check(struct readdir_hold *h, int dfd,
+ bool *stalled)
+{
+ pthread_t reader, creator, finder;
+ struct uffdio_register reg = {};
+ struct pollfd pfd;
+ int ret = -1, i;
+
+ h->dfd = dfd;
+ h->created = 0;
+ h->creator_tid = 0;
+ h->page = mmap(NULL, h->page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (h->page == MAP_FAILED)
+ return -1;
+ reg.range.start = (unsigned long)h->page;
+ reg.range.len = h->page_size;
+ reg.mode = UFFDIO_REGISTER_MODE_MISSING;
+ if (ioctl(h->uffd, UFFDIO_REGISTER, &reg) || pipe2(h->done, O_CLOEXEC))
+ goto out_page;
+
+ if (pthread_create(&reader, NULL, readdir_hold_reader, h))
+ goto out_pipe;
+ if (!readdir_hold_faulted(h))
+ goto out_reader;
+ if (pthread_create(&creator, NULL, readdir_hold_creator, h))
+ goto out_reader;
+ for (i = 0; i < HOLD_QUEUE_MS / 10 && !readdir_hold_creator_settled(h); i++)
+ usleep(10000);
+ if (pthread_create(&finder, NULL, readdir_hold_finder, h))
+ goto out_creator;
+
+ pfd.fd = h->done[0];
+ pfd.events = POLLIN;
+ *stalled = poll(&pfd, 1, HOLD_LOOKUP_MS) != 1;
+ ret = 0;
+
+ readdir_hold_release(h);
+ pthread_join(finder, NULL);
+out_creator:
+ if (ret)
+ readdir_hold_release(h);
+ pthread_join(creator, NULL);
+out_reader:
+ if (ret)
+ readdir_hold_release(h);
+ pthread_join(reader, NULL);
+out_pipe:
+ close(h->done[0]);
+ close(h->done[1]);
+out_page:
+ munmap(h->page, h->page_size);
+ h->page = MAP_FAILED;
+ return ret;
+}
+
+#endif /* __SELFTESTS_READDIR_HOLD_H */
diff --git a/tools/testing/selftests/filesystems/rw_hint_test.c b/tools/testing/selftests/filesystems/rw_hint_test.c
new file mode 100644
index 000000000000..d1930f82f63b
--- /dev/null
+++ b/tools/testing/selftests/filesystems/rw_hint_test.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * F_SET_RW_HINT is refused on an immutable inode. Nothing is ever written
+ * to it and it may be shared with everybody, like a namespace file or the
+ * root of an empty mount namespace.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+
+/* <linux/fs.h> and <linux/fcntl.h> don't mix with the libc headers */
+#ifndef FS_IOC_GETFLAGS
+#define FS_IOC_GETFLAGS _IOR('f', 1, long)
+#define FS_IOC_SETFLAGS _IOW('f', 2, long)
+#endif
+#ifndef FS_IMMUTABLE_FL
+#define FS_IMMUTABLE_FL 0x00000010
+#endif
+#ifndef F_LINUX_SPECIFIC_BASE
+#define F_LINUX_SPECIFIC_BASE 1024
+#endif
+#ifndef F_GET_RW_HINT
+#define F_GET_RW_HINT (F_LINUX_SPECIFIC_BASE + 11)
+#define F_SET_RW_HINT (F_LINUX_SPECIFIC_BASE + 12)
+#endif
+#ifndef RWH_WRITE_LIFE_SHORT
+#define RWH_WRITE_LIFE_SHORT 2
+#endif
+#ifndef UNSHARE_EMPTY_MNTNS
+#define UNSHARE_EMPTY_MNTNS 0x00100000
+#endif
+
+static int set_hint(int fd, uint64_t hint)
+{
+ return fcntl(fd, F_SET_RW_HINT, &hint);
+}
+
+static long get_hint(int fd)
+{
+ uint64_t hint;
+
+ if (fcntl(fd, F_GET_RW_HINT, &hint))
+ return -1;
+ return hint;
+}
+
+TEST(immutable_file)
+{
+ char path[] = "/tmp/rw_hint.XXXXXX";
+ int fd, flags;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ fd = mkstemp(path);
+ ASSERT_GE(fd, 0);
+ unlink(path);
+ ASSERT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0);
+ EXPECT_EQ(get_hint(fd), RWH_WRITE_LIFE_SHORT);
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) {
+ close(fd);
+ SKIP(return, "no file attributes on this filesystem");
+ }
+ flags |= FS_IMMUTABLE_FL;
+ ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1);
+ EXPECT_EQ(errno, EPERM);
+ flags &= ~FS_IMMUTABLE_FL;
+ ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0);
+ close(fd);
+}
+
+TEST(namespace_file)
+{
+ int fd;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1);
+ EXPECT_EQ(errno, EPERM);
+ close(fd);
+}
+
+TEST(empty_mntns_root)
+{
+ int status;
+ pid_t pid;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ int fd;
+
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ _exit(errno == EINVAL ? 100 : 1);
+ fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd < 0)
+ _exit(2);
+ if (set_hint(fd, RWH_WRITE_LIFE_SHORT) == 0)
+ _exit(3);
+ _exit(errno == EPERM ? 0 : 4);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 100)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/umount_propagation/Makefile b/tools/testing/selftests/filesystems/umount_propagation/Makefile
new file mode 100644
index 000000000000..31f783a92b23
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := umount_propagation_test shrink_submounts_test locked_mount_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c
new file mode 100644
index 000000000000..0799b2ba545d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c
@@ -0,0 +1,432 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * MNT_LOCKED keeps the owner of a user namespace from revealing what a
+ * mount covers. The lock has to be set on the copy in that namespace and
+ * only there, it has to survive the expiry of a mount placed beneath the
+ * locked one, and it has to survive the propagated umount of such a copy.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
+#endif
+#ifndef MOVE_MOUNT_BENEATH
+#define MOVE_MOUNT_BENEATH 0x00000200
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 192
+#define FLAGS (MS_NOSUID | MS_NODEV | MS_NOEXEC)
+
+/* exit codes of the children, each test says what they mean */
+enum {
+ CHILD_OK,
+ CHILD_SETUP,
+ CHILD_STEP1,
+ CHILD_STEP2,
+ CHILD_STEP3,
+ CHILD_STEP4,
+};
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+static int create_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Root in a new user namespace with a copy of the mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ prctl(PR_SET_DUMPABLE, 1);
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return -1;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return -1;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return -1;
+ return setgid(0) || setuid(0) ? -1 : 0;
+}
+
+static int wait_child(pid_t pid)
+{
+ int status;
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+static int wait_byte(int fd)
+{
+ char c;
+
+ return read(fd, &c, 1) == 1 ? 0 : -1;
+}
+
+static int send_byte(int fd)
+{
+ return write(fd, "x", 1) == 1 ? 0 : -1;
+}
+
+/* A read of @path fails: the file is covered. */
+static bool covered(const char *path)
+{
+ int fd = open(path, O_RDONLY | O_CLOEXEC);
+
+ if (fd >= 0)
+ close(fd);
+ return fd < 0;
+}
+
+FIXTURE(locked_mount) {
+ char base[DIR_LEN];
+ bool tracing; /* debugfs with the tracefs automount is there */
+};
+
+FIXTURE_SETUP(locked_mount)
+{
+ char p[PATH_LEN];
+ struct stat st;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ snprintf(self->base, sizeof(self->base), "/tmp/locked_mount.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, "mode=0755"), 0);
+
+ /* an automount that a user can name: the tracefs below debugfs */
+ snprintf(p, sizeof(p), "%s/dbg", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ self->tracing = !mount("debugfs", p, "debugfs", FLAGS, NULL);
+ if (self->tracing) {
+ /* the cases trigger it themselves */
+ snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
+ self->tracing = !fstatat(AT_FDCWD, p, &st, AT_NO_AUTOMOUNT) &&
+ S_ISDIR(st.st_mode);
+ }
+}
+
+FIXTURE_TEARDOWN(locked_mount)
+{
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+/*
+ * The child keeps a directory descriptor on the host's debugfs mount, moves
+ * to a user namespace of its own and triggers the automount through the
+ * descriptor. The mount goes below the host's mount and propagates into the
+ * child's copy. The child's copy has to be locked, the host's not.
+ */
+static int automount_child(const char *base, int dfd, int to_host, int from_host)
+{
+ char p[PATH_LEN];
+ struct stat st;
+
+ if (enter_userns())
+ return CHILD_SETUP;
+ if (fstatat(dfd, "tracing/.", &st, 0))
+ return CHILD_STEP1;
+ if (send_byte(to_host) || wait_byte(from_host))
+ return CHILD_SETUP;
+ /* the flags of the copy are locked: EPERM */
+ snprintf(p, sizeof(p), "%s/dbg/tracing", base);
+ if (!mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL) || errno != EPERM)
+ return CHILD_STEP2;
+ return CHILD_OK;
+}
+
+TEST_F(locked_mount, automount_locked_in_the_triggering_namespace)
+{
+ int to_host[2], from_host[2], dfd, ret;
+ char p[PATH_LEN];
+ pid_t pid;
+
+ if (!self->tracing)
+ SKIP(return, "test requires debugfs with the tracefs automount");
+
+ snprintf(p, sizeof(p), "%s/dbg", self->base);
+ ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
+ dfd = open(p, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(dfd, 0);
+ ASSERT_EQ(pipe(to_host), 0);
+ ASSERT_EQ(pipe(from_host), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(automount_child(self->base, dfd, to_host[1], from_host[0]));
+ close(dfd);
+ ASSERT_EQ(wait_byte(to_host[0]), 0);
+
+ /* the host's own mount isn't locked: the flags can go */
+ snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
+ EXPECT_EQ(mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL), 0);
+
+ ASSERT_EQ(send_byte(from_host[1]), 0);
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+/*
+ * A copy of a tree with a locked cover. The child puts a shrinkable mount
+ * beneath the cover, which hands the lock down, unmounts the cover and then
+ * asks for the umount of an unlocked ancestor. That expires the shrinkable
+ * mount on a kernel that doesn't look at the lock, and the covered
+ * directory is bare.
+ */
+static int expiry_child(const char *base)
+{
+ char srv[PATH_LEN], shr[PATH_LEN], x[PATH_LEN], c[PATH_LEN];
+ char hidden[PATH_LEN], secret[PATH_LEN];
+
+ snprintf(srv, sizeof(srv), "%s/srv", base);
+ snprintf(shr, sizeof(shr), "%s/shr", base);
+ snprintf(x, sizeof(x), "%s/x", base);
+ snprintf(c, sizeof(c), "%s/c", base);
+ snprintf(hidden, sizeof(hidden), "%s/x/hidden", base);
+ snprintf(secret, sizeof(secret), "%s/x/hidden/secret", base);
+
+ if (enter_userns())
+ return CHILD_SETUP;
+ if (mount(srv, x, NULL, MS_BIND | MS_REC, NULL) ||
+ mount(shr, c, NULL, MS_BIND, NULL))
+ return CHILD_SETUP;
+ /* the cover is locked */
+ if (!umount2(hidden, 0) || errno != EINVAL)
+ return CHILD_STEP1;
+ if (syscall(__NR_move_mount, AT_FDCWD, c, AT_FDCWD, hidden, MOVE_MOUNT_BENEATH))
+ return CHILD_STEP2;
+ /* the lock moved down, the cover may go */
+ if (umount2(hidden, 0))
+ return CHILD_STEP3;
+ /* the holder of the lock may not, in any way */
+ if (!umount2(hidden, 0) || errno != EINVAL)
+ return CHILD_STEP4;
+ if (chdir(x))
+ return CHILD_SETUP;
+ umount2(x, 0);
+ return covered(secret) ? CHILD_OK : CHILD_STEP4;
+}
+
+TEST_F(locked_mount, expiry_leaves_a_locked_mount_alone)
+{
+ char p[PATH_LEN], q[PATH_LEN];
+ pid_t pid;
+ int ret;
+
+ if (!self->tracing)
+ SKIP(return, "test requires debugfs with the tracefs automount");
+
+ snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
+ snprintf(q, sizeof(q), "%s/shr", self->base);
+ ASSERT_EQ(mkdir(q, 0755), 0);
+ /* a bind of an automount is shrinkable as well */
+ ASSERT_EQ(mount(p, q, NULL, MS_BIND, NULL), 0);
+ snprintf(p, sizeof(p), "%s/srv", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
+ snprintf(p, sizeof(p), "%s/srv/hidden", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ snprintf(p, sizeof(p), "%s/srv/hidden/secret", self->base);
+ ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
+ snprintf(p, sizeof(p), "%s/srv/hidden", self->base);
+ ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
+ snprintf(p, sizeof(p), "%s/x", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ snprintf(p, sizeof(p), "%s/c", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(expiry_child(self->base));
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+/*
+ * The copy of the mount tree that a user namespace gets at its creation has
+ * the automounts in it locked unless they are on an expiry list. A bind of
+ * such a tree inside the namespace copies the lock to the automount but not
+ * to the root of the bind, so the child may ask for the umount of that root.
+ * That must not expire the locked automount below it: the root is busy, the
+ * umount fails and the automount has to be there afterwards.
+ */
+static int copied_tree_child(const char *base)
+{
+ char dbg[PATH_LEN], x[PATH_LEN], tracing[PATH_LEN];
+ struct stat root, st;
+
+ snprintf(dbg, sizeof(dbg), "%s/dbg", base);
+ snprintf(x, sizeof(x), "%s/x", base);
+ snprintf(tracing, sizeof(tracing), "%s/x/tracing", base);
+
+ if (enter_userns())
+ return CHILD_SETUP;
+ if (mount(dbg, x, NULL, MS_BIND | MS_REC, NULL))
+ return CHILD_SETUP;
+ /* the copy of the automount carries the lock */
+ if (!umount2(tracing, 0) || errno != EINVAL)
+ return CHILD_STEP1;
+ /* the root of the bind doesn't; keep it busy */
+ if (chdir(x))
+ return CHILD_SETUP;
+ if (!umount2(x, 0) || errno != EBUSY)
+ return CHILD_STEP2;
+ /* the automount below it must not have gone */
+ if (stat(x, &root) || fstatat(AT_FDCWD, tracing, &st, AT_NO_AUTOMOUNT))
+ return CHILD_SETUP;
+ return st.st_dev != root.st_dev ? CHILD_OK : CHILD_STEP3;
+}
+
+TEST_F(locked_mount, umount_of_a_bind_leaves_a_locked_automount_alone)
+{
+ char p[PATH_LEN];
+ struct stat st;
+ pid_t pid;
+ int ret;
+
+ if (!self->tracing)
+ SKIP(return, "test requires debugfs with the tracefs automount");
+
+ /* the copy the child gets has to contain the automount */
+ snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base);
+ ASSERT_EQ(stat(p, &st), 0);
+ snprintf(p, sizeof(p), "%s/x", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(copied_tree_child(self->base));
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+/*
+ * Z is a user namespace with a copy of a tree in which P/d is covered by a
+ * locked mount. The host mounts X on P/d, which propagates beneath Z's
+ * cover, and unmounts it again. Z's cover has to be locked afterwards as
+ * it was before.
+ */
+static int propagation_child(const char *base, int to_host, int from_host)
+{
+ char d[PATH_LEN], secret[PATH_LEN];
+
+ snprintf(d, sizeof(d), "%s/P/d", base);
+ snprintf(secret, sizeof(secret), "%s/P/d/secret", base);
+
+ if (enter_userns())
+ return CHILD_SETUP;
+ /* the cover is locked */
+ if (!umount2(d, 0) || errno != EINVAL)
+ return CHILD_STEP1;
+ if (send_byte(to_host) || wait_byte(from_host))
+ return CHILD_SETUP;
+ /* X came and went beneath it: still locked */
+ if (!umount2(d, 0) || errno != EINVAL)
+ return CHILD_STEP2;
+ return covered(secret) ? CHILD_OK : CHILD_STEP3;
+}
+
+TEST_F(locked_mount, propagated_copy_keeps_the_cover_locked)
+{
+ int to_host[2], from_host[2], ret;
+ char p[PATH_LEN];
+ pid_t pid;
+
+ snprintf(p, sizeof(p), "%s/P", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
+ ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
+ snprintf(p, sizeof(p), "%s/P/d", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ snprintf(p, sizeof(p), "%s/P/d/secret", self->base);
+ ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
+ ASSERT_EQ(pipe(to_host), 0);
+ ASSERT_EQ(pipe(from_host), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ /* M: a manager's namespace that covers P/d for Z */
+ pid_t z;
+
+ if (unshare(CLONE_NEWNS))
+ _exit(CHILD_SETUP);
+ snprintf(p, sizeof(p), "%s/P", self->base);
+ if (mount(NULL, p, NULL, MS_SLAVE, NULL))
+ _exit(CHILD_SETUP);
+ snprintf(p, sizeof(p), "%s/P/d", self->base);
+ if (mount("tmpfs", p, "tmpfs", 0, "mode=0755"))
+ _exit(CHILD_SETUP);
+ z = fork();
+ if (z < 0)
+ _exit(CHILD_SETUP);
+ if (z == 0)
+ _exit(propagation_child(self->base, to_host[1], from_host[0]));
+ _exit(wait_child(z));
+ }
+ ASSERT_EQ(wait_byte(to_host[0]), 0);
+
+ /* the host mounts on P/d and unmounts again; both propagate */
+ snprintf(p, sizeof(p), "%s/P/d", self->base);
+ ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
+ ASSERT_EQ(umount2(p, 0), 0);
+
+ ASSERT_EQ(send_byte(from_host[1]), 0);
+ ret = wait_child(pid);
+ TH_LOG("child exit code %d", ret);
+ EXPECT_EQ(ret, CHILD_OK);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c
new file mode 100644
index 000000000000..b024ae3417ce
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A synchronous umount first unmounts the shrinkable submounts of the
+ * victim that aren't busy. Every one of them has to be checked right
+ * before it is unmounted: unmounting one can slide a busy mount to where
+ * the propagated copy of the next one is looked up.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/fanotify.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/vfs.h>
+#include <linux/magic.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef FAN_REPORT_MNT
+#define FAN_REPORT_MNT 0x00004000
+#endif
+#ifndef FAN_MARK_MNTNS
+#define FAN_MARK_MNTNS 0x00000110
+#endif
+#ifndef FAN_MNT_ATTACH
+#define FAN_MNT_ATTACH 0x01000000
+#endif
+#ifndef FAN_MNT_DETACH
+#define FAN_MNT_DETACH 0x02000000
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+FIXTURE(shrink_submounts) {
+ char base[DIR_LEN];
+ char automount[PATH_LEN];
+ bool mounted;
+ int fan;
+};
+
+/*
+ * Shrinkable mounts come from an automount. The tracefs mount below debugfs
+ * is one and bind mounts inherit the flag.
+ */
+FIXTURE_SETUP(shrink_submounts)
+{
+ struct stat st;
+ char p[PATH_LEN];
+
+ self->mounted = false;
+ self->fan = -1;
+
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/shrink_submounts.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+ self->mounted = true;
+ ASSERT_EQ(mount(NULL, self->base, NULL, MS_PRIVATE, NULL), 0);
+
+ snprintf(p, sizeof(p), "%s/dbg", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ if (mount("debugfs", p, "debugfs", 0, NULL)) {
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+ SKIP(return, "test requires debugfs");
+ }
+ snprintf(self->automount, sizeof(self->automount), "%s/dbg/tracing",
+ self->base);
+ snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base);
+ if (stat(p, &st)) {
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+ SKIP(return, "test requires the tracefs automount");
+ }
+}
+
+FIXTURE_TEARDOWN(shrink_submounts)
+{
+ if (self->fan >= 0)
+ close(self->fan);
+ chdir("/");
+ if (self->mounted)
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static bool mounted_tmpfs(const char *path)
+{
+ struct statfs st;
+
+ return !statfs(path, &st) && st.f_type == TMPFS_MAGIC;
+}
+
+/*
+ * P is a shared bind mount of the automount, P1 a slave of P that is shared
+ * in turn and P2 its peer. B, another bind mount of the automount, goes on
+ * P/options and propagates copies Bc1 and Bc2 onto P1/options and
+ * P2/options. R, a tmpfs and our working directory, sits on top of Bc2 which
+ * is made private first. P, with B on it, is moved to P1/instances.
+ *
+ * A synchronous umount of P1 unmounts the shrinkable submounts Bc1 and B
+ * first. Unmounting Bc1 takes Bc2 along and slides R to P2/options where
+ * the propagated copy of B is looked up next. R is busy, so B has to stay
+ * and the umount fails with EBUSY.
+ */
+TEST_F(shrink_submounts, busy_mount_moved_into_reach)
+{
+ char p[PATH_LEN], p1[PATH_LEN], p2[PATH_LEN], r[PATH_LEN], cwd[PATH_LEN];
+ int nsfd;
+
+ snprintf(p, sizeof(p), "%s/p", self->base);
+ snprintf(p1, sizeof(p1), "%s/p1", self->base);
+ snprintf(p2, sizeof(p2), "%s/p2", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(mkdir(p1, 0755), 0);
+ ASSERT_EQ(mkdir(p2, 0755), 0);
+
+ /* watch the mount namespace so that the detached mounts get queued */
+ self->fan = fanotify_init(FAN_REPORT_MNT, O_RDONLY);
+ if (self->fan >= 0) {
+ nsfd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(nsfd, 0);
+ EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD | FAN_MARK_MNTNS,
+ FAN_MNT_ATTACH | FAN_MNT_DETACH, nsfd, NULL), 0);
+ close(nsfd);
+ }
+
+ ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
+ ASSERT_EQ(mount(p, p1, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, p1, NULL, MS_SLAVE, NULL), 0);
+ ASSERT_EQ(mount(NULL, p1, NULL, MS_SHARED, NULL), 0);
+ ASSERT_EQ(mount(p1, p2, NULL, MS_BIND, NULL), 0);
+
+ /* B on P/options, copies on P1/options and P2/options */
+ snprintf(r, sizeof(r), "%s/p/options", self->base);
+ ASSERT_EQ(mount(self->automount, r, NULL, MS_BIND, NULL), 0);
+
+ /* R on top of Bc2 */
+ snprintf(r, sizeof(r), "%s/p2/options", self->base);
+ ASSERT_EQ(mount(NULL, r, NULL, MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("R", r, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(chdir(r), 0);
+
+ /* P, with B on it, below the victim */
+ snprintf(cwd, sizeof(cwd), "%s/p1/instances", self->base);
+ ASSERT_EQ(mount(p, cwd, NULL, MS_MOVE, NULL), 0);
+
+ ASSERT_TRUE(mounted_tmpfs(r));
+ ASSERT_EQ(umount2(p1, 0), -1);
+ EXPECT_EQ(errno, EBUSY);
+
+ /* R is still mounted and still our working directory */
+ EXPECT_TRUE(mounted_tmpfs(r));
+ ASSERT_NE(getcwd(cwd, sizeof(cwd)), NULL);
+ EXPECT_STREQ(cwd, r);
+}
+
+/*
+ * T is shared and Q, a slave of T, has T moved into it, so Q receives
+ * propagation from its own child. M, another bind mount of the automount, is
+ * on T/options and that is the dentry T sits on in Q. Unmounting M makes T
+ * the propagated victim at that dentry in Q and takes T along. The shrink
+ * walk of V has just unmounted M and continues in the children of T.
+ */
+TEST_F(shrink_submounts, parent_goes_with_child)
+{
+ char v[PATH_LEN], t[PATH_LEN], q[PATH_LEN], p[PATH_LEN];
+ struct stat before, after;
+
+ snprintf(v, sizeof(v), "%s/v", self->base);
+ snprintf(t, sizeof(t), "%s/v/t", self->base);
+ snprintf(q, sizeof(q), "%s/v/q", self->base);
+ ASSERT_EQ(mkdir(v, 0755), 0);
+ ASSERT_EQ(mount("V", v, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, v, NULL, MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(stat(v, &before), 0);
+ ASSERT_EQ(mkdir(t, 0755), 0);
+ ASSERT_EQ(mkdir(q, 0755), 0);
+
+ /* T shared, M on T/options before anything receives from T */
+ ASSERT_EQ(mount(self->automount, t, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, t, NULL, MS_SHARED, NULL), 0);
+ snprintf(p, sizeof(p), "%s/v/t/options", self->base);
+ ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0);
+
+ /* Q, a slave of T, and T moved into Q at the dentry M sits on */
+ ASSERT_EQ(mount(t, q, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, q, NULL, MS_SLAVE, NULL), 0);
+ snprintf(p, sizeof(p), "%s/v/q/options", self->base);
+ ASSERT_EQ(mount(t, p, NULL, MS_MOVE, NULL), 0);
+
+ /* M, T and then Q go, V is empty and can be unmounted */
+ ASSERT_EQ(umount2(v, 0), 0);
+ ASSERT_EQ(stat(v, &after), 0);
+ EXPECT_NE(before.st_dev, after.st_dev);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c
new file mode 100644
index 000000000000..9e18d54dfb32
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A synchronous umount fails with EBUSY when a mount it would pull out by
+ * propagation is still in use.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/mount.h>
+#include <linux/stat.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE 0x8000
+#endif
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
+#endif
+#ifndef MOVE_MOUNT_BENEATH
+#define MOVE_MOUNT_BENEATH 0x00000200
+#endif
+#ifndef STATX_MNT_ID
+#define STATX_MNT_ID 0x00001000U
+#endif
+
+static int sys_open_tree(int dfd, const char *filename, unsigned int flags)
+{
+ return syscall(__NR_open_tree, dfd, filename, flags);
+}
+
+static int sys_move_mount(int from_dfd, const char *from_pathname,
+ int to_dfd, const char *to_pathname,
+ unsigned int flags)
+{
+ return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd,
+ to_pathname, flags);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_UNSHARE, /* could not set up the slave namespace */
+ CHILD_OPEN_TREE, /* open_tree() failed */
+ CHILD_MOVE_MOUNT, /* move_mount() failed */
+ CHILD_STATX, /* statx() failed */
+ CHILD_PIPE, /* the parent went away */
+};
+
+/* Messages between parent and child. */
+enum {
+ MSG_READY = 'r', /* child: the copy is mounted and referenced */
+ MSG_CHECK = 'c', /* parent: check that the copy is still attached */
+ MSG_ATTACHED = 'a', /* child: it is */
+ MSG_DETACHED = 'd', /* child: it is not */
+ MSG_CLOSE = 'x', /* parent: drop the reference */
+ MSG_CLOSED = 'y', /* child: dropped */
+ MSG_EXIT = 'e', /* parent: done */
+};
+
+FIXTURE(umount_propagation)
+{
+ char base[64];
+ char victim[80];
+ bool mounted;
+};
+
+FIXTURE_SETUP(umount_propagation)
+{
+ self->mounted = false;
+
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/umount_propagation.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+ self->mounted = true;
+ ASSERT_EQ(mount(NULL, self->base, NULL, MS_SHARED, NULL), 0);
+
+ snprintf(self->victim, sizeof(self->victim), "%s/victim", self->base);
+ ASSERT_EQ(mkdir(self->victim, 0755), 0);
+ ASSERT_EQ(mount("tmpfs", self->victim, "tmpfs", 0, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(umount_propagation)
+{
+ if (self->mounted)
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static int send_msg(int fd, char msg)
+{
+ return write(fd, &msg, 1) == 1 ? 0 : -1;
+}
+
+static char recv_msg(int fd)
+{
+ char msg;
+
+ if (read(fd, &msg, 1) != 1)
+ return 0;
+ return msg;
+}
+
+/* Is the mount with id @mnt_id attached in this mount namespace? */
+static bool mount_attached(__u64 mnt_id)
+{
+ char line[4096];
+ bool found = false;
+ FILE *f;
+
+ f = fopen("/proc/self/mountinfo", "re");
+ if (!f)
+ return false;
+
+ while (fgets(line, sizeof(line), f)) {
+ if (strtoull(line, NULL, 10) == mnt_id) {
+ found = true;
+ break;
+ }
+ }
+ fclose(f);
+ return found;
+}
+
+/*
+ * The slave namespace: take a detached copy of the shared tree and move it
+ * beneath the propagated copy of the victim, keeping the open_tree()
+ * descriptor as a reference on it.
+ */
+static int slave_child(const char *base, const char *victim, int to_parent,
+ int from_parent)
+{
+ struct statx stx;
+ int fd;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ if (mount("", "/", NULL, MS_REC | MS_SLAVE, NULL))
+ return CHILD_UNSHARE;
+
+ fd = sys_open_tree(AT_FDCWD, base,
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+ if (fd < 0)
+ return CHILD_OPEN_TREE;
+ if (sys_move_mount(fd, "", AT_FDCWD, victim,
+ MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH))
+ return CHILD_MOVE_MOUNT;
+ if (statx(fd, "", AT_EMPTY_PATH, STATX_MNT_ID, &stx))
+ return CHILD_STATX;
+
+ if (send_msg(to_parent, MSG_READY) || recv_msg(from_parent) != MSG_CHECK)
+ return CHILD_PIPE;
+ if (send_msg(to_parent, mount_attached(stx.stx_mnt_id) ?
+ MSG_ATTACHED : MSG_DETACHED))
+ return CHILD_PIPE;
+
+ if (recv_msg(from_parent) != MSG_CLOSE)
+ return CHILD_PIPE;
+ close(fd);
+ if (send_msg(to_parent, MSG_CLOSED) || recv_msg(from_parent) != MSG_EXIT)
+ return CHILD_PIPE;
+ return CHILD_OK;
+}
+
+TEST_F(umount_propagation, busy_copy_pulled_out)
+{
+ int to_child[2], to_parent[2];
+ int status;
+ pid_t pid;
+
+ ASSERT_EQ(pipe(to_child), 0);
+ ASSERT_EQ(pipe(to_parent), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ close(to_child[1]);
+ close(to_parent[0]);
+ _exit(slave_child(self->base, self->victim, to_parent[1],
+ to_child[0]));
+ }
+ close(to_child[0]);
+ close(to_parent[1]);
+
+ ASSERT_EQ(recv_msg(to_parent[0]), MSG_READY);
+
+ /* the copy in the slave namespace is in use */
+ ASSERT_EQ(umount2(self->victim, 0), -1);
+ ASSERT_EQ(errno, EBUSY);
+
+ ASSERT_EQ(send_msg(to_child[1], MSG_CHECK), 0);
+ ASSERT_EQ(recv_msg(to_parent[0]), MSG_ATTACHED);
+
+ /* and once it is not, the umount goes through */
+ ASSERT_EQ(send_msg(to_child[1], MSG_CLOSE), 0);
+ ASSERT_EQ(recv_msg(to_parent[0]), MSG_CLOSED);
+ ASSERT_EQ(umount2(self->victim, 0), 0);
+
+ ASSERT_EQ(send_msg(to_child[1], MSG_EXIT), 0);
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c b/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c
index 12434415ec36..9c8fc8c7f62c 100644
--- a/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c
+++ b/tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c
@@ -146,17 +146,19 @@ static void null_endofword(char *word)
*word = '\0';
}
-static bool is_shared_mount(const char *path)
+/* Does the mount on @path carry the optional field @field in mountinfo? */
+static bool mount_has_field(const char *path, const char *field)
{
size_t len = 0;
char *line = NULL;
FILE *f = NULL;
+ bool found = false;
f = fopen("/proc/self/mountinfo", "re");
if (!f)
return false;
- while (getline(&line, &len, f) != -1) {
+ while (!found && getline(&line, &len, f) != -1) {
char *opts, *target;
target = get_field(line, 4);
@@ -172,15 +174,29 @@ static bool is_shared_mount(const char *path)
if (strcmp(target, path) != 0)
continue;
- null_endofword(opts);
- if (strstr(opts, "shared:"))
- return true;
+ /* the optional fields end at the "-" separator */
+ while (opts && *opts != '-') {
+ char *next = strchr(opts, ' ');
+
+ if (next)
+ *next++ = '\0';
+ if (!strncmp(opts, field, strlen(field))) {
+ found = true;
+ break;
+ }
+ opts = next;
+ }
}
free(line);
fclose(f);
- return false;
+ return found;
+}
+
+static bool is_shared_mount(const char *path)
+{
+ return mount_has_field(path, "shared:");
}
/* Attempt to de-conflict with the selftests tree. */
@@ -372,4 +388,50 @@ TEST_F(move_mount_set_group, complex_sharing_copying)
ASSERT_EQ(is_shared_mount(SET_GROUP_A), 1);
}
+#define SET_GROUP_B "/tmp/B"
+#define SET_GROUP_C "/tmp/C"
+
+/*
+ * An unbindable mount is neither shared nor a slave, so it must not be
+ * accepted as the target: with a slave source it would end up unbindable
+ * and a slave at the same time.
+ */
+TEST_F(move_mount_set_group, unbindable_target)
+{
+ bool ret;
+
+ ret = move_mount_set_group_supported();
+ ASSERT_GE(ret, 0);
+ if (!ret)
+ SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");
+
+ ASSERT_EQ(mount(NULL, SET_GROUP_A, NULL, MS_SHARED, 0), 0);
+
+ /* B: a slave of A's peer group */
+ ASSERT_EQ(mkdir(SET_GROUP_B, 0777), 0);
+ ASSERT_EQ(mount(SET_GROUP_A, SET_GROUP_B, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, SET_GROUP_B, NULL, MS_SLAVE, 0), 0);
+ ASSERT_TRUE(mount_has_field(SET_GROUP_B, "master:"));
+
+ /* C: unbindable */
+ ASSERT_EQ(mkdir(SET_GROUP_C, 0777), 0);
+ ASSERT_EQ(mount(SET_GROUP_A, SET_GROUP_C, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, SET_GROUP_C, NULL, MS_UNBINDABLE, 0), 0);
+ ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable"));
+
+ /* from a slave */
+ ASSERT_EQ(syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_B,
+ AT_FDCWD, SET_GROUP_C, MOVE_MOUNT_SET_GROUP), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_FALSE(mount_has_field(SET_GROUP_C, "master:"));
+ ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable"));
+
+ /* from a shared mount */
+ ASSERT_EQ(syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_A,
+ AT_FDCWD, SET_GROUP_C, MOVE_MOUNT_SET_GROUP), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_FALSE(mount_has_field(SET_GROUP_C, "shared:"));
+ ASSERT_TRUE(mount_has_field(SET_GROUP_C, "unbindable"));
+}
+
TEST_HARNESS_MAIN