From: Michael Bommarito <michael.bommarito@gmail.com>
To: Trond Myklebust <trondmy@kernel.org>, Anna Schumaker <anna@kernel.org>
Cc: linux-nfs@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode
Date: Sun, 14 Jun 2026 09:08:14 -0400 [thread overview]
Message-ID: <20260614130814.2521819-3-michael.bommarito@gmail.com> (raw)
In-Reply-To: <20260614130814.2521819-1-michael.bommarito@gmail.com>
Add a KUnit suite driving the real file-local decode_getdeviceinfo()
over a crafted GETDEVICEINFO reply to cover the notification-bitmap
length pass. It is included into nfs4xdr.c (gated by
CONFIG_NFS_GETDEVICEINFO_KUNIT_TEST) to reach the static decoder without
exporting it.
A trigger supplies the wrapping length and two benign controls drive the
same decoder in bounds. Integer overflow has no sanitizer, so the oracle
is the downstream KASAN slab-out-of-bounds read: on QEMU x86_64 with
KASAN the trigger faults on stock and passes after patch 1, while both
controls pass on stock and fixed trees.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
On QEMU x86_64 with KASAN: the trigger faults on stock (slab-out-of-
bounds READ) and passes after patch 1; both benign controls pass on
stock and patched. The suite builds the reply in a kmalloc(PAGE_SIZE)
buffer with the notification fields in the XDR tail.
fs/nfs/Kconfig | 14 ++++
fs/nfs/getdeviceinfo_notify_kunit.c | 110 ++++++++++++++++++++++++++++
fs/nfs/nfs4xdr.c | 4 +
3 files changed, 128 insertions(+)
create mode 100644 fs/nfs/getdeviceinfo_notify_kunit.c
diff --git a/fs/nfs/Kconfig b/fs/nfs/Kconfig
index 6bb30543eff00..73cdf201ebb23 100644
--- a/fs/nfs/Kconfig
+++ b/fs/nfs/Kconfig
@@ -215,3 +215,17 @@ config NFS_V4_2_READ_PLUS
default y
help
Choose Y here to enable use of the NFS v4.2 READ_PLUS operation.
+
+config NFS_GETDEVICEINFO_KUNIT_TEST
+ tristate "KUnit test for pNFS GETDEVICEINFO notification decode" if !KUNIT_ALL_TESTS
+ depends on NFS_V4 && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ Builds KUnit coverage for the notification-bitmap length pass in
+ the NFS client pNFS GETDEVICEINFO reply decoder. The test drives
+ the real decode_getdeviceinfo() over a crafted reply and, on an
+ unfixed kernel built with CONFIG_KASAN, reports the slab-out-of-
+ bounds read caused by a 32-bit overflow in the notification-bitmap
+ length handling.
+
+ If unsure, say N.
diff --git a/fs/nfs/getdeviceinfo_notify_kunit.c b/fs/nfs/getdeviceinfo_notify_kunit.c
new file mode 100644
index 0000000000000..1bfc40aabd15d
--- /dev/null
+++ b/fs/nfs/getdeviceinfo_notify_kunit.c
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit coverage for decode_getdeviceinfo()'s notification-bitmap length.
+ * Drives the real static decoder over a crafted reply: with len = 0x40000000
+ * the u32 "4 * len" wraps to 0, defeating the bounds check, and the verify
+ * loop reads past the buffer. The length word sits at the page edge so the
+ * first over-read word hits the KASAN redzone. Level 2; from nfs4xdr.c.
+ */
+#include <kunit/test.h>
+
+#define GDI_OPNUM 47U /* OP_GETDEVICEINFO */
+#define GDI_LAYOUT_TYPE 1U /* arbitrary; pdev->layout_type matches */
+
+/* Fixed fields in the XDR head, notification bitmap in the tail ending at @tail_end. */
+static int run_decode(struct kunit *test, __be32 *tail_end, u32 notify_len,
+ u32 notify_word0)
+{
+ struct pnfs_device pdev = { .layout_type = GDI_LAYOUT_TYPE };
+ struct nfs4_getdeviceinfo_res res = { .pdev = &pdev };
+ struct xdr_stream xdr;
+ struct xdr_buf buf;
+ __be32 head[4];
+ unsigned int tail_words = notify_len <= 2 ? notify_len + 1 : 1;
+ __be32 *tail = tail_end - tail_words;
+
+ head[0] = cpu_to_be32(GDI_OPNUM); /* op_hdr: opnum */
+ head[1] = cpu_to_be32(0); /* op_hdr: NFS_OK */
+ head[2] = cpu_to_be32(GDI_LAYOUT_TYPE); /* device type */
+ head[3] = cpu_to_be32(0); /* mincount = 0 */
+ tail[0] = cpu_to_be32(notify_len); /* notification len */
+ if (notify_len == 1) {
+ tail[1] = cpu_to_be32(notify_word0);
+ } else if (notify_len == 2) {
+ tail[1] = cpu_to_be32(notify_word0);
+ tail[2] = cpu_to_be32(1);
+ }
+
+ memset(&buf, 0, sizeof(buf));
+ buf.head[0].iov_base = head;
+ buf.head[0].iov_len = sizeof(head);
+ buf.tail[0].iov_base = tail;
+ buf.tail[0].iov_len = tail_words * sizeof(*tail);
+ buf.len = buf.head[0].iov_len + buf.tail[0].iov_len;
+ buf.buflen = buf.len;
+ xdr_init_decode(&xdr, &buf, head, NULL);
+ return decode_getdeviceinfo(&xdr, &res);
+}
+
+/* Control: one-word bitmap (len 1, word 0) decodes cleanly; PASS stock+patched. */
+static void getdeviceinfo_notify_control_len1(struct kunit *test)
+{
+ __be32 *obj = kmalloc(PAGE_SIZE, GFP_KERNEL);
+ int ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, obj);
+ /* Place reply mid-buffer; nothing reads past it. */
+ ret = run_decode(test, obj + 32, 1, 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ kfree(obj);
+}
+
+/* Control: len 2, nonzero unsupported word -> -EIO in bounds; PASS stock+patched. */
+static void getdeviceinfo_notify_control_unsupported_len2(struct kunit *test)
+{
+ __be32 *obj = kmalloc(PAGE_SIZE, GFP_KERNEL);
+ int ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, obj);
+ ret = run_decode(test, obj + 32, 2, 0);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ kfree(obj);
+}
+
+/* Trigger: wrapping len 0x40000000 at the page edge -> KASAN OOB on stock, -EIO patched. */
+static void getdeviceinfo_notify_trigger_oob(struct kunit *test)
+{
+ __be32 *obj = kmalloc(PAGE_SIZE, GFP_KERNEL);
+ __be32 *obj_end = (__be32 *)((char *)obj + PAGE_SIZE);
+ int ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, obj);
+ ret = run_decode(test, obj_end, 0x40000000U, 0);
+ /* Reached only on the patched tree. */
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ kfree(obj);
+}
+
+static struct kunit_case getdeviceinfo_notify_cases[] = {
+ KUNIT_CASE(getdeviceinfo_notify_control_len1),
+ KUNIT_CASE(getdeviceinfo_notify_control_unsupported_len2),
+ KUNIT_CASE(getdeviceinfo_notify_trigger_oob),
+ {}
+};
+
+static struct kunit_suite getdeviceinfo_notify_suite = {
+ .name = "nfs4_getdeviceinfo_notify",
+ .test_cases = getdeviceinfo_notify_cases,
+};
+
+kunit_test_suite(getdeviceinfo_notify_suite);
diff --git a/fs/nfs/nfs4xdr.c b/fs/nfs/nfs4xdr.c
index ca84d0c872a6c..42eb82ab0346f 100644
--- a/fs/nfs/nfs4xdr.c
+++ b/fs/nfs/nfs4xdr.c
@@ -7788,3 +7788,7 @@ const struct rpc_version nfs_version4 = {
.procs = nfs4_procedures,
.counts = nfs_version4_counts,
};
+
+#if IS_ENABLED(CONFIG_NFS_GETDEVICEINFO_KUNIT_TEST)
+#include "getdeviceinfo_notify_kunit.c"
+#endif
--
2.53.0
prev parent reply other threads:[~2026-06-14 13:08 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-14 13:08 [PATCH 0/2] NFSv4.1/pnfs: bound GETDEVICEINFO notification bitmap length Michael Bommarito
2026-06-14 13:08 ` [PATCH 1/2] NFSv4.1/pnfs: bound notification bitmap length in decode_getdeviceinfo Michael Bommarito
2026-06-14 13:08 ` Michael Bommarito [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260614130814.2521819-3-michael.bommarito@gmail.com \
--to=michael.bommarito@gmail.com \
--cc=anna@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=trondmy@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox