mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] NFSv4.1/pnfs: bound GETDEVICEINFO notification bitmap length
@ 2026-06-14 13:08 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 ` [PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode Michael Bommarito
  0 siblings, 2 replies; 3+ messages in thread
From: Michael Bommarito @ 2026-06-14 13:08 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, linux-kernel

decode_getdeviceinfo() scales a server-supplied notification bitmap
length by four for xdr_inline_decode(). The 32-bit multiply wraps to 0
for len >= 0x40000000, so the bounds check passes on an empty request
and the verify loop reads up to ~2^30 words past the inline XDR buffer.
A malicious or compromised pNFS server, or a man in the middle on an
unprotected mount, can drive this out-of-bounds read while the client
decodes a GETDEVICEINFO reply. It is a read, not a leak: the loop only
checks each word for nonzero and returns -EIO on the first, so over-read
contents are never returned.

A conformant reply uses a single notification word. Patch 1 bounds the
length before the scale so it cannot wrap. Patch 2 adds KUnit coverage
and is offered separately so it can be taken or dropped on its own.

Tested on QEMU x86_64 with KASAN: a Level-2 KUnit case drives the real
decode_getdeviceinfo() and reports a slab-out-of-bounds read on stock,
returning -EIO after patch 1; two benign controls drive the same decoder
in bounds and pass on both stock and fixed trees.

Michael Bommarito (2):
  NFSv4.1/pnfs: bound notification bitmap length in decode_getdeviceinfo
  NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode

 fs/nfs/Kconfig                      |  14 ++++
 fs/nfs/getdeviceinfo_notify_kunit.c | 110 ++++++++++++++++++++++++++++
 fs/nfs/nfs4xdr.c                    |  10 ++-
 include/linux/nfs4.h                |   3 +
 4 files changed, 136 insertions(+), 1 deletion(-)
 create mode 100644 fs/nfs/getdeviceinfo_notify_kunit.c


base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8
-- 
2.53.0


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

* [PATCH 1/2] NFSv4.1/pnfs: bound notification bitmap length in decode_getdeviceinfo
  2026-06-14 13:08 [PATCH 0/2] NFSv4.1/pnfs: bound GETDEVICEINFO notification bitmap length Michael Bommarito
@ 2026-06-14 13:08 ` Michael Bommarito
  2026-06-14 13:08 ` [PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode Michael Bommarito
  1 sibling, 0 replies; 3+ messages in thread
From: Michael Bommarito @ 2026-06-14 13:08 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, linux-kernel

decode_getdeviceinfo() reads a server-supplied notification bitmap
length into a u32 and scales it by four for xdr_inline_decode(). The
multiply is 32-bit and wraps to 0 for len >= 0x40000000, so the !p
bounds check passes on an empty request and the verify loop reads up to
~2^30 words past the inline XDR buffer. A malicious or compromised pNFS
server, or a man in the middle on an unprotected mount, can drive this
out-of-bounds read while the client decodes a GETDEVICEINFO reply.

RFC 8881 defines only the CHANGE and DELETE deviceid notification bits,
both in word 0, so a conformant reply uses a single word. Bound len to
NFS4_GETDEVICEINFO_NOTIFY_MAXWORDS before scaling, which cannot wrap and
still tolerates trailing zero words the decoder keeps verifying.

Reproduced under KASAN on QEMU via a KUnit case driving the real
decode_getdeviceinfo(); the out-of-bounds read is gone after this change.

Fixes: b1f69b754ee3 ("NFSv4.1: pnfs: add LAYOUTGET and GETDEVICEINFO infrastructure")
Cc: stable@vger.kernel.org
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---

Reproduction (v7.1-rc4, x86_64 QEMU/KVM, KASAN):

  BUG: KASAN: slab-out-of-bounds in decode_getdeviceinfo

A KUnit case (patch 2) drives the real decode_getdeviceinfo() with a
0x40000000 notification length placed at a page edge; the wrapped 4*len
multiply over-reads the receive buffer on stock and faults. Patched, the
length is rejected with -EIO. Two benign in-bounds controls pass on both
stock and patched. fs/nfs has no existing kselftest for this decoder.

 fs/nfs/nfs4xdr.c     | 6 +++++-
 include/linux/nfs4.h | 3 +++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/fs/nfs/nfs4xdr.c b/fs/nfs/nfs4xdr.c
index c23c2eee1b5c4..ca84d0c872a6c 100644
--- a/fs/nfs/nfs4xdr.c
+++ b/fs/nfs/nfs4xdr.c
@@ -6089,7 +6089,11 @@ static int decode_getdeviceinfo(struct xdr_stream *xdr,
 	if (len) {
 		uint32_t i;
 
-		p = xdr_inline_decode(xdr, 4 * len);
+		/* Bound len so len << 2 cannot wrap and defeat the check below. */
+		if (len > NFS4_GETDEVICEINFO_NOTIFY_MAXWORDS)
+			return -EIO;
+
+		p = xdr_inline_decode(xdr, len << 2);
 		if (unlikely(!p))
 			return -EIO;
 
diff --git a/include/linux/nfs4.h b/include/linux/nfs4.h
index d87be1f25273a..d6a84c4545864 100644
--- a/include/linux/nfs4.h
+++ b/include/linux/nfs4.h
@@ -752,6 +752,9 @@ enum pnfs_notify_deviceid_type4 {
 	NOTIFY_DEVICEID4_DELETE = 1 << 2,
 };
 
+/* All defined deviceid notification bits live in word 0; bound the decoder. */
+#define NFS4_GETDEVICEINFO_NOTIFY_MAXWORDS	4
+
 enum pnfs_block_volume_type {
 	PNFS_BLOCK_VOLUME_SIMPLE	= 0,
 	PNFS_BLOCK_VOLUME_SLICE		= 1,
-- 
2.53.0


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

* [PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode
  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
  1 sibling, 0 replies; 3+ messages in thread
From: Michael Bommarito @ 2026-06-14 13:08 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, linux-kernel

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


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

end of thread, other threads:[~2026-06-14 13:08 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH 2/2] NFSv4.1/pnfs: add KUnit coverage for GETDEVICEINFO notification decode Michael Bommarito

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox