Commit Graph

108670 Commits

Author SHA1 Message Date
Christian Brauner
214897acc8
Revert "namei: don't reveal overmounted entries in refwalk"
This reverts commit 8189ab688f.

This bug class is bigger than can be fixed with this simple patch.
Back out of it and give us some time to fix this properly.

Signed-off-by: Christian Brauner <brauner@kernel.org>
2026-10-16 10:50:57 +02:00
Christian Brauner
69fbe94f80
namespace: a mount that copy_tree() leaves out can't take over a lock
Since commit c62a476693 ("move_mount: transfer MNT_LOCKED") a mount
can be moved beneath a locked mount. The lock moves from the old cover
to the new mount which takes over the job of hiding what's under it and
the old cover can be unmounted. That works as long as the new mount
stays. But the caller picks the new mount and copy_tree() leaves a
mount of a mount namespace file out of every copy made without
CL_COPY_MNT_NS_FILE, together with everything mounted on it, and never
looks at MNT_LOCKED. unshare(CLONE_NEWNS) and mount propagation make
such copies.

So a task that is root in its own user namespace gets rid of a locked
cover on a file without a race and without touching the filesystem
below it. It makes a child with a mount namespace of its own, binds the
child's /proc/<pid>/ns/mnt beneath the cover with MOVE_MOUNT_BENEATH,
which hands the lock to that bind, and calls unshare(CLONE_NEWNS). The
new mount namespace has nothing on the file. That's the usual way to
mask a single file, a bind of /dev/null or of an empty file. With a
bind of an ordinary file the file stays covered.

copy_tree() already refuses to leave out a locked unbindable mount with
-EPERM since commit df7342b240 ("mount: Don't allow copying
MNT_UNBINDABLE|MNT_LOCKED mounts"). Do the same for a locked mount of a
mount namespace file. That also covers a tree copied with
CL_COPY_MNT_NS_FILE and attached into a mount namespace of another user
namespace where lock_mnt_tree() locks it. Note that unshare(CLONE_NEWNS)
and clone(CLONE_NEWNS) fail with EPERM from such a mount namespace from
then on. The mount can't be left out and it can't be unmounted.

And refuse to move a mount of a mount namespace file beneath a locked
mount in the first place. Otherwise the task ends up with a mount it
can't unmount and an unshare() that fails for good.

Chris also found a second mount that goes away by itself, a clone of an
automounted mount that shrink_submounts() took along with its parent.
Commit f78ddcc872 ("namespace: never expire a locked mount") took
care of that one.

Chris's reproducer is appended. Mode nsfile reads SECRET after the
unshare() on 7.3-rc7, with this patch the move_mount() fails with
EINVAL and the file stays covered. nsfile-control uses an ordinary file
and reads MASK both ways.

  // SPDX-License-Identifier: GPL-2.0
  /*
   * Get rid of a locked cover with MOVE_MOUNT_BENEATH.
   *
   * The "admin" (root) makes a tmpfs W:
   *     W/D         file, holds SECRET, covered by a bind of a file with MASK
   *     W/Dd/inner  file, holds SECRET, Dd is covered by a bind of a directory
   *                 whose "inner" holds MASK
   *     W/own       directory of uid 1000 with the file "mine" and the
   *                 directory "minedir", whose "inner" holds MINE
   *     W/dbg       debugfs, mode 755. dbg/tracing is an automount point
   * The "attacker" is uid 1000. It enters a new user namespace with a new mount
   * namespace, so W and both covers are locked there.
   *
   * usage: prog MODE
   *   plain      put a bind of own/mine beneath the cover of D, unmount the cover
   *   nsfile     put a bind of the mount namespace file of a child beneath the
   *              cover of D, then unshare(CLONE_NEWNS)
   *   nsfile-control    the same with a bind of own/mine
   *   shrink     put a bind of the automounted tracefs beneath the cover of Dd,
   *              unmount the cover, put a tmpfs beneath W, call umount(W), which fails
   *   shrink-control    the same with a bind of own/minedir
   * The environment variable AUTOMOUNT_DIR names another automount point for "shrink".
   */
  #define _GNU_SOURCE
  #include <errno.h>
  #include <fcntl.h>
  #include <grp.h>
  #include <sched.h>
  #include <signal.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  #include <sys/mount.h>
  #include <sys/prctl.h>
  #include <sys/stat.h>
  #include <sys/wait.h>

  #ifndef MOVE_MOUNT_BENEATH
  #define MOVE_MOUNT_BENEATH 0x00000200
  #endif
  #define W "/run/x2"

  static void die(const char *what)
  {
  	fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
  	exit(2);
  }

  static void put_file(const char *name, const char *content)
  {
  	int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, 0644);

  	if (fd < 0 || write(fd, content, strlen(content)) < 0)
  		die(name);
  	close(fd);
  }

  static void show(const char *path)
  {
  	char buf[32] = "";
  	int fd = open(path, O_RDONLY);

  	if (fd < 0) {
  		printf("attacker: open(%s) = %s\n", path, strerror(errno));
  		return;
  	}
  	if (read(fd, buf, sizeof(buf) - 1) < 0)
  		snprintf(buf, sizeof(buf), "<%s>", strerror(errno));
  	close(fd);
  	printf("attacker: read(%s) = \"%s\"%s\n", path, buf,
  	       strncmp(buf, "SECRET", 6) ? "" : "   <-- HIT");
  }

  static int say(const char *what, int ret)
  {
  	printf("attacker: %s: %s\n", what, ret ? strerror(errno) : "ok");
  	return ret;
  }

  static void write_str(const char *path, const char *s)
  {
  	int fd = open(path, O_WRONLY);

  	if (fd < 0 || write(fd, s, strlen(s)) < 0)
  		die(path);
  	close(fd);
  }

  static void mounts(void)
  {
  	if (system("grep x2 /proc/self/mountinfo"))
  		printf("attacker: no mounts listed\n");
  }

  static void admin_setup(void)
  {
  	if (unshare(CLONE_NEWNS) || mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
  		die("unshare");
  	mkdir(W, 0755);
  	if (mount("W", W, "tmpfs", 0, "mode=755"))
  		die("mount W");
  	if (mkdir(W "/Dd", 0755) || mkdir(W "/maskdir", 0755) || mkdir(W "/dbg", 0755) ||
  	    mkdir(W "/own", 0755) || mkdir(W "/own/minedir", 0755))
  		die("mkdir");
  	put_file(W "/D", "SECRET");
  	put_file(W "/Dd/inner", "SECRET");
  	put_file(W "/mask", "MASK");
  	put_file(W "/maskdir/inner", "MASK");
  	put_file(W "/own/mine", "MINE");
  	put_file(W "/own/minedir/inner", "MINE");
  	if (mount(W "/mask", W "/D", NULL, MS_BIND, NULL) ||
  	    mount(W "/maskdir", W "/Dd", NULL, MS_BIND, NULL))
  		die("covers");
  	if (mount("dbg", W "/dbg", "debugfs", 0, "mode=755"))
  		die("mount debugfs");
  }

  static void become_attacker(void)
  {
  	if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
  	    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1))
  		die("drop to uid 1000");
  	if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
  		die("unshare user and mount namespace");
  	write_str("/proc/self/setgroups", "deny");
  	write_str("/proc/self/uid_map", "0 1000 1");
  	write_str("/proc/self/gid_map", "0 1000 1");
  }

  /* clone the mount at @src and put the clone beneath the top mount at @where */
  static int beneath(const char *src, const char *where)
  {
  	char what[128];
  	int fd = open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
  	int ret;

  	snprintf(what, sizeof(what), "open_tree(%s, OPEN_TREE_CLONE)", src);
  	if (say(what, fd < 0))
  		return -1;
  	ret = move_mount(fd, "", AT_FDCWD, where,
  			 MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
  	snprintf(what, sizeof(what), "move_mount(clone, %s, MOVE_MOUNT_BENEATH)", where);
  	say(what, ret);
  	close(fd);
  	return ret;
  }

  static void do_nsfile(int control)
  {
  	char path[64] = W "/own/mine";
  	int pfd[2];
  	pid_t pid;
  	char c;

  	if (pipe(pfd))
  		die("pipe");
  	pid = fork();
  	if (pid < 0)
  		die("fork");
  	if (!pid) {
  		if (unshare(CLONE_NEWNS) || write(pfd[1], "g", 1) != 1)
  			die("child");
  		pause();
  		exit(0);
  	}
  	if (read(pfd[0], &c, 1) != 1)
  		die("sync");
  	if (!control)
  		snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid);
  	beneath(path, W "/D");
  	mounts();
  	say("unshare(CLONE_NEWNS)", unshare(CLONE_NEWNS));
  	show(W "/D");
  	mounts();
  	kill(pid, SIGKILL);
  	waitpid(pid, NULL, 0);
  }

  static void do_shrink(int control)
  {
  	const char *automount = getenv("AUTOMOUNT_DIR");
  	int fd, mfd;

  	if (!automount)
  		automount = W "/dbg/tracing";
  	beneath(control ? W "/own/minedir" : automount, W "/Dd");
  	say("umount2(Dd, 0), the old cover", umount2(W "/Dd", 0));
  	say("umount2(Dd, 0), the mount that was put beneath", umount2(W "/Dd", 0));
  	show(W "/Dd/inner");
  	say("umount2(W, 0)", umount2(W, 0));
  	fd = fsopen("tmpfs", FSOPEN_CLOEXEC);
  	if (fd < 0 || fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0))
  		die("fsopen tmpfs");
  	mfd = fsmount(fd, FSMOUNT_CLOEXEC, 0);
  	if (mfd < 0)
  		die("fsmount");
  	say("move_mount(new tmpfs, W, MOVE_MOUNT_BENEATH)",
  	    move_mount(mfd, "", AT_FDCWD, W, MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH));
  	close(mfd);
  	mounts();
  	say("umount2(W, 0)", umount2(W, 0));
  	show(W "/Dd/inner");
  	mounts();
  }

  int main(int argc, char **argv)
  {
  	const char *mode = argc > 1 ? argv[1] : "plain";

  	setvbuf(stdout, NULL, _IOLBF, 0);
  	admin_setup();
  	become_attacker();

  	show(W "/D");
  	show(W "/Dd/inner");
  	say("umount2(D, MNT_DETACH)", umount2(W "/D", MNT_DETACH));
  	say("umount2(Dd, MNT_DETACH)", umount2(W "/Dd", MNT_DETACH));

  	if (!strcmp(mode, "plain")) {
  		beneath(W "/own/mine", W "/D");
  		show(W "/D");
  		say("umount2(D, MNT_DETACH), the old cover", umount2(W "/D", MNT_DETACH));
  		show(W "/D");
  		say("umount2(D, MNT_DETACH), the mount that was put beneath",
  		    umount2(W "/D", MNT_DETACH));
  		show(W "/D");
  		mounts();
  	} else if (!strncmp(mode, "nsfile", 6)) {
  		do_nsfile(!strcmp(mode, "nsfile-control"));
  	} else {
  		do_shrink(!strcmp(mode, "shrink-control"));
  	}
  	return 0;
  }

Fixes: c62a476693 ("move_mount: transfer MNT_LOCKED")
Reported-by: Chris Mason <mason@kernel.org>
Cc: stable@vger.kernel.org # v7.1+
Link: https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-1-24035bd5d8e9@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:06 +02:00
Christian Brauner
98b6cf102b
namei: test for a negative dentry the way traverse_mounts() does
lookup_fast_for_open() only hands an O_CREAT open a cached dentry if
it's positive. Otherwise the open goes on to lookup_open(), which takes
the directory lock and creates the file. It tests for that with
!dentry->d_inode, but traverse_mounts() later decides with the type in
d_flags. And when a dentry is instantiated __d_set_inode_and_type()
stores ->d_inode first and only then releases the type into ->d_flags.

So a refwalk that took its reference to a negative dentry right before
another task instantiates it can find ->d_inode already set in
lookup_fast_for_open(), go on to step_into(), and still find the
negative type in traverse_mounts(). The open then fails with -ENOENT,
which an O_CREAT open must never do. rcuwalk doesn't get that far, the
d_seq check in step_into() catches the instantiation.

Use d_is_negative() so both look at the type. While the type is still
negative the open takes lookup_open() and waits for the directory lock
like any other O_CREAT open of a negative dentry.

On its own the window is a couple of instructions in the creating task,
so it takes an interrupt or a preempted vcpu in exactly the right spot.
I haven't caught it without help in about 600 million racing O_CREAT
opens.

The debug patch below stalls the creating task between the two stores
for names starting with "ocrace", the way an interrupt landing there
would. The reproducer below opens "ocrace" with O_CREAT through
symlinks on a strictatime tmpfs: following them updates their atime,
which drops the walk out of rcuwalk, so the name is always looked up in
refwalk. With

  echo 20 > /proc/sys/fs/ocrace_instantiate_us
  ./ocreat_enoent -t 60

