[PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode

Michael Bommarito <[email protected]>
Newsgroups gmane.linux.nfs,gmane.linux.kernel
Message-ID <[email protected]>
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 <[email protected]>
---

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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.