All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 1/9] tests/9p-client: add Txattrwalk / Rxattrwalk test client functions
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
@ 2026-10-07 10:26 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 2/9] tests/9p-race: add 9p race test suite (race FID-path mutations) Christian Schoenebeck
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:26 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add v9fs_txattrwalk() and v9fs_rxattrwalk() functions to the 9p test
client. This can be used to retrieve a list of xattrs or to acquire
a FID that can subsequently be used to retrieve the value of an xattr.

This is a preparatory patch, these functions will be used by the
subsequently added 9p race test.

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

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index 305b0dac63..b2f0037b7b 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -1227,3 +1227,51 @@ void v9fs_rxattrcreate(P9Req *req)
     v9fs_req_recv(req, P9_RXATTRCREATE);
     v9fs_req_free(req);
 }
+
+/* size[4] Txattrwalk tag[2] fid[4] newfid[4] name[s] */
+TXattrWalkRes v9fs_txattrwalk(TXattrWalkOpt opt)
+{
+    P9Req *req;
+    uint32_t err;
+    uint32_t body_size = 4 + 4;
+    uint16_t string_size;
+    uint64_t xattr_size = 0;
+
+    g_assert(opt.client);
+    g_assert(opt.name);
+
+    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_TXATTRWALK, opt.tag);
+    v9fs_uint32_write(req, opt.fid);
+    v9fs_uint32_write(req, opt.newfid);
+    v9fs_string_write(req, opt.name);
+    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_rxattrwalk(req, &xattr_size);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TXattrWalkRes) { .req = req, xattr_size };
+}
+
+/* size[4] Rxattrwalk tag[2] size[8] */
+void v9fs_rxattrwalk(P9Req *req, uint64_t *size)
+{
+    v9fs_req_recv(req, P9_RXATTRWALK);
+    if (size) {
+        v9fs_uint64_read(req, size);
+    } else {
+        v9fs_memskip(req, 8);
+    }
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index 4b04324503..c910407f70 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -553,6 +553,32 @@ typedef struct TXattrCreateRes {
     uint32_t err;
 } TXattrCreateRes;
 
+/* options for 'Txattrwalk' 9p request */
+typedef struct TXattrWalkOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID of file/dir from which to walk (required) */
+    uint32_t fid;
+    /* file ID that will refer to the walked-to xattr (required) */
+    uint32_t newfid;
+    /* xattr name, or empty for a list of all xattrs (required) */
+    const char *name;
+    /* only send Txattrwalk 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;
+} TXattrWalkOpt;
+
+/* result of 'Txattrwalk' 9p request */
+typedef struct TXattrWalkRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+    /* size of xattr */
+    uint64_t size;
+} TXattrWalkRes;
+
 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);
@@ -610,5 +636,7 @@ TClunkRes v9fs_tclunk(TClunkOpt opt);
 void v9fs_rclunk(P9Req *req);
 TXattrCreateRes v9fs_txattrcreate(TXattrCreateOpt opt);
 void v9fs_rxattrcreate(P9Req *req);
+TXattrWalkRes v9fs_txattrwalk(TXattrWalkOpt opt);
+void v9fs_rxattrwalk(P9Req *req, uint64_t *size);
 
 #endif
-- 
2.47.3



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

