All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 09/12] tests/9p: increase P9_MAX_SIZE for test client
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (7 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 02/12] hw/9pfs: add max_xattr option Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 03/12] qemu-options: document 9pfs max_xattr option Christian Schoenebeck
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Increase the maximum 9P message size from 4k to 32k to support larger
messages.

This is needed for the xattr tests being added with the subsequent
patches which are going to transmit xattrs of size 8k. It would have
also been possible to send them in multiple chunks, however let's not
overcomplicate things.

This new msize is still reasonable small compared to common msize
values on production systems.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/libqos/virtio-9p-client.h | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index c432b0daee..4b04324503 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -21,7 +21,8 @@
 #include "qgraph.h"
 #include "tests/qtest/libqtest-single.h"
 
-#define P9_MAX_SIZE 4096 /* Max size of a T-message or R-message */
+/* Max size of a T-message or R-message */
+#define P9_MAX_SIZE (32 * 1024)
 
 typedef struct {
     QTestState *qts;
-- 
2.47.3



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

* [PATCH 12/12] tests/9p: add 3 xattr FID limit test cases (local fs driver)
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 01/12] hw/9pfs: add xattr FID limit to prevent memory exhaustion Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 07/12] hw/9pfs: enable xattr (mockup) support for synth fs driver Christian Schoenebeck
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Analogue to the previously added 3 synth tests, add (similar) 3 test
cases using the "local" fs driver to verify correct xattr FID limit
enforcement of 9pfs server with a real filesystem.

These 3 new local tests use the shared test code of the previously
added 3 synth tests. The only difference is that the local fs driver
does not expose the current internal xattr FID counter, so we can't
verify this with the local tests.

These are "slow" tests, i.e. they are not running by default.
Use -m slow for running theses tests.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/virtio-9p-test.c | 69 ++++++++++++++++++++++++++++++++++--
 1 file changed, 66 insertions(+), 3 deletions(-)

diff --git a/tests/qtest/virtio-9p-test.c b/tests/qtest/virtio-9p-test.c
index 2e88429dfa..d559f0f8a0 100644
--- a/tests/qtest/virtio-9p-test.c
+++ b/tests/qtest/virtio-9p-test.c
@@ -395,6 +395,19 @@ static void do_xattr_limit(QVirtio9P *v9p, int max_xattr, bool check_counter)
     }
 }
 
+static void do_local_xattr_limit(QVirtio9P *v9p, int max_xattr)
+{
+    g_autofree char *test_file = virtio_9p_test_path("WRITE");
+
+    /*
+     * this file must be created for the test to work with the 'local' fs driver
+     */
+    g_file_set_contents(test_file, "", 0, NULL);
+
+    /* the actual test code shared with the 'synth' fs driver tests */
+    do_xattr_limit(v9p, max_xattr, false);
+}
+
 static void fs_walk_no_slash(void *obj, void *data, QGuestAllocator *t_alloc)
 {
     QVirtio9P *v9p = obj;
@@ -990,6 +1003,27 @@ static void fs_deep_absolute_path(void *obj, void *data,
     g_string_free(path, TRUE);
 }
 
+static void fs_local_xattr_limit_default(void *obj, void *data,
+                                         QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_local_xattr_limit(obj, V9FS_MAX_XATTR_DEFAULT);
+}
+
+static void fs_local_xattr_limit_custom(void *obj, void *data,
+                                        QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_local_xattr_limit(obj, 100);
+}
+
+static void fs_local_xattr_limit_unlimited(void *obj, void *data,
+                                           QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_local_xattr_limit(obj, -1);
+}
+
 static void *synth_max_xattr_custom_opt(GString *cmd_line, void *arg)
 {
     virtio_9p_add_synth_driver_args(cmd_line, "max_xattr=100");
@@ -1008,20 +1042,41 @@ static void cleanup_9p_local_driver(void *data)
     virtio_9p_remove_local_test_dir();
 }
 
-static void *assign_9p_local_driver(GString *cmd_line, void *arg)
+static void assign_9p_local_driver_with_args(GString *cmd_line,
+                                             const char *extra_opts)
 {
     /* make sure test dir for the 'local' tests exists */
     virtio_9p_create_local_test_dir();
 
-    virtio_9p_assign_local_driver(cmd_line, "security_model=mapped-xattr");
+    g_autofree char *opts = g_strdup("security_model=mapped-xattr");
+    if (extra_opts) {
+        opts = g_strdup_printf("%s,%s", opts, extra_opts);
+    }
+    virtio_9p_assign_local_driver(cmd_line, opts);
 
     g_test_queue_destroy(cleanup_9p_local_driver, NULL);
+}
+
+static void *assign_9p_local_driver(GString *cmd_line, void *arg)
+{
+    assign_9p_local_driver_with_args(cmd_line, NULL);
+    return arg;
+}
+
+static void *local_max_xattr_custom_opt(GString *cmd_line, void *arg)
+{
+    assign_9p_local_driver_with_args(cmd_line, "max_xattr=100");
+    return arg;
+}
+
+static void *local_max_xattr_unlimited_opt(GString *cmd_line, void *arg)
+{
+    assign_9p_local_driver_with_args(cmd_line, "max_xattr=0");
     return arg;
 }
 
 static void register_virtio_9p_test(void)
 {
-
     QOSGraphTestOptions opts = {
     };
 
@@ -1078,6 +1133,14 @@ static void register_virtio_9p_test(void)
                  &opts);
     qos_add_test("local/deep_absolute_path", "virtio-9p",
                  fs_deep_absolute_path, &opts);
+    qos_add_test("local/xattr_limit/default", "virtio-9p",
+                 fs_local_xattr_limit_default, &opts);
+    opts.before = local_max_xattr_custom_opt;
+    qos_add_test("local/xattr_limit/custom", "virtio-9p",
+                 fs_local_xattr_limit_custom, &opts);
+    opts.before = local_max_xattr_unlimited_opt;
+    qos_add_test("local/xattr_limit/unlimited", "virtio-9p",
+                 fs_local_xattr_limit_unlimited, &opts);
 }
 
 libqos_init(register_virtio_9p_test);
-- 
2.47.3



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

* [PATCH 04/12] tests/9p: add Tread / Rread test client functions
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (3 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 08/12] hw/9pfs: add xattr count query interface fo fs synth driver Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 10/12] tests/9p: add virtio_9p_add_synth_driver_args() test client function Christian Schoenebeck
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add v9fs_tread() and v9fs_rread() functions to the 9P test client
for reading files from 9pfs server in test cases.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/libqos/virtio-9p-client.c | 45 +++++++++++++++++++++++++++
 tests/qtest/libqos/virtio-9p-client.h | 33 ++++++++++++++++++++
 tests/qtest/virtio-9p-test.c          |  1 +
 3 files changed, 79 insertions(+)

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index af01d4c345..b9785ef8cb 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -241,6 +241,7 @@ static const char *rmessage_name(uint8_t id)
         id == P9_RUNLINKAT ? "RUNLINKAT" :
         id == P9_RFLUSH ? "RFLUSH" :
         id == P9_RREADDIR ? "RREADDIR" :
+        id == P9_RREAD ? "RREAD" :
         "<unknown>";
 }
 
@@ -1103,3 +1104,47 @@ void v9fs_runlinkat(P9Req *req)
     v9fs_req_recv(req, P9_RUNLINKAT);
     v9fs_req_free(req);
 }