on a KASAN kernel open() failed with -ENOENT 43 times in 4.9 million
calls. With this patch it never failed in 4.8 million calls. Tracing
step_into_slowpath() during such a run, each of the 98 -ENOENTs it
returned came right after the same task had entered it with a dentry
that was still typed negative.

  // SPDX-License-Identifier: GPL-2.0
  /*
   * ocreat_enoent: open(O_CREAT) racing the creation of the same name.
   *
   * Writers unlink "ocrace" and create it again. Readers open it with
   * O_CREAT through a symlink of their own on a strictatime tmpfs.
   * Following the symlink updates its atime, which drops the walk out of
   * rcu mode, so the name is always looked up in refwalk. An O_CREAT open
   * must never fail with ENOENT.
   *
   * -p arms a cpu-clock perf timer with that period in ns on every writer,
   * so that interrupts land at random points in it.
   *
   *   usage: ocreat_enoent [-t secs] [-w writers] [-r readers] [-p ns]
   */
  #define _GNU_SOURCE
  #include <errno.h>
  #include <fcntl.h>
  #include <pthread.h>
  #include <stdatomic.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <sys/mount.h>
  #include <sys/stat.h>
  #include <sys/syscall.h>
  #include <unistd.h>
  #include <linux/perf_event.h>

  #define DIR "/tmp/oc"

  static atomic_int stop;
  static atomic_long opens, creates, enoent, other;
  static long period;

  static void die(const char *what)
  {
  	fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
  	exit(2);
  }

  static void *writer(void *arg)
  {
  	(void)arg;
  	if (period) {
  		struct perf_event_attr attr = {
  			.type = PERF_TYPE_SOFTWARE,
  			.size = sizeof(attr),
  			.config = PERF_COUNT_SW_CPU_CLOCK,
  			.sample_period = period,
  		};

  		if (syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0) < 0)
  			die("perf_event_open");
  	}
  	while (!atomic_load(&stop)) {
  		int fd;

  		unlink(DIR "/ocrace");
  		fd = open(DIR "/ocrace", O_CREAT | O_RDWR | O_CLOEXEC, 0644);
  		if (fd >= 0)
  			close(fd);
  		atomic_fetch_add(&creates, 1);
  	}
  	return NULL;
  }

  static void *reader(void *arg)
  {
  	char lnk[64];

  	snprintf(lnk, sizeof(lnk), DIR "/lnk.%ld", (long)arg);
  	while (!atomic_load(&stop)) {
  		int fd = open(lnk, O_CREAT | O_RDWR | O_CLOEXEC, 0644);

  		if (fd >= 0)
  			close(fd);
  		else if (errno == ENOENT)
  			atomic_fetch_add(&enoent, 1);
  		else
  			atomic_fetch_add(&other, 1);
  		atomic_fetch_add(&opens, 1);
  	}
  	return NULL;
  }

  int main(int argc, char **argv)
  {
  	int secs = 60, nw = 4, nr = 12, c, n = 0;
  	pthread_t t[256];

  	while ((c = getopt(argc, argv, "t:w:r:p:")) != -1) {
  		switch (c) {
  		case 't': secs = atoi(optarg); break;
  		case 'w': nw = atoi(optarg); break;
  		case 'r': nr = atoi(optarg); break;
  		case 'p': period = atol(optarg); break;
  		default: return 2;
  		}
  	}
  	if (nw + nr > 256)
  		return 2;
  	mkdir(DIR, 0755);
  	if (mount("oc", DIR, "tmpfs", MS_STRICTATIME, "mode=0755"))
  		die("tmpfs");
  	for (long i = 0; i < nr; i++) {
  		char lnk[64];

  		snprintf(lnk, sizeof(lnk), DIR "/lnk.%ld", i);
  		if (symlink("ocrace", lnk))
  			die("symlink");
  	}
  	for (long i = 0; i < nw; i++)
  		if (pthread_create(&t[n++], NULL, writer, NULL))
  			die("writer");
  	for (long i = 0; i < nr; i++)
  		if (pthread_create(&t[n++], NULL, reader, (void *)i))
  			die("reader");
  	sleep(secs);
  	atomic_store(&stop, 1);
  	for (int i = 0; i < n; i++)
  		pthread_join(t[i], NULL);
  	printf("%ds: %ld opens, %ld creates, ENOENT %ld, other errors %ld\n",
  	       secs, atomic_load(&opens), atomic_load(&creates),
  	       atomic_load(&enoent), atomic_load(&other));
  	umount2(DIR, MNT_DETACH);
  	return atomic_load(&enoent) ? 1 : 0;
  }

  diff --git a/fs/dcache.c b/fs/dcache.c
  --- a/fs/dcache.c
  +++ b/fs/dcache.c
  @@ -32,9 +32,13 @@
   #include <linux/bit_spinlock.h>
   #include <linux/rculist_bl.h>
   #include <linux/list_lru.h>
  +#include <linux/delay.h>
   #include "internal.h"
   #include "mount.h"

  +/* debug: stall "ocrace*" names between ->d_inode and the type */
  +static int sysctl_ocrace_instantiate_us;
  +
   #include <asm/runtime-const.h>

   /*
  @@ -200,6 +204,13 @@
   }

   static const struct ctl_table fs_dcache_sysctls[] = {
  +	{
  +		.procname	= "ocrace_instantiate_us",
  +		.data		= &sysctl_ocrace_instantiate_us,
  +		.maxlen		= sizeof(int),
  +		.mode		= 0644,
  +		.proc_handler	= proc_dointvec,
  +	},
   	{
   		.procname	= "dentry-state",
   		.data		= &dentry_stat,
  @@ -405,6 +416,10 @@
   	unsigned flags;

   	dentry->d_inode = inode;
  +	/* debug: an interrupt landing right here */
  +	if (unlikely(READ_ONCE(sysctl_ocrace_instantiate_us)) &&
  +	    !strncmp(dentry->d_name.name, "ocrace", 6))
  +		udelay(READ_ONCE(sysctl_ocrace_instantiate_us));
   	flags = READ_ONCE(dentry->d_flags);
   	flags &= ~DCACHE_ENTRY_TYPE;
   	flags |= type_flags;

Fixes: e747e15156 ("fs: try an opportunistic lookup for O_CREAT opens too")
Cc: stable@vger.kernel.org # v6.12+
Link: https://patch.msgid.link/20261012-work-namei-ocreate-v1-1-c7883b17ee28@kernel.org
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:05 +02:00
Guanglei Zhu
99ae7dc439
ovl: reject lowerdir ending with unescaped backslash
In ovl_parse_param_split_lowerdirs() a backslash consumes the
character that follows it.  When the backslash is the last character
of the lowerdir value, it consumes the NUL terminator: the loop exits
normally and the backslash is kept in the output, so the lowerdir is
passed downstream with a trailing backslash and the mount fails later
with a confusing ENOENT from path lookup.

A trailing unescaped backslash escapes nothing, so reject it with
EINVAL.

Signed-off-by: Guanglei Zhu <zhugl3@xiaopeng.com>
Link: https://patch.msgid.link/20261012095538.938789-1-zhugl3@xiaopeng.com
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:05 +02:00
Kyle Zeng
36ebddca56
fsnotify: prevent mark additions during connector teardown
fsnotify_destroy_marks() keeps a reference to its current mark while it
drops conn->lock to destroy the mark. This stabilizes the forward-walk
cursor, but does not prevent a new mark from being inserted before it.
For example, an inotify watch re-added after IN_IGNORED sorts before the
old, detached mark for the same group. The walk can miss the replacement
and then detach the entire connector from the object.

The replacement is still on its group's marks_list and, for inotify, in
the watch IDR. The group mutex no longer protects the connector's object
from disappearing. An unprivileged fdinfo read racing the final detach
can consequently call igrab(NULL):

  BUG: KASAN: null-ptr-deref in igrab+0x24/0x100
  Write of size 4 at addr 0000000000000148
  Call Trace:
   igrab+0x24/0x100
   inotify_show_fdinfo+0x1dc/0x5e0
   seq_show+0x397/0x690

Set a DESTROYING flag under conn->lock before starting the walk, and
reject new marks on that connector with -ENOENT. The existing add-mark
error paths undo the temporary group-list entry and the backend's watch
allocation. Do not wait for teardown while holding the group mutex,
which the destroyer needs. Once the old connector is detached, an add
can install a fresh connector as before.

This closes the insertion race for all object types using the common
connector teardown, without changing the mark reference or SRCU lifetime
rules.

Fixes: 6b3f05d24d ("fsnotify: Detach mark from object list when last reference is dropped")
Cc: stable@vger.kernel.org
Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:05 +02:00
Christian Brauner
a291a5cf00
fs: drop the smp_mb()s that mnt_get_count() made redundant
Both mntput_no_expire_slowpath() and do_umount() have an smp_mb()
between taking mount_lock and summing up the reference count. They pair
with the smp_mb() in __legitimize_mnt().

A walker in __legitimize_mnt() increments the count and then re-reads
the seqcount of mount_lock. The unmounter writes the seqcount when it
takes mount_lock and then sums up the count. They must not both miss
each other's store. Otherwise the walker keeps a reference to a mount
that the unmounter finishes off. That's the store buffering pattern and
it needs a full barrier between the store and the load on both sides.