* [PATCH v2 2/9] tests/9p-race: add 9p race test suite (race FID-path mutations)
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
  2026-10-07 10:26 ` [PATCH v2 1/9] tests/9p-client: add Txattrwalk / Rxattrwalk test client functions Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 3/9] tests/9p-client: add Rerror function (legacy 9p2000.u) Christian Schoenebeck
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add a qtest-based stress test suite racing 9pfs server-side state.

This patch adds the first race test "race/fid-path" for this new
test suite: it races (per round) one Tlcreate (which is mutating
the FID path) against a number of reader requests (Twalk and
Txattrwalk) on the same FID.

This very first 9pfs race test guards CVE-2026-93834 (fixed by
commit fe9e30899f "hw/9pfs: Use read-write lock for protecting fid
path.").

Hitting the small race window is timing dependant, so this is a
probabilistic test, not a deterministic one. During my tests the
(unpatched) vulnerability triggered sometimes after few hundred
rounds, and sometimes after >100k rounds. Furthermore, it expects
QEMU being compiled with a sanitizer (e.g. ASan) which is
supposed to detect the actual race (e.g. by memory violation):

  ../configure --enable-virtfs --enable-debug \
  --extra-cflags="-fsanitize=address -fno-omit-frame-pointer" \
  --extra-ldflags="-fsanitize=address" ...

Even though this is a new test suite, in the codebase separate
from the already existing deterministic 9pfs test cases (while
9p test client code being shared between the two test suites),
this new race test suite is run with the same binary as the
other qtests. However being a very slow test, it is registered
as such, so the -m slow argument is required for this race test
to run (as it is skipped by default):

  ASAN_OPTIONS=detect_leaks=0 \
  tests/qtest/qos-test -m slow

Or if you only want to run this single race test (e.g. on x86_64):

  ASAN_OPTIONS=detect_leaks=0 \
  tests/qtest/qos-test -m slow -p \
  /x86_64/pc/i440FX-pcihost/pci-bus-pc/pci-bus/virtio-9p-pci/virtio-9p/virtio-9p-tests/race/fid-path

On other systems, use `tests/qtest/qos-test -l` to get the correct
(arch dependent) full test name.

`ASAN_OPTIONS=detect_leaks=0` is currently required, it suppresses
a memory leak error at the end of each race test, which is a known
QEMU shutdown memory leak, unrelated to 9p server or this race test
suite. The official QEMU CI pipeline sets
`ASAN_OPTIONS=detect_leaks=0` by default for this reason.

There are some knobs that can be controlled via environment
variables:

  V9FS_RACE: skip test suite by setting to 0 or force running by
             setting to 1

  V9FS_RACE_ROUNDS: total amount of rounds to run
                    (default: 20000)

  V9FS_RACE_NCONCURRENT: amount of concurrent reader requests
                         (default: 4)

A full run with 20000 rounds took around 7 minutes on my test
machine. If you can't hit the (narrow) race window with
unpatched QEMU, it might help to either add some load on the
host system and/or turning the two knobs above.

Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 MAINTAINERS                       |   1 +
 tests/qtest/meson.build           |   1 +
 tests/qtest/virtio-9p-race-test.c | 575 ++++++++++++++++++++++++++++++
 3 files changed, 577 insertions(+)
 create mode 100644 tests/qtest/virtio-9p-race-test.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 1b66c65f68..0aa36ab53f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2558,6 +2558,7 @@ F: hw/9pfs/
 X: hw/9pfs/xen-9p*
 F: fsdev/
 F: tests/qtest/virtio-9p-test.c
+F: tests/qtest/virtio-9p-race-test.c
 F: tests/qtest/libqos/virtio-9p*
 T: git https://github.com/cschoenebeck/qemu.git 9p.next
 
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 33f58b0e95..4d0f29a7d3 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -365,6 +365,7 @@ if host_os != 'windows'
 endif
 if have_virtfs
   qos_test_ss.add(files('virtio-9p-test.c'))
+  qos_test_ss.add(files('virtio-9p-race-test.c'))
 endif
 if have_vhost_user
   qos_test_ss.add(files('vhost-user-test.c'))
diff --git a/tests/qtest/virtio-9p-race-test.c b/tests/qtest/virtio-9p-race-test.c
new file mode 100644
index 0000000000..789fb7721d
--- /dev/null
+++ b/tests/qtest/virtio-9p-race-test.c
@@ -0,0 +1,575 @@
+/*
+ * virtio 9p fs concurrency race test
+ *
+ * Copyright (c) 2026 Christian Schoenebeck <qemu_oss@crudebyte.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Implements a probabilistic stress test suite (not deterministic reproducers)
+ * by simulating concurrent requests on the same 9p FID. This test suite is
+ * expected to be built and run with sanitizers (e.g. ASan) that are supposed
+ * to actually uncover potential races e.g. by detecting memory violations.
+ */
+
+/*
+ * Not so fast! You might want to read the 9p developer docs first:
+ * https://wiki.qemu.org/Documentation/9p
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "libqos/virtio.h"
+#include "libqos/virtio-9p-client.h"
+#include "libqos/virtio-9p.h"
+
+/*
+ * RACE TEST SUITE INFRASTRUCTURE
+ *
+ * The following functions implement the shared code for all 9p race
+ * tests.
+ */
+
+/* depth of the d1/d2/d3 directory chain created below the export root */
+#define V9P_RACE_DIR_DEPTH 3
+
+/* export root FID */
+#define V9P_RACE_ROOT_FID 0
+
+/* d1 FID of the d1/d2/d3 path chain */
+#define V9P_RACE_D1_FID   40
+/* d2 FID of the d1/d2/d3 path chain */
+#define V9P_RACE_D2_FID   41
+/* d3 FID of the d1/d2/d3 path chain */
+#define V9P_RACE_D3_FID   42
+
+/*
+ * FID range of automatically generated and recycled FIDs per race round.
+ * Kept small and away from the constant FIDs above.
+ */
+#define V9P_RACE_FID_ROUND_BASE 100
+#define V9P_RACE_FID_ROUND_SLOTS 32
+
+static uint16_t v9p_race_tag;
+
+/* returns a new tag for the next 9p request */
+static uint16_t v9p_race_new_tag(void)
+{
+    v9p_race_tag++;
+    /* tag 0 is the reserved no-tag (P9_NOTAG) value, not a real tag */
+    if (v9p_race_tag == 0) {
+        v9p_race_tag = 1;
+    }
+    return v9p_race_tag;
+}
+
+/*
+ * How long the test suite waits for one pending request to arrive before
+ * concluding the 9p server is stuck; ASan builds may be slow, so keep it
+ * generous.
+ */
+#define V9P_RACE_TIMEOUT_US (30 * G_USEC_PER_SEC)
+
+/*
+ * Maximum amount of pending 9p server responses ever expected.
+ *
+ * This is directly tied to server's MAX_REQ (which currently yields to 128).
+ */
+#define V9P_RACE_RCVD_REPL_MAX (MAX_REQ)
+
+/*
+ * A local stash of already received 9p responses.
+ *
+ * With several 9p requests in-flight, used ring entries can arrive in a
+ * different order than the requests were originally sent. A consumed virtio
+ * ring entry belonging to a not-yet-collected response is queued here as a
+ * received-but-unprocessed reply record. The response payload itself stays in
+ * the request's guest buffer either way, so the descriptor index alone is
+ * enough to acquire it later on.
+ */
+static struct {
+    uint32_t desc_idx;
+    uint32_t len;
+    bool used;
+} v9p_race_rcvd_resp[V9P_RACE_RCVD_REPL_MAX];
+
+/*
+ * Reset virtio test driver's descriptor pool.
+ *
+ * The qtest virtio driver never recycles used descriptors, so the
+ * descriptor table (one queue-size worth of entries) must be reset
+ * whenever it is about to run out. Safe as long as no request is in
+ * flight (call between completed round-trips or at the start of a
+ * round where all previous requests have been collected).
+ */
+static void v9p_race_reset_pool(QVirtio9P *v9p)
+{
+    qvirtqueue_reset_pool(v9p->vq);
+}
+
+/*
+ * Feed the QEMU main loop with one no-op qtest command round-trip.
+ *
+ * In qtest mode the 9p device state only advances while the QEMU main
+ * loop services a qtest command: servicing the command runs inside one
+ * main loop iteration, which also dispatches previously queued
+ * virtqueue kicks, steps the 9p handler coroutines on the main thread
+ * (their worker-side parts run on independent thread pool threads) and
+ * writes completed responses back into the virtio used ring.
+ */
+static void v9p_race_feed_main_loop(QVirtio9P *v9p)
+{
+    /*
+     * The read target is the virtqueue descriptor table, which only this
+     * test writes, so the command has no side effects. The read value is
+     * discarded.
+     */
+    qtest_readl(global_qtest, v9p->vq->desc);
+}
+
+/* stash the received reply identified by @desc_idx for later processing */
+static void v9p_race_rcvd_resp_add(uint32_t desc_idx, uint32_t len)
+{
+    int i;
+
+    for (i = 0; i < V9P_RACE_RCVD_REPL_MAX; i++) {
+        if (!v9p_race_rcvd_resp[i].used) {
+            v9p_race_rcvd_resp[i].used = true;
+            v9p_race_rcvd_resp[i].desc_idx = desc_idx;
+            v9p_race_rcvd_resp[i].len = len;
+            return;
+        }
+    }
+    g_assert_not_reached();
+}
+
+/*
+ * Pickup the previously stashed reply for @desc_idx, returning its length,
+ * or UINT32_MAX if no reply for that request has been received yet.
+ */
+static uint32_t v9p_race_rcvd_resp_remove(uint32_t desc_idx)
+{
+    int i;
+
+    for (i = 0; i < V9P_RACE_RCVD_REPL_MAX; i++) {
+        if (v9p_race_rcvd_resp[i].used &&
+            v9p_race_rcvd_resp[i].desc_idx == desc_idx) {
+            uint32_t len = v9p_race_rcvd_resp[i].len;
+
+            v9p_race_rcvd_resp[i].used = false;
+            return len;
+        }
+    }
+    return UINT32_MAX;
+}
+
+/*
+ * Wait for the used ring entry of @req, returns immediately if the response
+ * was already received.
+ *
+ * As responses may arrive in a different order than their requests were
+ * originally sent, received responses that belong to a different, in-flight
+ * request are locally stashed instead (waiting for later pickup).
+ *
+ * NOTE: this implementation polls the used ring directly instead of waiting
+ * on the legacy virtio interrupt status register (ISR) that
+ * qvirtio_wait_used_elem() uses: that register is a single boolean read-clear
+ * flag, not a counter. So it could hide several used-ring entries. And since
+ * that flag is not re-raised for entries that already arrived in the ring,
+ * waiting on the ISR could continue for good, even though all response
+ * entries were already received. Furthermore, qvirtio_wait_used_elem() asserts
+ * that it receives the response of its own request next from the virtio ring.
+ * So we can't use qvirtio_wait_used_elem(), as it is designed to receive
+ * exactly one (in-order) response, whereas this race test is designed to
+ * receive several out-of-order responses.
+ */
+static void v9p_race_req_wait(QVirtio9P *v9p, P9Req *req, uint32_t *len)
+{
+    gint64 start = g_get_monotonic_time();
+
+    /* did the response already arrive and was just stashed locally? */
+    *len = v9p_race_rcvd_resp_remove(req->free_head);
+    if (*len != UINT32_MAX) {
+        return;
+    }
+
+    /* poll virtio ring until requested response arrived */
+    while (true) {
+        uint32_t idx;
+
+        if (qvirtqueue_get_buf(global_qtest, v9p->vq, &idx, len)) {
+            if (idx == req->free_head) {
+                return;
+            }
+            /* not the response we asked for, stash it then continue */
+            v9p_race_rcvd_resp_add(idx, *len);
+        }
+        g_usleep(1000);
+        if (g_get_monotonic_time() - start > V9P_RACE_TIMEOUT_US) {
+            g_printerr("TIMEOUT waiting for tag=%u (free_head=%u)\n",
+                       req->tag, req->free_head);
+            fflush(stderr);
+            g_assert_not_reached();
+        }
+    }
+}
+
+/*
+ * Wait for the response of @req, verify the tag and return the response
+ * ID. Frees @req.
+ */
+static uint8_t v9p_race_resp_collect(QVirtio9P *v9p, P9Req *req)
+{
+    uint32_t len;
+    P9MsgHeader hdr;
+
+    v9p_race_req_wait(v9p, req, &len);
+    v9fs_memread(req, &hdr, sizeof(hdr));
+    g_assert_cmpint(hdr.tag_le, ==, req->tag);
+    g_assert_cmpint(hdr.size_le, >=, sizeof(hdr));
+    v9fs_req_free(req);
+    return hdr.id;
+}
+
+/* Tolerant Tclunk: the FID may or may not exist on the server. */
+static void v9p_race_clunk_relaxed(QVirtio9P *v9p, uint32_t fid)
+{
+    TClunkRes res = v9fs_tclunk((TClunkOpt) {
+        .client = v9p,
+        .fid = fid,
+        .tag = v9p_race_new_tag(),
+        .requestOnly = true,
+    });
+
+    v9p_race_resp_collect(v9p, res.req);
+}
+
+/* Tolerant Tunlinkat: the entry may or may not exist on the server. */
+static void v9p_race_unlink_relaxed(QVirtio9P *v9p, uint32_t dfid,
+                                    const char *name, uint32_t flags)
+{
+    TunlinkatRes res = v9fs_tunlinkat((TunlinkatOpt) {
+        .client = v9p,
+        .dirfd = dfid,
+        .name = name,
+        .flags = flags,
+        .tag = v9p_race_new_tag(),
+        .requestOnly = true,
+    });
+
+    v9p_race_resp_collect(v9p, res.req);
+}
+
+/*
+ * Create the d1/d2/d3 directory chain inside the attached export root,
+ * walking each level into its own FID (V9P_RACE_D1_FID .. V9P_RACE_D3_FID).
+ */
+static void v9p_race_create_subdir_chain(QVirtio9P *v9p)
+{
+    uint16_t dfid = V9P_RACE_ROOT_FID;
+    int i;
+
+    for (i = 1; i <= V9P_RACE_DIR_DEPTH; ++i) {
+        char dir[8];
+        char *wnames[] = { dir };
+        uint16_t newfid =
+            (i == 1) ? V9P_RACE_D1_FID
+                     : (i == 2) ? V9P_RACE_D2_FID : V9P_RACE_D3_FID;
+
+        snprintf(dir, sizeof(dir), "d%d", i);
+        v9fs_tmkdir((TMkdirOpt) {
+            .client = v9p,
+            .dfid = dfid,
+            .name = dir,
+            .mode = 0755,
+            .gid = 0,
+            .tag = v9p_race_new_tag(),
+        });
+        v9fs_twalk((TWalkOpt) {
+            .client = v9p,
+            .fid = dfid,
+            .newfid = newfid,
+            .nwname = 1,
+            .wnames = wnames,
+            .tag = v9p_race_new_tag(),
+        });
+        dfid = newfid;
+    }
+}
+
+/* Prepare this test suite for running a race test. */
+static void v9p_race_prepare(QVirtio9P *v9p)
+{
+    int i;
+
+    /* init local response stash */
+    for (i = 0; i < V9P_RACE_RCVD_REPL_MAX; i++) {
+        v9p_race_rcvd_resp[i].used = false;
+    }
+
+    /* attach to export root */
+    v9fs_tattach((TAttachOpt) {
+        .client = v9p,
+        .fid = V9P_RACE_ROOT_FID,
+        .tag = v9p_race_new_tag(),
+    });
+
+    /* create the d1/d2/d3 subdir chain */
+    v9p_race_create_subdir_chain(v9p);
+
+    /* open d2 and d3 as directory FIDs (V9P_RACE_D2_FID and V9P_RACE_D3_FID) */
+    v9fs_tlopen((TLOpenOpt) {
+        .client = v9p,
+        .fid = V9P_RACE_D2_FID,
+        .flags = O_DIRECTORY,
+        .tag = v9p_race_new_tag(),
+    });
+    v9fs_tlopen((TLOpenOpt) {
+        .client = v9p,
+        .fid = V9P_RACE_D3_FID,
+        .flags = O_DIRECTORY,
+        .tag = v9p_race_new_tag(),
+    });
+}
+
+/* Return true if race test shall be skipped.  */
+static bool v9p_race_skip(void) {
+    const char *val = getenv("V9FS_RACE");
+
+    if (val) {
+        if (!strcmp(val, "0")) {
+            g_test_skip("V9FS_RACE=0 was set");
+            return true;
+        }
+        if (!strcmp(val, "1")) {
+            g_print("V9FS_RACE=1 was set\n");
+            return false;
+        }
+    }
+    if (!g_test_slow()) {
+        g_test_skip("This is a slow test, run with -m slow or set V9FS_RACE=1");
+        return true;
+    }
+    g_print("Skip by setting V9FS_RACE=0\n");
+    return false;
+}
+
+/* Cleanup after running a race test. */
+static void v9p_race_cleanup(void *data)
+{
+    /* remove the temp directory used as export root */
+    virtio_9p_remove_local_test_dir();
+}
+
+/* number of rounds a race test runs, shared by all race tests */
+static uint32_t v9p_race_rounds(void)
+{
+    const char *val = getenv("V9FS_RACE_ROUNDS");
+
+    if (val && *val) {
+        return atoi(val);
+    }
+    return 20000;
+}
+
+/* number of unsynchronized in-flight race requests per round */
+static int v9p_race_nconcurrent(void)
+{
+    const char *val = getenv("V9FS_RACE_NCONCURRENT");
+    int nreqs = 4;
+
+    if (val && *val) {
+        nreqs = atoi(val);
+    }
+    /* an empty or invalid value falls back to the built-in default */
+    if (nreqs < 1) {
+        nreqs = 4;
+    }
+    return nreqs;
+}
+
+/*
+ * RACE TEST CASES
+ *
+ * The individual 9p race test scenarios.
+ */
+
+/*
+ * Race 9p server's internal FID-path string (CVE-2026-93834).
+ *
+ * Per round, race one Tlcreate (mutating the FID path) request against
+ * FID path reader requests (Twalk and Txattrwalk) on the same FID.
+ */
+static void v9p_race_fid_path(void *obj, void *data, QGuestAllocator *t_alloc)
+{
+    if (v9p_race_skip()) {
+        return;
+    }
+    QVirtio9P *v9p = obj;
+    uint32_t rounds = v9p_race_rounds();
+    unsigned long created_files = 0;
+    unsigned long xattr_fids = 0, walk_fids = 0;
+    uint32_t i, d, k;
+    int nconcurrent = v9p_race_nconcurrent();
+    g_autofree P9Req **readers = NULL;
+
+    v9fs_set_allocator(t_alloc);
+    v9p_race_prepare(v9p);
+
+    /*
+     * A round consumes one twalk, the Tlcreate itself, nconcurrent
+     * readers and up to nconcurrent + 2 clunks/unlinks from the
+     * per-round reset descriptor pool, so limit nconcurrent by the
+     * queue size.
+     */
+    if (4 + 2 * nconcurrent > v9p->vq->size / 2) {
+        g_error("V9FS_RACE_NCONCURRENT: %d readers need %d requests "
+                "per round, but the queue holds at most %d",
+                nconcurrent, 4 + 2 * nconcurrent, v9p->vq->size / 2);
+    }
+    readers = g_new0(P9Req *, nconcurrent);
+
+    for (i = 0; i < rounds; i++) {
+        uint16_t fid_a = V9P_RACE_FID_ROUND_BASE +
+                         (i % V9P_RACE_FID_ROUND_SLOTS);
+        uint16_t fid_b = fid_a + 100;
+        uint16_t fid_c = fid_a + 200;
+        char name[16];
+        uint16_t tag_a = v9p_race_new_tag();
+        P9Req *req_a;
+        uint8_t resp_a;
+
+        /* progress notification */
+        if (i % 2000 == 0) {
+            g_print("race/fid-path: %u/%u rounds\n", i, rounds);
+            fflush(stdout);
+        }
+
+        /*
+         * Reset the descriptor pool at the start of every round, where
+         * no request is in-flight anymore.
+         */
+        v9p_race_reset_pool(v9p);
+
+        /* clone a FID of the target directory */
+        v9fs_twalk((TWalkOpt) {
+            .client = v9p,
+            .fid = V9P_RACE_D3_FID,
+            .newfid = fid_a,
+            .nwname = 0,
+            .tag = v9p_race_new_tag(),
+        });
+
+        snprintf(name, sizeof(name), "r%05x", i);
+
+        /*
+         * writer: Tlcreate on fid_a mutates its (9p server internal) FID-path
+         * string
+         */
+        req_a = v9fs_tlcreate((TlcreateOpt) {
+            .client = v9p,
+            .fid = fid_a,
+            .name = name,
+            .flags = O_RDWR | O_CREAT,
+            .mode = 0644,
+            .gid = 0,
+            .tag = tag_a,
+            .requestOnly = true,
+        }).req;
+
+        /*
+         * readers: Twalk and Txattrwalk requests
+         *
+         * Each reader request is sent after a few v9p_race_feed_main_loop()
+         * calls. That allows the Tlcreate PDU advance processing.
+         */
+        for (k = 0; k < nconcurrent; k++) {
+            /*
+             * Feed the main loop between 0..4 times.
+             *
+             * The amount of main loop feeds is changed per round and per
+             * reader. That way we create different time gaps between the
+             * writer (Tlcreate) and readers, which increases probability
+             * to hit the quite small window where a FID path is mutated.
+             */
+            for (d = 0; d < ((i + k) % 5); d++) {
+                v9p_race_feed_main_loop(v9p);
+            }
+            if (!(k & 1)) {
+                uint16_t tag_c = v9p_race_new_tag();
+
+                /* Txattrwalk (list xattrs) */
+                readers[k] = v9fs_txattrwalk((TXattrWalkOpt) {
+                    .client = v9p,
+                    .fid = fid_a,
+                    .newfid = fid_c + k,
+                    .name = "", /* empty xattr name -> list xattrs */
+                    .tag = tag_c,
+                    .requestOnly = true,
+                }).req;
+            } else {
+                uint16_t tag_b = v9p_race_new_tag();
+
+                /* Twalk (clone FID) */
+                readers[k] = v9fs_twalk((TWalkOpt) {
+                    .client = v9p,
+                    .fid = fid_a,
+                    .newfid = fid_b + k,
+                    .nwname = 0, /* no names to be walked -> clone FID */
+                    .tag = tag_b,
+                    .requestOnly = true,
+                }).req;
+            }
+        }
+
+        resp_a = v9p_race_resp_collect(v9p, req_a);
+        for (k = 0; k < nconcurrent; k++) {
+            uint8_t resp_r = v9p_race_resp_collect(v9p, readers[k]);
+
+            if (!(k & 1) && resp_r == P9_RXATTRWALK) {
+                xattr_fids++;
+                v9p_race_clunk_relaxed(v9p, fid_c + k);
+            } else if ((k & 1) && resp_r == P9_RWALK) {
+                walk_fids++;
+                v9p_race_clunk_relaxed(v9p, fid_b + k);
+            }
+        }
+        v9p_race_clunk_relaxed(v9p, fid_a);
+        if (resp_a == P9_RLCREATE) {
+            created_files++;
+            v9p_race_unlink_relaxed(v9p, V9P_RACE_D3_FID, name, 0);
+        }
+    }
+
+    g_print("race/fid-path: %u rounds done, %lu files created, %lu walk "
+            "fids, %lu xattr fids\n",
+            rounds, created_files, walk_fids, xattr_fids);
+    fflush(stdout);
+}
+
+/* setup and cleanup for race tests using the 9p 'local' fs driver */
+static void *v9p_race_local_driver(GString *cmd_line, void *arg)
+{
+    /* create a temp directory for being used as 9p export root */
+    virtio_9p_create_local_test_dir();
+    /*
+     * Use the "mapped-file" security model (not "mapped-xattr"): xattr only
+     * works on file systems and systems that support and permit writing
+     * xattrs. mapped-file (hidden .virtfs_metadata files) OTOH is universally
+     * more tolerant.
+     */
+    virtio_9p_assign_local_driver(cmd_line, "security_model=mapped-file");
+    g_test_queue_destroy(v9p_race_cleanup, NULL);
+    return arg;
+}
+
+/* register this overall test-suite */
+static void v9p_race_register_tests(void)
+{
+    QOSGraphTestOptions opts = {
+        .before = v9p_race_local_driver,
+    };
+
+    qos_add_test("race/fid-path", "virtio-9p", v9p_race_fid_path, &opts);
+}
+
+libqos_init(v9p_race_register_tests);
-- 
2.47.3



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

* [PATCH v2 3/9] tests/9p-client: add Rerror function (legacy 9p2000.u)
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
  2026-10-07 10:26 ` [PATCH v2 1/9] tests/9p-client: add Txattrwalk / Rxattrwalk test client functions Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 2/9] tests/9p-race: add 9p race test suite (race FID-path mutations) Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 4/9] tests/9p-client: add Tstat / Rstat test client functions Christian Schoenebeck
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add v9fs_rerror() function to the 9p test client. This function is
the legacy 9p2000.u variant for returned errors (i.e. in contrast
to the already existing Rlerror response handler function
v9fs_rlerror() for the 9p2000.L protocol version).

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

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index b2f0037b7b..005ae53a3f 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -287,6 +287,17 @@ void v9fs_req_free(P9Req *req)
     g_free(req);
 }
 
