Linux NFS development
 help / color / mirror / Atom feed
* [PATCH v2 0/7] NFS: harden pNFS XDR decoding
@ 2026-08-31 11:48 Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 1/7] NFS: free netid when discarding pNFS DS addresses Prabhakar Pujeri
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri

pNFS device and layout replies contain several counts and strings supplied
by the server.  The current decoders trust some of those values before the
reply has proved that the corresponding XDR objects are present.  A broken
or malicious server can therefore provoke oversized allocations, very long
decode loops, malformed data-server ports, or leaked netid strings on
temporary-address cleanup paths.

This series makes those paths proportional to the received XDR length.  It
also validates both decimal port octets and uses the common data-server
address destructor wherever a temporary address is discarded.

The last patch adds focused, page-backed KUnit coverage for the address,
GETDEVICEINFO, and LAYOUTGET decoders.  It covers 16 universal-address cases
and six flexfiles cases, including cache reuse and malformed multipath,
version, and filehandle counts.

This version is based on v7.3-rc1.

Validation:

  - nfs_pnfs_decode KUnit suite: 16/16 passed
  - nfs_flexfile_deviceid KUnit suite: 6/6 passed
  - GCC 16 W=1 build of fs/nfs/: passed with no NFS diagnostics
  - Clang 22 W=1 build of fs/nfs/: passed with no NFS diagnostics
  - GCC 16 W=1 build of fs/nfs/ with KUnit disabled: passed
  - Sparse C=2 checks of every touched C source: passed with no diagnostics
  - GCC 16 W=1 full kernel and modules build: passed
  - git diff --check: clean
  - per-patch checkpatch: no errors or code warnings; patch 7 reports only
    the generic FILE_PATH_CHANGES warning for the new test files

Changes in v2:

  - rebase from the NFS client linux-next integration commit 10f307e525a1
    to v7.3-rc1
  - free da_netid on file-layout and flexfiles temporary-address paths
  - bound flexfiles GETDEVICEINFO version_count before allocation
  - bound flexfiles LAYOUTGET fh_count before allocation
  - expand flexfiles KUnit coverage from three to six cases, including the
    cache-hit and malformed-version cleanup paths
  - rename the test patch to reflect the broader XDR coverage

v1: https://lore.kernel.org/r/20260823114244.3883-1-prabhakar.pujeri@dell.com

Prabhakar Pujeri (7):
  NFS: free netid when discarding pNFS DS addresses
  NFS: bound flexfiles GETDEVICEINFO version count
  NFS: bound flexfiles filehandle version count
  NFS: validate pNFS data server port octets
  NFS: bound multipath address count in file-layout GETDEVICEINFO
  NFS: bound multipath address count in flexfiles GETDEVICEINFO
  NFS: add KUnit coverage for pNFS XDR decoding

 fs/nfs/Kconfig                               |  34 +++
 fs/nfs/Makefile                              |   3 +
 fs/nfs/filelayout/filelayoutdev.c            |  11 +-
 fs/nfs/flexfilelayout/Makefile               |   3 +
 fs/nfs/flexfilelayout/flexfilelayout.c       |   7 +-
 fs/nfs/flexfilelayout/flexfilelayout.h       |   6 +
 fs/nfs/flexfilelayout/flexfilelayoutdev.c    |  22 +-
 fs/nfs/flexfilelayout/tests/deviceid_kunit.c | 286 +++++++++++++++++++
 fs/nfs/pnfs.h                                |   9 +
 fs/nfs/pnfs_nfs.c                            |  50 ++--
 fs/nfs/tests/pnfs_decode_kunit.c             | 222 ++++++++++++++
 11 files changed, 619 insertions(+), 34 deletions(-)
 create mode 100644 fs/nfs/flexfilelayout/tests/deviceid_kunit.c
 create mode 100644 fs/nfs/tests/pnfs_decode_kunit.c


base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
-- 
2.54.0

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

* [PATCH v2 1/7] NFS: free netid when discarding pNFS DS addresses
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 2/7] NFS: bound flexfiles GETDEVICEINFO version count Prabhakar Pujeri
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

nfs4_decode_mp_ds_addr() transfers its r_netid allocation to
da_netid. The generic data server destructor releases that field, but the
file-layout and flexfiles drivers open-code the disposal of temporary
address objects and omit it. Cache hits and later decode failures therefore
leak one netid string per discarded address.

Make nfs4_pnfs_ds_addr_free() available to the layout modules and use it
for every temporary address list.

Fixes: 4be78d26810b ("NFSv4/pNFS: Store the transport type in struct nfs4_pnfs_ds_addr")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/filelayout/filelayoutdev.c         | 6 ++----
 fs/nfs/flexfilelayout/flexfilelayoutdev.c | 6 ++----
 fs/nfs/pnfs.h                             | 1 +
 fs/nfs/pnfs_nfs.c                         | 4 ++--
 4 files changed, 7 insertions(+), 10 deletions(-)

diff --git a/fs/nfs/filelayout/filelayoutdev.c b/fs/nfs/filelayout/filelayoutdev.c
index 88bc79ec3459..878f1f13be16 100644
--- a/fs/nfs/filelayout/filelayoutdev.c
+++ b/fs/nfs/filelayout/filelayoutdev.c
@@ -182,8 +182,7 @@ nfs4_fl_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 					      struct nfs4_pnfs_ds_addr,
 					      da_node);
 			list_del_init(&da->da_node);
-			kfree(da->da_remotestr);
-			kfree(da);
+			nfs4_pnfs_ds_addr_free(da);
 		}
 	}
 
@@ -195,8 +194,7 @@ nfs4_fl_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 		da = list_first_entry(&dsaddrs, struct nfs4_pnfs_ds_addr,
 				      da_node);
 		list_del_init(&da->da_node);
-		kfree(da->da_remotestr);
-		kfree(da);
+		nfs4_pnfs_ds_addr_free(da);
 	}
 out_err_free_deviceid:
 	nfs4_fl_free_deviceid(dsaddr);
diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
index 5c0216bd5fce..95cb54c498a9 100644
--- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c
+++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
@@ -164,8 +164,7 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 				      struct nfs4_pnfs_ds_addr,
 				      da_node);
 		list_del_init(&da->da_node);
-		kfree(da->da_remotestr);
-		kfree(da);
+		nfs4_pnfs_ds_addr_free(da);
 	}
 
 	folio_put(scratch);