In commit 119e1ef80e ("fix __legitimize_mnt()/mntput() race") the
barrier in the slow path of mntput_no_expire() as added and the one in
do_umount() in commit 65781e19dc ("do_umount(): add missing barrier
before refcount checks in sync case"). Back then mnt_get_count() didn't
have a barrier of its own.

Since commit 7eb84d54fa ("fs: don't let a migrating task hide its
reference from do_umount()") it does. It sums up the puts, does an
smp_mb() and then sums up the gets. In program order that barrier sits
after the seqcount write in lock_mount_hash() and before the loads of
the gets. That's all the walker needs. If the sum misses the walker's
increment the walker sees the seqcount change and drops its reference
under mount_lock where it finds MNT_DOOMED or MNT_SYNC_UMOUNT.

The only thing the outer barrier orders on top of that is the seqcount
write against the loads of the puts and nothing relies on it:

(1) A put from the fast path of mntput_no_expire() never looks at the
    seqcount. If the sum misses such a put that's what the grace period
    is for.

(2) All other puts happen under mount_lock and are ordered by it.

(3) The slow path's own decrement right before the sum is a store on the
    same cpu and is read back in program order.

So the outer barriers don't order anything that the inner one doesn't.
That argument is made in the kernel memory model and every architecture
has to implement its barriers according to it. herd7 against
tools/memory-model agrees. Without the outer smp_mb() the walker against
the slow path and against the busy check in do_umount() is "Never". If
the inner smp_mb() goes as well it's "Sometimes". The litmus tests for
this are in the next patch.

Make the comments in __legitimize_mnt() and mnt_get_count() point at
each other.

Link: https://patch.msgid.link/20261011-work-mount-fixes-5-v1-1-a9b9a8ecf565@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:04 +02:00
Sung Byeongchan
65676cd420
autofs: ignore notification errors from a replaced pipe
autofs_notify_daemon() takes a reference to the current notification
pipe under wq_mutex, then drops the mutex before writing the request. The
write can block while the daemon enters catatonic mode and installs a
replacement pipe.

If the old pipe's reader is then closed, the delayed write returns -EPIPE
and the generic error path enters catatonic mode again. That transition
acts on the current superblock state, closes the replacement pipe, and
releases the new daemon generation's wait queues.

Only enter catatonic mode when the pipe which failed is still the current
pipe. Factor the already-locked transition so the identity check and state
change are atomic with respect to another replacement.

This was reproduced on v7.3-rc5-337-gff47652a4b66c in a local QEMU guest.
An unprivileged UID 65534 lookup filled the old packet pipe before a
normal CATATONIC/SETPIPEFD restart. On the unmodified kernel, the new pipe
received HUP and both a pending victim lookup and a later lookup failed
with ENOENT in two out of two fresh-mount runs.

With this change, the new pipe remained active, received an intact
304-byte request, and both victim lookups completed in two out of two
runs. Normal lookup and restart-without-a-blocked-writer controls passed.
No KASAN, oops, refcount, or lock diagnostic was observed. A source
reproducer and complete local logs are available privately on request.

The demonstrated impact is limited to denial of a shared autofs service
during a legitimate daemon restart. No memory corruption, information leak,
privilege escalation, or code execution primitive was observed.

Fixes: 8d7b48e0bc ("autofs4: add miscellaneous device for ioctls")
Cc: stable@vger.kernel.org
Signed-off-by: Sung Byeongchan <tjdqudcks0424@naver.com>
Link: https://patch.msgid.link/20261007173542.593624-1-tjdqudcks0424@naver.com
Acked-by: Ian Kent <raven@themaw.net>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:04 +02:00
Christian Brauner
56c3824de5
ntfs3: free replaced mount options after a grace period
ntfs_fs_reconfigure() replaces the old mount options with new mount
options in sbi->options. The old mount options land in fs_context and
are freed as soon as remount returns. With a pathwalk held up in
ntfs_nls_to_utf16() between loading sbi->options and dereferencing it
while a remount replaces them:

  BUG: KASAN: slab-use-after-free in ntfs_nls_to_utf16+0x7ae/0x7c0
  Read of size 8 at addr ff110001044fd448 by task walker/98
  Call Trace:
   <TASK>
   ntfs_nls_to_utf16+0x7ae/0x7c0
   ntfs_d_hash+0x22b/0x500
   link_path_walk+0x11ad/0x1d30
   path_lookupat+0x63/0xb00
   filename_lookup+0x1f1/0x5b0
   vfs_statx+0xd7/0x3b0
  [...]
  Allocated by task 95:
   __kmalloc_node_track_caller_noprof+0x2a2/0x730
   kmemdup_noprof+0x25/0x50
   ntfs_fill_super+0x1e5/0x4550
  [...]
  Freed by task 97:
   kfree+0x1db/0x5b0
   put_fs_context+0x13c/0x7d0
   path_mount+0x54a/0x2130
   __x64_sys_mount+0x20c/0x270

That info is needed in rcu lookup in the dentry hash and dentry compary
methods for ntfs3 and causes a UAF today. Delay freeing with an rcu
grace period.

ntfs_nls_to_utf16() reads ->nls and ->ads once under rcu_read_lock() so
the hooks pair with the release in ntfs_fs_reconfigure(). That also
covers callers outside of rcu pathwalk such as ntfs_lookup(). They don't
hold rcu_read_lock() themselves and wouldn't be protected by the grace
period otherwise. Remount refuses to change the iocharset so ->nls stays
valid after rcu_read_unlock().

Fixes: a3a956c78e ("fs/ntfs3: Add option "nocase"")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-8-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:04 +02:00
Christian Brauner
c01b091b06
ntfs3: free the sbi after a grace period
If the "nocase" mount option ntfs3 uses custom hash and dentry compare
functions. Both access ntfs3 superblock private information in rcu
pathwalk. ntfs_put_super() and ntfs3_kill_sb() drop the info without any
rcu delay. A lookup that's still inside the filesystem when the last
reference to the mount is dropped causes a UAF. With a pathwalk held up
in ntfs_d_hash() while the last reference to the mount goes away:

  BUG: KASAN: slab-use-after-free in ntfs_nls_to_utf16+0x752/0x940
  Read of size 8 at addr ff1100010453c850 by task walker/99
  Call Trace:
   <TASK>
   ntfs_nls_to_utf16+0x752/0x940
   ntfs_d_hash+0x22b/0x500
   link_path_walk+0x11ad/0x1d30
   path_lookupat+0x63/0xb00
   filename_lookup+0x1f1/0x5b0
   vfs_statx+0xd7/0x3b0
  [...]
  Allocated by task 95:
   __kmalloc_cache_noprof+0x249/0x560
   ntfs_init_fs_context+0x2db/0x5a0
   alloc_fs_context+0x52e/0xc70
  [...]
  Freed by task 98:
   kfree+0x1db/0x5b0
   deactivate_locked_super+0x9c/0xf0
   cleanup_mnt+0x9f/0x130
   task_work_run+0x131/0x250

The bug is currently masked by the grace period in namespace_unlock()
for umount(2). Keep the necessary information until the filesystem
private info is freed and free it with an rcu delay.

ntfs3_free_sbi() now runs from an rcu callback and drops the upcase
table via ntfs_put_shared(). So switch s_shared_lock over to
spin_lock_bh(). Otherwise the callback can deadlock against a task
holding the lock on the same cpu.

Fixes: a3a956c78e ("fs/ntfs3: Add option "nocase"")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-7-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:03 +02:00
Christian Brauner
a96c5299bd
unicode: free the map after a grace period
generic_ci_d_hash() and generic_ci_d_compare() read sb->s_encoding and
dereference the unicode map while a pathwalk in rcu mode holds nothing
but rcu_read_lock(). The filesystems that use them, ext4, f2fs and
tmpfs, drop the map with utf8_unload() from ->put_super(), which runs
when the last reference to the mount goes away. A lookup that is still
inside such a filesystem at that point reads a freed map.

With a pathwalk held up in utf8_casefold_hash() while the last reference
to a casefolded tmpfs goes away:

  BUG: KASAN: slab-use-after-free in utf8nlookup+0x74c/0x950
  Read of size 8 at addr ff110001030fe658 by task walker/98
  Call Trace:
   <TASK>
   utf8nlookup+0x74c/0x950
   utf8byte+0x160/0x1560
   utf8_casefold_hash+0x167/0x230
   generic_ci_d_hash+0xdd/0x1c0
   link_path_walk+0x11ad/0x1d30
   path_lookupat+0x63/0xb00
   filename_lookup+0x1f1/0x5b0
   vfs_statx+0xd7/0x3b0
  [...]
  Allocated by task 94:
   __kmalloc_cache_noprof+0x249/0x560
   utf8_load+0x4e/0x570
   shmem_parse_opt_casefold.isra.0+0x92/0x270
  [...]
  Freed by task 97:
   kfree+0x1db/0x5b0
   shmem_put_super+0x72/0x170
   generic_shutdown_super+0x14c/0x530
   kill_anon_super+0x3a/0x60
   deactivate_locked_super+0x9c/0xf0
   cleanup_mnt+0x9f/0x130

So far that has been hard to hit. umount(2) waits for a grace period
in namespace_unlock() before it drops the mount, so only a mount whose
last reference goes away elsewhere, like the close of a file or a chdir
out of a lazily unmounted directory, can race a walker. So it is
reachable.

Delay the free until a grace period has passed. The map and the data
table it holds a reference to then outlive any pathwalk that found the
superblock before the mount went away. The reference on the data table
module is dropped from the rcu callback as well. utf8-core is built in
whenever CONFIG_UNICODE is set, so there is no module exit that would
have to wait for the callbacks.

Overlayfs points the s_encoding of an overlay at the map of its first
layer without taking a reference of its own. That stays correct. The
layer is held by the overlay, so its map is dropped only after the
overlay is gone.

Fixes: b886ee3e77 ("ext4: Support case-insensitive file name lookups")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-6-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:03 +02:00
Christian Brauner
6e6928b396
eventfs: free the eventfs_inode after an rcu grace period too
tracefs_d_revalidate() reads ->is_freed of the eventfs_inode that
dentry->d_fsdata points to, and ->d_revalidate() is called in rcu
pathwalk mode with nothing but rcu_read_lock() held. The dentry itself
stays around for such a walker, but tracefs_d_release() drops its
reference to the eventfs_inode when the dentry is killed, and
release_ei() then frees it after a grace period of eventfs_srcu. An
SRCU grace period doesn't wait for rcu_read_lock() sections, so the
walker can read an eventfs_inode that is already gone.

With a pathwalk held up in tracefs_d_revalidate() while the dentry goes
away:

  BUG: KASAN: slab-use-after-free in tracefs_d_revalidate+0x76/0x90
  Read of size 1 at addr ff1100010152d8d0 by task walker/97
  Call Trace:
   <TASK>
   tracefs_d_revalidate+0x76/0x90
   lookup_fast+0x320/0x640
   path_lookupat+0x160/0xb00
   filename_lookup+0x1f1/0x5b0
   vfs_statx+0xd7/0x3b0
   vfs_fstatat+0x4d/0x90
   __do_sys_newfstatat+0xa0/0x100
   do_syscall_64+0x10e/0x5e0
   entry_SYSCALL_64_after_hwframe+0x77/0x7f
  [...]
  Allocated by task 1:
   __kmalloc_cache_noprof+0x249/0x560
   eventfs_create_dir+0x7a/0x590
   event_create_dir+0x2fb/0xcf0
   trace_add_event_call+0xbf/0x150
  [...]
  Freed by task 11:
   kfree+0x1db/0x5b0
   srcu_invoke_callbacks+0x3a3/0x620
   process_one_work+0x937/0x1810
   worker_thread+0x4f1/0xd60

Before commit 8e55643247 ("eventfs: Use SRCU for freeing
eventfs_inodes") release_ei() used call_rcu(), which covered the walker,
while eventfs_iterate() needs the SRCU grace period. Wait for both and
chain call_rcu() from the SRCU callback.

Fixes: 8e55643247 ("eventfs: Use SRCU for freeing eventfs_inodes")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-4-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:03 +02:00
Christian Brauner
bcec7fc2e2
9p: free the session after a grace period
v9fs_vfs_get_link_dotl() reads ->cache from the v9fs_session_info in
sb->s_fs_info to decide whether the symlink target sits in the page
cache. It does that before it looks at the dentry, so it also does it in
rcu pathwalk mode. Without loose or metadata caching the dentries have
no ->d_revalidate() that would make the walk leave rcu mode before it
gets there.

v9fs_kill_super() frees the session and clears sb->s_fs_info without
waiting for a grace period. Right now a pathwalk can't still be in
v9fs_vfs_get_link_dotl() at that point, but only by accident. umount(2)
waits for a grace period in namespace_unlock() before it drops the
mount. And a mount whose last reference goes away elsewhere, like the
chdir out of a lazily unmounted directory, still gets a grace period
from bdi_unregister(). v9fs_fill_super() sets up a private bdi, so
generic_shutdown_super() unregisters it from kill_anon_super() and
bdi_remove_from_list() waits in synchronize_rcu_expedited() before
v9fs_kill_super() frees the session. That grace period protects the rcu
readers of bdi_list in the writeback code. It has nothing to do with
pathwalk.

Don't rely on it. Keep sb->s_fs_info and free the session after a grace
period.

Fixes: 712da38d13 ("9p: Enable symlink caching in page cache")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-3-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:03 +02:00
Christian Brauner
59e483739f
afs: free the super info after a grace period
afs_atcell_get_link() resolves the @cell and .@cell symlinks of a
dynamic root in rcu pathwalk mode. Before it looks at the dentry it gets
the afs_net through afs_i2net(), which reads ->net_ns from the
afs_super_info in sb->s_fs_info. The @cell dentries are kept around when
unused and the dynamic root has no ->d_revalidate(), so a lookup that
finds @cell in the dcache gets to ->get_link() in rcu mode.

afs_kill_super() frees the afs_super_info with a plain kfree() in
afs_destroy_sbi(). Right now a pathwalk can't still be in
afs_atcell_get_link() at that point, but only by accident. umount(2)
waits for a grace period in namespace_unlock() before it drops the
mount. And a mount whose last reference goes away elsewhere, like the
chdir out of a lazily unmounted directory, still gets a grace period
from bdi_unregister(). afs_fill_super() sets up a private bdi, so
generic_shutdown_super() unregisters it from kill_anon_super() and
bdi_remove_from_list() waits in synchronize_rcu_expedited() before
afs_destroy_sbi() runs. That grace period protects the rcu readers of
bdi_list in the writeback code. It has nothing to do with pathwalk.

Don't rely on it and free the afs_super_info after a grace period.

Fixes: 30bca65bbb ("afs: Make /afs/@cell and /afs/.@cell symlinks")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-2-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:02 +02:00
Christian Brauner
c17b44266a
ext4: free the sbi after a grace period
ext4_get_link() follows a symlink that keeps its target in a data block
in rcu pathwalk mode with ext4_getblk(EXT4_GET_BLOCKS_CACHED_NOWAIT).
That reads s_mount_state in the ASSERT() of ext4_getblk() and again in
ext4_es_lookup_extent(), and the lookup bumps the extent status hit or
miss counter, all through sb->s_fs_info.

ext4_put_super() destroys those counters, clears sb->s_fs_info and frees
the sbi without waiting for a grace period. A pathwalk that is still in
ext4_get_link() when the last reference to the mount goes away then
dereferences a NULL s_fs_info, the freed sbi or the destroyed percpu
counters.

With a pathwalk held up in ext4_get_link() while the last reference
goes away:

  KASAN: null-ptr-deref in range [0x00000000000000a8-0x00000000000000af]
  RIP: 0010:ext4_getblk+0xf0/0x640
  Call Trace:
   <TASK>
   ext4_get_link+0x26c/0x410
   pick_link+0xf96/0x1780
   step_into_slowpath+0x9b8/0x1250
   path_lookupat+0x484/0xb00
   filename_lookup+0x1f1/0x5b0
   vfs_statx+0xd7/0x3b0
   vfs_fstatat+0x4d/0x90
   __do_sys_newfstatat+0xa0/0x100
   do_syscall_64+0x10e/0x5e0
   entry_SYSCALL_64_after_hwframe+0x77/0x7f

So far that has been hard to hit. umount(2) waits for a grace period in
namespace_unlock() before it drops the mount, so only a mount whose last
reference goes away elsewhere, like the close of a file or a chdir out of
a lazily unmounted directory, can race a walker.

Keep sb->s_fs_info and free the sbi together with the extent status
counters after a grace period. Split the counters out of
ext4_es_unregister_shrinker() for that. The shrinker still gets wasted
in ext4_put_super(), the counters go with the sbi.

Fixes: 6493792d32 ("ext4: convert symlink external data block mapping to bdev")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-1-7446d2d335e6@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:02 +02:00
Kyle Zeng
40bb03d3ea
pipe: preserve existing fasync readers on writer setup failure
F_SETFL and FIOASYNC decide whether to invoke ->fasync() before
pipe_fasync() takes pipe->mutex. Two enables of one O_RDWR FIFO can
therefore both enter the callback.

fasync_helper() allocates before checking for an existing entry. If the
first enable installs both entries, a second enable can update the
reader entry and then fail the allocation for the writer entry. The
error path removes the pre-existing reader entry, clearing FASYNC while
the writer entry remains registered. __fput() then skips ->fasync(), so a
later pipe notification can dereference the freed file or its f_owner.
An unprivileged memory-pressure reproducer triggers a KASAN
slab-use-after-free in send_sigio() through this path.

Keep the reader helper's result and undo the registration only if this
call added it. The pipe mutex serializes changes to both lists, so a
failed new registration still removes its reader, while a failed
duplicate preserves the existing registrations and FASYNC. Successful
duplicate enables continue to update fa_fd as before.

Fixes: 76398425bb ("Move FASYNC bit handling to f_op->fasync()")
Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:02 +02:00
Cen Zhang
4c43582893
ocfs2/dlm: Protect the recovery list during debugfs reads
Recovery participant records must remain alive while debug_state_print()
walks reco.node_data and formats their fields. The formatter holds
dlm->spinlock, but the recovery participant list is protected by
dlm_reco_state_lock. In particular, dlm_destroy_recovery_area() detaches
the list under dlm_reco_state_lock and frees the detached records after
releasing it. A concurrent debugfs open can therefore read freed memory.

Protect the participant traversal and field reads in debug_state_print()
with dlm_reco_state_lock, matching the existing list writers. This also
serializes the traversal against list_add_tail() during recovery-area
initialization. Make the existing lock available to the debug code
through dlmcommon.h; the lock remains internal to the OCFS2 DLM module.

Release dlm->spinlock after formatting the other domain state and before
acquiring dlm_reco_state_lock, so the debug reader does not nest the two
locks. Leave recovery list insertion, detachment and freeing unchanged.

KASAN report as below:

    BUG: KASAN: slab-use-after-free in debug_state_open+0x1169/0x12d0
    Read of size 4 at addr ffff88810662ef80 by task dlm-state-stres/896

    CPU: 1 UID: 0 PID: 896 Comm: dlm-state-stres Not tainted 7.3.0-rc4-next-20260921-pmb-ocfs2-functional-v1+ #1 PREEMPT(lazy)
    Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
    Call Trace:
     <TASK>
     dump_stack_lvl+0x93/0xd0
     print_report+0xce/0x630
     ? debug_state_open+0x1169/0x12d0
     ? srso_alias_return_thunk+0x5/0xfbef5
     ? __virt_addr_valid+0x20e/0x420
     ? debug_state_open+0x1169/0x12d0
     kasan_report+0xe0/0x110
     ? debug_state_open+0x1169/0x12d0
     debug_state_open+0x1169/0x12d0
     ? __pfx_debug_state_open+0x10/0x10
     full_proxy_open_regular+0x193/0x310
     do_dentry_open+0x595/0x12d0
     ? __pfx_full_proxy_open_regular+0x10/0x10
     vfs_open_consume+0xd1/0x400
     ? srso_alias_return_thunk+0x5/0xfbef5
     path_openat+0x18d0/0x2020
     ? __pfx_path_openat+0x10/0x10
     do_file_open+0x21e/0x470
     ? __pfx_do_file_open+0x10/0x10
     ? _raw_spin_unlock+0x23/0x40
     ? srso_alias_return_thunk+0x5/0xfbef5
     ? alloc_fd+0x3a6/0x6b0
     do_sys_openat2+0xf4/0x1b0
     ? __pfx_do_sys_openat2+0x10/0x10
     ? srso_alias_return_thunk+0x5/0xfbef5
     ? __fput+0x5b5/0xa60
     __x64_sys_openat+0x136/0x1e0
     ? __pfx___x64_sys_openat+0x10/0x10
     do_syscall_64+0x114/0x620
     entry_SYSCALL_64_after_hwframe+0x77/0x7f
    [Register dump omitted.]
     </TASK>

    Allocated by task 880:
     kasan_save_stack+0x33/0x60
     kasan_save_track+0x14/0x30
     __kasan_kmalloc+0xaa/0xb0
     __kmalloc_cache_noprof+0x28d/0x610
     dlm_remaster_locks+0x147/0x1d90
     dlm_do_recovery+0xde1/0x1580
     dlm_recovery_thread+0x109/0x300
     kthread+0x351/0x460
     ret_from_fork+0x659/0x940
     ret_from_fork_asm+0x1a/0x30

    Freed by task 880:
     kasan_save_stack+0x33/0x60
     kasan_save_track+0x14/0x30
     kasan_save_free_info+0x3b/0x60
     __kasan_slab_free+0x5f/0x80
     kfree+0x308/0x580
     dlm_destroy_recovery_area+0x285/0x460
     dlm_remaster_locks+0x17c4/0x1d90
     dlm_do_recovery+0xde1/0x1580
     dlm_recovery_thread+0x109/0x300
     kthread+0x351/0x460
     ret_from_fork+0x659/0x940
     ret_from_fork_asm+0x1a/0x30

    The buggy address belongs to the object at ffff88810662ef80
     which belongs to the cache kmalloc-32 of size 32
    The buggy address is located 0 bytes inside of
     freed 32-byte region [ffff88810662ef80, ffff88810662efa0)

    [Page and memory-state dumps omitted.]

Fixes: 007dce53a2 ("ocfs2/dlm: Dump the dlm state in a debugfs file")
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
Link: https://patch.msgid.link/pm-ocfs2-objects-candidate-0049-v3-184e46c47f5283f06191@gmail.com
Reviewed-by: Joseph Qi <joseph.qi@linux.alibaba.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-14 22:08:01 +02:00
Darrick J. Wong
7dfb218c82 xfs: fix exchange-range-to-eof file size exchange
Norbert Szetei posted a patch containing an incomplete description of a
bug in XFS_IOC_EXCHANGE_RANGE's TO_EOF flag.  The bug tricks
exchange-range into modifying a file so that it has shared blocks
starting beyond EOF, which is never allowed because files never have
written data beyond EOF, and you can only share written data blocks.
This is key to all the incorrect behavior that follows.

Eventually I got from Norbert a description of what's going wrong:

> Fair. Here it is with physical block numbers, 4k blocks. peer is the file
> that gets rewritten and file1 is the one that ends up with the flag
> cleared.
>
> Start, nothing shared:
>
>	peer size 4096   -> [100]
>	file1 size 12288 -> [200] [201] [202]
>	donor1 size 4096 -> [300]
>	donor2 size 4096 -> [400]
>
> 1. FICLONERANGE peer's block into file1's middle slot:
>
>	peer size 4096   -> [100]
>	file1 size 12288 -> [200] [100] [202] reflink flag set
>                             ^^^^^ both files own block 100
>
> 2. XFS_IOC_EXCHANGE_RANGE file1 against donor1, file1_offset 0,
> file2_offset 8192, length 0, TO_EOF. One block is exchanged, and the sizes
> are exchanged with it:
>
>	file1 size 4096   -> [200] [100] [300] reflink flag set
>                              ^^^^^^^^^^^ still owned, now past the size
>	donor1 size 12288 -> [202]
>
> file1 says it is one block long while it still owns three. Nothing was
> unmapped. file2_offset is block 2, so this exchange cannot clear a flag.
>
> 3. XFS_IOC_EXCHANGE_RANGE file1 against donor2, both offsets 0, length 4096.
> By i_disk_size this is two one-block files exchanging everything, so
> xmi_can_exchange_reflink_flags() agrees and the flag is cleared:
>
>	file1 size 4096   -> [400] [100] [300] reflink flag CLEARED
>	donor2 size 4096  -> [200]
>
> Block 100 is still shared with peer.
>
> 4. pwrite(file1, 4096, 4096), which is file1's second slot, block 100.
> xfs_is_cow_inode(file1) is false, so no CoW:
>
>	peer size 4096 -> [100] same block, new contents
>
> Block 100 is the only one that matters here. PoC below, it reads a file
> called peer in the current directory. The filesystem needs reflink and
> exchange-range, which mkfs.xfs 7.x turns on by default.

So yes, TO_EOF is screwing up the file size even if it's actually
exchanging the data block correctly.  The following is the intended
behavior of XFS_EXCHANGE_RANGE_TO_EOF, as described by its manpage:

"Ignore the length parameter.  All bytes in file1_fd from file1_offset
 to EOF are moved to file2_fd, and file2's size is set to
 (file2_offset+(file1_length-file1_offset)).  Meanwhile, all bytes in
 file2 from file2_offset to EOF are moved to file1 and file1's size is
 set to (file1_offset+(file2_length-file2_offset))."

In other words, if we only exchange a portion of the file data, then we
should only exchange the portion of the file size that relates to the
exchanged data.  Unfortunately, the code swaps the ondisk file size and
the incore file sizes, which is incorrect if the caller passes in a
nonzero offset.

Having confused the file sizes, the PoC uses a second exchange-range
call on what looks like a non-reflinked single-block file.  Because the
file size is set incorrectly, exchange-range thinks it's exchanging the
full contents of two files and clears the reflink flag on the broken
file.  That enables an extending write of the broken file to rewrite the
shared block that's just past EOF.  This has become known colloquially
as refluxfs.

Once the file sizes are set correctly, the second exchange-range no
longer thinks that it's doing a full-contents swap, so it won't clear
the reflink flag on either of its file arguments.

Norbert followed up to an earlier version of this patch with a second
complaint about us not handling the i_disk_size state correctly:

> The offsets are bounded against the incore i_size but subtracted from
> the ondisk i_disk_size, so a file that has not been written back gives
> xmi_isize1 = 0 + (0 - 12288).
>
> For instance, you can do:
>
> $ xfs_io -f -c "pwrite -q 0 4096" -c fsync file1
> $ xfs_io -f -c "pwrite -q 0 12288" \
>   -c "exchangerange -s 0 -d 12288 file1" dirty
> $ stat -c %s file1
> 0
> $ stat -c %i file1
> 131
> $ sudo xfs_io -r -c "bulkstat_single 131" . | grep bs_size
> bs_size = 18446744073709539328

That's wrong, and the new file size update logic depends on the ondisk
i_disk_size matching the incore i_size, so flush the pagecache as needed
to push it upwards.

Link: https://lore.kernel.org/linux-xfs/1E196589-DEBE-40AC-AFEA-D420DAAB067F@doyensec.com/
Reported-by: Norbert Szetei <norbert@doyensec.com>
Cc: stable@vger.kernel.org # v6.10
Fixes: 966ceafc7a ("xfs: create deferred log items for file mapping exchanges")
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
Signed-off-by: Carlos Maiolino <cem@kernel.org>
2026-10-11 08:23:43 +02:00
Norbert Szetei
d84d6c1a7f xfs: stop exchanging reflink flags when exchanging mappings
When an exchange covers both files from offset 0 to i_disk_size,
xmi_can_exchange_reflink_flags() assumes every mapping moved and hands
the reflink flag over to the other file.

That assumption is wrong.  A file can still own shared blocks past
i_disk_size, e.g. after EXCHANGE_RANGE_TO_EOF sets a smaller size
without unmapping anything.  The flag is cleared anyway, and the next
write to such a block happens in place instead of CoW, corrupting the
other file that shares it.

Commit a23eca8844 ("xfs: fix exchange-range reflink flag clearing
issue with INO1_WRITTEN") fixed one instance of this.  Rather than add
another special case, stop moving the flags.
xfs_exchmaps_ensure_reflink() already sets the flag on both inodes
before the exchange, so both just keep it.

Nothing sets CLEAR_INO{1,2}_REFLINK anymore.  Intents logged by older
kernels may still carry them; recovery already masks them off, so they
stay in the log format mask and are ignored.

XFS_IOC_SWAPEXT has the same problem: it moves the reflink flag from
one inode to the other along with the extents.  Just removing that is
not enough here: the exchange path flags both inodes up front in
xfs_exchmaps_ensure_reflink(), but swapext has no such step, so the
inode receiving the shared extents would be left without the flag.
Set it on both inodes instead, and make sure both have a CoW fork,
since an inode with the reflink flag is expected to have one.

The flag now stays until FALLOC_FL_UNSHARE_RANGE, xfs_scrub, or a
truncate to zero removes it.

Fixes: 966ceafc7a ("xfs: create deferred log items for file mapping exchanges")
Fixes: f0bc4d134b ("xfs: swap inode reflink flags when swapping inode extents")
Cc: stable@vger.kernel.org # v6.10
Assisted-by: LLM
Signed-off-by: Norbert Szetei <norbert@doyensec.com>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
Reviewed-by: Darrick J. Wong <djwong@kernel.org>
Signed-off-by: Carlos Maiolino <cem@kernel.org>
2026-10-11 08:23:38 +02:00
Linus Torvalds
fc1c25014f fsverity fix for v7.3-rc7
Fix a regression from commit f77f281b61 ("fsverity: use a hashtable
 to find the fsverity_info").
 -----BEGIN PGP SIGNATURE-----
 
 iIoEABYIADIWIQSacvsUNc7UX4ntmEPzXCl4vpKOKwUCasjoLRQcZWJpZ2dlcnNA
 a2VybmVsLm9yZwAKCRDzXCl4vpKOK6rwAP9uhXe8Y0S1iRKDAuPcEncEBZQbmjcb
 AcIAon4KWSwt/AEAqDemNWVHeYtoD9yK6YvfoS4xo5Kc/S2o3t/pq4W8uwk=
 =/OVa
 -----END PGP SIGNATURE-----

Merge tag 'fsverity-for-linus' of git://git.kernel.org/pub/scm/fs/fsverity/linux

Pull fsverity fix from Eric Biggers:
 "Fix a regression from commit f77f281b61 ("fsverity: use a hashtable
  to find the fsverity_info")"

* tag 'fsverity-for-linus' of git://git.kernel.org/pub/scm/fs/fsverity/linux:
  fsverity: RCU-delay the freeing of struct fsverity_info
2026-10-10 00:01:12 +02:00
Linus Torvalds
f99005fbed vfs-7.3-rc7.fixes
Please consider pulling these changes from the signed vfs-7.3-rc7.fixes tag.
 
 Thanks!
 Christian
 -----BEGIN PGP SIGNATURE-----
 
 iHUEABYKAB0WIQRAhzRXHqcMeLMyaSiRxhvAZXjcogUCasilxwAKCRCRxhvAZXjc
 ooQTAQD6awSrQcUIG0ca5hbowpn7sLi8kWGnmzf2yydKSyp3bwEAnpSQEw3BXUKH
 0O7JEyZFL0VcMTTceSPKkKCqGg5nugI=
 =Wc48
 -----END PGP SIGNATURE-----

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
  ...
2026-10-09 23:37:44 +02:00
Linus Torvalds
6c377d19d4 Mark the ext4 data=journal feature as being deprecated and will be
removed in 2028.
 
 Also designate the primary branch that Sashiko and other tools use to
 find the primary development branch in the ext4 tree.
 -----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEEK2m5VNv+CHkogTfJ8vlZVpUNgaMFAmrHtq4ACgkQ8vlZVpUN
 gaMFZAf/eACeBhy6Tm9s6MDAewS84BDeNd/WuUrIn+Jpt1jwpL83sNPY3rgleR7s
 spPpg7fJQJfFbRiyOj4YUNbwShCOJ61cOw/BrLBYD5HlkCNuexBNWEOEVB7HWAVv
 f+J7FLF8k/6lgzuXI4CNzC4NIpKiWLUi3zbGbjxZgtdsr1j7zThVn83WwuHqnJic
 dLlW5Twcwox0j2UDKRmERL1x8QBeghJPSX9u1xNEtx8/gEkAKGJ601MrbVmZPDDy
 FuXZUdOuyctLuiFMjvB20jVXMVCO0SMj5DiJ8vwGYV/OHVYcLxx7WP7KxJ/IKmK8
 lk748C174vVDTLTSzBDIAnqctfkUxQ==
 =zILx
 -----END PGP SIGNATURE-----

Merge tag 'ext4_for_linus-7.3-rc7' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4

Pull ext4 fixes from Ted Ts'o:
 "Mark the ext4 data=journal feature as being deprecated and will be
  removed in 2028.

  Also designate the primary branch that Sashiko and other tools use to
  find the primary development branch in the ext4 tree"

* tag 'ext4_for_linus-7.3-rc7' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4:
  ext4: mark data=journal as deprecated and will be removed in January 2028.
  MAINTAINERS: name the ext4 dev branch
2026-10-08 17:37:49 +02:00
Theodore Ts'o
3e78d7b063 ext4: mark data=journal as deprecated and will be removed in January 2028.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
2026-10-08 11:21:30 -04:00
Linus Torvalds
7b63ef2d55 for-7.3-rc6-tag
-----BEGIN PGP SIGNATURE-----
 
 iQJPBAABCgA5FiEE8rQSAMVO+zA4DBdWxWXV+ddtWDsFAmrGSIUbFIAAAAAABAAO
 bWFudTIsMi41KzEuMTIsMiwyAAoJEMVl1fnXbVg7eQEP/0T5xO7uJw7S42kKi4w+
 J/XP482GUb5xv0BmwOlITl5dGQHAHWKtq7zXflA+pNSoPPWUq+eTfKw1r/yYHGtu
 S1kHei3K3l9qWpeIOJYTdpcVayFRdn+ZZuMwvb5uojT7VbtV2UB01LQfMWM8mcaY
 PukCIMX+PiM8ue9l2iQZ5EXzFoaJzKpvyZP0T7ar+IIRsfE3n0qMmZeGiYKuCJOF
 A9N2IWNU/UIUtHpZGHZYwVzGuFdDH3Dc5b0ZzQUlL/dUvZ6345bX6NCXLceU+II1
 SjlQwEKLpfY0IxfwnR3cLkAu+YE5ER6VfHCzcvVV1mZFVOQwF6+UuMTrZ3AdO6tX
 RFHnhVUVZSvULCRJlpsV5LIALDow/vSAif7n02oh3j2pou50UQVLI1q5olZhPOLS
 rpYSF7HIpj2R/1MStDko9FTp78atG+0g6DlZDsz1Dys3l0gaKx+WJ/tzh60cvPj8
 aM2zck1Dw/9wsg8KMXLqlllFQRoCavmngmttONxqXYlRwz8h9CSf4uLPZzgSzORm
 /MR0Cb+o5W3c1LJgjRsdXa/j0NTdKGmGqbWRN+Hnwi3ZbR3GC5+HL7mGpH28TqHU
 /X93+/pDoGR11E8VMMgRkTDJwfuDPheKno98lwkhpC9x6znXwuhh43/6iT/gYA9k
 cbklJF1ok+18QTQLPTTSyIYf
 =Mk4y
 -----END PGP SIGNATURE-----

Merge tag 'for-7.3-rc6-tag' of git://git.kernel.org/pub/scm/linux/kernel/git/kdave/linux

Pull btrfs fixes from David Sterba:

 - fix command queuing and cleanup in encoded read/write ioctls

 - fix root and transaction association to avoid unnecessary lock
   contention and transaction start

 - properly handle replacing multiple xattrs in the same item

 - in scrub, fix root reference leak after reporting an unresolved file
   path

* tag 'for-7.3-rc6-tag' of git://git.kernel.org/pub/scm/linux/kernel/git/kdave/linux:
  btrfs: fix lost error return value in btrfs_listxattr()
  btrfs: fix xattr replace when multiple xattrs are packed in the same item
  btrfs: don't stash io_uring encoded data across -EAGAIN
  btrfs: unlock inode and extent in caller when io_uring read extent fails
  btrfs: free iov when btrfs_uring_read_extent() fails
  btrfs: always return -EIOCBQUEUED after btrfs_uring_read_extent_endio
  btrfs: scrub: fix local_root reference leak in scrub_print_warning_inode()
  btrfs: clear BTRFS_ROOT_IN_TRANS_SETUP on early exit from record_root_in_trans()
2026-10-07 15:58:48 +02:00
Christian Brauner
3d39922442
namespace: simplify disconnect_mount()
Rename disconnect_mount() and simplify it.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-07 00:45:50 +02:00
Christian Brauner
01cbebb420
namespace: rework connected mounts
UMOUNT_CONNECTED as implemented allows for the creation of reference
count cycles. Here's a simple example

  mkdir /x; mkfifo /ready /go
  unshare -m sh -c 'mount -t tmpfs tmpfs /x
                    truncate -s 8M /x/img; mkfs.ext4 -q /x/img
                    dev=$(losetup -f --show /x/img)
                    mkdir /x/mp; mount $dev /x/mp
                    echo $dev > /ready; read r < /go' &
  read dev < /ready
  rmdir /x
  echo > /go
  wait
  losetup -d $dev
  losetup -a

Take a directory /x on the host, create a new mount namespace, mount a
tmpfs on /x, use a file on that tmpfs as the backing file for a loop
device, mount that loop device on that tmpfs. Now rmdir /x on the host.
This will lazily unmount the mount on top of /x in the container with
UMOUNT_CONNECTED. Once the namespace exits nothing references the mount
anymore. Now the tmpfs is pinned by the backing file of the loop device
and the loop mount is owned by the tmpfs superblock.

Fun fact, such cycles can be formed by at least the following
subsystems and I have added reproducers for all of them:

(1) a loop mount P from an image on a tmpfs next to it, so that P's
    death shows as the loop device giving up its backing file

(2) autofs with a FIFO on P as its pipe, zram with a device node on P as
    its writeback device, both on a minix image since vfat has neither

(3) ecryptfs with its lower directory on P, under a passphrase token
    added to the session keyring

(4) binfmt_misc in a new user namespace with an 'F' interpreter on P

(5) a fuse server that answers FUSE_INIT with passthrough on and
    registers a file on P as a backing file

(6) zloop with its zone files in a directory on P

(7) a mass storage gadget on the dummy UDC with its LUN file on P,
    mounted from the SCSI disk the gadget shows up as

(8) md with a RAID1 of one loop device and its bitmap file on P, which
    skips while SET_BITMAP_FILE has no way to succeed

(9) rmdir of P's mountpoint from the parent, then the child exits, then
    the device must be free and LOOP_CLR_FD must release the file

The underlying mechanism is UMOUNT_CONNECTED (MNT_LOCKED falls into the
same class). With UMOUNT_CONNECTED an unmounted mount stays attached to
its parent. This is used to protect revealing the underlying mount and
is a non-negotiable security mechanism. So now the parent owns that
mount and is put on the parent's final mntput(). That moves it to
mnt_stuck_children and ultimately it's cleaned up by cleanup_mnt().

The fact that ownership of the child mount gets transferred to the
parent turns every reference from a child's superblock back to one of
its ancestors into a cycle.

Don't keep the child attached at all. What the parent needs is that a
lookup at the child's mountpoint keeps finding some mount, not the child
itself and it's not a guarantee we have given really.

When an unmounted mount would have stayed attached to its unmounted
parent disconnect it like every other unmounted mount and leave a
marker behind.

A lookup on the parent that misses the mount hash and hits a marker
finds knullfs. Either a file or a directory. Nothing leads from a marker
to any other mount.

The marker is owned by the parent and dropped by the parent's final
mntput() or by __detach_mounts() when the mountpoint is deleted from
under it. It is allocated together with the mount.

With that every unmounted mount is a root and holds only its own
reference which namespace_unlock() drops. No unmounted mount owns
another one. A superblock that pins an ancestor can't form a cycle.

It has a visible change. Mounts left connected (rmdir etc.) used to stay
traversable through the parent for as long as something held the parent.
Now it is detached with the umount. It lives as long as something
references it but it isn't reachable through the parent anymore and ".."
inside it leads nowhere which is the same as for every other lazily
unmounted mount.

Link: https://gist.github.com/mvo5/63ef46482349f3b1c3957d463a0c9c6f
Link: https://patch.msgid.link/20261002-work-mount-cover-v1-2-232a8f52b43c@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-07 00:45:49 +02:00
Christian Brauner
54867884fe
nullfs: add an empty immutable regular file
Add nullfs_new_file() to allocate an empty immutable regular file on a
nullfs instance as a dentry of its own. It is never hashed under the
root and so can't be found by lookup. Reads return nothing, changes are
refused, file locks, leases and delegations are refused as.

Link: https://patch.msgid.link/20261002-work-mount-cover-v1-1-232a8f52b43c@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-07 00:45:49 +02:00
Filipe Manana
41c8899d59 btrfs: fix lost error return value in btrfs_listxattr()
If the input buffer does not have enough space to store the current xattr,
we set 'iter_ret' to -ERANGE and then do "break", but that only exits the
while loop over the xattrs in the current btrfs_dir_item, and then we
continue the btrfs_for_each_slot() iteration, which overwrites the value
of 'iter_ret' causing us to lose the error return value and proceed as if
the buffer has enough space.

Fix this by returning -ERANGE directly (the path is automatically freed)
instead of breaking from the while loop.

Fixes: 184b3d1900 ("btrfs: use btrfs_for_each_slot in btrfs_listxattr")
Assisted-by: LLM (found the bug)
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:44 +02:00
Filipe Manana
858bee1c77 btrfs: fix xattr replace when multiple xattrs are packed in the same item
If we have a btrfs_dir_item item that packs multiple xattrs and then we
replace the value of one of them (with the setxattr(2) family of syscalls)
with another value of a different size, we end up not having a fully
initialized btrfs_dir_item, resulting in a corruption that the tree
checker will detect at extent buffer writeback time.

This is because in btrfs_setxattr() when we find a btrfs_dir_item with
multiple xattrs (due to the crc32c hash of their name being the same)
we delete one of the xattr items (btrfs_dir_item) and then insert a new
one, but the deletion and insertion results in shifting existing data in
the leaf and therefore when the new value of a xattr has a different size,
the new btrfs_dir_item is placed in a leaf section that was not
initialized and we only copy the value's data and set the value's length
in the new btrfs_dir_item, without setting the name, the name's length,
the key (which must be all zeroes for xattrs), flags (BTRFS_FT_XATTR) and
transaction ID.

The following script reproduces the issue:

  $ cat test.sh
  #!/bin/bash

  DEV=/dev/sdi
  MNT=/mnt/sdi

  mkfs.btrfs -f $DEV
  mount $DEV $MNT

  touch $MNT/testfile

  # Add two xattrs that, on btrfs, have the same hash (crc32c) for their
  # name and therefore are packed into the same btrfs_dir_item.
  setfattr -n user.foobar -v 123 $MNT/testfile
  setfattr -n user.WvG1c1Td -v qwerty $MNT/testfile

  # Verify the xattrs are present.
  echo "xattrs before:"
  getfattr --absolute-names --dump $MNT/testfile

  # Now replace the value of the foobar xattr with a significantly larger
  # value.
  setfattr -n user.foobar -v abcdefghijklmnopqrstuvwxyz $MNT/testfile

  # Check the xattrs have the expected values.
  echo "xattrs after:"
  getfattr --absolute-names --dump $MNT/testfile

  umount $MNT

Running it:

  $ ./test.sh
  (...)
  xattrs before:
  # file: /mnt/sdi/testfile
  user.WvG1c1Td="qwerty"
  user.foobar="123"

  xattrs after:
  # file: /mnt/sdi/testfile
  user.WvG1c1Td="qwerty"

So the "user.foobar" xattr is missing and there was a transaction abort
when unmounting the fs with the following traces in dmesg:

  $ dmesg
  [869800.159271] BTRFS warning (device sdi): access to eb bytenr 30474240 len 16384 out of range start 16015 len 25964
  [869800.159293] ------------[ cut here ]------------
  [869800.159296] WARNING: fs/btrfs/extent_io.c:4408 at report_eb_range+0x44/0x60 [btrfs], CPU#8: getfattr/2605179
  [869800.168961] Modules linked in: btrfs dm_thin_pool (...)
  [869800.190527] CPU: 8 UID: 0 PID: 2605179 Comm: getfattr Tainted: G        W           7.3.0-rc3-btrfs-next-244+ #1 PREEMPT(full)
  [869800.193726] Tainted: [W]=WARN
  [869800.194502] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014
  [869800.197576] RIP: 0010:report_eb_range+0x44/0x60 [btrfs]
  [869800.198985] Code: 48 8b 7b 18 (...)
  [869800.203531] RSP: 0018:ffffce4541937d58 EFLAGS: 00010246
  [869800.204609] RAX: 0000000000000000 RBX: ffff8dde054b8738 RCX: 0000000000000000
  [869800.206137] RDX: 0000000000000000 RSI: 0000000000000001 RDI: ffffffffc04c92a0
  [869800.207645] RBP: 0000000000003e8f R08: 0000000000000000 R09: 3fffffffffefffff
  [869800.226175] R10: ffffce4541937a88 R11: 0000000000000003 R12: 000000000000656c
  [869800.227594] R13: ffff8dde14be800f R14: 000000000000656c R15: 0000000000000069
  [869800.229097] FS:  00007f59023bb780(0000) GS:ffff8de5788ed000(0000) knlGS:0000000000000000
  [869800.231137] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
  [869800.232321] CR2: 0000558dc45e6a78 CR3: 0000000765b16004 CR4: 0000000000370ef0
  [869800.233783] Call Trace:
  [869800.234330]  <TASK>
  [869800.234791]  read_extent_buffer+0x4d/0x100 [btrfs]
  [869800.235898]  btrfs_listxattr+0x199/0x240 [btrfs]
  [869800.236912]  vfs_listxattr+0x51/0xa0
  [869800.237683]  listxattr+0x7e/0x100
  [869800.238398]  path_listxattrat+0x9e/0x190
  [869800.239117]  do_syscall_64+0x89/0x470
  [869800.239877]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
  [869800.240914] RIP: 0033:0x7f59024cdcb7
  [869800.241686] Code: f0 ff ff 73 (...)
  [869800.245353] RSP: 002b:00007ffd056dcdb8 EFLAGS: 00000246 ORIG_RAX: 00000000000000c2
  [869800.246892] RAX: ffffffffffffffda RBX: 00007ffd056df2e2 RCX: 00007f59024cdcb7
  [869800.248853] RDX: 0000000000006600 RSI: 0000558dc45e0470 RDI: 00007ffd056df2e2
  [869800.250514] RBP: 00007ffd056df2e2 R08: 0000000000006600 R09: 0000000000006600
  [869800.252295] R10: 0000000000000004 R11: 0000000000000246 R12: 00000000ffffff9c
  [869800.254105] R13: 0000558dc45e0470 R14: 0000000000006600 R15: 0000000000000000
  [869800.255931]  </TASK>
  [869800.256529] ---[ end trace 0000000000000000 ]---
  [869800.260071] page: refcount:2 mapcount:0 mapping:000000007ccfc77f index:0x1d10 pfn:0x608c69
  [869800.260075] memcg:ffff8dde00344d40
  [869800.260076] aops:btree_aops [btrfs] ino:1
  [869800.260151] flags: 0x17fffc00000402a(uptodate|lru|private|writeback|node=0|zone=2|lastcpupid=0x1ffff)
  [869800.260154] raw: 017fffc00000402a fffff4adc773d4c8 fffff4adc48f3d88 ffff8de36504ba30
  [869800.260155] raw: 0000000000001d10 ffff8dde054b8738 00000002ffffffff ffff8dde00344d40
  [869800.260156] page dumped because: eb page dump
  [869800.260157] BTRFS critical (device sdi): corrupt leaf: root=5 block=30474240 slot=5 ino=257, invalid location key type, have 46, expect 132 or 1
  [869800.260161] BTRFS info (device sdi): leaf 30474240 gen 9 total ptrs 6 free space 15629 owner 5
  [869800.260163] BTRFS info (device sdi): refs 3 lock_owner 0 current 2550294
  [869800.260164] 	item 0 key (256 INODE_ITEM 0) itemoff 16123 itemsize 160
  [869800.260165] 		inode generation 3 transid 0 size 0 nbytes 16384
  [869800.260166] 		block group 0 mode 40755 links 1 uid 0 gid 0
  [869800.260167] 		rdev 0 sequence 0 flags 0x0
  [869800.260168] 		atime 1790340637.0
  [869800.260169] 		ctime 1790340637.0
  [869800.260169] 		mtime 1790340637.0
  [869800.260170] 		otime 1790340637.0
  [869800.260170] 	item 1 key (256 INODE_REF 256) itemoff 16111 itemsize 12
  [869800.260172] 		index 0 name_len 2
  [869800.260172] 	item 2 key (256 DIR_ITEM 982728850) itemoff 16073 itemsize 38
  [869800.260173] 		location key (257 1 0) type 1
  [869800.260174] 		transid 9 data_len 0 name_len 8
  [869800.260175] 	item 3 key (257 INODE_ITEM 0) itemoff 15913 itemsize 160
  [869800.260176] 		inode generation 9 transid 9 size 0 nbytes 0
  [869800.260177] 		block group 0 mode 100664 links 1 uid 0 gid 0
  [869800.260177] 		rdev 0 sequence 0 flags 0x0
  [869800.260178] 		atime 1790340638.38778502
  [869800.260179] 		ctime 1790340638.38778502
  [869800.260179] 		mtime 1790340638.38778502
  [869800.260180] 		otime 1790340638.38778502
  [869800.260180] 	item 4 key (257 INODE_REF 256) itemoff 15895 itemsize 18
  [869800.260181] 		index 2 name_len 8
  [869800.260182] 	item 5 key (257 XATTR_ITEM 751495445) itemoff 15779 itemsize 116
  [869800.260183] 		location key (0 0 0) type 8
  [869800.266052] 		transid 9 data_len 6 name_len 13
  [869800.266053] 		location key (8243121639454149888 46 7229457603934778967) type 0
  [869800.266055] 		transid 113 data_len 26 name_len 0
  [869800.266056] 		location key (8608196880778817904 120 162425) type 9
  [869800.266057] 		transid 8391162079612502016 data_len 26982 name_len 25964
  [869800.266058] BTRFS error (device sdi): block=30474240 write time tree block corruption detected
  [869800.266091] ------------[ cut here ]------------
  [869800.266092] WARNING: fs/btrfs/disk-io.c:336 at btree_csum_one_bio+0x20b/0x220 [btrfs], CPU#7: kworker/u50:7/2550294
  [869800.268392] Modules linked in: btrfs dm_thin_pool (...)
  [869800.365851] CPU: 7 UID: 0 PID: 2550294 Comm: kworker/u50:7 Tainted: G        W           7.3.0-rc3-btrfs-next-244+ #1 PREEMPT(full)
  [869800.368937] Tainted: [W]=WARN
  [869800.369834] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014
  [869800.372778] Workqueue: writeback wb_workfn (flush-btrfs-3821)
  [869800.374314] RIP: 0010:btree_csum_one_bio+0x20b/0x220 [btrfs]
  [869800.375915] Code: 89 44 24 04 (...)
  [869800.380639] RSP: 0018:ffffce4548e3f7d0 EFLAGS: 00010246
  [869800.382008] RAX: 0000000000000000 RBX: ffff8dde054b8738 RCX: 0000000000000000
  [869800.383850] RDX: 0000000000000000 RSI: 0000000000000001 RDI: ffff8de091c2ddc0
  [869800.385710] RBP: ffff8dde196a2000 R08: 0000000000000000 R09: 3fffffffffefffff
  [869800.387388] R10: ffffce4548e3f500 R11: 0000000000000003 R12: ffffce4548e3f7d8
  [869800.388973] R13: ffff8dde196a2000 R14: ffff8de36504b750 R15: ffff8dde4c497b00
  [869800.390414] FS:  0000000000000000(0000) GS:ffff8de5788ad000(0000) knlGS:0000000000000000
  [869800.392002] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
  [869800.393160] CR2: 000055cd6e92ad5c CR3: 00000007cb264001 CR4: 0000000000370ef0
  [869800.394595] Call Trace:
  [869800.395113]  <TASK>
  [869800.395570]  btrfs_submit_bbio+0x872/0x890 [btrfs]
  [869800.397284]  write_meta_extent_buffer+0x70/0x80 [btrfs]
  [869800.398940]  btree_writepages+0x141/0x4f0 [btrfs]
  [869800.400426]  ? get_random_u32+0x8a/0xf0
  [869800.401417]  ? build_slab_freelist+0x47/0x130
  [869800.402574]  ? preempt_count_add+0x6b/0xa0
  [869800.403633]  ? _raw_spin_lock_irqsave+0x23/0x50
  [869800.404807]  ? _raw_spin_unlock_irqrestore+0x22/0x40
  [869800.406085]  ? alloc_from_new_slab+0x18f/0x330
  [869800.407223]  do_writepages+0xc6/0x160
  [869800.408191]  ? refill_objects+0xd8/0x300
  [869800.409211]  __writeback_single_inode+0x42/0x350
  [869800.410408]  writeback_sb_inodes+0x231/0x560
  [869800.411511]  wb_writeback+0x8a/0x300
  [869800.412440]  wb_workfn+0xbf/0x460
  [869800.413291]  ? _raw_spin_unlock+0x14/0x30
  [869800.414328]  ? finish_task_switch.isra.0+0xb9/0x380
  [869800.415105]  process_one_work+0x1d1/0x3d0
  [869800.416633]  worker_thread+0x1c4/0x330
  [869800.417467]  ? __pfx_worker_thread+0x10/0x10
  [869800.418452]  kthread+0xfc/0x130
  [869800.419257]  ? __pfx_kthread+0x10/0x10
  [869800.420089]  ret_from_fork+0x1f7/0x2c0
  [869800.420863]  ? __pfx_kthread+0x10/0x10
  [869800.421654]  ret_from_fork_asm+0x1a/0x30
  [869800.422484]  </TASK>
  [869800.422953] ---[ end trace 0000000000000000 ]---
  [869800.424005] BTRFS error (device sdi state A): Transaction 9 aborted (-EIO)
  [869800.424010] BTRFS: error (device sdi state A) in __btrfs_run_delayed_items:1162: errno=-5 IO failure
  [869800.424011] BTRFS info (device sdi state EA): forced readonly
  [869800.424013] BTRFS warning (device sdi state EA): Skipping commit of aborted transaction.
  [869800.424014] BTRFS: error (device sdi state EA) in cleanup_transaction:2076: errno=-5 IO failure

Fix this by always setting all fields in the new btrfs_dir_item when we
replace an existing xattr.

Fixes: 5f5bc6b1e2 ("Btrfs: make xattr replace operations atomic")
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Yang Xiuwei
51562a8cb1 btrfs: don't stash io_uring encoded data across -EAGAIN
Returning -EAGAIN while leaving btrfs_uring_encoded_data in the cmd PDU
leaks if the request is cancelled or the ring exits before reissue.
io_uring does not free driver PDU allocations on cleanup.

Write: io_queue_sqe() always issues with IO_URING_F_NONBLOCK first, so
return -EAGAIN before allocating and free data on every exit.

Read: free on nowait -EAGAIN too; only -EIOCBQUEUED keeps the allocation
for btrfs_uring_read_finished().

Fixes: 34310c442e ("btrfs: add io_uring command for encoded reads (ENCODED_READ ioctl)")
Fixes: e32dcdb0af ("btrfs: add io_uring interface for encoded writes")
Signed-off-by: Yang Xiuwei <yangxiuwei@kylinos.cn>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Yang Xiuwei
b6e8add5d7 btrfs: unlock inode and extent in caller when io_uring read extent fails
btrfs_uring_read_extent() runs only after btrfs_encoded_read() has taken
the inode shared lock and the extent lock.  On failure it used to unlock
in out_fail, and a pages-array allocation failure returned -ENOMEM
without unlocking at all.

Unlock in the caller instead on all failure returns, matching the
copy_to_user() error path.  The deferred -EIOCBQUEUED path still unlocks
in btrfs_uring_read_finished().

Fixes: 34310c442e ("btrfs: add io_uring command for encoded reads (ENCODED_READ ioctl)")
Suggested-by: Qu Wenruo <quwenruo.btrfs@gmx.com>
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Yang Xiuwei <yangxiuwei@kylinos.cn>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Yang Xiuwei
a8ea92830b btrfs: free iov when btrfs_uring_read_extent() fails
After btrfs_uring_read_extent(), the caller always jumped to out_acct.
That skips kfree(data->iov), which is only correct for -EIOCBQUEUED
where the deferred path owns the iov. On failure, fall through to
out_free instead.

Fixes: 34310c442e ("btrfs: add io_uring command for encoded reads (ENCODED_READ ioctl)")
Signed-off-by: Yang Xiuwei <yangxiuwei@kylinos.cn>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Yang Xiuwei
80a8dbb30d btrfs: always return -EIOCBQUEUED after btrfs_uring_read_extent_endio
If all bios finish before btrfs_encoded_read_regular_fill_pages()
returns, it calls btrfs_uring_read_extent_endio() and previously
returned the I/O status.  A negative errno then made
btrfs_uring_read_extent() unlock and free while
btrfs_uring_read_finished() did the same again.

Return -EIOCBQUEUED so only the deferred path cleans up.

Reported-by: Yue Sun <samsun1006219@gmail.com>
Closes: https://lore.kernel.org/linux-btrfs/20260630091609.3414-1-samsun1006219@gmail.com/
Suggested-by: Jens Axboe <axboe@kernel.dk>
Fixes: 34310c442e ("btrfs: add io_uring command for encoded reads (ENCODED_READ ioctl)")
Signed-off-by: Yang Xiuwei <yangxiuwei@kylinos.cn>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Wentao Liang
1fd742c747 btrfs: scrub: fix local_root reference leak in scrub_print_warning_inode()
When paths_from_inode() fails, scrub_print_warning_inode() jumps to err
without dropping the reference taken by btrfs_get_fs_root(), leaking a
reference to the root every time path resolution fails while printing
scrub warnings.  Every other error and success path of the function
drops the reference.

Drop the reference on the paths_from_inode() failure path too.

Fixes: 558540c177 ("btrfs scrub: print paths of corrupted files")
CC: stable@vger.kernel.org
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Wentao Liang <vulab@iscas.ac.cn>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Filipe Manana
3e6ff34afc btrfs: clear BTRFS_ROOT_IN_TRANS_SETUP on early exit from record_root_in_trans()
If we exit early because the transaction that last used the root already
matches the current transaction, we leave the BTRFS_ROOT_IN_TRANS_SETUP
bit set in the root (which we just set right before the exit). While this
does not cause any functional issue, it makes callers of
btrfs_record_root_in_trans() lock fs_info->reloc_mutex and call
record_root_in_trans() for nothing, causing unnecessary lock contention,
until one of them clears the bit in record_root_in_trans().
One caller of btrfs_record_root_in_trans() is start_transaction(), used to
start new transaction or joining an existing one, which is a hot path.

So clear BTRFS_ROOT_IN_TRANS_SETUP on early exit.

Assisted-by: LLM
Reviewed-by: Boris Burkov <boris@bur.io>
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2026-10-05 16:05:43 +02:00
Eric Biggers
e58c3805ed fsverity: RCU-delay the freeing of struct fsverity_info
__fsverity_get_info() calls rhashtable_lookup_fast(), which just uses
rcu_read_lock() and doesn't directly synchronize with
fsverity_remove_info().

For the same inode this isn't a problem: its fsverity_info is removed
only at inode eviction time or upon failure to enable verity, when the
inode no longer needs its fsverity_info and it will no longer be
accessed via that inode.

However, this is broken and can cause a use-after-free for concurrent
__fsverity_get_info() for *different* inodes.  Those rely on following
fsverity_info::rhash_head in the rhashtable under rcu_read_lock() only.
They also use fsverity_info::inode to do the key comparison.

Fix this by RCU-delaying the freeing of 'struct fsverity_info' after
it's been removed from the rhashtable.

Fixes: f77f281b61 ("fsverity: use a hashtable to find the fsverity_info")
Cc: stable@vger.kernel.org
Reviewed-by: Christoph Hellwig <hch@lst.de>
Reviewed-by: Sandeep Dhavale <dhavale@google.com>
Link: https://patch.msgid.link/20261001171349.81454-1-ebiggers@kernel.org
Signed-off-by: Eric Biggers <ebiggers@kernel.org>
2026-10-05 10:50:37 +02:00
Christian Brauner
e08f57a7dc readdir: take no inode lock on an immutable directory
iterate_dir() takes the directory's i_rwsem shared and holds it across
->iterate_shared(). For a directory that never has an entry and is
never removed the lock keeps nothing still, it only orders every reader
and every writer of that inode behind each other.

For the directory of a nullfs instance that matters. The instance of
the initial mount namespace is the root of every empty mount namespace
and the private instance is the root of every kernel thread, so one
inode is shared across users who have nothing else in common. And a
reader can hold the lock for as long as it likes: back the getdents()
buffer with a mapping of a file on a FUSE mount of your own, let the
copy of "." and ".." fault and let the server wait. Queue an exclusive
taker behind it, a mkdir() in that directory goes through start_dirop()
before the read-only mount is reported, and from then on every lookup
that misses the dcache in that directory, every create and every mount
on it waits until the server answers. One user of an empty mount
namespace stalls all the others.

Add FOP_IMMUTABLE for the file operations of a directory that never
changes and is never removed and let iterate_dir() skip the lock for
it. The flag never changes for a file, ->f_pos is protected by
f_pos_lock since directories are FMODE_ATOMIC_POS, IS_DEADDIR can't be
set on such a directory and neither touch_atime() nor fsnotify take
i_rwsem. Set it on the nullfs directory. The placeholder directories of
libfs never have an entry either but their owners remove them, so they
keep the lock.

Fixes: 9d4e752a24 ("namespace: allow creating empty mount namespaces")
Cc: stable@vger.kernel.org # v7.1+
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-19-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:36:13 +02:00
Christian Brauner
31a1fb9062
nullfs: refuse file locks
Refuse flock() and POSIX locks on nullfs. Its one inode is the root of
every kernel thread and the following patches make it the directory
that stands in for an unmounted mount, so a lock taken through one such
directory would block the locks of every other holder and F_GETLK would
tell them the pid of the holder. Give the directory file operations of
its own: what libfs gives an empty directory plus ->lock and ->flock
that fail with ENOLCK, the way a filesystem without lock support does.

Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-17-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:40 +02:00
Christian Brauner
c9ec8607b4
fsnotify: let a filesystem refuse marks on its objects
Don't let nullfs be watched. fanotify refuses mount and filesystem marks
on SB_NOUSER superblocks but inode marks of inotify, fanotify and
dnotify go through. The one inode of knullfs is the root of every
kernel thread and the following patches make it reachable from
userspace as the directory that stands in for an unmounted mount. A
watch placed through one such directory would report the opens through
all the others, across users.

Add FS_DISALLOW_NOTIFY next to FS_DISALLOW_NOTIFY_PERM, refuse a mark on
any object of such a filesystem in fsnotify_add_mark_list() where every
backend ends up and set it for nullfs. There's nothing to watch on a
permanently empty and immutable filesystem.

Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-16-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:40 +02:00
Christian Brauner
e22e69c2ad
namespace: nothing is mounted on or written through knullfs
Mark the root of knullfs with dont_mount(). Nothing is ever mounted on
the root of a kernel thread and the following patches make that root
reachable from userspace, so say it on the dentry where it doesn't
depend on the mount being in no namespace. Let do_lock_mount() refuse
such a target before it takes the inode lock and namespace_sem. The
flag is sticky so the check needs no lock. The one under the locks
stays for a mountpoint that is being removed.

Make the mount read-only as well. Its one inode is immutable so nothing
could be changed through it anyway, but MNT_READONLY makes that visible
the usual way: EROFS instead of EPERM and ST_RDONLY in statvfs().

Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-15-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:40 +02:00
Christian Brauner
4209cd7f54
namespace: keep the private nullfs instance in knullfs
The private nullfs instance that kernel threads are confined to is only
ever reachable through init_task's root and pwd. The following patches
point mounts at it as well, so keep it in a global.

No functional changes.

Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-14-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:40 +02:00
Christian Brauner
67b69b8a6e
fhandle: decide the subtree check under mount_lock
may_decode_fh() lets a caller who is privileged over the mount
namespace of @root decode handles below @root->dentry as long as the
mount is mounted and no locked child covers something below it. The
three parts are read one after the other without a lock: is_mounted()
and capable_wrt_mount() read ->mnt_ns and has_locked_children() walks
->mnt_mounts under a mount_lock of its own.

Today the gaps are harmless. A lazy umount in between clears ->mnt_ns,
but the locked children stay attached to their unmounted parent, so the
walk still finds them and the decode is refused either way. The
following patches detach every unmounted mount from its parent. Then
an umount between is_mounted() and the walk makes the walk come back
empty and the caller decodes into what a locked child covered.

So take mount_lock once and answer all three questions under it.
is_mounted() is stable there, umount_tree() clears ->mnt_ns on the
write side, and a mounted mount still has its locked children on its
list. ns_capable() under the spinlock is fine, generic_permission()
calls it in RCU walk already. has_locked_children() loses its locking
wrapper, its other callers hold namespace_sem or mount_lock anyway.

Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-13-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:40 +02:00
Christian Brauner
d74d66aa70
namespace: keep the lock on a mount that a propagated copy is moved beneath
attach_recursive_mnt() transfers MNT_LOCKED from the top mount to the
mount that is moved beneath it with MOVE_MOUNT_BENEATH. This allows the
owner of a user namespace to replace its locked /proc or its root. The
mount beneath takes on the job of covering the underlying mountpoint
allowing the top mount to be unmounted.

Consider two mount namespaces:

(H) The host H has a shared mount P with a secret in P/d
(Z) Z is a user namespace made from M. Its copy of P receives
    propagation from the host's P and its copy of M's cover on P/d is
    locked Z's owner must not get to see P/d.

Now (H) mounts X on P/d. This propagates into (Z). The copy of X lands
beneath (Z)'s locked cover. The locked property is transfered from (Z)'s
cover to the copy of X propagated beneath it. The cover is now unlocked.

Now (H) unmounts X again. The copy of X in (Z) gets unmounted and the
covering mount is left unlocked on top of P/d. (Z) can now unmount it:

    Z: umount2(P/d)             = EINVAL            /* the cover is locked */
    H: mount X on P/d, umount X                     /* both propagate into Z /*
    Z: umount2(P/d)             = 0                 /* the cover is now unlocked */
    Z: read P/d/secret          = "covered-by-root" /* secret revealed */

So only transfer the locked property to the mount beneath for mounts the
caller has placed. A propagated copy that lands beneath a locked mount
is locked as well so that the mount at the bottom of the stack carries a
lock the way every check expects. The mount on top of it remains locked
to ensure that it keeps covering even if the propagated mount is
unmounted again.

Fixes: c62a476693 ("move_mount: transfer MNT_LOCKED")
Cc: stable@vger.kernel.org # v7.1+
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-9-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:39 +02:00
Christian Brauner
f78ddcc872
namespace: never expire a locked mount
Locked mounts are special. They protect the underlying files and
directories from being revealed. do_umount() refuses to unmount locked
mounts but shrink_submounts() doesn't.

A shrinkable mount can become locked once the owner of a user namespace
puts it beneath a locked mount with MOVE_MOUNT_BENEATH. The locked
property now moves to the mount at the bottom making it possible to
unmount the top mount.

So umount() of an unlocked ancestor now expires the bottom mount first
and the covered directory is revealed:

  move_mount(c -> x/hidden, MOVE_MOUNT_BENEATH) = 0
  the cover after the lock moved:  umount2(x/hidden) = 0
  the holder of the lock:          umount2(x/hidden) = EINVAL
  the root of the copy, busy:      umount2(x) = EBUSY
  reads x/hidden/secret: "covered-by-root"

So leave a locked mount alone as it dies together with its parent. A
plain umount() of an unlocked mount with a locked one below it is EBUSY
from now on. It's the same for any other locked child. A lazy umount
still takes the whole tree.

mark_mounts_for_expiry() never sees a locked mount. lock_mnt_tree()
leaves a mount on an expiry list alone and nothing else puts a locked
one on such a list. Add an assert for this.

Fixes: 5ff9d8a65c ("vfs: Lock in place mounts from more privileged users")
Fixes: c62a476693 ("move_mount: transfer MNT_LOCKED")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-8-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:39 +02:00
Christian Brauner
caf16f9b4a
nullfs: don't update the access time
There's no point in updating access times of the nullfs instance. Raise
S_IMMUTABLE.

Fixes: 9d4e752a24 ("namespace: allow creating empty mount namespaces")
Cc: stable@vger.kernel.org # v7.1+
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-7-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:39 +02:00
Christian Brauner
33aadc7c72
namespace: handle mount locking for automounts correctly
If mounts are propagated across user namespaces, attach_recursive_mnt()
locks every copy of the source mount to protect overmounts from
vanishing and revealing the underlying files or directories.

The user namespace is taken from the caller's mount namespaces since
this is where the mounts end up. Except, that's not always true.
Automounts may legitimately get popped in by tasks located in a
different mount and user namespace during path lookup.

Then check doesn't make sense at that point. The copy in the namespace
of the parent - which may be the host's - is now locked and the host
cannot change the flags of its own mounts anymore.

Here's the reproducer:

- host has debugfs mounted nosuid,nodev,noexec and shared
- tracefs automount below it is not yet active
- hand a child process a directory descriptor on that mount
- child process enters auser and mount namespace
- child process' namespace now holds a locked copy of the host's debugfs
  mount that receives propagation from it
- child process stats "tracing/." through the descriptor
- lookup runs on the host's mount so the automount lands below the host's mount
- propagation puts a copy below the child's copy
- both try to clear the flags on the mount they got, with a bind remount:

    host, on its own automount:            MS_REMOUNT|MS_BIND = EPERM
    child, on the copy in its namespace:   MS_REMOUNT|MS_BIND = 0

So the lock landed on the host's mount instead of the child's copy.
Congrats. So we need to compare with the owner of the namespace the
mount actually gets mounted on. For all regular cases that is the
caller's mount namespace and so nothing changes.

Detached trees in anonymous mount namespaces by be handed over via
SCM_RIGHTS or inherited in other ways on purpose so the attaching task's
mount namespace is authoritative, not the creator of the detached tree.

Fixes: 132c94e31b ("vfs: Carefully propogate mounts across user namespaces")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-6-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:39 +02:00
Christian Brauner
ba985ce084
namespace: refuse an automount below a mount that is in no namespace
It's possible to add autmounts even when the parent mount isn't in the
mount namespace of the caller. The only requirement we have is that the
parent mount namespace must not be NULL, i.e., unmounted.

Problem is that clone_private_mount() has MNT_NS_INTERNAL which makes
that trivially true. So that passes the test and attach_recursive_mnt()
accepts that as a mount point and funny enough, count_mounts()
dereferences MNT_NS_INTERNAL. The problem is it is an error pointer...

So we can reach this in userspace via fanotify. A filesystem mark on the
lower filesystem of an overlay reports paths on the layer clone and
reading the event hands out a descriptor on it. For example with debugfs
as the lower layer it goes kaboom:

  openat(evfd, "tracing", O_DIRECTORY)

  Oops: general protection fault
  KASAN: null-ptr-deref in range [0x1d0-0x1d7]
  RIP: 0010:count_mounts+0x35/0x200
   attach_recursive_mnt
   finish_automount

And since that sleeping beauty happens under namespace_sem held for
writing every mount operation on the system blocks from then on.
Congrats.

Use is_mounted() instead which rejects unmounted and internal mounts
alike. The open fails with EINVAL just as it did before
clone_private_mount() used MNT_NS_INTERNAL.

Fixes: df820f8de4 ("ovl: make private mounts longterm")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-5-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:39 +02:00
Christian Brauner
b0d1022b9b
fcntl: refuse F_SET_RW_HINT on an immutable inode
An immutable inode is never written to so write hints are pointless.
Refuse the hint on an immutable inode the way setattr(), fallocate() and
etxattr() refuse their changes.

Fixes: c75b1d9421 ("fs: add fcntl() interface for setting/getting write life time hints")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-3-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:38 +02:00
Christian Brauner
8189ab688f
namei: don't reveal overmounted entries in refwalk
In rcuwalk the dentry is validated before it is accepted. step_into()
step_into() rechecks d_seq and __follow_mount_rcu() rechecks mount_lock.
An entry that gets unlinked in between causes the lookup to retry and
miss.

A refwalk doesn't do this. lookup_fast() takes a reference on the hashed
dentry and simply accepts it. So an unlink that happens after the
reference was taken isn't seen by refwalk. That's fine for a simple
file. We just happened to open it before it was unlinked, no problem.

For a mountpoint and specifically a locked mountpoint it very much
isn't. The unlink detaches all mounts and then removes the name. Any
refwalk that hasn't traversed the mounts yet simply reveals the
underlying entry. It's a very narrow window but it can be hit:

  reads of a covered file in 60 s, 3 walkers, ~15000 unlinks
    no widening            27
    that step + 200 us   3741

See the appended patch for a more reliable reproducer.

So check the entry after step_into(). unlink(), rmdir() and rename()
mark the dentry with dont_mount() before they detach the mounts and
remove the dentry.

So a dentry that is marked with DCACHE_CANT_MOUNT and is unhashed by the
time its mounts were looked at is a name that was unlinked under the
refwalk. DCACHE_CANT_MOUNT is read after the mounts were looked up and
it is set before they are detached by detach_mounts(). A refwalk that
missed the mounts will see DCACHE_CANT_MOUNT.

Plain d_unlinked() is fine. A rename takes the dentry off its hash chain
but ___d_drop() leaves d_hash.pprev set. So only __d_drop() and a rename
over the dentry unhash it. The only move that flips IS_ROOT splices in a
disconnected alias. That doesn't have DCACHE_CANT_MOUNT set.

Add the to step_into_slowpath(). ".." and LOOKUP_DOWN may legitimately
land on an unhashed directory. A refwalk that crossed onto a mount has
path.mnt different from nd->path.mnt. A dentry that a filesystem dropped
on its own (d_invalidate(), d_drop()) doesn't have the flag set and is
treated as before.

  // SPDX-License-Identifier: GPL-2.0
  /*
   * unlink_covered: unlink a file that is a mountpoint in a detached copy of
   * its mount, against walkers that open it through that copy.
   *
   * A is a tmpfs with the file f1 (content SECRET_F) and the plain file
   * MARK_A. A' is an open_tree(OPEN_TREE_CLONE) copy of A with MARK_A bound
   * on A'/f1, held through an O_PATH fd on its root once the tree fd is
   * closed. Walkers open f1 through that fd while the driver unlinks A/f1,
   * where nothing is mounted on it. A walker may read MARK_A or get ENOENT.
   * A read of SECRET_F is a hit: the name was found after its mount was
   * gone. The lockless walks use openat2(RESOLVE_CACHED).
   *
   * usage: unlink_covered [-t seconds] [-w walkers]
   */
  #ifndef _GNU_SOURCE
  #define _GNU_SOURCE
  #endif
  #include <errno.h>
  #include <fcntl.h>
  #include <pthread.h>
  #include <stdatomic.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  #include <sys/mount.h>
  #include <sys/stat.h>
  #include <sys/syscall.h>
  #include <linux/openat2.h>

  #ifndef __NR_open_tree
  #define __NR_open_tree 428
  #endif
  #ifndef __NR_move_mount
  #define __NR_move_mount 429
  #endif
  #ifndef __NR_openat2
  #define __NR_openat2 437
  #endif
  #ifndef OPEN_TREE_CLONE
  #define OPEN_TREE_CLONE 1
  #endif
  #ifndef OPEN_TREE_CLOEXEC
  #define OPEN_TREE_CLOEXEC O_CLOEXEC
  #endif
  #ifndef MOVE_MOUNT_F_EMPTY_PATH
  #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
  #endif

  #define WORK "/tmp/uc"
  #define ADIR WORK "/A"

  static int duration = 60, nwalkers = 3;
  static atomic_int stop, writer_waiting;
  static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER;
  static int cur_fd = -1;		/* the root of A', -1 while there is none */
  static atomic_long n_unlink, n_walk, n_mark, n_enoent, n_other, n_secret,
  		   n_secret_cached;

  static void die(const char *what)
  {
  	fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
  	exit(2);
  }

  static void put_file(int dfd, const char *name, const char *content)
  {
  	int fd = openat(dfd, name, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
  			0644);

  	if (fd < 0 || write(fd, content, strlen(content)) < 0)
  		die(name);
  	close(fd);
  }

  /* "plain/../" @n times, then f1: a longer walk that checks nothing on its way */
  static char *longpath(int n)
  {
  	char *p = malloc(n * 9 + 3), *q = p;

  	for (int i = 0; i < n; i++, q += 9)
  		memcpy(q, "plain/../", 9);
  	strcpy(q, "f1");
  	return p;
  }

  static void try_read(int dfd, const char *path, int cached)
  {
  	struct open_how how = { .flags = O_RDONLY | O_CLOEXEC,
  				.resolve = RESOLVE_CACHED };
  	char buf[32] = "";
  	long n;
  	int fd;

  	if (cached)
  		fd = syscall(__NR_openat2, dfd, path, &how, sizeof(how));
  	else
  		fd = openat(dfd, path, O_RDONLY | O_CLOEXEC);
  	atomic_fetch_add(&n_walk, 1);
  	if (fd < 0) {
  		if (errno == ENOENT)
  			atomic_fetch_add(&n_enoent, 1);
  		else if (!cached || errno != EAGAIN)
  			atomic_fetch_add(&n_other, 1);
  		return;
  	}
  	n = read(fd, buf, sizeof(buf) - 1);
  	close(fd);
  	if (n >= 6 && !strncmp(buf, "SECRET", 6)) {
  		if (cached)
  			atomic_fetch_add(&n_secret_cached, 1);
  		if (!atomic_fetch_add(&n_secret, 1))
  			printf("HIT: read \"%s\" through %s (%s walk)\n", buf,
  			       path, cached ? "lockless" : "any");
  	} else {
  		atomic_fetch_add(&n_mark, 1);
  	}
  }

  static void *walker(void *arg)
  {
  	unsigned int r = (long)arg * 2654435761u;

  	while (!atomic_load(&stop)) {
  		char *p_long, *p_short;
  		int dfd;

  		while (atomic_load(&writer_waiting) && !atomic_load(&stop))
  			usleep(20);
  		pthread_rwlock_rdlock(&cur_lock);
  		dfd = cur_fd;
  		if (dfd < 0) {
  			pthread_rwlock_unlock(&cur_lock);
  			usleep(100);
  			continue;
  		}
  		r = r * 1103515245u + 12345u;
  		p_long = longpath(1 + (r >> 8) % 400);
  		p_short = longpath(0);
  		try_read(dfd, p_long, 0);
  		try_read(dfd, p_short, 0);
  		try_read(dfd, p_long, 1);
  		pthread_rwlock_unlock(&cur_lock);
  		free(p_long);
  		free(p_short);
  	}
  	return NULL;
  }

  /* hand the walkers a new A' (or none), close the old one */
  static void publish(int fd)
  {
  	int old;

  	atomic_fetch_add(&writer_waiting, 1);
  	pthread_rwlock_wrlock(&cur_lock);
  	old = cur_fd;
  	cur_fd = fd;
  	pthread_rwlock_unlock(&cur_lock);
  	atomic_fetch_sub(&writer_waiting, 1);
  	if (old >= 0)
  		close(old);
  }

  static void *driver(void *arg __attribute__((unused)))
  {
  	int a;

  	if (mkdir(ADIR, 0755) && errno != EEXIST)
  		die("mkdir A");
  	if (mount("A", ADIR, "tmpfs", 0, "size=4M"))
  		die("mount A");
  	a = open(ADIR, O_PATH | O_DIRECTORY | O_CLOEXEC);
  	if (a < 0 || mkdirat(a, "plain", 0755))
  		die("A/plain");
  	put_file(a, "MARK_A", "MARK_A");
  	while (!atomic_load(&stop)) {
  		int t, m, fd_a;

  		put_file(a, "f1", "SECRET_F");
  		t = syscall(__NR_open_tree, a, "",
  			    OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH);
  		if (t < 0)
  			die("open_tree A");
  		m = syscall(__NR_open_tree, a, "MARK_A",
  			    OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
  		if (m < 0)
  			die("open_tree MARK_A");
  		if (syscall(__NR_move_mount, m, "", t, "f1",
  			    MOVE_MOUNT_F_EMPTY_PATH))
  			die("move_mount");
  		close(m);
  		fd_a = openat(t, ".", O_PATH | O_DIRECTORY | O_CLOEXEC);
  		if (fd_a < 0)
  			die("open A'");
  		publish(fd_a);
  		usleep(200 + rand() % 1000);
  		close(t);			/* A' is unmounted, fd_a holds it */
  		usleep(200 + rand() % 1000);
  		if (unlinkat(a, "f1", 0))	/* through A, a plain file there */
  			die("unlink f1");
  		atomic_fetch_add(&n_unlink, 1);
  		usleep(rand() % 300);
  		publish(-1);
  	}
  	close(a);
  	umount2(ADIR, MNT_DETACH);
  	return NULL;
  }

  int main(int argc, char **argv)
  {
  	pthread_t d, *w;
  	int c, i;

  	setvbuf(stdout, NULL, _IOLBF, 0);
  	while ((c = getopt(argc, argv, "t:w:")) != -1) {
  		switch (c) {
  		case 't':
  			duration = atoi(optarg);
  			break;
  		case 'w':
  			nwalkers = atoi(optarg);
  			break;
  		default:
  			fprintf(stderr, "usage: unlink_covered [-t seconds] [-w walkers]\n");
  			return 2;
  		}
  	}
  	if (unshare(CLONE_NEWNS) ||
  	    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
  		die("unshare");
  	if (mkdir(WORK, 0755) && errno != EEXIST)
  		die("mkdir");
  	w = calloc(nwalkers, sizeof(*w));
  	for (i = 0; i < nwalkers; i++)
  		if (pthread_create(&w[i], NULL, walker, (void *)(long)i))
  			die("pthread_create");
  	if (pthread_create(&d, NULL, driver, NULL))
  		die("pthread_create");
  	sleep(duration);
  	atomic_store(&stop, 1);
  	pthread_join(d, NULL);
  	for (i = 0; i < nwalkers; i++)
  		pthread_join(w[i], NULL);
  	printf("unlink_covered: %d s, %d walkers: unlinks %ld walks %ld mark %ld enoent %ld other %ld SECRET %ld (lockless %ld)\n",
  	       duration, nwalkers, atomic_load(&n_unlink), atomic_load(&n_walk),
  	       atomic_load(&n_mark), atomic_load(&n_enoent),
  	       atomic_load(&n_other), atomic_load(&n_secret),
  	       atomic_load(&n_secret_cached));
  	return atomic_load(&n_secret) ? 1 : 0;
  }

  diff --git a/fs/namei.c b/fs/namei.c
  --- a/fs/namei.c
  +++ b/fs/namei.c
  @@ -35,6 +35,7 @@
   #include <linux/fcntl.h>
   #include <linux/device_cgroup.h>
   #include <linux/fs_struct.h>
  +#include <linux/delay.h>
   #include <linux/posix_acl.h>
   #include <linux/hash.h>
   #include <linux/bitops.h>
  @@ -1205,9 +1206,26 @@ static int sysctl_protected_symlinks __read_mostly;
   static int sysctl_protected_hardlinks __read_mostly;
   static int sysctl_protected_fifos __read_mostly;
   static int sysctl_protected_regular __read_mostly;
  +/* debug: widen the two windows of the detach_mounts() race */
  +static int sysctl_detach_race_walk_us __read_mostly;
  +static int sysctl_detach_race_unlink_us __read_mostly;

   #ifdef CONFIG_SYSCTL
   static const struct ctl_table namei_sysctls[] = {
  +	{
  +		.procname	= "detach_race_walk_us",
  +		.data		= &sysctl_detach_race_walk_us,
  +		.maxlen		= sizeof(int),
  +		.mode		= 0644,
  +		.proc_handler	= proc_dointvec,
  +	},
  +	{
  +		.procname	= "detach_race_unlink_us",
  +		.data		= &sysctl_detach_race_unlink_us,
  +		.maxlen		= sizeof(int),
  +		.mode		= 0644,
  +		.proc_handler	= proc_dointvec,
  +	},
   	{
   		.procname	= "protected_symlinks",
   		.data		= &sysctl_protected_symlinks,
  @@ -1878,6 +1896,10 @@ static struct dentry *lookup_fast(struct nameidata *nd)
   		dentry = __d_lookup(parent, &nd->last);
   		if (unlikely(!dentry))
   			return NULL;
  +		/* debug: between finding a mountpoint and crossing its mounts */
  +		if (unlikely(sysctl_detach_race_walk_us) && d_mountpoint(dentry))
  +			usleep_range(sysctl_detach_race_walk_us,
  +				     sysctl_detach_race_walk_us + 10);
   		status = d_revalidate(nd->inode, &nd->last, dentry, nd->flags);
   	}
   	if (unlikely(status <= 0)) {
  @@ -5693,6 +5715,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir,
   			if (!error) {
   				dont_mount(dentry);
   				detach_mounts(dentry);
  +				/* debug: the mounts are gone, d_delete() is still to come */
  +				if (unlikely(sysctl_detach_race_unlink_us))
  +					usleep_range(sysctl_detach_race_unlink_us,
  +						     sysctl_detach_race_unlink_us + 10);
   			}
   		}
   	}

Fixes: 8ed936b567 ("vfs: Lazily remove mounts on unlinked files and directories.")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-2-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:38 +02:00
Christian Brauner
528b383dc3
namespace: unhash a dentry before detaching the mounts on it
unlink(), rmdir() and rename() remove the entry from the filesystem,
call detach_mounts() on the entry with the inode locked and then call
d_delete() once the inode is unlocked.

The thing is that between detach_mounts() and d_delete() the dentry is
still hashed and positive. But after detach_mounts() nothing covers the
dentry anymore. A lookup that finds this dentry in the dcache can
uncover what the mounts hid.

That's a problem when unlinking files or directories that are
mountpoints in other mount namespaces. Everybody who had the underlying
entry covered can race the detach_mount() call until d_delete() has run.

The race window isn't all that small. It encompasses namespace_unlock()
with a full synchronize_rcu_expedited() grace period and the
inode_unlock() of the entry.

In my experiments three walkers that kept trying read an overmounted
file in 11938 of the 16140 unlinks that removed its mountpoint from a
bind mount of the filesystem within a minute.

Fun fact, d_invalidate() has the same ordering problem but gets it
right. It unhashes the dentry first and detaches the mounts afterwards.
Let's do the same in __detach_mounts():

- A lookup that hasn't found the dentry yet misses it in the dcache and
  waits for the directory lock that the caller holds until the name is
  gone for good.

- A lockless lookup that found it already fails the mount_lock check
  while the dentry still counts as a mountpoint and the d_seq check once
  it doesn't, and retries.

This fixes the lockless path. We still need to fix the reference count
lookup in a follow-up patch.

So d_drop() the dentry. The dentry stays positive and held but can't be
found anymore. Then proceed with the detach and unlink.

Reproducer:

The reads were counted with the program below, three walkers for 60 s
in a VM with 4 CPUs. The race window is stretched with the debug patch
pasted here. fs.detach_race_walk_us sleeps in lookup_fast() once it
found a mountpoint dentry and before its mounts are crossed.
fs.detach_race_unlink_us sleeps in vfs_unlink() between detach_mounts()
and d_delete().

  // SPDX-License-Identifier: GPL-2.0
  /*
   * unlink_covered: unlink a file that is a mountpoint in a detached copy of
   * its mount, against lookups that open it through that copy.
   *
   * A is a tmpfs with the file f1 (content SECRET_F) and the plain file
   * MARK_A. A' is an open_tree(OPEN_TREE_CLONE) copy of A with MARK_A bound
   * on A'/f1, held through an O_PATH fd on its root once the tree fd is
   * closed. Walkers open f1 through that fd while the driver unlinks A/f1,
   * where nothing is mounted on it. A walker may read MARK_A or get ENOENT.
   * A read of SECRET_F is a hit: the name was found after its mount was
   * gone. The lockless walks use openat2(RESOLVE_CACHED).
   *
   * usage: unlink_covered [-t seconds] [-w walkers]
   */
  #ifndef _GNU_SOURCE
  #define _GNU_SOURCE
  #endif
  #include <errno.h>
  #include <fcntl.h>
  #include <pthread.h>
  #include <stdatomic.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  #include <sys/mount.h>
  #include <sys/stat.h>
  #include <sys/syscall.h>
  #include <linux/openat2.h>

  #ifndef __NR_open_tree
  #define __NR_open_tree 428
  #endif
  #ifndef __NR_move_mount
  #define __NR_move_mount 429
  #endif
  #ifndef __NR_openat2
  #define __NR_openat2 437
  #endif
  #ifndef OPEN_TREE_CLONE
  #define OPEN_TREE_CLONE 1
  #endif
  #ifndef OPEN_TREE_CLOEXEC
  #define OPEN_TREE_CLOEXEC O_CLOEXEC
  #endif
  #ifndef MOVE_MOUNT_F_EMPTY_PATH
  #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
  #endif

  #define WORK "/tmp/uc"
  #define ADIR WORK "/A"

  static int duration = 60, nwalkers = 3;
  static atomic_int stop, writer_waiting;
  static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER;
  static int cur_fd = -1;		/* the root of A', -1 while there is none */
  static atomic_long n_unlink, n_walk, n_mark, n_enoent, n_other, n_secret,
  		   n_secret_cached;

  static void die(const char *what)
  {
  	fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno));
  	exit(2);
  }

  static void put_file(int dfd, const char *name, const char *content)
  {
  	int fd = openat(dfd, name, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
  			0644);

  	if (fd < 0 || write(fd, content, strlen(content)) < 0)
  		die(name);
  	close(fd);
  }

  /* "plain/../" @n times, then f1: a longer walk that checks nothing on its way */
  static char *longpath(int n)
  {
  	char *p = malloc(n * 9 + 3), *q = p;

  	for (int i = 0; i < n; i++, q += 9)
  		memcpy(q, "plain/../", 9);
  	strcpy(q, "f1");
  	return p;
  }

  static void try_read(int dfd, const char *path, int cached)
  {
  	struct open_how how = { .flags = O_RDONLY | O_CLOEXEC,
  				.resolve = RESOLVE_CACHED };
  	char buf[32] = "";
  	long n;
  	int fd;

  	if (cached)
  		fd = syscall(__NR_openat2, dfd, path, &how, sizeof(how));
  	else
  		fd = openat(dfd, path, O_RDONLY | O_CLOEXEC);
  	atomic_fetch_add(&n_walk, 1);
  	if (fd < 0) {
  		if (errno == ENOENT)
  			atomic_fetch_add(&n_enoent, 1);
  		else if (!cached || errno != EAGAIN)
  			atomic_fetch_add(&n_other, 1);
  		return;
  	}
  	n = read(fd, buf, sizeof(buf) - 1);
  	close(fd);
  	if (n >= 6 && !strncmp(buf, "SECRET", 6)) {
  		if (cached)
  			atomic_fetch_add(&n_secret_cached, 1);
  		if (!atomic_fetch_add(&n_secret, 1))
  			printf("HIT: read \"%s\" through %s (%s walk)\n", buf,
  			       path, cached ? "lockless" : "any");
  	} else {
  		atomic_fetch_add(&n_mark, 1);
  	}
  }

  static void *walker(void *arg)
  {
  	unsigned int r = (long)arg * 2654435761u;

  	while (!atomic_load(&stop)) {
  		char *p_long, *p_short;
  		int dfd;

  		while (atomic_load(&writer_waiting) && !atomic_load(&stop))
  			usleep(20);
  		pthread_rwlock_rdlock(&cur_lock);
  		dfd = cur_fd;
  		if (dfd < 0) {
  			pthread_rwlock_unlock(&cur_lock);
  			usleep(100);
  			continue;
  		}
  		r = r * 1103515245u + 12345u;
  		p_long = longpath(1 + (r >> 8) % 400);
  		p_short = longpath(0);
  		try_read(dfd, p_long, 0);
  		try_read(dfd, p_short, 0);
  		try_read(dfd, p_long, 1);
  		pthread_rwlock_unlock(&cur_lock);
  		free(p_long);
  		free(p_short);
  	}
  	return NULL;
  }

  /* hand the walkers a new A' (or none), close the old one */
  static void publish(int fd)
  {
  	int old;

  	atomic_fetch_add(&writer_waiting, 1);
  	pthread_rwlock_wrlock(&cur_lock);
  	old = cur_fd;
  	cur_fd = fd;
  	pthread_rwlock_unlock(&cur_lock);
  	atomic_fetch_sub(&writer_waiting, 1);
  	if (old >= 0)
  		close(old);
  }

  static void *driver(void *arg __attribute__((unused)))
  {
  	int a;

  	if (mkdir(ADIR, 0755) && errno != EEXIST)
  		die("mkdir A");
  	if (mount("A", ADIR, "tmpfs", 0, "size=4M"))
  		die("mount A");
  	a = open(ADIR, O_PATH | O_DIRECTORY | O_CLOEXEC);
  	if (a < 0 || mkdirat(a, "plain", 0755))
  		die("A/plain");
  	put_file(a, "MARK_A", "MARK_A");
  	while (!atomic_load(&stop)) {
  		int t, m, fd_a;

  		put_file(a, "f1", "SECRET_F");
  		t = syscall(__NR_open_tree, a, "",
  			    OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH);
  		if (t < 0)
  			die("open_tree A");
  		m = syscall(__NR_open_tree, a, "MARK_A",
  			    OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
  		if (m < 0)
  			die("open_tree MARK_A");
  		if (syscall(__NR_move_mount, m, "", t, "f1",
  			    MOVE_MOUNT_F_EMPTY_PATH))
  			die("move_mount");
  		close(m);
  		fd_a = openat(t, ".", O_PATH | O_DIRECTORY | O_CLOEXEC);
  		if (fd_a < 0)
  			die("open A'");
  		publish(fd_a);
  		usleep(200 + rand() % 1000);
  		close(t);			/* A' is unmounted, fd_a holds it */
  		usleep(200 + rand() % 1000);
  		if (unlinkat(a, "f1", 0))	/* through A, a plain file there */
  			die("unlink f1");
  		atomic_fetch_add(&n_unlink, 1);
  		usleep(rand() % 300);
  		publish(-1);
  	}
  	close(a);
  	umount2(ADIR, MNT_DETACH);
  	return NULL;
  }

  int main(int argc, char **argv)
  {
  	pthread_t d, *w;
  	int c, i;

  	setvbuf(stdout, NULL, _IOLBF, 0);
  	while ((c = getopt(argc, argv, "t:w:")) != -1) {
  		switch (c) {
  		case 't':
  			duration = atoi(optarg);
  			break;
  		case 'w':
  			nwalkers = atoi(optarg);
  			break;
  		default:
  			fprintf(stderr, "usage: unlink_covered [-t seconds] [-w walkers]\n");
  			return 2;
  		}
  	}
  	if (unshare(CLONE_NEWNS) ||
  	    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
  		die("unshare");
  	if (mkdir(WORK, 0755) && errno != EEXIST)
  		die("mkdir");
  	w = calloc(nwalkers, sizeof(*w));
  	for (i = 0; i < nwalkers; i++)
  		if (pthread_create(&w[i], NULL, walker, (void *)(long)i))
  			die("pthread_create");
  	if (pthread_create(&d, NULL, driver, NULL))
  		die("pthread_create");
  	sleep(duration);
  	atomic_store(&stop, 1);
  	pthread_join(d, NULL);
  	for (i = 0; i < nwalkers; i++)
  		pthread_join(w[i], NULL);
  	printf("unlink_covered: %d s, %d walkers: unlinks %ld walks %ld mark %ld enoent %ld other %ld SECRET %ld (lockless %ld)\n",
  	       duration, nwalkers, atomic_load(&n_unlink), atomic_load(&n_walk),
  	       atomic_load(&n_mark), atomic_load(&n_enoent),
  	       atomic_load(&n_other), atomic_load(&n_secret),
  	       atomic_load(&n_secret_cached));
  	return atomic_load(&n_secret) ? 1 : 0;
  }

  diff --git a/fs/namei.c b/fs/namei.c
  --- a/fs/namei.c
  +++ b/fs/namei.c
  @@ -35,6 +35,7 @@
   #include <linux/fcntl.h>
   #include <linux/device_cgroup.h>
   #include <linux/fs_struct.h>
  +#include <linux/delay.h>
   #include <linux/posix_acl.h>
   #include <linux/hash.h>
   #include <linux/bitops.h>
  @@ -1205,9 +1206,26 @@ static int sysctl_protected_symlinks __read_mostly;
   static int sysctl_protected_hardlinks __read_mostly;
   static int sysctl_protected_fifos __read_mostly;
   static int sysctl_protected_regular __read_mostly;
  +/* debug: widen the two windows of the detach_mounts() race */
  +static int sysctl_detach_race_walk_us __read_mostly;
  +static int sysctl_detach_race_unlink_us __read_mostly;

   #ifdef CONFIG_SYSCTL
   static const struct ctl_table namei_sysctls[] = {
  +	{
  +		.procname	= "detach_race_walk_us",
  +		.data		= &sysctl_detach_race_walk_us,
  +		.maxlen		= sizeof(int),
  +		.mode		= 0644,
  +		.proc_handler	= proc_dointvec,
  +	},
  +	{
  +		.procname	= "detach_race_unlink_us",
  +		.data		= &sysctl_detach_race_unlink_us,
  +		.maxlen		= sizeof(int),
  +		.mode		= 0644,
  +		.proc_handler	= proc_dointvec,
  +	},
   	{
   		.procname	= "protected_symlinks",
   		.data		= &sysctl_protected_symlinks,
  @@ -1878,6 +1896,10 @@ static struct dentry *lookup_fast(struct nameidata *nd)
   		dentry = __d_lookup(parent, &nd->last);
   		if (unlikely(!dentry))
   			return NULL;
  +		/* debug: between finding a mountpoint and crossing its mounts */
  +		if (unlikely(sysctl_detach_race_walk_us) && d_mountpoint(dentry))
  +			usleep_range(sysctl_detach_race_walk_us,
  +				     sysctl_detach_race_walk_us + 10);
   		status = d_revalidate(nd->inode, &nd->last, dentry, nd->flags);
   	}
   	if (unlikely(status <= 0)) {
  @@ -5693,6 +5715,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir,
   			if (!error) {
   				dont_mount(dentry);
   				detach_mounts(dentry);
  +				/* debug: the mounts are gone, d_delete() is still to come */
  +				if (unlikely(sysctl_detach_race_unlink_us))
  +					usleep_range(sysctl_detach_race_unlink_us,
  +						     sysctl_detach_race_unlink_us + 10);
   			}
   		}
   	}

Fixes: 8ed936b567 ("vfs: Lazily remove mounts on unlinked files and directories.")
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20261002-work-mount-fixes-4-v1-1-dd44b89d44ce@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-10-04 12:31:38 +02:00
Linus Torvalds
ff47652a4b smb client fixes for v7.3-rc6
- Stop data dirtied past EOF through an mmap from reappearing as file
    content once the file is extended by a write, truncate, zero range,
    copy range or clone range
 
  - Flush dirty data and drain in-flight I/O before operations that
    assume the pagecache and the server agree on the file: querying
    allocated ranges, the O_TRUNC open, interior zero range, and
    server-side copy/clone
 
  - Stop a genuine size-extending zero range or preallocate from being
    refused with -EOPNOTSUPP when the inode is not read caching, by
    querying the server's authoritative EOF instead of trusting a stale
    cached i_size
 
  - Zero the untransferred tail of a short read, both in the netfs
    read-gaps path (where stale folio content could otherwise be written
    back to the server) and in the DIO/unbuffered read collector, and
    tell a real EOF apart from a stale cached remote_i_size after a lease
    downgrade
 
  - Require stable pages on signed connections so a buffered write can't
    modify a folio whose signature has already been computed and is in
    flight, which the server rejected with STATUS_ACCESS_DENIED and the
    client surfaced as -EIO
 
  - Split several cifsFileInfo flags out of a shared bitfield byte so
    concurrent updates taken under different locks no longer clobber each
    other through a byte-level RMW
 -----BEGIN PGP SIGNATURE-----
 
 iHUEABYKAB0WIQTcqRusfSdYROJQwGkpVtNKoQNdYwUCasAVCQAKCRApVtNKoQNd
 YxLEAQDBsM7NZk2+hUlPZMDq/NK+8C12Vm8WnN8nqb3Dpt8jCwEA2JJGMeO+p9/L
 /hwKubpM6CuWiG2yMbqyUHWVZSkOFwQ=
 =8v7I
 -----END PGP SIGNATURE-----

Merge tag 'cifs-fixes-7.3-rc6' of https://git.manguebit.org/linux

Pull smb client fixes from Paulo Alcantara:
 "Fix a series of data corruption and I/O error bugs found by running
  generic/363 (fsx) in a loop against Windows Server 2022 and Samba.

   - Stop data dirtied past EOF through an mmap from reappearing as file
     content once the file is extended by a write, truncate, zero range,
     copy range or clone range

   - Flush dirty data and drain in-flight I/O before operations that
     assume the pagecache and the server agree on the file: querying
     allocated ranges, the O_TRUNC open, interior zero range, and
     server-side copy/clone

   - Stop a genuine size-extending zero range or preallocate from being
     refused with -EOPNOTSUPP when the inode is not read caching, by
     querying the server's authoritative EOF instead of trusting a stale
     cached i_size

   - Zero the untransferred tail of a short read, both in the netfs
     read-gaps path (where stale folio content could otherwise be
     written back to the server) and in the DIO/unbuffered read
     collector, and tell a real EOF apart from a stale cached
     remote_i_size after a lease downgrade

   - Require stable pages on signed connections so a buffered write
     can't modify a folio whose signature has already been computed and
     is in flight, which the server rejected with STATUS_ACCESS_DENIED
     and the client surfaced as -EIO

   - Split several cifsFileInfo flags out of a shared bitfield byte so
     concurrent updates taken under different locks no longer clobber
     each other through a byte-level RMW"

* tag 'cifs-fixes-7.3-rc6' of https://git.manguebit.org/linux:
  smb: client: split cifsFileInfo bitfields to avoid shared-byte RMW races
  smb: client: require stable pages for signed connections
  smb: client: distinguish real EOF from a stale remote_i_size on read
  netfs: zero the tail of a short DIO/unbuffered read
  smb: client: only require read lease for size-extending preallocate
  netfs: zero gaps in read-gaps folio to avoid writing back stale data
  smb: client: only require read lease for size-extending zero range
  smb: client: drain and invalidate before server-side copy/clone
  smb: client: flush dirty data before zeroing a range
  smb: client: drain outstanding I/O before truncating on O_TRUNC open
  smb: client: flush and commit data before querying allocated ranges
  smb: client: discard post-EOF pagecache when extending a file via clone range
  smb: client: discard post-EOF pagecache when extending a file via copy range
  smb: client: discard post-EOF pagecache when extending a file via zero range
  smb: client: clear post-EOF pagecache when extending a file via truncate
  netfs: clear post-EOF pagecache when extending a file via write
2026-10-02 14:04:11 -07:00