+
+/* size[4] Tread tag[2] fid[4] offset[8] count[4] */
+TReadRes v9fs_tread(TReadOpt opt)
+{
+    P9Req *req;
+    uint32_t err;
+
+    g_assert(opt.client);
+
+    uint32_t body_size = 4 + 8 + 4;
+
+    req = v9fs_req_init(opt.client, body_size, P9_TREAD, opt.tag);
+    v9fs_uint32_write(req, opt.fid);
+    v9fs_uint64_write(req, opt.offset);
+    v9fs_uint32_write(req, opt.count);
+    v9fs_req_send(req);
+
+    if (!opt.requestOnly) {
+        v9fs_req_wait_for_reply(req, NULL);
+        if (opt.expectErr) {
+            v9fs_rlerror(req, &err);
+            g_assert_cmpint(err, ==, opt.expectErr);
+        } else {
+            v9fs_rread(req, opt.rread.count, opt.rread.data);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TReadRes) {
+        .req = req,
+        .count = opt.rread.count ? *opt.rread.count : 0
+    };
+}
+
+/* size[4] Rread tag[2] count[4] data[count] */
+void v9fs_rread(P9Req *req, uint32_t *count, void *data)
+{
+    v9fs_req_recv(req, P9_RREAD);
+    v9fs_uint32_read(req, count);
+    if (data && *count > 0) {
+        v9fs_memread(req, data, *count);
+    }
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index e3221a3104..37f2517cff 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -473,6 +473,37 @@ typedef struct TunlinkatRes {
     P9Req *req;
 } TunlinkatRes;
 
+/* options for 'Tread' 9p request */
+typedef struct TReadOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID of file to read from (required) */
+    uint32_t fid;
+    /* start position of read from beginning of file (optional) */
+    uint64_t offset;
+    /* how many bytes to read (required) */
+    uint32_t count;
+    /* data being received from 9p server as 'Rread' response (optional) */
+    struct {
+        uint32_t *count;
+        void *data;
+    } rread;
+    /* only send Tread request but not wait for a reply? (optional) */
+    bool requestOnly;
+    /* do we expect an Rlerror response, if yes which error code? (optional) */
+    uint32_t expectErr;
+} TReadOpt;
+
+/* result of 'Tread' 9p request */
+typedef struct TReadRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+    /* amount of bytes read */
+    uint32_t count;
+} TReadRes;
+
 void v9fs_set_allocator(QGuestAllocator *t_alloc);
 void v9fs_memwrite(P9Req *req, const void *addr, size_t len);
 void v9fs_memskip(P9Req *req, size_t len);
@@ -524,5 +555,7 @@ TlinkRes v9fs_tlink(TlinkOpt);
 void v9fs_rlink(P9Req *req);
 TunlinkatRes v9fs_tunlinkat(TunlinkatOpt);
 void v9fs_runlinkat(P9Req *req);
+TReadRes v9fs_tread(TReadOpt opt);
+void v9fs_rread(P9Req *req, uint32_t *count, void *data);
 
 #endif
diff --git a/tests/qtest/virtio-9p-test.c b/tests/qtest/virtio-9p-test.c
index 1c69d41e33..8ccec77e70 100644
--- a/tests/qtest/virtio-9p-test.c
+++ b/tests/qtest/virtio-9p-test.c
@@ -31,6 +31,7 @@
 #define tsymlink(...) v9fs_tsymlink((TsymlinkOpt) __VA_ARGS__)
 #define tlink(...) v9fs_tlink((TlinkOpt) __VA_ARGS__)
 #define tunlinkat(...) v9fs_tunlinkat((TunlinkatOpt) __VA_ARGS__)
+#define tread(...) v9fs_tread((TReadOpt) __VA_ARGS__)
 
 static void pci_config(void *obj, void *data, QGuestAllocator *t_alloc)
 {
-- 
2.47.3



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

* [PATCH 08/12] hw/9pfs: add xattr count query interface fo fs synth driver
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (2 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 07/12] hw/9pfs: enable xattr (mockup) support for synth fs driver Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 04/12] tests/9p: add Tread / Rread test client functions Christian Schoenebeck
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add a synthetic /stat/xattr_count file path that, if being read by 9p
client, returns the the 9p server internal xattr FID counter to
client.

This allows to test and verify that the xattr FID limit is being
enforced correctly.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 hw/9pfs/9p-synth.c | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/hw/9pfs/9p-synth.c b/hw/9pfs/9p-synth.c
index 4b0732e093..3b3654b282 100644
--- a/hw/9pfs/9p-synth.c
+++ b/hw/9pfs/9p-synth.c
@@ -565,6 +565,19 @@ static ssize_t v9fs_synth_qtest_flush_write(void *buf, int len, off_t offset,
     return 1;
 }
 
+/* transmits internal xattr counter to client */
+static ssize_t v9fs_synth_read_xattr_count(void *buf, int len, off_t offset,
+                                           void *arg)
+{
+    FsContext *ctx = arg;
+    size_t local_count = ctx->xattr_fid_count;
+    if (len < (int)sizeof(size_t)) {
+        return -ENOSPC;
+    }
+    memcpy(buf, &local_count, sizeof(size_t));
+    return sizeof(size_t);
+}
+
 static int synth_init(FsContext *ctx, Error **errp)
 {
     QLIST_INIT(&synth_root.child);
@@ -626,6 +639,19 @@ static int synth_init(FsContext *ctx, Error **errp)
                 g_free(name);
             }
         }
+
+        /* Directory for internal statistic queries */
+        {
+            V9fsSynthNode *stat_dir = NULL;
+            ret = qemu_v9fs_synth_mkdir(NULL, 0755, "stat", &stat_dir);
+            assert(!ret);
+
+            /* File for internal xattr count query */
+            ret = qemu_v9fs_synth_add_file(stat_dir, 0444, "xattr_count",
+                                           v9fs_synth_read_xattr_count,
+                                           NULL, ctx);
+            assert(!ret);
+        }
     }
 
     return 0;
-- 
2.47.3



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

* [PATCH 06/12] tests/9p: add Txattrcreate / Rxattrcreate test client functions
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (10 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 05/12] tests/9p: add Tclunk / Rclunk test client functions Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add v9fs_txattrcreate() and v9fs_rxattrcreate() functions to the
9P test client for testing creation of xattrs with 9pfs server.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/libqos/virtio-9p-client.c | 45 +++++++++++++++++++++++++++
 tests/qtest/libqos/virtio-9p-client.h | 30 ++++++++++++++++++
 tests/qtest/virtio-9p-test.c          |  1 +
 3 files changed, 76 insertions(+)

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index 83af6dab5d..305b0dac63 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -243,6 +243,7 @@ static const char *rmessage_name(uint8_t id)
         id == P9_RREADDIR ? "RREADDIR" :
         id == P9_RREAD ? "RREAD" :
         id == P9_RCLUNK ? "RCLUNK" :
+        id == P9_RXATTRCREATE ? "RXATTRCREATE" :
         "<unknown>";
 }
 
@@ -1182,3 +1183,47 @@ void v9fs_rclunk(P9Req *req)
     v9fs_req_recv(req, P9_RCLUNK);
     v9fs_req_free(req);
 }