+/* size[4] Rerror tag[2] ename[s] errno[4] */
+void v9fs_rerror(P9Req *req, uint32_t *err)
+{
+    g_autofree char *ename = NULL;
+
+    v9fs_req_recv(req, P9_RERROR);
+    v9fs_string_read(req, NULL, &ename);
+    v9fs_uint32_read(req, err);
+    v9fs_req_free(req);
+}
+
 /* size[4] Rlerror tag[2] ecode[4] */
 void v9fs_rlerror(P9Req *req, uint32_t *err)
 {
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index c910407f70..5a00a0870c 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -599,6 +599,7 @@ void v9fs_req_send(P9Req *req);
 void v9fs_req_wait_for_reply(P9Req *req, uint32_t *len);
 void v9fs_req_recv(P9Req *req, uint8_t id);
 void v9fs_req_free(P9Req *req);
+void v9fs_rerror(P9Req *req, uint32_t *err);
 void v9fs_rlerror(P9Req *req, uint32_t *err);
 TVersionRes v9fs_tversion(TVersionOpt);
 void v9fs_rversion(P9Req *req, uint16_t *len, char **version);
-- 
2.47.3



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

* [PATCH v2 4/9] tests/9p-client: add Tstat / Rstat test client functions
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (2 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 3/9] tests/9p-client: add Rerror function (legacy 9p2000.u) Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 5/9] tests/9p-race: add Treaddir race test Christian Schoenebeck
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add v9fs_tstat() and v9fs_rstat() functions to the 9p test client.

