[PATCH] SUNRPC: fix out-of-bounds write in xdr_inline_pages()
From: Paula Moutafian
Date: Thu Aug 20 2026 - 10:32:22 EST
xdr_inline_pages() re-carves an existing receive buffer into a head, a
page list and a tail at a caller-supplied offset, computing the tail
length as buflen - offset. It does not check that the offset lies
within the buffer being carved, and iov_len is unsigned, so an offset
past the end wraps the tail length instead of failing.
The offset reaches it from wire data. rpc_prepare_reply_pages() derives
it from the authentication flavour's au_ralign, which is read on every
encode, while the receive buffer's size is fixed once in call_allocate()
from au_rslack. unx_validate() raises au_verfsize, au_rslack and
au_ralign from the reply verifier's length, and runs before
rpc_decode_header() reads the accept_stat, so a GARBAGE_ARGS reply
poisons the alignment and only then selects call_encode().
call_allocate() short-circuits on the existing buffer, so rq_rcvsize is
not recomputed and the retry carves a new offset against the old size.
auth_unix keeps these fields in a file-scope static rpc_auth shared by
every AUTH_SYS client.
For an NFSv3 READ the design slack is exactly one XDR word, so the
underflow needs only au_ralign at encode to exceed au_rslack at
allocation by two -- a five-byte verifier body. Observed with eight:
rq_rcvsize is 132 with a 128-byte head and a 4-byte tail, the verifier
raises au_ralign from 2 to 4, and the retry carves at (26 + 4 + 4) << 2
= 136 against an unchanged 132-byte buffer. The in-tree
sunrpc:rpc_xdr_reply_pages tracepoint shows the transition on one task:
rpc_xdr_reply_pages: head=[..a18,128] tail=[..a98,4]
rpc_xdr_reply_pages: head=[..a18,136] tail=[..aa0,4294967292]
(one task, before and after the re-encode; addresses abbreviated)
xs_read_xdr_buf() then fills the tail against that length, and the TCP
record length is not capped against rq_rcvsize, so the reply to the
retried call writes peer-controlled bytes past the object:
BUG: KASAN: slab-out-of-bounds in _copy_to_iter+0x7c8/0x1538
Write of size 1664 at addr ffff0000c7640aa0 by task kworker/u8:0/12
Workqueue: xprtiod xs_stream_data_receive_workfn
_copy_to_iter+0x7c8/0x1538
__skb_datagram_iter+0x33c/0x560
skb_copy_datagram_iter+0x3c/0x454
tcp_recvmsg_locked+0x110c/0x2308
xs_sock_recvmsg.constprop.0+0x34/0xe4
xs_read_stream_request.constprop.0+0x410/0x1140
xs_read_stream.constprop.0+0x680/0xe9c
xs_stream_data_receive_workfn+0xcc/0x420
Allocated by task 12:
rpc_malloc+0x174/0x2d0
call_allocate+0x25c/0x944
The buggy address belongs to the object at ffff0000c7640880
which belongs to the cache rpc_buffers of size 2048
The buggy address is located 544 bytes inside of
allocated 2048-byte region [ffff0000c7640880, ffff0000c7641080)
That is 160 bytes past the object, with both the length and every byte
chosen by the peer. Under KASAN the interposed __asan_memcpy() returns
without calling __memcpy once the range check fails, so the report
establishes that the write is reachable and out of bounds; it does not
establish that the copy executed. The enlarged head stays inside the
same slab object and is not itself a violation.
Reproduced twice on an unmodified v7.2-rc7 arm64 KASAN kernel with
CONFIG_NFS_V3=y and no other configuration change; the geometry above
came from tracepoints that already ship in the kernel, not from added
instrumentation. The trigger is an ordinary read(2) on an already
mounted NFSv3 AUTH_SYS share over TCP and needs no client privilege;
every byte the attacker supplies is a legal server reply. Under
RPCSEC_GSS the verifier is authenticated and cannot be forged. UDP is
bounded by the datagram length in xs_udp_data_read_skb() and is not
affected. NFSv4 does not take the retry path, since
RPC_TASK_NO_RETRANS_TIMEOUT suppresses the re-encode.
Commit 53bc19f17f21 ("SUNRPC: receive buffer size estimation values
almost never change") placed the equivalent slack update behind
RPCAUTH_AUTH_UPDATE_SLACK in gss_update_rslack(), but auth_unix was
never converted and still assigns unconditionally. Gating it the same
way narrows the window without closing it, because the first update can
still land between the allocation and the re-encode of an in-flight
request, and it would leave the other caller of xdr_inline_pages()
unguarded.
Clamp the offset to the length of the buffer being carved so the tail
length cannot underflow.
Assisted-by: Bynario AI
Signed-off-by: Paula Moutafian <paula@xxxxxxxx>
---
Note: the defect is reproduced 2/2 on an unmodified v7.2-rc7 arm64 KASAN
kernel; the patched kernel has not been built or booted. A reproducer
exists and can be shared privately on request.
No Fixes: tag: the unbounded carve is original to xdr_inline_pages()
(unchanged since the initial git import) and unx_validate() has written
the shared rpc_auth's slack from the reply verifier since at least
v2.6.32, so there is no commit that introduced this.
net/sunrpc/xdr.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/net/sunrpc/xdr.c b/net/sunrpc/xdr.c
index fa6a30b..f77dbdb 100644
--- a/net/sunrpc/xdr.c
+++ b/net/sunrpc/xdr.c
@@ -409,6 +409,9 @@ xdr_inline_pages(struct xdr_buf *xdr, unsigned int offset,
char *buf = (char *)head->iov_base;
unsigned int buflen = head->iov_len;
+ if (offset > buflen)
+ offset = buflen;
+
head->iov_len = offset;
xdr->pages = pages;
base-commit: a4ff2be345d0abc943da8dd8da98151843b750dc
--
2.50.1 (Apple Git-155)