+
+/* size[4] Txattrcreate tag[2] fid[4] name[s] attr_size[8] flags[4] */
+TXattrCreateRes v9fs_txattrcreate(TXattrCreateOpt opt)
+{
+    P9Req *req;
+    uint32_t err;
+
+    g_assert(opt.client);
+    g_assert(opt.name);
+
+    uint32_t body_size = 4 + 8 + 4;
+    uint16_t string_size = v9fs_string_size(opt.name);
+
+    g_assert_cmpint(body_size, <=, UINT32_MAX - string_size);
+    body_size += string_size;
+
+    req = v9fs_req_init(opt.client, body_size, P9_TXATTRCREATE, opt.tag);
+    v9fs_uint32_write(req, opt.fid);
+    v9fs_string_write(req, opt.name);
+    v9fs_uint64_write(req, opt.size);
+    v9fs_uint32_write(req, opt.flags);
+    v9fs_req_send(req);
+
+    err = 0;
+    if (!opt.requestOnly) {
+        v9fs_req_wait_for_reply(req, NULL);
+        if (opt.expectErr) {
+            v9fs_rlerror(req, &err);
+            g_assert_cmpint(err, ==, opt.expectErr);
+        } else {
+            v9fs_rxattrcreate(req);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TXattrCreateRes) { .req = req, .err = err };
+}
+
+/* size[4] Rxattrcreate tag[2] */
+void v9fs_rxattrcreate(P9Req *req)
+{
+    v9fs_req_recv(req, P9_RXATTRCREATE);
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index f7ef7f0067..c432b0daee 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -524,6 +524,34 @@ typedef struct TClunkRes {
     P9Req *req;
 } TClunkRes;
 
+/* options for 'Txattrcreate' 9p request */
+typedef struct TXattrCreateOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID to convert to xattr fid (required) */
+    uint32_t fid;
+    /* name of the xattr (required) */
+    const char *name;
+    /* size of the xattr value (required) */
+    uint64_t size;
+    /* flags: P9_XATTR_CREATE or P9_XATTR_REPLACE (optional) */
+    uint32_t flags;
+    /* only send Txattrcreate request but not wait for a reply? (optional) */
+    bool requestOnly;
+    /* do we expect an Rlerror response, if yes which error code? (optional) */
+    uint32_t expectErr;
+} TXattrCreateOpt;
+
+/* result of 'Txattrcreate' 9p request */
+typedef struct TXattrCreateRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+    /* error code if Rlerror received */
+    uint32_t err;
+} TXattrCreateRes;
+
 void v9fs_set_allocator(QGuestAllocator *t_alloc);
 void v9fs_memwrite(P9Req *req, const void *addr, size_t len);
 void v9fs_memskip(P9Req *req, size_t len);
@@ -579,5 +607,7 @@ TReadRes v9fs_tread(TReadOpt opt);
 void v9fs_rread(P9Req *req, uint32_t *count, void *data);
 TClunkRes v9fs_tclunk(TClunkOpt opt);
 void v9fs_rclunk(P9Req *req);
+TXattrCreateRes v9fs_txattrcreate(TXattrCreateOpt opt);
+void v9fs_rxattrcreate(P9Req *req);
 
 #endif
diff --git a/tests/qtest/virtio-9p-test.c b/tests/qtest/virtio-9p-test.c
index 099c5e0ca0..99a897b158 100644
--- a/tests/qtest/virtio-9p-test.c
+++ b/tests/qtest/virtio-9p-test.c
@@ -33,6 +33,7 @@
 #define tunlinkat(...) v9fs_tunlinkat((TunlinkatOpt) __VA_ARGS__)
 #define tread(...) v9fs_tread((TReadOpt) __VA_ARGS__)
 #define tclunk(...) v9fs_tclunk((TClunkOpt) __VA_ARGS__)
+#define txattrcreate(...) v9fs_txattrcreate((TXattrCreateOpt) __VA_ARGS__)
 
 static void pci_config(void *obj, void *data, QGuestAllocator *t_alloc)
 {
-- 
2.47.3



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

* [PATCH 05/12] tests/9p: add Tclunk / Rclunk test client functions
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (9 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 03/12] qemu-options: document 9pfs max_xattr option Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 06/12] tests/9p: add Txattrcreate / Rxattrcreate " Christian Schoenebeck
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add v9fs_tclunk() and v9fs_rclunk() functions to the 9P test client
for closing file handles (or "FIDs") in test cases.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/libqos/virtio-9p-client.c | 34 +++++++++++++++++++++++++++
 tests/qtest/libqos/virtio-9p-client.h | 22 +++++++++++++++++
 tests/qtest/virtio-9p-test.c          |  1 +
 3 files changed, 57 insertions(+)

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index b9785ef8cb..83af6dab5d 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -242,6 +242,7 @@ static const char *rmessage_name(uint8_t id)
         id == P9_RFLUSH ? "RFLUSH" :
         id == P9_RREADDIR ? "RREADDIR" :
         id == P9_RREAD ? "RREAD" :
+        id == P9_RCLUNK ? "RCLUNK" :
         "<unknown>";
 }
 
@@ -1148,3 +1149,36 @@ void v9fs_rread(P9Req *req, uint32_t *count, void *data)
     }
     v9fs_req_free(req);
 }
+
+/* size[4] Tclunk tag[2] fid[4] */
+TClunkRes v9fs_tclunk(TClunkOpt opt)
+{
+    P9Req *req;
+    uint32_t err;
+
+    g_assert(opt.client);
+
+    req = v9fs_req_init(opt.client, 4, P9_TCLUNK, opt.tag);
+    v9fs_uint32_write(req, opt.fid);
+    v9fs_req_send(req);
+
+    if (!opt.requestOnly) {
+        v9fs_req_wait_for_reply(req, NULL);
+        if (opt.expectErr) {
+            v9fs_rlerror(req, &err);
+            g_assert_cmpint(err, ==, opt.expectErr);
+        } else {
+            v9fs_rclunk(req);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TClunkRes) { .req = req };
+}
+
+/* size[4] Rclunk tag[2] */
+void v9fs_rclunk(P9Req *req)
+{
+    v9fs_req_recv(req, P9_RCLUNK);
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index 37f2517cff..f7ef7f0067 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -504,6 +504,26 @@ typedef struct TReadRes {
     uint32_t count;
 } TReadRes;
 
+/* options for 'Tclunk' 9p request */
+typedef struct TClunkOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID to clunk (required) */
+    uint32_t fid;
+    /* only send Tclunk request but not wait for a reply? (optional) */
+    bool requestOnly;
+    /* do we expect an Rlerror response, if yes which error code? (optional) */
+    uint32_t expectErr;
+} TClunkOpt;
+
+/* result of 'Tclunk' 9p request */
+typedef struct TClunkRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+} TClunkRes;
+
 void v9fs_set_allocator(QGuestAllocator *t_alloc);
 void v9fs_memwrite(P9Req *req, const void *addr, size_t len);
 void v9fs_memskip(P9Req *req, size_t len);
@@ -557,5 +577,7 @@ TunlinkatRes v9fs_tunlinkat(TunlinkatOpt);
 void v9fs_runlinkat(P9Req *req);
 TReadRes v9fs_tread(TReadOpt opt);
 void v9fs_rread(P9Req *req, uint32_t *count, void *data);
+TClunkRes v9fs_tclunk(TClunkOpt opt);
+void v9fs_rclunk(P9Req *req);
 
 #endif
diff --git a/tests/qtest/virtio-9p-test.c b/tests/qtest/virtio-9p-test.c
index 8ccec77e70..099c5e0ca0 100644
--- a/tests/qtest/virtio-9p-test.c
+++ b/tests/qtest/virtio-9p-test.c
@@ -32,6 +32,7 @@
 #define tlink(...) v9fs_tlink((TlinkOpt) __VA_ARGS__)
 #define tunlinkat(...) v9fs_tunlinkat((TunlinkatOpt) __VA_ARGS__)
 #define tread(...) v9fs_tread((TReadOpt) __VA_ARGS__)
+#define tclunk(...) v9fs_tclunk((TClunkOpt) __VA_ARGS__)
 
 static void pci_config(void *obj, void *data, QGuestAllocator *t_alloc)
 {
-- 
2.47.3



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

* [PATCH 02/12] hw/9pfs: add max_xattr option
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (6 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 11/12] tests/9p: add 3 xattr FID limit test cases (synth fs driver) Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 09/12] tests/9p: increase P9_MAX_SIZE for test client Christian Schoenebeck
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Previous patch introduced a limit of max. 1024 simultanious xattr FIDs.

