Re: [PATCH] SUNRPC: fix out-of-bounds write in xdr_inline_pages()

"Anna Schumaker" <[email protected]>
Newsgroups org.kernel.vger.linux-nfs,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Hi Paula,

On Thu, Aug 20, 2026, at 10:30 AM, Paula Moutafian wrote:
> 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 <[email protected]>
> ---
> 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.

Question: Am I understanding you correctly that you have a reproducer, but
haven't tested or even compiled the code to verify that it works?

Thanks,
Anna

>
> 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)
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.