Since this is a request type that exists both in legacy 9p2000.u
and modern 9p2000.L protocol version, error responses types might
differ (Rerror vs. Rlerror). Therefore v9fs_tstat() is using an
error type tolerant helper function v9fs_req_check_err() for
processing expected errors with this request type.

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

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index 005ae53a3f..4a3fc1511b 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -306,6 +306,33 @@ void v9fs_rlerror(P9Req *req, uint32_t *err)
     v9fs_req_free(req);
 }
 
+/*
+ * Asserts for caller that the request's response returned with the passed,
+ * expected error code (in a protocol version agnostic manner).
+ */
+static void v9fs_req_check_err(P9Req *req, uint32_t expect_err)
+{
+    P9Hdr hdr;
+    uint32_t err;
+
+    /* peek the response header */
+    v9fs_memread(req, &hdr, sizeof(hdr));
+    /* reset offset after peek, so v9fs_rlerror() etc. read correctly */
+    req->r_off = 0;
+
+    if (hdr.id == P9_RLERROR) {
+        v9fs_rlerror(req, &err);
+    } else if (hdr.id == P9_RERROR) {
+        v9fs_rerror(req, &err);
+    } else {
+        g_printerr("Received response %d (%s) instead of an error response\n",
+                   hdr.id, rmessage_name(hdr.id));
+        v9fs_req_free(req);
+        g_assert_not_reached();
+    }
+    g_assert_cmpint(err, ==, expect_err);
+}
+
 /* size[4] Tversion tag[2] msize[4] version[s] */
 TVersionRes v9fs_tversion(TVersionOpt opt)
 {
@@ -1286,3 +1313,55 @@ void v9fs_rxattrwalk(P9Req *req, uint64_t *size)
     }
     v9fs_req_free(req);
 }
+
+/* size[4] Tstat tag[2] fid[4] */
+TStatRes v9fs_tstat(TStatOpt opt)
+{
+    P9Req *req;
+
+    g_assert(opt.client);
+    /* expecting either Rstat or an error response, but obviously not both */
+    g_assert(!opt.expectErr || !opt.rstat.stat);
+
+    req = v9fs_req_init(opt.client, 4, P9_TSTAT, 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_req_check_err(req, opt.expectErr);
+        } else {
+            v9fs_rstat(req, opt.rstat.stat);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TStatRes) { .req = req };
+}
+
+/* size[4] Rstat tag[2] stat[n] */
+void v9fs_rstat(P9Req *req, v9fs_stat *stat)
+{
+    v9fs_req_recv(req, P9_RSTAT);
+
+    v9fs_memskip(req, 2); /* protocol quirk: size appears twice */
+    v9fs_uint16_read(req, &stat->size);
+    v9fs_uint16_read(req, &stat->type);
+    v9fs_uint32_read(req, &stat->dev);
+    v9fs_memread(req, &stat->qid, 13);
+    v9fs_uint32_read(req, &stat->mode);
+    v9fs_uint32_read(req, &stat->atime);
+    v9fs_uint32_read(req, &stat->mtime);
+    v9fs_uint64_read(req, &stat->length);
+    v9fs_string_read(req, NULL, &stat->name);
+    v9fs_string_read(req, NULL, &stat->uid);
+    v9fs_string_read(req, NULL, &stat->gid);
+    v9fs_string_read(req, NULL, &stat->muid);
+    v9fs_string_read(req, NULL, &stat->extension);
+    v9fs_uint32_read(req, &stat->n_uid);
+    v9fs_uint32_read(req, &stat->n_gid);
+    v9fs_uint32_read(req, &stat->n_muid);
+
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index 5a00a0870c..f604cc0ee5 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -63,6 +63,25 @@ typedef struct v9fs_attr {
     uint64_t data_version;
 } v9fs_attr;
 
+typedef struct v9fs_stat {
+    uint16_t size;
+    uint16_t type;
+    uint32_t dev;
+    v9fs_qid qid;
+    uint32_t mode;
+    uint32_t atime;
+    uint32_t mtime;
+    uint64_t length;
+    char *name;
+    char *uid;
+    char *gid;
+    char *muid;
+    char *extension;
+    uint32_t n_uid;
+    uint32_t n_gid;
+    uint32_t n_muid;
+} v9fs_stat;
+
 #define P9_GETATTR_BASIC    0x000007ffULL /* Mask for fields up to BLOCKS */
 #define P9_GETATTR_ALL      0x00003fffULL /* Mask for ALL fields */
 
@@ -579,6 +598,30 @@ typedef struct TXattrWalkRes {
     uint64_t size;
 } TXattrWalkRes;
 
+/* options for 'Tstat' 9p request */
+typedef struct TStatOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID to get the Rstat for (required) */
+    uint32_t fid;
+    /* data being received from 9p server as 'Rstat' response (optional) */
+    struct {
+        v9fs_stat *stat;
+    } rstat;
+    /* only send Tstat request but not wait for a reply? (optional) */
+    bool requestOnly;
+    /* do we expect an error response, if yes which error code? (optional) */
+    uint32_t expectErr;
+} TStatOpt;
+
+/* result of 'Tstat' 9p request */
+typedef struct TStatRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+} TStatRes;
+
 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);
@@ -639,5 +682,7 @@ TXattrCreateRes v9fs_txattrcreate(TXattrCreateOpt opt);
 void v9fs_rxattrcreate(P9Req *req);
 TXattrWalkRes v9fs_txattrwalk(TXattrWalkOpt opt);
 void v9fs_rxattrwalk(P9Req *req, uint64_t *size);
+TStatRes v9fs_tstat(TStatOpt opt);
+void v9fs_rstat(P9Req *req, v9fs_stat *stat);
 
 #endif
-- 
2.47.3



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

* [PATCH v2 5/9] tests/9p-race: add Treaddir race test
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (3 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 4/9] tests/9p-client: add Tstat / Rstat test client functions Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 6/9] hw/9pfs: fix dirent stream protection for legacy (9p2000.u) Tread Christian Schoenebeck
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add "race/readdir" test, a test racing Treaddir's directory stream.

In each round a Treaddir request is sent against another Treaddir
request or Tstat request on the same FID.

This test is registered as slow test (i.e. skipped by default) and
therefore requires -m slow to run.

On my test machine a full run of 20000 rounds took around 1 minute
to complete.

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

diff --git a/tests/qtest/virtio-9p-race-test.c b/tests/qtest/virtio-9p-race-test.c
index 789fb7721d..d2b76bf075 100644
--- a/tests/qtest/virtio-9p-race-test.c
+++ b/tests/qtest/virtio-9p-race-test.c
@@ -546,6 +546,89 @@ static void v9p_race_fid_path(void *obj, void *data, QGuestAllocator *t_alloc)
     fflush(stdout);
 }
 