This patch introduces an option "max_attr" that allows to override this
limit, just for the case that some user might run into this limit for
some reason, even if unlikely; or for reducing the limit further down
(e.g. that default limit of 1024 would cap at max. 64 MiB host memory,
at least on Linux hosts where the limit per xattr is 64k).

This new "max_xattr" option can be specified with both -fsdev and
-virtfs command line options, with the "local" and the "synth" fs
drivers.

The previous limit of 1024 is preserved as default value.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 fsdev/file-op-9p.h      |  2 ++
 fsdev/qemu-fsdev-opts.c |  6 ++++++
 fsdev/qemu-fsdev.c      |  2 +-
 hw/9pfs/9p-local.c      |  9 +++++++++
 hw/9pfs/9p-synth.c      | 17 +++++++++++++++++
 hw/9pfs/9p.c            |  4 ++--
 system/vl.c             |  7 ++++++-
 7 files changed, 43 insertions(+), 4 deletions(-)

diff --git a/fsdev/file-op-9p.h b/fsdev/file-op-9p.h
index d67c4687b3..e8a0728c78 100644
--- a/fsdev/file-op-9p.h
+++ b/fsdev/file-op-9p.h
@@ -101,6 +101,8 @@ typedef struct FsDriverEntry {
     FsThrottle fst;
     mode_t fmode;
     mode_t dmode;
+    /* temporary storage for parse_opts only */
+    uint32_t max_xattr;
 } FsDriverEntry;
 
 struct FsContext {
diff --git a/fsdev/qemu-fsdev-opts.c b/fsdev/qemu-fsdev-opts.c
index 07a18c6e48..c2c1e83611 100644
--- a/fsdev/qemu-fsdev-opts.c
+++ b/fsdev/qemu-fsdev-opts.c
@@ -46,6 +46,9 @@ static QemuOptsList qemu_fsdev_opts = {
         }, {
             .name = "dmode",
             .type = QEMU_OPT_NUMBER,
+        }, {
+            .name = "max_xattr",
+            .type = QEMU_OPT_NUMBER,
         },
 
         THROTTLE_OPTS,
@@ -92,6 +95,9 @@ static QemuOptsList qemu_virtfs_opts = {
         }, {
             .name = "dmode",
             .type = QEMU_OPT_NUMBER,
+        }, {
+            .name = "max_xattr",
+            .type = QEMU_OPT_NUMBER,
         },
 
         { /*End of list */ }
diff --git a/fsdev/qemu-fsdev.c b/fsdev/qemu-fsdev.c
index 57877dad0a..f97103cf44 100644
--- a/fsdev/qemu-fsdev.c
+++ b/fsdev/qemu-fsdev.c
@@ -45,7 +45,7 @@ typedef struct FsDriverListEntry {
 static QTAILQ_HEAD(, FsDriverListEntry) fsdriver_entries =
     QTAILQ_HEAD_INITIALIZER(fsdriver_entries);
 
-#define COMMON_FS_DRIVER_OPTIONS "id", "fsdriver", "readonly"
+#define COMMON_FS_DRIVER_OPTIONS "id", "fsdriver", "readonly", "max_xattr"
 
 static FsDriverTable FsDrivers[] = {
     {
diff --git a/hw/9pfs/9p-local.c b/hw/9pfs/9p-local.c
index aa48306b0e..4708e170a4 100644
--- a/hw/9pfs/9p-local.c
+++ b/hw/9pfs/9p-local.c
@@ -1527,6 +1527,15 @@ static int local_parse_opts(QemuOpts *opts, FsDriverEntry *fse, Error **errp)
     const char *path = qemu_opt_get(opts, "path");
     const char *multidevs = qemu_opt_get(opts, "multidevs");
 
+    uint64_t val = qemu_opt_get_number(opts, "max_xattr",
+                                       V9FS_MAX_XATTR_DEFAULT);
+    if (val > UINT32_MAX) {
+        error_setg(errp, "max_xattr value '%s' too large",
+                   qemu_opt_get(opts, "max_xattr"));
+        return -1;
+    }
+    fse->max_xattr = val;
+
     if (!sec_model) {
         error_setg(errp, "security_model property not set");
         error_append_security_model_hint(errp);
diff --git a/hw/9pfs/9p-synth.c b/hw/9pfs/9p-synth.c
index b3743f6169..322dc3bb69 100644
--- a/hw/9pfs/9p-synth.c
+++ b/hw/9pfs/9p-synth.c
@@ -25,6 +25,8 @@
 #include "qemu/rcu_queue.h"
 #include "qemu/cutils.h"
 #include "system/qtest.h"
+#include "qapi/error.h"
+#include "qemu/option.h"
 
 /* Root node for synth file system */
 static V9fsSynthNode synth_root = {
@@ -629,12 +631,27 @@ static int synth_init(FsContext *ctx, Error **errp)
     return 0;
 }
 
+static int synth_parse_opts(QemuOpts *opts, FsDriverEntry *fse, Error **errp)
+{
+    uint64_t val = qemu_opt_get_number(opts, "max_xattr",
+                                       V9FS_MAX_XATTR_DEFAULT);
+    if (val > UINT32_MAX) {
+        error_setg(errp, "max_xattr value '%s' too large",
+                   qemu_opt_get(opts, "max_xattr"));
+        return -1;
+    }
+    fse->max_xattr = val;
+
+    return 0;
+}
+
 static bool synth_has_valid_file_handle(int fid_type, V9fsFidOpenState *fs)
 {
     return false;
 }
 
 FileOperations synth_ops = {
+    .parse_opts   = synth_parse_opts,
     .init         = synth_init,
     .lstat        = synth_lstat,
     .readlink     = synth_readlink,
diff --git a/hw/9pfs/9p.c b/hw/9pfs/9p.c
index 10e2e53c01..46db172a0b 100644
--- a/hw/9pfs/9p.c
+++ b/hw/9pfs/9p.c
@@ -4464,8 +4464,8 @@ int v9fs_device_realize_common(V9fsState *s, const V9fsTransport *t,
 
     s->reclaiming = false;
 
-    /* init xattr FID limit */
-    s->ctx.xattr_fid_limit = V9FS_MAX_XATTR_DEFAULT;
+    /* init xattr FID limit from fsdev config */
+    s->ctx.xattr_fid_limit = fse->max_xattr;
     s->ctx.xattr_fid_count = 0;
 
     rc = 0;
diff --git a/system/vl.c b/system/vl.c
index dbdd4f2257..eb86d725bd 100644
--- a/system/vl.c
+++ b/system/vl.c
@@ -3259,7 +3259,7 @@ void qemu_init(int argc, char **argv)
                 QemuOpts *fsdev;
                 QemuOpts *device;
                 const char *writeout, *sock_fd, *socket, *path, *security_model,
-                           *multidevs;
+                           *multidevs, *max_xattr_str;
 
                 olist = qemu_find_opts("virtfs");
                 if (!olist) {
@@ -3323,6 +3323,11 @@ void qemu_init(int argc, char **argv)
                 if (multidevs) {
                     qemu_opt_set(fsdev, "multidevs", multidevs, &error_abort);
                 }
+                max_xattr_str = qemu_opt_get(opts, "max_xattr");
+                if (max_xattr_str) {
+                    qemu_opt_set(fsdev, "max_xattr", max_xattr_str,
+                                 &error_abort);
+                }
                 device = qemu_opts_create(qemu_find_opts("device"), NULL, 0,
                                           &error_abort);
                 qemu_opt_set(device, "driver", "virtio-9p-pci", &error_abort);
-- 
2.47.3



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

* [PATCH 11/12] tests/9p: add 3 xattr FID limit test cases (synth fs driver)
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (5 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 10/12] tests/9p: add virtio_9p_add_synth_driver_args() test client function Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 02/12] hw/9pfs: add max_xattr option Christian Schoenebeck
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add 3 test cases to verify correct xattr FID limit enforcement of
9pfs server.

 - 1. test with default max_xattr=1024
 - 2. test with custom max_xattr=100
 - 3. test with unlimited max_xattr=0

These are tests using the synth driver. Advantage: by using the
synth driver the tests cannot only check when the xattr FID limit
kicks in (server would return an Rlerror response with ENOSPC),
but also validate the current 9p server internal xattr FID
counter at any moment.

These are "slow" tests, i.e. they are not running by default.
Use -m slow for running theses tests.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/virtio-9p-test.c | 189 ++++++++++++++++++++++++++++++++++-
 1 file changed, 188 insertions(+), 1 deletion(-)

diff --git a/tests/qtest/virtio-9p-test.c b/tests/qtest/virtio-9p-test.c
index 99a897b158..2e88429dfa 100644
--- a/tests/qtest/virtio-9p-test.c
+++ b/tests/qtest/virtio-9p-test.c
@@ -35,6 +35,15 @@
 #define tclunk(...) v9fs_tclunk((TClunkOpt) __VA_ARGS__)
 #define txattrcreate(...) v9fs_txattrcreate((TXattrCreateOpt) __VA_ARGS__)
 
+/*
+ * xattr size to be used for xattr tests
+ *
+ * 64k is the max. xattr size supported by the Linux kernel, However btrfs
+ * for instance supports only 16219 bytes. So let's be conservative and
+ * just use 8k for the xattr tests.
+ */
+#define TEST_XATTR_SIZE (8 * 1024)
+
 static void pci_config(void *obj, void *data, QGuestAllocator *t_alloc)
 {
     QVirtio9P *v9p = obj;
@@ -107,6 +116,42 @@ static bool fs_dirents_contain_name(struct V9fsDirent *e, const char* name)
     return false;
 }
 
+/*
+ * Returns the current internal xattr FID count (works with synth driver only).
+ */
+static size_t get_xattr_count(QVirtio9P *v9p)
+{
+    uint16_t nwqid;
+    v9fs_qid *wqid;
+    const char *xattr_count_path[] = { "stat", "xattr_count" };
+    size_t xattr_count;
+    uint32_t bytes_read;
+
+    /* walk to /stat/xattr_count file */
+    uint32_t fid = twalk({
+        .client = v9p, .fid = 0,
+        .nwname = 2, .wnames = (char **)xattr_count_path,
+        .rwalk = { .nwqid = &nwqid, .wqid = &wqid }
+    }).newfid;
+
+    /* open for read */
+    tlopen({
+        .client = v9p, .fid = fid, .flags = O_RDONLY,
+        .rlopen = { .qid = NULL, .iounit = NULL }
+    });
+
+    /* read the internal xattr FID count */
+    tread({
+        .client = v9p, .fid = fid, .offset = 0, .count = sizeof(xattr_count),
+        .rread = { .count = &bytes_read, .data = &xattr_count }
+    });
+
+    /* cleanup */
+    tclunk({ .client = v9p, .fid = fid });
+
+    return xattr_count;
+}
+
 /* basic readdir test where reply fits into a single response message */
 static void fs_readdir(void *obj, void *data, QGuestAllocator *t_alloc)
 {
@@ -248,6 +293,108 @@ static void do_readdir_split(QVirtio9P *v9p, uint32_t count)
     g_free(wnames[0]);
 }
 
+/*
+ * Test 9p server's xattr FID count limit enforcement.
+ *
+ * Shared test code for both 'synth' and 'local' driver to verify correct
+ * behaviour of 9p server enforcing preconfigured xattr FID count limit
+ * correctly.
+ *
+ * @v9p: 9pfs client
+ *
+ * @max_xattr: max. allowed xattr FIDs, or -1 for infinite
+ *
+ * @check_counter: whether to verify 9p server internal xattr FID counter
+ *                 (only works with 'synth' fs driver)
+ */
+static void do_xattr_limit(QVirtio9P *v9p, int max_xattr, bool check_counter)
+{
+    size_t count;
+    int i;
+    int limit = (max_xattr != -1) ? max_xattr : V9FS_MAX_XATTR_DEFAULT + 100;
+    g_autofree uint32_t *fids = g_new0(uint32_t, limit);
+    uint32_t err_fid = 0;
+    const char *file_path[] = { QTEST_V9FS_SYNTH_WRITE_FILE };
+    g_autofree uint8_t *xattr_data = g_malloc(TEST_XATTR_SIZE);
+
+    if (!g_test_slow()) {
+        g_test_skip("This is a slow test, run with -m slow");
+        return;
+    }
+
+    /* prepare xattr data with 'X' characters */
+    memset(xattr_data, 'X', TEST_XATTR_SIZE);
+
+    tattach({ .client = v9p });
+
+    /* create max. amount of permitted xattrs */
+    for (i = 0; i < limit; i++) {
+        /* walk to create a new fid */
+        fids[i] = twalk({
+            .client = v9p, .fid = 0,
+            .nwname = 1, .wnames = (char **) file_path
+        }).newfid;
+
+        /* create new xattr fid */
+        txattrcreate({
+            .client = v9p, .fid = fids[i], .name = "user.test",
+            .size = TEST_XATTR_SIZE, .flags = 0
+        });
+
+        /* transfer the xattr data */
+        twrite({
+            .client = v9p, .fid = fids[i], .offset = 0,
+            .count = TEST_XATTR_SIZE, .data = xattr_data
+        });
+
+        /* verify server internal xattr counter */
+        if (check_counter) {
+            count = get_xattr_count(v9p);
+            g_assert_cmpuint(count, ==, (i + 1));
+        }
+
+        /* avoid virtio descriptor exhaustion */
+        qvirtqueue_reset_pool(v9p->vq);
+    }
+
+    /* if xattrs are limited, the next xattr should fail */
+    if (max_xattr != -1) {
+        /* walk to create another fid */
+        err_fid = twalk({
+            .client = v9p, .fid = 0,
+            .nwname = 1, .wnames = (char **) file_path
+        }).newfid;
+
+        /* try to create one more xattr fid - should fail */
+        txattrcreate({
+            .client = v9p, .fid = err_fid, .name = "user.test_exceed",
+            .size = TEST_XATTR_SIZE, .flags = 0,
+            .expectErr = ENOSPC
+        });
+
+        /* verify internal xattr counter hasn't changed */
+        if (check_counter) {
+            count = get_xattr_count(v9p);
+            g_assert_cmpuint(count, ==, limit);
+        }
+    }
+
+    /* clunk all fids (should decrement xattr counter) */
+    for (i = 0; i < limit; i++) {
+        tclunk({ .client = v9p, .fid = fids[i] });
+        qvirtqueue_reset_pool(v9p->vq);
+    }
+    if (err_fid) {
+        tclunk({ .client = v9p, .fid = err_fid });
+    }
+
+    /* verify internal xattr counter is zero */
+    if (check_counter) {
+        count = get_xattr_count(v9p);
+        g_assert_cmpuint(count, ==, 0);
+    }
+}
+
 static void fs_walk_no_slash(void *obj, void *data, QGuestAllocator *t_alloc)
 {
     QVirtio9P *v9p = obj;
@@ -508,6 +655,27 @@ static void fs_readdir_split_512(void *obj, void *data,
     do_readdir_split(obj, 512);
 }
 
+static void fs_synth_xattr_limit_default(void *obj, void *data,
+                                         QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_xattr_limit(obj, V9FS_MAX_XATTR_DEFAULT, true);
+}
+
+static void fs_synth_xattr_limit_custom(void *obj, void *data,
+                                        QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_xattr_limit(obj, 100, true);
+}
+
+static void fs_synth_xattr_limit_unlimited(void *obj, void *data,
+                                           QGuestAllocator *t_alloc)
+{
+    v9fs_set_allocator(t_alloc);
+    do_xattr_limit(obj, -1, true);
+}
+
 
 /* tests using the 9pfs 'local' fs driver */
 
@@ -822,6 +990,18 @@ static void fs_deep_absolute_path(void *obj, void *data,
     g_string_free(path, TRUE);
 }
 
+static void *synth_max_xattr_custom_opt(GString *cmd_line, void *arg)
+{
+    virtio_9p_add_synth_driver_args(cmd_line, "max_xattr=100");
+    return arg;
+}
+
+static void *synth_max_xattr_unlimited_opt(GString *cmd_line, void *arg)
+{
+    virtio_9p_add_synth_driver_args(cmd_line, "max_xattr=0");
+    return arg;
+}
+
 static void cleanup_9p_local_driver(void *data)
 {
     /* remove previously created test dir when test is completed */
@@ -872,7 +1052,14 @@ static void register_virtio_9p_test(void)
                  fs_readdir_split_256,  &opts);
     qos_add_test("synth/readdir/split_128", "virtio-9p",
                  fs_readdir_split_128,  &opts);
-
+    qos_add_test("synth/xattr_limit/default", "virtio-9p",
+                 fs_synth_xattr_limit_default, &opts);
+    opts.before = synth_max_xattr_custom_opt;
+    qos_add_test("synth/xattr_limit/custom", "virtio-9p",
+                 fs_synth_xattr_limit_custom, &opts);
+    opts.before = synth_max_xattr_unlimited_opt;
+    qos_add_test("synth/xattr_limit/unlimited", "virtio-9p",
+                 fs_synth_xattr_limit_unlimited, &opts);
 
     /* 9pfs test cases using the 'local' filesystem driver */
     opts.before = assign_9p_local_driver;
-- 
2.47.3



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

* [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348)
@ 2026-06-07 17:22 Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 01/12] hw/9pfs: add xattr FID limit to prevent memory exhaustion Christian Schoenebeck
                   ` (11 more replies)
  0 siblings, 12 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

This series adds a limit on the number of simultaneously open xattr FIDs
in QEMU's 9p filesystem server to prevent host memory exhaustion attacks.

The Txattrcreate 9p request creates FIDs for extended attribute operations.
Each xattr FID contains a buffer for the xattr value. Without a limit, a
malicious priviliged guest with direct communication access to 9p server
could create a huge number of xattr FIDs, leading to potential host memory
exhaustion (DoS, potentially affecting other services on host).

Overview Patches:

 - Patch 1 is the core fix that limits the amount of xattr FIDs to 1024.

 - Patch 2 adds option "max_xattr" allowing to override the default value.

 - Patch 3 updates QEMU documentation with this new option.

 - All other patches are basically just test case changes that guard
   correct behaviour of this new limit.

Christian Schoenebeck (12):
  hw/9pfs: add xattr FID limit to prevent memory exhaustion
  hw/9pfs: add max_xattr option
  qemu-options: document 9pfs max_xattr option
  tests/9p: add Tread / Rread test client functions
  tests/9p: add Tclunk / Rclunk test client functions
  tests/9p: add Txattrcreate / Rxattrcreate test client functions
  hw/9pfs: enable xattr (mockup) support for synth fs driver
  hw/9pfs: add xattr count query interface fo fs synth driver
  tests/9p: increase P9_MAX_SIZE for test client
  tests/9p: add virtio_9p_add_synth_driver_args() test client function
  tests/9p: add 3 xattr FID limit test cases (synth fs driver)
  tests/9p: add 3 xattr FID limit test cases (local fs driver)

 fsdev/file-op-9p.h                    |  11 ++
 fsdev/qemu-fsdev-opts.c               |   6 +
 fsdev/qemu-fsdev.c                    |   2 +-
 hw/9pfs/9p-local.c                    |   9 +
 hw/9pfs/9p-synth.c                    |  51 ++++-
 hw/9pfs/9p.c                          |  60 ++++++
 qemu-options.hx                       |  28 ++-
 system/vl.c                           |   7 +-
 tests/qtest/libqos/virtio-9p-client.c | 124 ++++++++++++
 tests/qtest/libqos/virtio-9p-client.h |  88 ++++++++-
 tests/qtest/libqos/virtio-9p.c        |   6 +
 tests/qtest/libqos/virtio-9p.h        |   6 +
 tests/qtest/virtio-9p-test.c          | 261 +++++++++++++++++++++++++-
 13 files changed, 640 insertions(+), 19 deletions(-)

-- 
2.47.3



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

* [PATCH 07/12] hw/9pfs: enable xattr (mockup) support for synth fs driver
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 01/12] hw/9pfs: add xattr FID limit to prevent memory exhaustion Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 12/12] tests/9p: add 3 xattr FID limit test cases (local fs driver) Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 08/12] hw/9pfs: add xattr count query interface fo fs synth driver Christian Schoenebeck
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

The synth backend is used for testing only. Enable xattr operations
by making lsetxattr and lremovexattr callbacks to return success
result.

They are still actually not doing anything, they just pretend to be
working to prevent 9pfs server from erroring out on xattr requests.

This allows the subsequent test case patches to verify xattr FID
limit enforcement.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 hw/9pfs/9p-synth.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/hw/9pfs/9p-synth.c b/hw/9pfs/9p-synth.c
index 322dc3bb69..4b0732e093 100644
--- a/hw/9pfs/9p-synth.c
+++ b/hw/9pfs/9p-synth.c
@@ -477,15 +477,15 @@ static int synth_lsetxattr(FsContext *ctx, V9fsPath *path,
                                 const char *name, void *value,
                                 size_t size, int flags)
 {
-    errno = ENOTSUP;
-    return -1;
+    /* pretend it worked */
+    return 0;
 }
 
 static int synth_lremovexattr(FsContext *ctx,
                                    V9fsPath *path, const char *name)
 {
-    errno = ENOTSUP;
-    return -1;
+    /* pretend it worked */
+    return 0;
 }
 
 static int synth_name_to_path(FsContext *ctx, V9fsPath *dir_path,
-- 
2.47.3



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

* [PATCH 10/12] tests/9p: add virtio_9p_add_synth_driver_args() test client function
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (4 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 04/12] tests/9p: add Tread / Rread test client functions Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 11/12] tests/9p: add 3 xattr FID limit test cases (synth fs driver) Christian Schoenebeck
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add virtio_9p_add_synth_driver_args() to allow appending custom
QEMU options for individual 9p synth tests.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 tests/qtest/libqos/virtio-9p.c | 6 ++++++
 tests/qtest/libqos/virtio-9p.h | 6 ++++++
 2 files changed, 12 insertions(+)

diff --git a/tests/qtest/libqos/virtio-9p.c b/tests/qtest/libqos/virtio-9p.c
index 186fcc1141..823756de8c 100644
--- a/tests/qtest/libqos/virtio-9p.c
+++ b/tests/qtest/libqos/virtio-9p.c
@@ -228,6 +228,12 @@ static void regex_replace(GString *haystack, const char *pattern,
     g_string_assign(haystack, s);
 }
 
+void virtio_9p_add_synth_driver_args(GString *cmd_line, const char *args)
+{
+    /* append passed args to '-fsdev ...' group */
+    regex_replace(cmd_line, "(-fsdev \\w[^ ]*)", "\\1,%s", args);
+}
+
 void virtio_9p_assign_local_driver(GString *cmd_line, const char *args)
 {
     g_assert_nonnull(local_test_path);
diff --git a/tests/qtest/libqos/virtio-9p.h b/tests/qtest/libqos/virtio-9p.h
index 480727120e..e7efeef7a1 100644
--- a/tests/qtest/libqos/virtio-9p.h
+++ b/tests/qtest/libqos/virtio-9p.h
@@ -44,6 +44,12 @@ struct QVirtio9PDevice {
     QVirtio9P v9p;
 };
 
+/**
+ * Add required test specific args to the QEMU command line for the 9pfs
+ * 'synth' fs driver.
+ */
+void virtio_9p_add_synth_driver_args(GString *cmd_line, const char *args);
+
 /**
  * Creates the directory for the 9pfs 'local' filesystem driver to access.
  */
-- 
2.47.3



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

* [PATCH 01/12] hw/9pfs: add xattr FID limit to prevent memory exhaustion
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 12/12] tests/9p: add 3 xattr FID limit test cases (local fs driver) Christian Schoenebeck
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add a limit on the number of simultaneously open xattr FIDs to prevent
host memory exhaustion attacks. Each xattr FID contains a buffer for the
xattr value, and without a limit, a malicious priviliged guest with
direct communication access to 9p server could create a huge number of
xattr FIDs until host memory is eventually exhausted.

Fix this by:

 - add xattr_fid_limit to struct FsContext for the max. amount
 - add xattr_fid_count to struct FsContext for the current amount
 - init xattr_fid_limit with 1024
 - init xattr_fid_count with 0
 - add function xattr_fid_count_inc() to increment the count
 - add function xattr_fid_count_decr() to decrement the count
 - call xattr_fid_count_inc() in Txattrcreate handler
 - call xattr_fid_count_inc() in Txattrwalk handler
 - call xattr_fid_count_decr() when a xattr FID is freed

Additionally:

 - reset the xattr FID counter in virtfs_reset()

When the limit is reached then xattr_fid_count_inc() returns -ENOSPC and
the request handler is aborted on its error path without turning the
FID into an xattr type and without allocating memory for the xattr.

The default value of 1024 was chosen, as (sane usage of) xattr requests
in the 9p protocol are usually very short-lived, and even machines with
128 cores with very high xattr activity should have plenty of head room
without ever hitting this limit.

Fixes: 10b468bdc5 ("virtio-9p: Implement TXATTRCREATE")
Fixes: CVE-2026-8348
Reported-by: Feifan Qian <bea1e@proton.me>
Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 fsdev/file-op-9p.h |  9 +++++++
 hw/9pfs/9p.c       | 60 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 69 insertions(+)

diff --git a/fsdev/file-op-9p.h b/fsdev/file-op-9p.h
index e8d0661c4b..d67c4687b3 100644
--- a/fsdev/file-op-9p.h
+++ b/fsdev/file-op-9p.h
@@ -81,6 +81,11 @@ typedef struct ExtendedOps {
 
 #define V9FS_SEC_MASK               0x0000003C
 
+/*
+ * Limits the maximum amount of simultaniously open xattr FIDs to prevent
+ * host memory exhaustion (as each xattr FID contains a xattr value buffer).
+ */
+#define V9FS_MAX_XATTR_DEFAULT  1024
 
 typedef struct FileOperations FileOperations;
 typedef struct XattrOperations XattrOperations;
@@ -109,6 +114,10 @@ struct FsContext {
     void *private;
     mode_t fmode;
     mode_t dmode;
+    /* max. amount of simultaniously open xattr FIDs */
+    uint32_t xattr_fid_limit;
+    /* current amount of open xattr FIDs */
+    uint32_t xattr_fid_count;
 };
 
 struct V9fsPath {
diff --git a/hw/9pfs/9p.c b/hw/9pfs/9p.c
index b4314d2549..10e2e53c01 100644
--- a/hw/9pfs/9p.c
+++ b/hw/9pfs/9p.c
@@ -265,6 +265,29 @@ static size_t v9fs_string_size(V9fsString *str)
     return str->size;
 }
 
+static int xattr_fid_count_inc(V9fsPDU *pdu)
+{
+    V9fsState *s = pdu->s;
+
+    if (s->ctx.xattr_fid_limit > 0 &&
+        s->ctx.xattr_fid_count >= s->ctx.xattr_fid_limit) {
+        return -ENOSPC;
+    }
+    s->ctx.xattr_fid_count++;
+    return 0;
+}
+
+static void xattr_fid_count_decr(V9fsPDU *pdu)
+{
+    V9fsState *s = pdu->s;
+
+    if (s->ctx.xattr_fid_count > 0) {
+        s->ctx.xattr_fid_count--;
+    } else {
+        error_report_once("9pfs: xattr_fid_count underflow detected");
+    }
+}
+
 /*
  * returns 0 if fid got re-opened, 1 if not, < 0 on error
  */
@@ -397,6 +420,7 @@ static int coroutine_fn free_fid(V9fsPDU *pdu, V9fsFidState *fidp)
         }
     } else if (fidp->fid_type == P9_FID_XATTR) {
         retval = v9fs_xattr_fid_clunk(pdu, fidp);
+        xattr_fid_count_decr(pdu);
     }
     v9fs_path_free(&fidp->path);
     g_free(fidp);
@@ -634,6 +658,14 @@ static void coroutine_fn virtfs_reset(V9fsPDU *pdu)
         fidp->clunked = true;
         put_fid(pdu, fidp);
     }
+
+    /*
+     * Explicitly reset the xattr FID counter.
+     *
+     * free_fid() already decrements the counter for each P9_FID_XATTR, so the
+     * counter should already be zero, hence this is just a defensive measure.
+     */
+    s->ctx.xattr_fid_count = 0;
 }
 
 #define P9_QID_TYPE_DIR         0x80
@@ -4006,6 +4038,14 @@ static void coroutine_fn v9fs_xattrwalk(void *opaque)
             clunk_fid(s, xattr_fidp->fid);
             goto out;
         }
+
+        /* Check xattr FID limit */
+        err = xattr_fid_count_inc(pdu);
+        if (err < 0) {
+            clunk_fid(s, xattr_fidp->fid);
+            goto out;
+        }
+
         /*
          * Read the xattr value
          */
@@ -4013,6 +4053,7 @@ static void coroutine_fn v9fs_xattrwalk(void *opaque)
         xattr_fidp->fid_type = P9_FID_XATTR;
         xattr_fidp->fs.xattr.xattrwalk_fid = true;
         xattr_fidp->fs.xattr.value = g_malloc0(size);
+
         if (size) {
             err = v9fs_co_llistxattr(pdu, &xattr_fidp->path,
                                      xattr_fidp->fs.xattr.value,
@@ -4039,6 +4080,14 @@ static void coroutine_fn v9fs_xattrwalk(void *opaque)
             clunk_fid(s, xattr_fidp->fid);
             goto out;
         }
+
+        /* Check xattr FID limit */
+        err = xattr_fid_count_inc(pdu);
+        if (err < 0) {
+            clunk_fid(s, xattr_fidp->fid);
+            goto out;
+        }
+
         /*
          * Read the xattr value
          */
@@ -4046,6 +4095,7 @@ static void coroutine_fn v9fs_xattrwalk(void *opaque)
         xattr_fidp->fid_type = P9_FID_XATTR;
         xattr_fidp->fs.xattr.xattrwalk_fid = true;
         xattr_fidp->fs.xattr.value = g_malloc0(size);
+
         if (size) {
             err = v9fs_co_lgetxattr(pdu, &xattr_fidp->path,
                                     &name, xattr_fidp->fs.xattr.value,
@@ -4147,6 +4197,12 @@ static void coroutine_fn v9fs_xattrcreate(void *opaque)
         goto out_put_fid;
     }
 
+    /* Check xattr FID limit */
+    err = xattr_fid_count_inc(pdu);
+    if (err < 0) {
+        goto out_put_fid;
+    }
+
     /* Make the file fid point to xattr */
     xattr_fidp = file_fidp;
     xattr_fidp->fid_type = P9_FID_XATTR;
@@ -4408,6 +4464,10 @@ int v9fs_device_realize_common(V9fsState *s, const V9fsTransport *t,
 
     s->reclaiming = false;
 
+    /* init xattr FID limit */
+    s->ctx.xattr_fid_limit = V9FS_MAX_XATTR_DEFAULT;
+    s->ctx.xattr_fid_count = 0;
+
     rc = 0;
 out:
     if (rc) {
-- 
2.47.3



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

* [PATCH 03/12] qemu-options: document 9pfs max_xattr option
  2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
                   ` (8 preceding siblings ...)
  2026-06-07 17:22 ` [PATCH 09/12] tests/9p: increase P9_MAX_SIZE for test client Christian Schoenebeck
@ 2026-06-07 17:22 ` Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 05/12] tests/9p: add Tclunk / Rclunk test client functions Christian Schoenebeck
  2026-06-07 17:22 ` [PATCH 06/12] tests/9p: add Txattrcreate / Rxattrcreate " Christian Schoenebeck
  11 siblings, 0 replies; 13+ messages in thread
From: Christian Schoenebeck @ 2026-06-07 17:22 UTC (permalink / raw)
  To: qemu-devel, qemu-stable; +Cc: Greg Kurz, Feifan Qian

Add documentation for the new "max_xattr" command line option
of 9pfs server, introduced by the previous commit.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 qemu-options.hx | 28 ++++++++++++++++++++--------
 1 file changed, 20 insertions(+), 8 deletions(-)

diff --git a/qemu-options.hx b/qemu-options.hx
index 96ae41f787..558d084742 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -1914,19 +1914,19 @@ ERST
 
 DEF("fsdev", HAS_ARG, QEMU_OPTION_fsdev,
     "-fsdev local,id=id,path=path,security_model=mapped-xattr|mapped-file|passthrough|none\n"
-    " [,writeout=immediate][,readonly=on][,fmode=fmode][,dmode=dmode]\n"
+    " [,writeout=immediate][,readonly=on][,fmode=fmode][,dmode=dmode][,max_xattr=max]\n"
     " [[,throttling.bps-total=b]|[[,throttling.bps-read=r][,throttling.bps-write=w]]]\n"
     " [[,throttling.iops-total=i]|[[,throttling.iops-read=r][,throttling.iops-write=w]]]\n"
     " [[,throttling.bps-total-max=bm]|[[,throttling.bps-read-max=rm][,throttling.bps-write-max=wm]]]\n"
     " [[,throttling.iops-total-max=im]|[[,throttling.iops-read-max=irm][,throttling.iops-write-max=iwm]]]\n"
     " [[,throttling.iops-size=is]]\n"
-    "-fsdev synth,id=id\n",
+    "-fsdev synth,id=id[,max_xattr=max]\n",
     QEMU_ARCH_ALL)
 
 SRST
-``-fsdev local,id=id,path=path,security_model=security_model [,writeout=writeout][,readonly=on][,fmode=fmode][,dmode=dmode] [,throttling.option=value[,throttling.option=value[,...]]]``
+``-fsdev local,id=id,path=path,security_model=security_model [,writeout=writeout][,readonly=on][,fmode=fmode][,dmode=dmode][,max_xattr=max] [,throttling.option=value[,throttling.option=value[,...]]]``
   \ 
-``-fsdev synth,id=id[,readonly=on]``
+``-fsdev synth,id=id[,readonly=on][,max_xattr=max]``
     Define a new file system device. Valid options are:
 
     ``local``
@@ -2000,6 +2000,12 @@ SRST
         Let every is bytes of a request count as a new request for iops
         throttling purposes.
 
+    ``max_xattr=max``
+        Sets the maximum number of concurrent xattr FIDs allowed for
+        this export. The default is 1024. Set to 0 for allowing an infinite
+        number of xattr FIDs. This limit prevents host memory exhaustion
+        attacks by capping the number of current xattr FIDs.
+
     -fsdev option is used along with -device driver "virtio-9p-...".
 
 ``-device virtio-9p-type,fsdev=id,mount_tag=mount_tag``
@@ -2019,14 +2025,14 @@ ERST
 
 DEF("virtfs", HAS_ARG, QEMU_OPTION_virtfs,
     "-virtfs local,path=path,mount_tag=tag,security_model=mapped-xattr|mapped-file|passthrough|none\n"
-    "        [,id=id][,writeout=immediate][,readonly=on][,fmode=fmode][,dmode=dmode][,multidevs=remap|forbid|warn]\n"
-    "-virtfs synth,mount_tag=tag[,id=id][,readonly=on]\n",
+    "        [,id=id][,writeout=immediate][,readonly=on][,fmode=fmode][,dmode=dmode][,multidevs=remap|forbid|warn][,max_xattr=max]\n"
+    "-virtfs synth,mount_tag=tag[,id=id][,readonly=on][,max_xattr=max]\n",
     QEMU_ARCH_ALL)
 
 SRST
-``-virtfs local,path=path,mount_tag=mount_tag ,security_model=security_model[,writeout=writeout][,readonly=on] [,fmode=fmode][,dmode=dmode][,multidevs=multidevs]``
+``-virtfs local,path=path,mount_tag=mount_tag ,security_model=security_model[,writeout=writeout][,readonly=on] [,fmode=fmode][,dmode=dmode][,multidevs=multidevs][,max_xattr=max]``
   \ 
-``-virtfs synth,mount_tag=mount_tag``
+``-virtfs synth,mount_tag=mount_tag[,max_xattr=max]``
     Define a new virtual filesystem device and expose it to the guest using
     a virtio-9p-device (a.k.a. 9pfs), which essentially means that a certain
     directory on host is made directly accessible by guest as a pass-through
@@ -2109,6 +2115,12 @@ SRST
         on host would otherwise cause a file ID collision and hence
         potential severe misbehaviours on guest.
 
+        ``max_xattr`` : sets the maximum number of concurrent xattr FIDs
+        allowed for this export. The default is 1024. Set to 0 for
+        allowing an infinite number of xattr FIDs. This limit prevents
+        host memory exhaustion attacks by capping the number of current
+        xattr FIDs.
+
         ``warn`` : virtfs 9p expects only one device to be shared with
         the same export. If however more than one device is shared and
         accessed via the same 9p export then only a warning message is
-- 
2.47.3



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

end of thread, other threads:[~2026-06-07 18:01 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-07 17:22 [PATCH 00/12] 9pfs: add xattr FID limit (CVE-2026-8348) Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 01/12] hw/9pfs: add xattr FID limit to prevent memory exhaustion Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 12/12] tests/9p: add 3 xattr FID limit test cases (local fs driver) Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 07/12] hw/9pfs: enable xattr (mockup) support for synth fs driver Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 08/12] hw/9pfs: add xattr count query interface fo fs synth driver Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 04/12] tests/9p: add Tread / Rread test client functions Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 10/12] tests/9p: add virtio_9p_add_synth_driver_args() test client function Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 11/12] tests/9p: add 3 xattr FID limit test cases (synth fs driver) Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 02/12] hw/9pfs: add max_xattr option Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 09/12] tests/9p: increase P9_MAX_SIZE for test client Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 03/12] qemu-options: document 9pfs max_xattr option Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 05/12] tests/9p: add Tclunk / Rclunk test client functions Christian Schoenebeck
2026-06-07 17:22 ` [PATCH 06/12] tests/9p: add Txattrcreate / Rxattrcreate " Christian Schoenebeck

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.