@@ -176,8 +175,7 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 		da = list_first_entry(&dsaddrs, struct nfs4_pnfs_ds_addr,
 				      da_node);
 		list_del_init(&da->da_node);
-		kfree(da->da_remotestr);
-		kfree(da);
+		nfs4_pnfs_ds_addr_free(da);
 	}
 
 	kfree(ds_versions);
diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h
index 70d20f779678..6ea390c8cd9a 100644
--- a/fs/nfs/pnfs.h
+++ b/fs/nfs/pnfs.h
@@ -415,6 +415,7 @@ int pnfs_generic_commit_pagelist(struct inode *inode,
 							int how));
 int pnfs_generic_scan_commit_lists(struct nfs_commit_info *cinfo, int max);
 void pnfs_generic_write_commit_done(struct rpc_task *task, void *data);
+void nfs4_pnfs_ds_addr_free(struct nfs4_pnfs_ds_addr *da);
 void nfs4_pnfs_ds_put(struct nfs4_pnfs_ds *ds);
 struct nfs4_pnfs_ds *nfs4_pnfs_ds_add(const struct net *net,
 				      struct list_head *dsaddrs,
diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c
index 93d63f75a355..33d64287e500 100644
--- a/fs/nfs/pnfs_nfs.c
+++ b/fs/nfs/pnfs_nfs.c
@@ -626,12 +626,13 @@ static struct nfs4_pnfs_ds_addr *nfs4_pnfs_ds_addr_alloc(gfp_t gfp_flags)
 	return da;
 }
 
-static void nfs4_pnfs_ds_addr_free(struct nfs4_pnfs_ds_addr *da)
+void nfs4_pnfs_ds_addr_free(struct nfs4_pnfs_ds_addr *da)
 {
 	kfree(da->da_remotestr);
 	kfree(da->da_netid);
 	kfree(da);
 }
+EXPORT_SYMBOL_GPL(nfs4_pnfs_ds_addr_free);
 
 static void destroy_ds(struct nfs4_pnfs_ds *ds)
 {
@@ -1230,4 +1231,3 @@ pnfs_nfs_generic_sync(struct inode *inode, bool datasync)
 	return pnfs_layoutcommit_inode(inode, true);
 }
 EXPORT_SYMBOL_GPL(pnfs_nfs_generic_sync);
-
-- 
2.54.0


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

* [PATCH v2 2/7] NFS: bound flexfiles GETDEVICEINFO version count
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 1/7] NFS: free netid when discarding pNFS DS addresses Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 3/7] NFS: bound flexfiles filehandle " Prabhakar Pujeri
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

The flexfiles GETDEVICEINFO decoder allocates ds_versions from a
server-provided count before proving that the corresponding fixed-size XDR
tuples are present. A large count can therefore provoke an oversized
allocation from a short reply.

Each version tuple contains five XDR words. Bound the count by the bytes
remaining in the stream before allocating the array, and use the same
expression when decoding each tuple.

Fixes: d67ae825a59d ("pnfs/flexfiles: Add the FlexFile Layout Driver")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/flexfilelayout/flexfilelayoutdev.c | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
index 95cb54c498a9..96f14c1f070e 100644
--- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c
+++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
@@ -98,7 +98,8 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 		goto out_err_drain_dsaddrs;
 	version_count = be32_to_cpup(p);
 
-	if (version_count == 0) {
+	if (version_count == 0 ||
+	    version_count > xdr_stream_remaining(&stream) / (5 * XDR_UNIT)) {
 		ret = -EINVAL;
 		goto out_err_drain_dsaddrs;
 	}
@@ -110,9 +111,7 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 		goto out_err_drain_dsaddrs;
 
 	for (i = 0; i < version_count; i++) {
-		/* 20 = version(4) + minor_version(4) + rsize(4) + wsize(4) +
-		 * tightly_coupled(4) */
-		p = xdr_inline_decode(&stream, 20);
+		p = xdr_inline_decode(&stream, 5 * XDR_UNIT);
 		if (unlikely(!p))
 			goto out_err_drain_dsaddrs;
 		ds_versions[i].version = be32_to_cpup(p++);
-- 
2.54.0


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

* [PATCH v2 3/7] NFS: bound flexfiles filehandle version count
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 1/7] NFS: free netid when discarding pNFS DS addresses Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 2/7] NFS: bound flexfiles GETDEVICEINFO version count Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 4/7] NFS: validate pNFS data server port octets Prabhakar Pujeri
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

ff_layout_alloc_lseg() allocates a filehandle-version array from the
server-provided fh_count before checking that the layout body can contain
that many filehandles. A large count in a short reply can therefore trigger
an oversized allocation.

Even an empty XDR opaque value consumes a length word. Bound fh_count by
the number of length words remaining in the stream before allocating the
array.