+/*
+ * Race 9p server's internal directory stream for Treaddir.
+ *
+ * Race Treaddir request against another Treaddir or Tstat request on the same
+ * FID.
+ */
+static void v9p_race_readdir(void *obj, void *data, QGuestAllocator *t_alloc)
+{
+    QVirtio9P *v9p = obj;
+    uint32_t rounds;
+    uint32_t i;
+
+    if (v9p_race_skip()) {
+        return;
+    }
+    rounds = v9p_race_rounds();
+
+    v9fs_set_allocator(t_alloc);
+    v9p_race_prepare(v9p);
+
+    for (i = 0; i < rounds; i++) {
+        P9Req *req_a, *req_b;
+        uint16_t tag_a = v9p_race_new_tag();
+        uint16_t tag_b = v9p_race_new_tag();
+
+        if (i % 2000 == 0) {
+            g_print("race/readdir: %u/%u rounds\n", i, rounds);
+            fflush(stdout);
+        }
+
+        /*
+         * Reset the descriptor pool at the start of every round, where
+         * no request is in-flight anymore.
+         */
+        v9p_race_reset_pool(v9p);
+
+        /* initial Treaddir request */
+        req_a = v9fs_treaddir((TReadDirOpt) {
+            .client = v9p,
+            .fid = V9P_RACE_D2_FID,
+            .offset = 0,
+            .count = 64,
+            .tag = tag_a,
+            .requestOnly = true,
+        }).req;
+
+        /*
+         * On every 3rd round send a concurrent Tstat, otherwise a
+         * concurrent Treaddir request.
+         */
+        if (i % 3 == 0) {
+            /* a Tstat on the same directory FID */
+            req_b = v9fs_tstat((TStatOpt) {
+                .client = v9p,
+                .fid = V9P_RACE_D2_FID,
+                .tag = tag_b,
+                .requestOnly = true,
+            }).req;
+        } else {
+            /* a concurrent Treaddir on the same FID */
+            req_b = v9fs_treaddir((TReadDirOpt) {
+                .client = v9p,
+                .fid = V9P_RACE_D2_FID,
+                .offset = 0,
+                .count = 64,
+                .tag = tag_b,
+                .requestOnly = true,
+            }).req;
+        }
+
+        uint8_t resp_a, resp_b;
+
+        resp_a = v9p_race_resp_collect(v9p, req_a);
+        g_assert_cmpint(resp_a, ==, P9_RREADDIR);
+        resp_b = v9p_race_resp_collect(v9p, req_b);
+        g_assert_cmpint(resp_b, ==,
+                        (i % 3 == 0) ? P9_RSTAT : P9_RREADDIR);
+    }
+
+    g_print("race/readdir: %u rounds done\n", rounds);
+    fflush(stdout);
+}
+
 /* setup and cleanup for race tests using the 9p 'local' fs driver */
 static void *v9p_race_local_driver(GString *cmd_line, void *arg)
 {
@@ -570,6 +653,7 @@ static void v9p_race_register_tests(void)
     };
 
     qos_add_test("race/fid-path", "virtio-9p", v9p_race_fid_path, &opts);
+    qos_add_test("race/readdir", "virtio-9p", v9p_race_readdir, &opts);
 }
 
 libqos_init(v9p_race_register_tests);
-- 
2.47.3



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

* [PATCH v2 6/9] hw/9pfs: fix dirent stream protection for legacy (9p2000.u) Tread
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (4 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 5/9] tests/9p-race: add Treaddir race test Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 7/9] tests/9p-client: add Tcreate / Rcreate test client functions Christian Schoenebeck
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-stable, Greg Kurz

Concurrent legacy Tread requests (9p2000.u) on the same directory FID
race the FID's shared dirent stream.

In the initial Tread implementation (a92315553d) the dirent stream
was entirely unprotected. Commit 7cde47d4a8 introduced a lock, which
mitigated the problem, but without sufficient full coverage for this
request type, as it only held the lock during individual dirent
stream steps. A concurrent request could mutate the dirent stream in
between. Furthermore the initial rewinddir() (to offset 0) was still
not protected at all.

This could cause corrupted directory entries being returned to
client, with entries missing, duplicate entries and wrong order, up
to heap corruption of 9p server's QEMU process due to garbage seek
targets.

Fix this by holding the lock for the entire directory reading
transaction (i.e. for the whole v9fs_do_readdir_with_stat() call).

Fixes: a92315553d ("virtio-9p: Add P9_TREAD support")
Signed-off-by: Christian Schoenebeck <qemu_oss@crudebyte.com>
---
 hw/9pfs/9p.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/hw/9pfs/9p.c b/hw/9pfs/9p.c
index 4764d9db13..730d1279bb 100644
--- a/hw/9pfs/9p.c
+++ b/hw/9pfs/9p.c
@@ -2421,6 +2421,10 @@ v9fs_xattr_read(V9fsState *s, V9fsPDU *pdu, V9fsFidState *fidp,
     return offset;
 }
 
+/*
+ * The only caller of this function (v9fs_read()) holds the per-FID readdir
+ * lock during the whole call of this function.
+ */
 static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
                                                   V9fsFidState *fidp,
                                                   uint32_t max_count)
@@ -2431,6 +2435,7 @@ static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
     int32_t count = 0;
     struct stat stbuf;
     off_t saved_dir_pos;
+    off_t current_dir_pos;
     struct dirent *dent;
 
     /* save the directory position */
