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

From: Paula Moutafian

Date: Fri Aug 21 2026 - 11:36:34 EST


Hello Anna,

The note was a mistake, I confirm that it compiles fine and the patch works. :)

Kind regards,
Paula


On Fri, Aug 21, 2026 at 4:32 PM Paula Moutafian <paula@xxxxxxxx> wrote:
>
> Hello Anna,
>
> The note was a mistake, I confirm that it compiles fine and the patch works. :)
>
> Kind regards,
> Paula
>
> On Fri, Aug 21, 2026 at 1:56 PM Anna Schumaker <anna@xxxxxxxxxx> wrote:
>>
>> 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 <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.
>>
>> 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)