Fixes: d67ae825a59d ("pnfs/flexfiles: Add the FlexFile Layout Driver")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/flexfilelayout/flexfilelayout.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c
index 7fe8b91fa47c..f94d328b67cd 100644
--- a/fs/nfs/flexfilelayout/flexfilelayout.c
+++ b/fs/nfs/flexfilelayout/flexfilelayout.c
@@ -551,7 +551,8 @@ ff_layout_alloc_lseg(struct pnfs_layout_hdr *lh,
 			if (!p)
 				goto out_err_free;
 			fh_count = be32_to_cpup(p);
-			if (fh_count == 0) {
+			if (fh_count == 0 ||
+			    fh_count > xdr_stream_remaining(&stream) / XDR_UNIT) {
 				rc = -EINVAL;
 				goto out_err_free;
 			}
-- 
2.54.0


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

* [PATCH v2 4/7] NFS: validate pNFS data server port octets
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
                   ` (2 preceding siblings ...)
  2026-08-31 11:48 ` [PATCH v2 3/7] NFS: bound flexfiles filehandle " Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 5/7] NFS: bound multipath address count in file-layout GETDEVICEINFO Prabhakar Pujeri
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

nfs4_decode_mp_ds_addr() parses the two decimal port octets in a
server-provided universal address with an unchecked sscanf(). A missing
or non-numeric field leaves part of the temporary array uninitialized,
while values outside the octet range are silently folded into the
resulting port.

Split the two port fields at their delimiters and parse each with
kstrtou8(). This rejects missing fields, trailing garbage, integer
overflow, and values outside 0 through 255 before constructing the data
server socket address.

Fixes: 6b7f3cf96364 ("nfs41: pull decode_ds_addr from file layout to generic pnfs")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/pnfs_nfs.c | 46 ++++++++++++++++++++++++++++------------------
 1 file changed, 28 insertions(+), 18 deletions(-)

diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c
index 33d64287e500..f09826415251 100644
--- a/fs/nfs/pnfs_nfs.c
+++ b/fs/nfs/pnfs_nfs.c
@@ -8,6 +8,7 @@
  * Tom Haynes <loghyr@primarydata.com>
  */
 
+#include <linux/ctype.h>
 #include <linux/nfs_fs.h>
 #include <linux/nfs_page.h>
 #include <linux/sunrpc/addr.h>
@@ -1064,6 +1065,19 @@ int nfs4_pnfs_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds,
 }
 EXPORT_SYMBOL_GPL(nfs4_pnfs_ds_connect);
 
+static int
+nfs4_parse_port_octet(const char *str, u8 *port)
+{
+	const char *p;
+
+	if (!*str)
+		return -EINVAL;
+	for (p = str; *p; p++)
+		if (!isdigit(*p))
+			return -EINVAL;
+	return kstrtou8(str, 10, port);
+}
+
 /*
  * Currently only supports ipv4, ipv6 and one multi-path address.
  */
@@ -1071,10 +1085,10 @@ struct nfs4_pnfs_ds_addr *
 nfs4_decode_mp_ds_addr(struct net *net, struct xdr_stream *xdr, gfp_t gfp_flags)
 {
 	struct nfs4_pnfs_ds_addr *da = NULL;
-	char *buf, *portstr;
+	char *buf, *portsep;
 	__be16 port;
 	ssize_t nlen, rlen;
-	int tmp[2];
+	u8 porthi, portlo;
 	char *netid;
 	size_t len;
 	char *startsep = "";
@@ -1094,37 +1108,33 @@ nfs4_decode_mp_ds_addr(struct net *net, struct xdr_stream *xdr, gfp_t gfp_flags)
 	if (unlikely(rlen <= 0))
 		goto out_free_netid;
 
-	/* replace port '.' with '-' */
-	portstr = strrchr(buf, '.');
-	if (!portstr) {
-		dprintk("%s: Failed finding expected dot in port\n",
-			__func__);
+	/* Parse the low port octet and remove it from the address string. */
+	portsep = strrchr(buf, '.');
+	if (!portsep || nfs4_parse_port_octet(portsep + 1, &portlo)) {
+		dprintk("%s: Failed parsing low port octet\n", __func__);
 		goto out_free_buf;
 	}
-	*portstr = '-';
+	*portsep = '\0';
 
-	/* find '.' between address and port */
-	portstr = strrchr(buf, '.');
-	if (!portstr) {
-		dprintk("%s: Failed finding expected dot between address and "
-			"port\n", __func__);
+	/* Parse the high port octet and terminate the address string. */
+	portsep = strrchr(buf, '.');
+	if (!portsep || nfs4_parse_port_octet(portsep + 1, &porthi)) {
+		dprintk("%s: Failed parsing high port octet\n", __func__);
 		goto out_free_buf;
 	}
-	*portstr = '\0';
+	*portsep = '\0';
 
 	da = nfs4_pnfs_ds_addr_alloc(gfp_flags);
 	if (unlikely(!da))
 		goto out_free_buf;
 
-	if (!rpc_pton(net, buf, portstr-buf, (struct sockaddr *)&da->da_addr,
+	if (!rpc_pton(net, buf, portsep - buf, (struct sockaddr *)&da->da_addr,
 		      sizeof(da->da_addr))) {
 		dprintk("%s: error parsing address %s\n", __func__, buf);
 		goto out_free_da;
 	}
 
-	portstr++;
-	sscanf(portstr, "%d-%d", &tmp[0], &tmp[1]);
-	port = htons((tmp[0] << 8) | (tmp[1]));
+	port = htons((porthi << 8) | portlo);
 
 	switch (da->da_addr.ss_family) {
 	case AF_INET:
-- 
2.54.0


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

* [PATCH v2 5/7] NFS: bound multipath address count in file-layout GETDEVICEINFO
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
                   ` (3 preceding siblings ...)
  2026-08-31 11:48 ` [PATCH v2 4/7] NFS: validate pNFS data server port octets Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 6/7] NFS: bound multipath address count in flexfiles GETDEVICEINFO Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 7/7] NFS: add KUnit coverage for pNFS XDR decoding Prabhakar Pujeri
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

nfs4_fl_alloc_deviceid_node() bounds the number of data-server multipath
lists, but not the number of addresses in each list. The latter count is
supplied by the server and can be as large as U32_MAX.

Once the XDR stream is exhausted, nfs4_decode_mp_ds_addr() returns NULL
without consuming input. The enclosing loop can therefore spin for
billions of iterations in kernel context when decoding a malicious
GETDEVICEINFO response.

A netaddr4 contains at least the two XDR length words for its netid and
universal-address strings. Reject a count when that minimum representation
cannot fit in the remaining XDR stream. This makes the loop bound
proportional to the reply length without imposing an arbitrary protocol
limit.

Keep the existing file-layout multipath-list limit separate because that
limit is tied to the u8 stripe-index representation.

Fixes: 14f9a6076f53 ("NFS: Parse and store all multipath DS addresses")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/filelayout/filelayoutdev.c | 5 +++++
 fs/nfs/pnfs.h                     | 8 ++++++++
 2 files changed, 13 insertions(+)

diff --git a/fs/nfs/filelayout/filelayoutdev.c b/fs/nfs/filelayout/filelayoutdev.c
index 878f1f13be16..18b2e8909bed 100644
--- a/fs/nfs/filelayout/filelayoutdev.c
+++ b/fs/nfs/filelayout/filelayoutdev.c
@@ -159,6 +159,11 @@ nfs4_fl_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 			goto out_err_free_deviceid;
 
 		mp_count = be32_to_cpup(p); /* multipath count */
+		if (!nfs4_pnfs_ds_addr_count_valid(&stream, mp_count)) {
+			pr_warn_ratelimited("NFS: %s: multipath address count %u exceeds XDR capacity\n",
+					    __func__, mp_count);
+			goto out_err_drain_dsaddrs;
+		}
 		for (j = 0; j < mp_count; j++) {
 			da = nfs4_decode_mp_ds_addr(net, &stream, gfp_flags);
 			if (da)
diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h
index 6ea390c8cd9a..0951c39afdbd 100644
--- a/fs/nfs/pnfs.h
+++ b/fs/nfs/pnfs.h
@@ -33,11 +33,19 @@
 #include <linux/refcount.h>
 #include <linux/nfs_fs.h>
 #include <linux/nfs_page.h>
+#include <linux/sunrpc/xdr.h>
 #include <linux/workqueue.h>
 
 struct nfs4_exception;
 struct nfs4_opendata;
 
+/* A netaddr4 contains at least the length words of its two XDR strings. */
+static inline bool
+nfs4_pnfs_ds_addr_count_valid(const struct xdr_stream *xdr, u32 count)
+{
+	return count <= xdr_stream_remaining(xdr) / (2 * sizeof(__be32));
+}
+
 enum {
 	NFS_LSEG_VALID = 0,	/* cleared when lseg is recalled/returned */
 	NFS_LSEG_ROC,		/* roc bit received from server */
-- 
2.54.0


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

* [PATCH v2 6/7] NFS: bound multipath address count in flexfiles GETDEVICEINFO
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
                   ` (4 preceding siblings ...)
  2026-08-31 11:48 ` [PATCH v2 5/7] NFS: bound multipath address count in file-layout GETDEVICEINFO Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  2026-08-31 11:48 ` [PATCH v2 7/7] NFS: add KUnit coverage for pNFS XDR decoding Prabhakar Pujeri
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri, stable

nfs4_ff_alloc_deviceid_node() loops over a server-provided multipath
address count without validating it. Once the XDR stream is exhausted,
nfs4_decode_mp_ds_addr() returns NULL without consuming input, so a count
near U32_MAX can keep the client spinning in kernel context for billions
of iterations.

A netaddr4 contains at least the two XDR length words for its netid and
universal-address strings. Reject a count when that minimum representation
cannot fit in the remaining XDR stream. This makes the loop bound
proportional to the reply length without imposing an arbitrary protocol
limit, matching the protection in the file-layout decoder.

Fixes: d67ae825a59d ("pnfs/flexfiles: Add the FlexFile Layout Driver")
Cc: stable@vger.kernel.org
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/flexfilelayout/flexfilelayoutdev.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
index 96f14c1f070e..6c104b1e5ff4 100644
--- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c
+++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
@@ -78,6 +78,11 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 		goto out_err_drain_dsaddrs;
 	mp_count = be32_to_cpup(p);
 	dprintk("%s: multipath ds count %d\n", __func__, mp_count);
+	if (!nfs4_pnfs_ds_addr_count_valid(&stream, mp_count)) {
+		pr_warn_ratelimited("NFS: %s: multipath address count %u exceeds XDR capacity\n",
+				    __func__, mp_count);
+		goto out_err_drain_dsaddrs;
+	}
 
 	for (i = 0; i < mp_count; i++) {
 		/* multipath ds */
-- 
2.54.0


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

* [PATCH v2 7/7] NFS: add KUnit coverage for pNFS XDR decoding
  2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
                   ` (5 preceding siblings ...)
  2026-08-31 11:48 ` [PATCH v2 6/7] NFS: bound multipath address count in flexfiles GETDEVICEINFO Prabhakar Pujeri
@ 2026-08-31 11:48 ` Prabhakar Pujeri
  6 siblings, 0 replies; 8+ messages in thread
From: Prabhakar Pujeri @ 2026-08-31 11:48 UTC (permalink / raw)
  To: Trond Myklebust, Anna Schumaker; +Cc: linux-nfs, Prabhakar Pujeri

Add page-backed tests for nfs4_decode_mp_ds_addr(). Cover valid IPv4
and IPv6 universal addresses, boundary port values, missing fields,
non-numeric fields, out-of-range values, integer overflow, and trailing
garbage.

Exercise the flexfiles GETDEVICEINFO decoder with valid device data,
cache reuse, and zero or oversized multipath and version counts. Exercise
the LAYOUTGET decoder with an oversized filehandle-version count. The
cache-reuse and malformed-version cases also drive both temporary-address
cleanup paths.

Export the flexfiles decoder helpers only when KUnit is enabled and keep
the suites behind guideline-compliant Kconfig entries that default with
KUNIT_ALL_TESTS.

Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@dell.com>
---
 fs/nfs/Kconfig                               |  34 +++
 fs/nfs/Makefile                              |   3 +
 fs/nfs/flexfilelayout/Makefile               |   3 +
 fs/nfs/flexfilelayout/flexfilelayout.c       |   4 +-
 fs/nfs/flexfilelayout/flexfilelayout.h       |   6 +
 fs/nfs/flexfilelayout/flexfilelayoutdev.c    |   4 +
 fs/nfs/flexfilelayout/tests/deviceid_kunit.c | 286 +++++++++++++++++++
 fs/nfs/tests/pnfs_decode_kunit.c             | 222 ++++++++++++++
 8 files changed, 561 insertions(+), 1 deletion(-)
 create mode 100644 fs/nfs/flexfilelayout/tests/deviceid_kunit.c
 create mode 100644 fs/nfs/tests/pnfs_decode_kunit.c

diff --git a/fs/nfs/Kconfig b/fs/nfs/Kconfig
index 6bb30543eff0..13b5a80d8f8e 100644
--- a/fs/nfs/Kconfig
+++ b/fs/nfs/Kconfig
@@ -215,3 +215,37 @@ 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_PNFS_DECODE_KUNIT_TEST
+	tristate "KUnit tests for pNFS data server address decoding" if !KUNIT_ALL_TESTS
+	depends on NFS_V4 && KUNIT
+	default KUNIT_ALL_TESTS
+	help
+	  This builds KUnit tests for the pNFS data server address decoder.
+	  The tests cover valid IPv4 and IPv6 universal addresses and malformed
+	  port fields supplied by an NFS server. They verify that invalid port
+	  octets cannot produce a usable data server socket address.
+	  Results are reported in TAP format when the test module is loaded on a
+	  KUnit-enabled kernel.
+
+	  For more information on KUnit and unit tests in general, refer to
+	  Documentation/dev-tools/kunit/.
+
+	  If unsure, say N.
+
+config NFS_FLEXFILE_DEVICEID_KUNIT_TEST
+	tristate "KUnit tests for flexfiles XDR decoding" if !KUNIT_ALL_TESTS
+	depends on PNFS_FLEXFILE_LAYOUT && KUNIT
+	default KUNIT_ALL_TESTS
+	help
+	  This builds KUnit tests for the flexfiles GETDEVICEINFO and LAYOUTGET
+	  decoders. The tests cover valid device information, data-server cache
+	  reuse, and malformed address, version, and filehandle counts supplied
+	  by an NFS server.
+	  Results are reported in TAP format when the test module is loaded on a
+	  KUnit-enabled kernel.
+
+	  For more information on KUnit and unit tests in general, refer to
+	  Documentation/dev-tools/kunit/.
+
+	  If unsure, say N.
diff --git a/fs/nfs/Makefile b/fs/nfs/Makefile
index c895521f27f3..e68648f236b0 100644
--- a/fs/nfs/Makefile
+++ b/fs/nfs/Makefile
@@ -33,6 +33,9 @@ nfsv4-$(CONFIG_SYSCTL)	+= nfs4sysctl.o
 nfsv4-$(CONFIG_NFS_V4_0)	+= nfs40client.o nfs40proc.o
 nfsv4-$(CONFIG_NFS_V4_2)	+= nfs42proc.o nfs42xattr.o
 
+obj-$(CONFIG_NFS_PNFS_DECODE_KUNIT_TEST) += nfs_pnfs_decode_kunit.o
+nfs_pnfs_decode_kunit-y := tests/pnfs_decode_kunit.o
+
 obj-$(CONFIG_PNFS_FILE_LAYOUT) += filelayout/
 obj-$(CONFIG_PNFS_BLOCK) += blocklayout/
 obj-$(CONFIG_PNFS_FLEXFILE_LAYOUT) += flexfilelayout/
diff --git a/fs/nfs/flexfilelayout/Makefile b/fs/nfs/flexfilelayout/Makefile
index 49f03422b6ad..8dcc8fad4106 100644
--- a/fs/nfs/flexfilelayout/Makefile
+++ b/fs/nfs/flexfilelayout/Makefile
@@ -4,3 +4,6 @@
 #
 obj-$(CONFIG_PNFS_FLEXFILE_LAYOUT) += nfs_layout_flexfiles.o
 nfs_layout_flexfiles-y := flexfilelayout.o flexfilelayoutdev.o
+
+obj-$(CONFIG_NFS_FLEXFILE_DEVICEID_KUNIT_TEST) += nfs_flexfile_deviceid_kunit.o
+nfs_flexfile_deviceid_kunit-y := tests/deviceid_kunit.o
diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c
index f94d328b67cd..816eb7f77853 100644
--- a/fs/nfs/flexfilelayout/flexfilelayout.c
+++ b/fs/nfs/flexfilelayout/flexfilelayout.c
@@ -13,6 +13,7 @@
 #include <linux/module.h>
 #include <linux/file.h>
 #include <linux/sched/mm.h>
+#include <kunit/visibility.h>
 
 #include <linux/sunrpc/metrics.h>
 
@@ -442,7 +443,7 @@ static void ff_layout_sort_mirrors(struct nfs4_ff_layout_segment *fls)
 	}
 }
 
-static struct pnfs_layout_segment *
+VISIBLE_IF_KUNIT struct pnfs_layout_segment *
 ff_layout_alloc_lseg(struct pnfs_layout_hdr *lh,
 		     struct nfs4_layoutget_res *lgr,
 		     gfp_t gfp_flags)
@@ -660,6 +661,7 @@ ff_layout_alloc_lseg(struct pnfs_layout_hdr *lh,
 	dprintk("<-- %s (%d)\n", __func__, rc);
 	goto out_free_page;
 }
+EXPORT_SYMBOL_IF_KUNIT(ff_layout_alloc_lseg);
 
 static void
 ff_layout_free_lseg(struct pnfs_layout_segment *lseg)
diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h
index a5bd00f69e82..34cba1ec160b 100644
--- a/fs/nfs/flexfilelayout/flexfilelayout.h
+++ b/fs/nfs/flexfilelayout/flexfilelayout.h
@@ -228,6 +228,12 @@ nfs4_ff_layout_calc_dss_id(const u64 stripe_unit, const u32 dss_count, const lof
 struct nfs4_ff_layout_ds *
 nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 			    gfp_t gfp_flags);
+#if IS_ENABLED(CONFIG_KUNIT)
+struct pnfs_layout_segment *
+ff_layout_alloc_lseg(struct pnfs_layout_hdr *lh,
+		     struct nfs4_layoutget_res *lgr,
+		     gfp_t gfp_flags);
+#endif
 void nfs4_ff_layout_put_deviceid(struct nfs4_ff_layout_ds *mirror_ds);
 void nfs4_ff_layout_free_deviceid(struct nfs4_ff_layout_ds *mirror_ds);
 int ff_layout_track_ds_error(struct nfs4_flexfile_layout *flo,
diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
index 6c104b1e5ff4..81cf8cfa84fd 100644
--- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c
+++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c
@@ -12,6 +12,8 @@
 #include <linux/module.h>
 #include <linux/sunrpc/addr.h>
 
+#include <kunit/visibility.h>
+
 #include "../internal.h"
 #include "../nfs4session.h"
 #include "flexfilelayout.h"
@@ -36,6 +38,7 @@ void nfs4_ff_layout_free_deviceid(struct nfs4_ff_layout_ds *mirror_ds)
 	kfree(mirror_ds->ds_versions);
 	kfree_rcu(mirror_ds, id_node.rcu);
 }
+EXPORT_SYMBOL_IF_KUNIT(nfs4_ff_layout_free_deviceid);
 
 /* Decode opaque device data and construct new_ds using it */
 struct nfs4_ff_layout_ds *
@@ -191,6 +194,7 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev,
 	dprintk("%s ERROR: returning %d\n", __func__, ret);
 	return NULL;
 }
+EXPORT_SYMBOL_IF_KUNIT(nfs4_ff_alloc_deviceid_node);
 
 static void extend_ds_error(struct nfs4_ff_layout_ds_err *err,
 			    u64 offset, u64 length)
diff --git a/fs/nfs/flexfilelayout/tests/deviceid_kunit.c b/fs/nfs/flexfilelayout/tests/deviceid_kunit.c
new file mode 100644
index 000000000000..2b4e08600bf9
--- /dev/null
+++ b/fs/nfs/flexfilelayout/tests/deviceid_kunit.c
@@ -0,0 +1,286 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the NFS flexfiles XDR decoders.
+ */
+
+#include <kunit/test.h>
+#include <linux/mm.h>
+#include <net/net_namespace.h>
+
+#include "../flexfilelayout.h"
+
+static void flexfile_deviceid_put_u32(char **p, u32 value)
+{
+	*(__be32 *)*p = cpu_to_be32(value);
+	*p += XDR_UNIT;
+}
+
+static void flexfile_deviceid_put_string(char **p, const char *string)
+{
+	size_t len = strlen(string);
+
+	flexfile_deviceid_put_u32(p, len);
+	memcpy(*p, string, len);
+	*p += xdr_align_size(len);
+}
+
+static void flexfile_deviceid_put_address(char **p)
+{
+	flexfile_deviceid_put_u32(p, 1);
+	flexfile_deviceid_put_string(p, "tcp");
+	flexfile_deviceid_put_string(p, "10.0.0.9.8.1");
+}
+
+static void flexfile_deviceid_put_version(char **p)
+{
+	flexfile_deviceid_put_u32(p, 1);
+	flexfile_deviceid_put_u32(p, 4);
+	flexfile_deviceid_put_u32(p, 2);
+	flexfile_deviceid_put_u32(p, 1048576);
+	flexfile_deviceid_put_u32(p, 1048576);
+	flexfile_deviceid_put_u32(p, 0);
+}
+
+static struct nfs_server *flexfile_deviceid_server(struct kunit *test)
+{
+	struct nfs_server *server;
+	struct nfs_client *client;
+
+	server = kunit_kzalloc(test, sizeof(*server), GFP_KERNEL);
+	if (!server)
+		return NULL;
+	client = kunit_kzalloc(test, sizeof(*client), GFP_KERNEL);
+	if (!client)
+		return NULL;
+
+	client->cl_net = &init_net;
+	client->cl_proto = IPPROTO_TCP;
+	server->nfs_client = client;
+	return server;
+}
+
+static struct pnfs_device *
+flexfile_deviceid_pdev(struct kunit *test, struct page *page, size_t len)
+{
+	struct pnfs_device *pdev;
+
+	pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL);
+	if (!pdev)
+		return NULL;
+	pdev->pages = kunit_kzalloc(test, sizeof(*pdev->pages), GFP_KERNEL);
+	if (!pdev->pages)
+		return NULL;
+	pdev->pages[0] = page;
+	pdev->pglen = len;
+	return pdev;
+}
+
+static void flexfile_deviceid_free_page(void *data)
+{
+	__free_page(data);
+}
+
+static struct page *flexfile_deviceid_page(struct kunit *test)
+{
+	struct page *page;
+
+	page = alloc_page(GFP_KERNEL | __GFP_ZERO);
+	if (!page)
+		return NULL;
+	if (kunit_add_action_or_reset(test, flexfile_deviceid_free_page,
+				      page))
+		return NULL;
+	return page;
+}
+
+static void flexfile_deviceid_free(void *data)
+{
+	nfs4_ff_layout_free_deviceid(data);
+}
+
+static int
+flexfile_deviceid_track(struct kunit *test, struct nfs4_ff_layout_ds *device)
+{
+	if (!device)
+		return 0;
+	return kunit_add_action_or_reset(test, flexfile_deviceid_free, device);
+}
+
+static void flexfile_deviceid_reject_huge_multipath_count(struct kunit *test)
+{
+	struct nfs4_ff_layout_ds *device;
+	struct nfs_server *server;
+	struct pnfs_device *pdev;
+	struct page *page;
+	char *start, *p;
+
+	server = flexfile_deviceid_server(test);
+	KUNIT_ASSERT_NOT_NULL(test, server);
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_u32(&p, U32_MAX);
+	pdev = flexfile_deviceid_pdev(test, page, p - start);
+	KUNIT_ASSERT_NOT_NULL(test, pdev);
+
+	device = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, device), 0);
+	KUNIT_EXPECT_NULL(test, device);
+}
+
+static void flexfile_deviceid_reject_zero_multipath_count(struct kunit *test)
+{
+	struct nfs4_ff_layout_ds *device;
+	struct nfs_server *server;
+	struct pnfs_device *pdev;
+	struct page *page;
+	char *start, *p;
+
+	server = flexfile_deviceid_server(test);
+	KUNIT_ASSERT_NOT_NULL(test, server);
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_u32(&p, 0);
+	pdev = flexfile_deviceid_pdev(test, page, p - start);
+	KUNIT_ASSERT_NOT_NULL(test, pdev);
+
+	device = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, device), 0);
+	KUNIT_EXPECT_NULL(test, device);
+}
+
+static void flexfile_deviceid_decode_single_address(struct kunit *test)
+{
+	struct nfs4_ff_layout_ds *device;
+	struct nfs_server *server;
+	struct pnfs_device *pdev;
+	struct page *page;
+	char *start, *p;
+
+	server = flexfile_deviceid_server(test);
+	KUNIT_ASSERT_NOT_NULL(test, server);
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_address(&p);
+	flexfile_deviceid_put_version(&p);
+	pdev = flexfile_deviceid_pdev(test, page, p - start);
+	KUNIT_ASSERT_NOT_NULL(test, pdev);
+
+	device = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, device);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, device), 0);
+	KUNIT_EXPECT_EQ(test, device->ds_versions_cnt, (u32)1);
+	KUNIT_EXPECT_EQ(test, device->ds_versions[0].version, (u32)4);
+	KUNIT_EXPECT_EQ(test, device->ds_versions[0].minor_version, (u32)2);
+}
+
+static void flexfile_deviceid_reject_huge_version_count(struct kunit *test)
+{
+	struct nfs4_ff_layout_ds *device;
+	struct nfs_server *server;
+	struct pnfs_device *pdev;
+	struct page *page;
+	char *start, *p;
+
+	server = flexfile_deviceid_server(test);
+	KUNIT_ASSERT_NOT_NULL(test, server);
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_address(&p);
+	flexfile_deviceid_put_u32(&p, U32_MAX);
+	pdev = flexfile_deviceid_pdev(test, page, p - start);
+	KUNIT_ASSERT_NOT_NULL(test, pdev);
+
+	device = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, device), 0);
+	KUNIT_EXPECT_NULL(test, device);
+}
+
+static void flexfile_deviceid_decode_cached_address(struct kunit *test)
+{
+	struct nfs4_ff_layout_ds *first, *second;
+	struct nfs_server *server;
+	struct pnfs_device *pdev;
+	struct page *page;
+	char *start, *p;
+
+	server = flexfile_deviceid_server(test);
+	KUNIT_ASSERT_NOT_NULL(test, server);
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_address(&p);
+	flexfile_deviceid_put_version(&p);
+	pdev = flexfile_deviceid_pdev(test, page, p - start);
+	KUNIT_ASSERT_NOT_NULL(test, pdev);
+
+	first = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, first);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, first), 0);
+	second = nfs4_ff_alloc_deviceid_node(server, pdev, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, second);
+	KUNIT_ASSERT_EQ(test, flexfile_deviceid_track(test, second), 0);
+	KUNIT_EXPECT_PTR_EQ(test, first->ds, second->ds);
+}
+
+static void flexfile_layout_reject_huge_fh_count(struct kunit *test)
+{
+	struct nfs4_layoutdriver_data layout = {};
+	struct nfs4_layoutget_res lgr = {};
+	struct pnfs_layout_hdr lh = {};
+	struct pnfs_layout_segment *lseg;
+	struct page *pages[1];
+	struct page *page;
+	char *start, *p;
+
+	page = flexfile_deviceid_page(test);
+	KUNIT_ASSERT_NOT_NULL(test, page);
+	pages[0] = page;
+	start = page_address(page);
+	p = start;
+	flexfile_deviceid_put_u32(&p, 0);
+	flexfile_deviceid_put_u32(&p, 0);
+	flexfile_deviceid_put_u32(&p, 1);
+	flexfile_deviceid_put_u32(&p, 1);
+	p += NFS4_DEVICEID4_SIZE;
+	flexfile_deviceid_put_u32(&p, 1);
+	p += NFS4_STATEID_SIZE;
+	flexfile_deviceid_put_u32(&p, U32_MAX);
+
+	layout.pages = pages;
+	layout.len = p - start;
+	lgr.layoutp = &layout;
+	lgr.range.iomode = IOMODE_READ;
+	lseg = ff_layout_alloc_lseg(&lh, &lgr, GFP_KERNEL);
+	KUNIT_ASSERT_TRUE(test, IS_ERR(lseg));
+	KUNIT_EXPECT_EQ(test, PTR_ERR(lseg), -EINVAL);
+}
+
+static struct kunit_case flexfile_deviceid_test_cases[] = {
+	KUNIT_CASE(flexfile_deviceid_reject_huge_multipath_count),
+	KUNIT_CASE(flexfile_deviceid_reject_zero_multipath_count),
+	KUNIT_CASE(flexfile_deviceid_reject_huge_version_count),
+	KUNIT_CASE(flexfile_deviceid_decode_single_address),
+	KUNIT_CASE(flexfile_deviceid_decode_cached_address),
+	KUNIT_CASE(flexfile_layout_reject_huge_fh_count),
+	{}
+};
+
+static struct kunit_suite flexfile_deviceid_test_suite = {
+	.name = "nfs_flexfile_deviceid",
+	.test_cases = flexfile_deviceid_test_cases,
+};
+
+kunit_test_suite(flexfile_deviceid_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for flexfiles XDR decoding");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
diff --git a/fs/nfs/tests/pnfs_decode_kunit.c b/fs/nfs/tests/pnfs_decode_kunit.c
new file mode 100644
index 000000000000..74ffe14a089c
--- /dev/null
+++ b/fs/nfs/tests/pnfs_decode_kunit.c
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for pNFS data server address decoding.
+ */
+
+#include <kunit/test.h>
+#include <linux/mm.h>
+#include <linux/sunrpc/xdr.h>
+#include <net/net_namespace.h>
+
+#include "../pnfs.h"
+
+static struct page *
+pnfs_decode_make_stream(const char *netid, const char *addr,
+			struct xdr_stream *xdr, struct xdr_buf *buf,
+			struct page **pages)
+{
+	size_t netid_len = strlen(netid);
+	size_t addr_len = strlen(addr);
+	struct page *page;
+	char *p;
+	size_t offset = 0;
+
+	if (XDR_UNIT * 2 + xdr_align_size(netid_len) +
+	    xdr_align_size(addr_len) > PAGE_SIZE)
+		return ERR_PTR(-E2BIG);
+
+	page = alloc_page(GFP_KERNEL | __GFP_ZERO);
+	if (!page)
+		return ERR_PTR(-ENOMEM);
+	pages[0] = page;
+	p = page_address(page);
+
+	*(__be32 *)(p + offset) = cpu_to_be32(netid_len);
+	offset += XDR_UNIT;
+	memcpy(p + offset, netid, netid_len);
+	offset += xdr_align_size(netid_len);
+	*(__be32 *)(p + offset) = cpu_to_be32(addr_len);
+	offset += XDR_UNIT;
+	memcpy(p + offset, addr, addr_len);
+	offset += xdr_align_size(addr_len);
+
+	xdr_init_decode_pages(xdr, buf, pages, offset);
+	return page;
+}
+
+static void pnfs_decode_free_addr(struct nfs4_pnfs_ds_addr *addr)
+{
+	kfree(addr->da_remotestr);
+	kfree(addr->da_netid);
+	kfree(addr);
+}
+
+static struct nfs4_pnfs_ds_addr *
+pnfs_decode_addr(const char *netid, const char *addr)
+{
+	struct page *pages[1] = { NULL };
+	struct xdr_buf buf = {};
+	struct xdr_stream xdr;
+	struct nfs4_pnfs_ds_addr *decoded;
+	struct page *page;
+
+	page = pnfs_decode_make_stream(netid, addr, &xdr, &buf, pages);
+	if (IS_ERR(page))
+		return ERR_CAST(page);
+
+	decoded = nfs4_decode_mp_ds_addr(&init_net, &xdr, GFP_KERNEL);
+	xdr_finish_decode(&xdr);
+	__free_page(page);
+	return decoded;
+}
+
+static void pnfs_decode_valid_ipv4(struct kunit *test)
+{
+	struct nfs4_pnfs_ds_addr *addr;
+	struct sockaddr_in *sin;
+
+	addr = pnfs_decode_addr("tcp", "10.0.0.4.8.1");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, addr);
+	sin = (struct sockaddr_in *)&addr->da_addr;
+	KUNIT_EXPECT_EQ(test, sin->sin_family, AF_INET);
+	KUNIT_EXPECT_EQ(test, ntohs(sin->sin_port), 2049);
+	KUNIT_EXPECT_EQ(test, be32_to_cpu(sin->sin_addr.s_addr), 0x0a000004);
+	pnfs_decode_free_addr(addr);
+}
+
+static void pnfs_decode_valid_ipv4_max_port(struct kunit *test)
+{
+	struct nfs4_pnfs_ds_addr *addr;
+	struct sockaddr_in *sin;
+
+	addr = pnfs_decode_addr("tcp", "10.0.0.4.255.255");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, addr);
+	sin = (struct sockaddr_in *)&addr->da_addr;
+	KUNIT_EXPECT_EQ(test, ntohs(sin->sin_port), 65535);
+	pnfs_decode_free_addr(addr);
+}
+
+static void pnfs_decode_valid_ipv4_zero_port(struct kunit *test)
+{
+	struct nfs4_pnfs_ds_addr *addr;
+	struct sockaddr_in *sin;
+
+	addr = pnfs_decode_addr("tcp", "10.0.0.4.0.0");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, addr);
+	sin = (struct sockaddr_in *)&addr->da_addr;
+	KUNIT_EXPECT_EQ(test, ntohs(sin->sin_port), 0);
+	pnfs_decode_free_addr(addr);
+}
+
+static void pnfs_decode_valid_ipv6(struct kunit *test)
+{
+	struct nfs4_pnfs_ds_addr *addr;
+	struct sockaddr_in6 *sin6;
+
+	addr = pnfs_decode_addr("tcp6", "2001:db8::5.8.1");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, addr);
+	sin6 = (struct sockaddr_in6 *)&addr->da_addr;
+	KUNIT_EXPECT_EQ(test, sin6->sin6_family, AF_INET6);
+	KUNIT_EXPECT_EQ(test, ntohs(sin6->sin6_port), 2049);
+	pnfs_decode_free_addr(addr);
+}
+
+static void pnfs_decode_expect_invalid(struct kunit *test, const char *addr)
+{
+	struct nfs4_pnfs_ds_addr *decoded = pnfs_decode_addr("tcp", addr);
+
+	KUNIT_ASSERT_FALSE(test, IS_ERR(decoded));
+	KUNIT_EXPECT_NULL(test, decoded);
+	if (decoded)
+		pnfs_decode_free_addr(decoded);
+}
+
+static void pnfs_decode_reject_high_octet_over_255(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.300.1");
+}
+
+static void pnfs_decode_reject_low_octet_over_255(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.1.300");
+}
+
+static void pnfs_decode_reject_integer_overflow(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.999999999999999999999.1");
+}
+
+static void pnfs_decode_reject_empty_low_octet(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8.");
+}
+
+static void pnfs_decode_reject_empty_high_octet(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4..1");
+}
+
+static void pnfs_decode_reject_negative_octet(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8.-1");
+}
+
+static void pnfs_decode_reject_leading_plus(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.+8.1");
+}
+
+static void pnfs_decode_reject_non_numeric_octet(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8.x");
+}
+
+static void pnfs_decode_reject_trailing_garbage(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8.1x");
+}
+
+static void pnfs_decode_reject_trailing_newline(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8.1\n");
+}
+
+static void pnfs_decode_reject_bad_address(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "999.1.1.1.8.1");
+}
+
+static void pnfs_decode_reject_missing_port_octet(struct kunit *test)
+{
+	pnfs_decode_expect_invalid(test, "10.0.0.4.8");
+}
+
+static struct kunit_case pnfs_decode_test_cases[] = {
+	KUNIT_CASE(pnfs_decode_valid_ipv4),
+	KUNIT_CASE(pnfs_decode_valid_ipv4_max_port),
+	KUNIT_CASE(pnfs_decode_valid_ipv4_zero_port),
+	KUNIT_CASE(pnfs_decode_valid_ipv6),
+	KUNIT_CASE(pnfs_decode_reject_high_octet_over_255),
+	KUNIT_CASE(pnfs_decode_reject_low_octet_over_255),
+	KUNIT_CASE(pnfs_decode_reject_integer_overflow),
+	KUNIT_CASE(pnfs_decode_reject_empty_low_octet),
+	KUNIT_CASE(pnfs_decode_reject_empty_high_octet),
+	KUNIT_CASE(pnfs_decode_reject_negative_octet),
+	KUNIT_CASE(pnfs_decode_reject_leading_plus),
+	KUNIT_CASE(pnfs_decode_reject_non_numeric_octet),
+	KUNIT_CASE(pnfs_decode_reject_trailing_garbage),
+	KUNIT_CASE(pnfs_decode_reject_trailing_newline),
+	KUNIT_CASE(pnfs_decode_reject_bad_address),
+	KUNIT_CASE(pnfs_decode_reject_missing_port_octet),
+	{}
+};
+
+static struct kunit_suite pnfs_decode_test_suite = {
+	.name = "nfs_pnfs_decode",
+	.test_cases = pnfs_decode_test_cases,
+};
+
+kunit_test_suite(pnfs_decode_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for pNFS data server address decoding");
+MODULE_LICENSE("GPL");
-- 
2.54.0


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

end of thread, other threads:[~2026-08-31 11:48 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 11:48 [PATCH v2 0/7] NFS: harden pNFS XDR decoding Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 1/7] NFS: free netid when discarding pNFS DS addresses Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 2/7] NFS: bound flexfiles GETDEVICEINFO version count Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 3/7] NFS: bound flexfiles filehandle " Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 4/7] NFS: validate pNFS data server port octets Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 5/7] NFS: bound multipath address count in file-layout GETDEVICEINFO Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 6/7] NFS: bound multipath address count in flexfiles GETDEVICEINFO Prabhakar Pujeri
2026-08-31 11:48 ` [PATCH v2 7/7] NFS: add KUnit coverage for pNFS XDR decoding Prabhakar Pujeri

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