Re: [PATCH] SUNRPC: fix out-of-bounds write in xdr_inline_pages()
Paula Moutafian <[email protected]>
| Newsgroups | org.kernel.vger.linux-nfs,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <CANegxYHr3=tB+OhemrJdLh2ciM4ZUjCPq9SXbJmh38NdRVtVGg@mail.gmail.com> |
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 <[email protected]> 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 <[email protected]> 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 <[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)