@@ -2442,12 +2447,11 @@ static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
     while (1) {
         v9fs_path_init(&path);
 
-        v9fs_readdir_lock(&fidp->fs.dir);
-
         err = v9fs_co_readdir(pdu, fidp, &dent);
         if (err || !dent) {
             break;
         }
+        current_dir_pos = qemu_dirent_off(dent);
         err = v9fs_co_name_to_path(pdu, &fidp->path, dent->d_name, &path);
         if (err < 0) {
             break;
@@ -2461,8 +2465,6 @@ static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
             break;
         }
         if ((count + v9stat.size + 2) > max_count) {
-            v9fs_readdir_unlock(&fidp->fs.dir);
-
             /* Ran out of buffer. Set dir back to old position and return */
             v9fs_co_seekdir(pdu, fidp, saved_dir_pos);
             v9fs_stat_free(&v9stat);
@@ -2473,8 +2475,6 @@ static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
         /* 11 = 7 + 4 (7 = start offset, 4 = space for storing count) */
         len = pdu_marshal(pdu, 11 + count, "S", &v9stat);
 
-        v9fs_readdir_unlock(&fidp->fs.dir);
-
         if (len < 0) {
             v9fs_co_seekdir(pdu, fidp, saved_dir_pos);
             v9fs_stat_free(&v9stat);
@@ -2484,11 +2484,9 @@ static int coroutine_fn v9fs_do_readdir_with_stat(V9fsPDU *pdu,
         count += len;
         v9fs_stat_free(&v9stat);
         v9fs_path_free(&path);
-        saved_dir_pos = qemu_dirent_off(dent);
+        saved_dir_pos = current_dir_pos;
     }
 
-    v9fs_readdir_unlock(&fidp->fs.dir);
-
     v9fs_path_free(&path);
     if (err < 0) {
         return err;
@@ -2528,10 +2526,17 @@ static void coroutine_fn v9fs_read(void *opaque)
             err = -EOPNOTSUPP;
             goto out;
         }
+        /*
+         * Hold the lock for the entire readdir transaction (rewind and
+         * getdents) to prevent dirent stream corruption on concurrent requests
+         * on the same FID.
+         */
+        v9fs_readdir_lock(&fidp->fs.dir);
         if (off == 0) {
             v9fs_co_rewinddir(pdu, fidp);
         }
         count = v9fs_do_readdir_with_stat(pdu, fidp, max_count);
+        v9fs_readdir_unlock(&fidp->fs.dir);
         if (count < 0) {
             err = count;
             goto out;
-- 
2.47.3



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

* [PATCH v2 7/9] tests/9p-client: add Tcreate / Rcreate test client functions
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (5 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 6/9] hw/9pfs: fix dirent stream protection for legacy (9p2000.u) Tread Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:27 ` [PATCH v2 8/9] tests/9p-client: track the negotiated 9p version for session Christian Schoenebeck
  2026-10-07 10:28 ` [PATCH v2 9/9] tests/9p-race: add Tread-on-dir race test Christian Schoenebeck
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add v9fs_tcreate() and v9fs_rcreate() functions to the 9p test
client for the legacy 9p2000(.u) Tcreate request type.

This is a preparatory patch, these functions will be used by the
subsequently added 9p race test.

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

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index 4a3fc1511b..ce67a4b8c5 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -1365,3 +1365,63 @@ void v9fs_rstat(P9Req *req, v9fs_stat *stat)
 
     v9fs_req_free(req);
 }
+
+/* size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] extension[s] */
+TCreateRes v9fs_tcreate(TCreateOpt opt)
+{
+    P9Req *req;
+    uint8_t mode = opt.mode;
+    uint32_t body_size = 4 + 4 + 1;
+    uint16_t string_size;
+
+    g_assert(opt.client);
+    /* expecting either Rcreate or an error response, but obviously not both */
+    g_assert(!opt.expectErr || !(opt.rcreate.qid || opt.rcreate.iounit));
+
+    if (!opt.perm) {
+        opt.perm = 0644;
+    }
+    if (!opt.extension) {
+        opt.extension = "";
+    }
+
+    string_size = v9fs_string_size(opt.name) +
+                  v9fs_string_size(opt.extension);
+    g_assert_cmpint(body_size, <=, UINT32_MAX - string_size);
+    body_size += string_size;
+
+    req = v9fs_req_init(opt.client, body_size, P9_TCREATE, opt.tag);
+    v9fs_uint32_write(req, opt.fid);
+    v9fs_string_write(req, opt.name);
+    v9fs_uint32_write(req, opt.perm);
+    v9fs_memwrite(req, &mode, 1);
+    v9fs_string_write(req, opt.extension);
+    v9fs_req_send(req);
+
+    if (!opt.requestOnly) {
+        v9fs_req_wait_for_reply(req, NULL);
+        if (opt.expectErr) {
+            v9fs_req_check_err(req, opt.expectErr);
+        } else {
+            v9fs_rcreate(req, opt.rcreate.qid, opt.rcreate.iounit);
+        }
+        req = NULL; /* request was freed */
+    }
+
+    return (TCreateRes) { .req = req };
+}
+
+/* size[4] Rcreate tag[2] qid[13] iounit[4] */
+void v9fs_rcreate(P9Req *req, v9fs_qid *qid, uint32_t *iounit)
+{
+    v9fs_req_recv(req, P9_RCREATE);
+    if (qid) {
+        v9fs_memread(req, qid, 13);
+    } else {
+        v9fs_memskip(req, 13);
+    }
+    if (iounit) {
+        v9fs_uint32_read(req, iounit);
+    }
+    v9fs_req_free(req);
+}
diff --git a/tests/qtest/libqos/virtio-9p-client.h b/tests/qtest/libqos/virtio-9p-client.h
index f604cc0ee5..88e0f41ff0 100644
--- a/tests/qtest/libqos/virtio-9p-client.h
+++ b/tests/qtest/libqos/virtio-9p-client.h
@@ -372,6 +372,54 @@ typedef struct TMkdirRes {
     P9Req *req;
 } TMkdirRes;
 
+/* create directory */
+#define P9_CREATE_PERM_DMDIR     0x80000000
+
+/* options for 'Tcreate' 9p request (legacy 9p2000.u) */
+typedef struct TCreateOpt {
+    /* 9P client being used (mandatory) */
+    QVirtio9P *client;
+    /* user supplied tag number being returned with response (optional) */
+    uint16_t tag;
+    /* file ID of the directory the new file shall be created in (required) */
+    uint32_t fid;
+    /* name of the new file (required) */
+    const char *name;
+    /*
+     * Create mode bits (including file type bits):
+     * e.g. 0644 for a regular file,
+     * or P9_CREATE_PERM_DMDIR | 0755 for a directory
+     * (optional)
+     */
+    uint32_t perm;
+    /*
+     * Open mode byte:
+     * OREAD (0), OWRITE (1), ORDWR (2), OEXEC(3), OTRUNC(0x10), ORCLOSE(0x40)
+     * (optional)
+     */
+    uint8_t mode;
+    /*
+     * Extension data for special files (e.g. symlink target), empty for
+     * regular files. (optional)
+     */
+    const char *extension;
+    /* data being received from 9p server as 'Rcreate' response (optional) */
+    struct {
+        v9fs_qid *qid;
+        uint32_t *iounit;
+    } rcreate;
+    /* only send Tcreate request but not wait for a reply? (optional) */
+    bool requestOnly;
+    /* do we expect an error response, if yes which error code? (optional) */
+    uint32_t expectErr;
+} TCreateOpt;
+
+/* result of 'Tcreate' 9p request */
+typedef struct TCreateRes {
+    /* if requestOnly was set: request object for further processing */
+    P9Req *req;
+} TCreateRes;
+
 /* options for 'Tlcreate' 9p request */
 typedef struct TlcreateOpt {
     /* 9P client being used (mandatory) */
@@ -666,6 +714,8 @@ TFlushRes v9fs_tflush(TFlushOpt);
 void v9fs_rflush(P9Req *req);
 TMkdirRes v9fs_tmkdir(TMkdirOpt);
 void v9fs_rmkdir(P9Req *req, v9fs_qid *qid);
+TCreateRes v9fs_tcreate(TCreateOpt opt);
+void v9fs_rcreate(P9Req *req, v9fs_qid *qid, uint32_t *iounit);
 TlcreateRes v9fs_tlcreate(TlcreateOpt);
 void v9fs_rlcreate(P9Req *req, v9fs_qid *qid, uint32_t *iounit);
 TsymlinkRes v9fs_tsymlink(TsymlinkOpt);
-- 
2.47.3



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

* [PATCH v2 8/9] tests/9p-client: track the negotiated 9p version for session
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (6 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 7/9] tests/9p-client: add Tcreate / Rcreate test client functions Christian Schoenebeck
@ 2026-10-07 10:27 ` Christian Schoenebeck
  2026-10-07 10:28 ` [PATCH v2 9/9] tests/9p-race: add Tread-on-dir race test Christian Schoenebeck
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:27 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

So far v9fs_tattach() always sent an implied Tversion request, which
was supposed to be a convenient mechanisms such that not every test
case code would need to call v9fs_tversion() prior to a
v9fs_tattach(). The problem however is that this implicit Tversion
defaults to 9p2000.L, and it would also be (re)sent if an explicit
v9fs_tversion() call had been issued with 9p2000.u before, which then
would reset the session to the unintended 9p2000.L protocol version.

Fix this by storing the initial Tversion request's (desired) protocol
version to the client object. And only issue the implicit Tversion
request inside of v9fs_tattach() if no protocol version had been
negotiated already.

This is a preparatory patch for the subsequently added race test,
which negotiates a 9p2000.u session.

It also allows request / response handling to later become more
protocol dialect aware (e.g. expecting specific request / response
types according to negotiated protocol version during a session).

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

diff --git a/tests/qtest/libqos/virtio-9p-client.c b/tests/qtest/libqos/virtio-9p-client.c
index ce67a4b8c5..20c7af91bd 100644
--- a/tests/qtest/libqos/virtio-9p-client.c
+++ b/tests/qtest/libqos/virtio-9p-client.c
@@ -366,6 +366,15 @@ TVersionRes v9fs_tversion(TVersionOpt opt)
     v9fs_string_write(req, opt.version);
     v9fs_req_send(req);
 
+    /* store session's protocol version to client object */
+    if (!strcmp(opt.version, "9P2000.u")) {
+        opt.client->proto_version = V9FS_PROTO_2000U;
+    } else if (!strcmp(opt.version, "9P2000.L")) {
+        opt.client->proto_version = V9FS_PROTO_2000L;
+    } else {
+        g_assert_not_reached();
+    }
+
     if (!opt.requestOnly) {
         v9fs_req_wait_for_reply(req, NULL);
         if (opt.expectErr) {
@@ -412,7 +421,11 @@ TAttachRes v9fs_tattach(TAttachOpt opt)
     /* expecting either Rattach or Rlerror, but obviously not both */
     g_assert(!opt.expectErr || !opt.rattach.qid);
 
-    if (!opt.requestOnly) {
+    /*
+     * convenience feature: send an implicit Tversion request, however only if
+     * Tversion has not been issued for this session before.
+     */
+    if (!opt.requestOnly && opt.client->proto_version == 0) {
         v9fs_tversion((TVersionOpt) { .client = opt.client });
     }
 
diff --git a/tests/qtest/libqos/virtio-9p.h b/tests/qtest/libqos/virtio-9p.h
index e7efeef7a1..db21c5059c 100644
--- a/tests/qtest/libqos/virtio-9p.h
+++ b/tests/qtest/libqos/virtio-9p.h
@@ -22,6 +22,7 @@
 #include "qgraph.h"
 #include "virtio.h"
 #include "virtio-pci.h"
+#include "hw/9pfs/9p.h"
 
 typedef struct QVirtio9P QVirtio9P;
 typedef struct QVirtio9PPCI QVirtio9PPCI;
@@ -32,6 +33,11 @@ typedef struct QVirtio9PDevice QVirtio9PDevice;
 struct QVirtio9P {
     QVirtioDevice *vdev;
     QVirtQueue *vq;
+    /*
+     * The protocol version negotiated for this 9p session via Tversion request
+     * (0 if no protocol version negotiated yet)
+     */
+    P9ProtoVersion proto_version;
 };
 
 struct QVirtio9PPCI {
-- 
2.47.3



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

* [PATCH v2 9/9] tests/9p-race: add Tread-on-dir race test
  2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
                   ` (7 preceding siblings ...)
  2026-10-07 10:27 ` [PATCH v2 8/9] tests/9p-client: track the negotiated 9p version for session Christian Schoenebeck
@ 2026-10-07 10:28 ` Christian Schoenebeck
  8 siblings, 0 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:28 UTC (permalink / raw)
  To: qemu-devel; +Cc: Greg Kurz

Add "race/tread-dir" test, a test racing legacy (9p2000.u)
Tread requests against each other on the same directory FID.

This test guards the previous fix patch ("hw/9pfs: fix dirent
stream protection for legacy (9p2000.u) Tread").

In each round a configurable number (V9FS_RACE_NCONCURRENT) of
concurrent Tread requests are issued against each other, verifying
that every Tread response lists the same (leading) directory entries
as a reference directory entry list taken before the actual
concurrent Tread request sequence starts.

A vulnerable server would return directory entries either with
corrupted entry names, or entries in varying order, or even crash
the QEMU process due to garbage dirent seek targets.

This test is explicitly using a 9p2000.u session, as Tread on
directories is not a 9p2000.L feature (the latter uses Treaddir
instead).

This test is registered as slow test (i.e. skipped by default) and
therefore requires -m slow to run.

On my test machine a full run of 20000 rounds took around 5 minutes
to complete.

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

diff --git a/tests/qtest/virtio-9p-race-test.c b/tests/qtest/virtio-9p-race-test.c
index d2b76bf075..9d3c9f0a23 100644
--- a/tests/qtest/virtio-9p-race-test.c
+++ b/tests/qtest/virtio-9p-race-test.c
@@ -270,28 +270,52 @@ static void v9p_race_create_subdir_chain(QVirtio9P *v9p)
 
     for (i = 1; i <= V9P_RACE_DIR_DEPTH; ++i) {
         char dir[8];
-        char *wnames[] = { dir };
         uint16_t newfid =
             (i == 1) ? V9P_RACE_D1_FID
                      : (i == 2) ? V9P_RACE_D2_FID : V9P_RACE_D3_FID;
 
         snprintf(dir, sizeof(dir), "d%d", i);
-        v9fs_tmkdir((TMkdirOpt) {
-            .client = v9p,
-            .dfid = dfid,
-            .name = dir,
-            .mode = 0755,
-            .gid = 0,
-            .tag = v9p_race_new_tag(),
-        });
-        v9fs_twalk((TWalkOpt) {
-            .client = v9p,
-            .fid = dfid,
-            .newfid = newfid,
-            .nwname = 1,
-            .wnames = wnames,
-            .tag = v9p_race_new_tag(),
-        });
+        if (v9p->proto_version == V9FS_PROTO_2000L) {
+            char *wnames[] = { dir };
+
+            v9fs_tmkdir((TMkdirOpt) {
+                .client = v9p,
+                .dfid = dfid,
+                .name = dir,
+                .mode = 0755,
+                .gid = 0,
+                .tag = v9p_race_new_tag(),
+            });
+            v9fs_twalk((TWalkOpt) {
+                .client = v9p,
+                .fid = dfid,
+                .newfid = newfid,
+                .nwname = 1,
+                .wnames = wnames,
+                .tag = v9p_race_new_tag(),
+            });
+        } else {
+            /*
+             * clone dfid for Tcreate
+             *
+             * The legacy 9p2000(.u) Tcreate re-points the supplied FID to the
+             * created (in this case) directory and requires it to be unopened.
+             */
+            v9fs_twalk((TWalkOpt) {
+                .client = v9p,
+                .fid = dfid,
+                .newfid = newfid,
+                .nwname = 0, /* 0 -> clone FID */
+                .tag = v9p_race_new_tag(),
+            });
+            v9fs_tcreate((TCreateOpt) {
+                .client = v9p,
+                .fid = newfid,
+                .name = dir,
+                .perm = P9_CREATE_PERM_DMDIR | 0755,
+                .tag = v9p_race_new_tag(),
+            });
+        }
         dfid = newfid;
     }
 }
@@ -629,6 +653,357 @@ static void v9p_race_readdir(void *obj, void *data, QGuestAllocator *t_alloc)
     fflush(stdout);
 }
 
+/* number of files created for "race/tread-dir" test */
+#define V9P_RACE_TREAD_DIR_NFILES 40
+
+/* returns true if @name is one of the dir entries known to exist in d3 */
+static bool v9p_race_tread_dir_entry_name_valid(const char *name)
+{
+    int n;
+
+    if (!strcmp(name, ".") || !strcmp(name, "..")) {
+        return true;
+    }
+    if (name[0] != 'f' || strlen(name) != 3) {
+        return false;
+    }
+    /*
+     * atoi() returns 0 on error, therefore the files created for this test
+     * start with 1, not zero.
+     */
+    n = atoi(&name[1]);
+    return n >= 1 && n <= V9P_RACE_TREAD_DIR_NFILES;
+}
+
+/*
+ * Read one directory entry (as v9stat struct) from Rread response.
+ *
+ * Only the directory entry's name is returned
+ * (in @name, at most @name_size - 1 chars).
+ *
+ * Returns the number of response body bytes consumed for this entry.
+ */
+static size_t v9p_race_tread_dir_next_entry(P9Req *req, char *name,
+                                            size_t name_size)
+{
+    /*
+     * Each directory entry within Tread response is (with 9p2000.u):
+     *
+     * size[2] type[2] dev[4] qid[13] mode[4] atime[4] mtime[4] length[8]
+     * name[s] uid[s] gid[s] muid[s] ext[s] n_uid[4] n_gid[4] n_muid[4]
+     */
+    uint16_t len = 0;
+    size_t consumed = 2 + 2 + 4 + 13 + 4 + 4 + 4 + 8;
+    int s;
+
+    /* fast forward to name[s] */
+    v9fs_memskip(req, consumed);
+
+    /* read name[s] */
+    v9fs_uint16_read(req, &len);
+    g_assert_cmpint(len, <, name_size);
+    v9fs_memread(req, name, len);
+    name[len] = '\0';
+    consumed += 2 + len;
+
+    /* consume the rest of this entry */
+    for (s = 0; s < 4; s++) { /* uid, gid, muid, ext */
+        v9fs_string_read(req, &len, NULL);
+        consumed += 2 + len;
+    }
+    v9fs_memskip(req, 4 + 4 + 4); /* n_uid, n_gid, n_muid */
+    consumed += 4 + 4 + 4;
+
+    return consumed;
+}
+
+/*
+ * Parse and verify an Rread response.
+ *
+ * We have created V9P_RACE_TREAD_DIR_NFILES number of files in directory d3,
+ * so we already know the file name scheme to verify for individual entries.
+ *
+ * Additionally, compare the returned directory entries with the passed
+ * reference list of entries. They must always match exactly in name and order.
+ */
+static void v9p_race_tread_dir_verify_rread(P9Req *req,
+                                            const char *const *ref_names,
+                                            int n_ref_names)
+{
+    uint32_t count = 0, consumed = 0;
+    char name[64];
+    int ientry;
+
+    v9fs_uint32_read(req, &count);
+
+    for (ientry = 0; consumed < count; ientry++) {
+        consumed += v9p_race_tread_dir_next_entry(req, name, sizeof(name));
+        if (!v9p_race_tread_dir_entry_name_valid(name)) {
+            g_printerr("invalid dirent name: %.16s\n", name);
+            fflush(stderr);
+            g_assert_not_reached();
+        }
+        if (ref_names && ientry < n_ref_names) {
+            if (strcmp(name, ref_names[ientry]) != 0) {
+                g_printerr("entry-order mismatch at #%d: got '%s' expected "
+                           "'%s'\n", ientry, name, ref_names[ientry]);
+                fflush(stderr);
+                g_assert_not_reached();
+            }
+        }
+    }
+    /*
+     * an empty Rread body means the dirent stream was corrupted
+     * (e.g. by a garbage seek)
+     */
+    g_assert_cmpint(ientry, >, 0);
+}
+
+/*
+ * Wait for response for @req, then verify if response is an Rread response
+ * with expected tag, then verify all directory entries.
+ */
+static void v9p_race_tread_dir_collect(QVirtio9P *v9p, P9Req *req,
+                                       const char *const *ref_names,
+                                       int n_ref_names)
+{
+    uint32_t len;
+    P9MsgHeader hdr;
+
+    v9p_race_req_wait(v9p, req, &len);
+    v9fs_memread(req, &hdr, sizeof(hdr));
+    g_assert_cmpint(hdr.tag_le, ==, req->tag);
+    if (hdr.id != P9_RREAD) {
+        uint32_t ecode = 0;
+
+        if (hdr.id == P9_RERROR) {
+            v9fs_string_read(req, NULL, NULL); /* skip error string */
+        }
+        if (hdr.id == P9_RLERROR || hdr.id == P9_RERROR) {
+            v9fs_uint32_read(req, &ecode);
+        }
+        g_error("Tread on dir FID returned ID %d (ecode %u, %s)", hdr.id,
+                ecode, strerror(ecode));
+    }
+    g_assert_cmpint(hdr.size_le, >=, 11);
+    v9p_race_tread_dir_verify_rread(req, ref_names, n_ref_names);
+    v9fs_req_free(req);
+}
+
+/*
+ * Race legacy (9p2000.u) Tread requests against each other on the same
+ * directory FID.
+ *
+ * This test creates V9P_RACE_TREAD_DIR_NFILES number of files under directory
+ * d3 and verifies that (a portion of) these files are always returned by
+ * server in a stable manner (i.e. always exact same entry names, and in exact
+ * same order) for each Tread request sent on the same directory FID.
+ *
+ * We actually read less directory entries than we created files, just to keep
+ * things simple. A small amount of dir entries retrieval is still enough to
+ * detect corrupted Rread responses.
+ */
+static void v9p_race_tread_dir(void *obj, void *data, QGuestAllocator *t_alloc)
+{
+    if (v9p_race_skip()) {
+        return;
+    }
+
+    QVirtio9P *v9p = obj;
+    uint32_t rounds = v9p_race_rounds();
+    uint32_t i = 0, n;
+
+    /* max. Rread bytes requested per Tread request */
+    const uint32_t tread_size = 512;
+
+    /*
+     * Max. amount of reference directory entries taken for verification
+     * (kept small enough to fit into one tread_size Rread response)
+     */
+    const uint32_t ref_max = 8;
+
+    v9fs_set_allocator(t_alloc);
+
+    /*
+     * Need to set this session to legacy 9P2000.u dialect, because Tread on
+     * directories is only valid there (9p2000.L provides Treaddir instead).
+     */
+    v9fs_tversion((TVersionOpt) {
+        .client = v9p,
+        .version = "9P2000.u",
+        .tag = v9p_race_new_tag(),
+    });
+    v9fs_tattach((TAttachOpt) {
+        .client = v9p,
+        .fid = V9P_RACE_ROOT_FID,
+        .tag = v9p_race_new_tag(),
+    });
+
+    /* create d1/d2/d3 directory chain */
+    v9p_race_create_subdir_chain(v9p);
+
+    /* populate d3 with known directory entries */
+    for (n = 0; n < V9P_RACE_TREAD_DIR_NFILES; n++) {
+        char fname[8];
+
+        /*
+         * use a new FID for each file created, just to be sure
+         * (probably not really needed as we clunk it at the end of the loop)
+         */
+        uint16_t fid = 60 + (n % 32);
+
+        /* no in-flight requests, good to reset virtio descriptor pool */
+        v9p_race_reset_pool(v9p);
+
+        /*
+         * starting to create files by 1, not zero
+         * (see v9p_race_tread_dir_entry_name_valid() why)
+         */
+        snprintf(fname, sizeof(fname), "f%02d", n + 1);
+
+        /*
+         * clone d3 FID for Tcreate
+         *
+         * The legacy 9p2000(.u) Tcreate re-points the supplied FID to the
+         * created (in this case) file and requires it to be unopened.
+         */
+        v9fs_twalk((TWalkOpt) {
+            .client = v9p,
+            .fid = V9P_RACE_D3_FID,
+            .newfid = fid,
+            .nwname = 0, /* 0 -> clone FID */
+            .tag = v9p_race_new_tag(),
+        });
+        /* create the file */
+        v9fs_tcreate((TCreateOpt) {
+            .client = v9p,
+            .fid = fid,
+            .name = fname,
+            .tag = v9p_race_new_tag(),
+        });
+        v9p_race_clunk_relaxed(v9p, fid);
+    }
+
+    /* start the actual Tread-dir race test ... */
+    {
+        g_autofree char **ref_storage = g_new0(char *, ref_max);
+        g_autofree const char **ref_names = g_new0(const char *, ref_max);
+        uint32_t len = 0, count = 0, consumed = 0;
+        int idx = 0;
+        int ref_count = 0;
+        g_autofree P9Req **reqs = NULL;
+        int nreq, r;
+
+        /* no in-flight requests, good to reset virtio descriptor pool */
+        v9p_race_reset_pool(v9p);
+
+        /*
+         * Send and collect the initial "reference" Tread.
+         *
+         * One fundamental verification mechanism of this race test is to
+         * verify the order of the individual directory entries returned by
+         * server to be clean and stable. However the order is fs
+         * implementation specific, so we cannot simply assume the same order
+         * in which we created the files.
+         *
+         * Instead we first send out one safe (no race) Tread and use its
+         * entries returned as THE reference list of directory entries.
+         *
+         * Then in the concurrent Tread loop below, we compare the subsequently
+         * collected follow-up Tread responses to always exactly match those
+         * reference directory entries.
+         *
+         * One caveat: POSIX does not guarantee order of entries to be stable,
+         * however in practice for non-modified directories it is actually
+         * stable. Let's not overcomplicate things (exotic systems) as this
+         * test is not run by default anyway.
+         */
+        P9Req *req = v9fs_tread((TReadOpt) {
+            .client = v9p,
+            .fid = V9P_RACE_D3_FID,
+            .count = tread_size,
+            .tag = v9p_race_new_tag(),
+            .requestOnly = true,
+        }).req;
+        v9p_race_req_wait(v9p, req, &len);
+        {
+            P9MsgHeader hdr;
+
+            v9fs_memread(req, &hdr, sizeof(hdr));
+            g_assert_cmpint(hdr.id, ==, P9_RREAD);
+        }
+        v9fs_uint32_read(req, &count);
+
+        /* collect reference directory entries */
+        for (consumed = 0; consumed < count && idx < ref_max; idx++) {
+            char nbuf[64];
+
+            consumed += v9p_race_tread_dir_next_entry(req, nbuf, sizeof(nbuf));
+            g_assert(v9p_race_tread_dir_entry_name_valid(nbuf));
+            ref_storage[idx] = g_strdup(nbuf);
+            ref_names[idx] = ref_storage[idx];
+        }
+        v9fs_req_free(req);
+        ref_count = idx;
+        g_assert_cmpint(ref_count, >=, 2);
+
+        /* dump the reference list of entries */
+        g_print("race/tread-dir: reference listing: %d entries (as follows):\n",
+                ref_count);
+        for (i = 0; i < ref_count; i++) {
+            g_print("race/tread-dir: reference entry #%u: '%s'\n",
+                    i, ref_storage[i]);
+        }
+        fflush(stdout);
+
+        nreq = v9p_race_nconcurrent();
+        /*
+         * Limit the number of concurrent Tread requests to the descriptor pool
+         * size.
+         */
+        if (nreq > v9p->vq->size / 2) {
+            g_error("V9FS_RACE_NCONCURRENT: %d concurrent requests "
+                    "exceeds the queue's max. request amount of %d",
+                    nreq, v9p->vq->size / 2);
+        }
+        reqs = g_new0(P9Req *, nreq);
+
+        /* run the requested number of test rounds */
+        for (i = 0; i < rounds; i++) {
+            if (i % 2000 == 0) {
+                g_print("race/tread-dir: %u/%u rounds\n", i, rounds);
+                fflush(stdout);
+            }
+
+            /* no in-flight requests, good to reset virtio descriptor pool */
+            v9p_race_reset_pool(v9p);
+
+            /* send the actual concurrent Tread requests */
+            for (r = 0; r < nreq; r++) {
+                reqs[r] = v9fs_tread((TReadOpt) {
+                    .client = v9p,
+                    .fid = V9P_RACE_D3_FID,
+                    .count = tread_size,
+                    .tag = v9p_race_new_tag(),
+                    .requestOnly = true,
+                }).req;
+            }
+
+            /* collect and verify all responses */
+            for (r = 0; r < nreq; r++) {
+                v9p_race_tread_dir_collect(v9p, reqs[r], ref_names, ref_count);
+            }
+        }
+
+        for (i = 0; i < ref_max; i++) {
+            g_free(ref_storage[i]);
+        }
+    }
+
+    g_print("race/tread-dir: %u rounds done\n", rounds);
+    fflush(stdout);
+}
+
 /* setup and cleanup for race tests using the 9p 'local' fs driver */
 static void *v9p_race_local_driver(GString *cmd_line, void *arg)
 {
@@ -654,6 +1029,7 @@ static void v9p_race_register_tests(void)
 
     qos_add_test("race/fid-path", "virtio-9p", v9p_race_fid_path, &opts);
     qos_add_test("race/readdir", "virtio-9p", v9p_race_readdir, &opts);
+    qos_add_test("race/tread-dir", "virtio-9p", v9p_race_tread_dir, &opts);
 }
 
 libqos_init(v9p_race_register_tests);
-- 
2.47.3



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

* [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race
@ 2026-10-07 10:29 Christian Schoenebeck
  2026-10-07 10:26 ` [PATCH v2 1/9] tests/9p-client: add Txattrwalk / Rxattrwalk test client functions Christian Schoenebeck
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Christian Schoenebeck @ 2026-10-07 10:29 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-stable, Greg Kurz

This series adds a race test suite for 9p and fixes a race on legacy
9p2000(.u)'s Tread request on directories, which was uncovered by this
new test suite.

This new race test suite challenges races by sending crafted concurrent 9p
requests on certain aspects of the 9p protocol. This series covers 3 race
tests:

  1. Patch 2 adds "race/fid-path": this race test guards CVE-2026-93834
     (fixed by commit fe9e30899f "hw/9pfs: Use read-write lock for protecting
     fid path.").

     Please refer to the commit log message of this patch for how to run
     these new 9p race tests in general.

  2. Patch 5 adds "race/readdir": this test guards directory stream races
     on 9p2000.L's Treaddir request.

  3. Patch 9 adds "race/tread-dir": this tests legacy 9p2000(.u)'s equivalent
     of Treaddir for directory stream races, and this test got triggered during
     the work on this series.

Therefore:

  - Fix: Patch 6 fixes the directory stream race vulnerability uncovered by
    race test #3.

All the other patches are basically just 9p test client additions required
for these new tests.

Changes:

  v2:
    - Allow skipping these race tests by setting environment variable
      V9FS_RACE=0, or force running them by setting V9FS_RACE=1 respectively.

Christian Schoenebeck (9):
  tests/9p-client: add Txattrwalk / Rxattrwalk test client functions
  tests/9p-race: add 9p race test suite (race FID-path mutations)
  tests/9p-client: add Rerror function (legacy 9p2000.u)
  tests/9p-client: add Tstat / Rstat test client functions
  tests/9p-race: add Treaddir race test
  hw/9pfs: fix dirent stream protection for legacy (9p2000.u) Tread
  tests/9p-client: add Tcreate / Rcreate test client functions
  tests/9p-client: track the negotiated 9p version for session
  tests/9p-race: add Tread-on-dir race test

 MAINTAINERS                           |    1 +
 hw/9pfs/9p.c                          |   23 +-
 tests/qtest/libqos/virtio-9p-client.c |  213 ++++-
 tests/qtest/libqos/virtio-9p-client.h |  124 +++
 tests/qtest/libqos/virtio-9p.h        |    6 +
 tests/qtest/meson.build               |    1 +
 tests/qtest/virtio-9p-race-test.c     | 1035 +++++++++++++++++++++++++
 7 files changed, 1393 insertions(+), 10 deletions(-)
 create mode 100644 tests/qtest/virtio-9p-race-test.c

-- 
2.47.3



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

end of thread, other threads:[~2026-10-07 10:41 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-07 10:29 [PATCH v2 0/9] 9pfs: add race test suite + fix 9p2000.u dir stream race Christian Schoenebeck
2026-10-07 10:26 ` [PATCH v2 1/9] tests/9p-client: add Txattrwalk / Rxattrwalk test client functions Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 2/9] tests/9p-race: add 9p race test suite (race FID-path mutations) Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 3/9] tests/9p-client: add Rerror function (legacy 9p2000.u) Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 4/9] tests/9p-client: add Tstat / Rstat test client functions Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 5/9] tests/9p-race: add Treaddir race test Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 6/9] hw/9pfs: fix dirent stream protection for legacy (9p2000.u) Tread Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 7/9] tests/9p-client: add Tcreate / Rcreate test client functions Christian Schoenebeck
2026-10-07 10:27 ` [PATCH v2 8/9] tests/9p-client: track the negotiated 9p version for session Christian Schoenebeck
2026-10-07 10:28 ` [PATCH v2 9/9] tests/9p-race: add Tread-on-dir race test 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.