svcrdma: Validate Read chunk positions at decode time

Read chunk position and length validation is currently scattered
across three consumer functions: svc_rdma_read_data_item(),
svc_rdma_read_multiple_chunks(), and svc_rdma_read_call_chunk().
Each independently guards against the same class of unsigned
arithmetic underflow from untrusted wire values. Any new consumer
of the parsed Read chunk list must replicate these checks or risk
re-introducing the defects fixed by earlier patches in this series.

Add pcl_check_read_chunk_positions() to consolidate position and
length validation into a single post-decode pass, called from
svc_rdma_xdr_decode_req() after all three chunk lists have been
parsed and the inline body length is known. The pass verifies
three properties:

 - Each Read chunk's inline-body offset (its unreduced-stream
   position minus the cumulative length of preceding Read chunks)
   falls within the inline body length, or within the Call chunk
   length for interleaved reads.

 - Adjacent Read chunk positions do not overlap: cumulative read
   bytes at each transition do not exceed the next position.

 - Each chunk length does not exceed the receive context's page
   budget.

Malformed frames are rejected before reaching any consumer. The
existing consumer-side guards remain as defense in depth.

Acked-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260526-rpc-kernel-bugs-v1-6-e251306ccca9@oracle.com
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
This commit is contained in:
Chuck Lever
2026-07-27 08:49:57 -04:00
committed by Chuck Lever
parent e47b963805
commit 8a4a67efd2
3 changed files with 63 additions and 3 deletions
+2
View File
@@ -119,6 +119,8 @@ extern bool pcl_alloc_call(struct svc_rdma_recv_ctxt *rctxt, __be32 *p);
extern bool pcl_alloc_read(struct svc_rdma_recv_ctxt *rctxt, __be32 *p);
extern bool pcl_alloc_write(struct svc_rdma_recv_ctxt *rctxt,
struct svc_rdma_pcl *pcl, __be32 *p);
extern bool pcl_check_read_chunk_positions(struct svc_rdma_recv_ctxt *rctxt,
unsigned int inline_len);
extern int pcl_process_nonpayloads(const struct svc_rdma_pcl *pcl,
const struct xdr_buf *xdr,
int (*actor)(const struct xdr_buf *,
+58 -3
View File
@@ -149,9 +149,6 @@ bool pcl_alloc_call(struct svc_rdma_recv_ctxt *rctxt, __be32 *p)
* cl_count is updated to be the number of chunks (ie.
* unique position values) in the Read list.
* %false: Memory allocation failed.
*
* TODO:
* - Check for chunk range overlaps
*/
bool pcl_alloc_read(struct svc_rdma_recv_ctxt *rctxt, __be32 *p)
{
@@ -229,6 +226,64 @@ bool pcl_alloc_write(struct svc_rdma_recv_ctxt *rctxt,
return true;
}
/**
* pcl_check_read_chunk_positions - Validate Read chunk positions
* @rctxt: Ingress receive context with populated chunk lists
* @inline_len: Length of the inline RPC body after the transport header
*
* Read chunk positions are offsets in the unreduced XDR stream
* (RFC 8166 Section 3.4.4), so each position includes the
* cumulative length of preceding Read chunks. This function
* subtracts those lengths to recover the inline-body offset
* before comparing against @inline_len or the Call chunk length.
*
* Rejects frames where a Read chunk's inline-body offset exceeds
* the bound, where adjacent Read chunks overlap, or where any
* single chunk length exceeds the page budget.
*
* Return values:
* %true: Read chunk positions and lengths are valid
* %false: Malformed chunk list detected
*/
bool pcl_check_read_chunk_positions(struct svc_rdma_recv_ctxt *rctxt,
unsigned int inline_len)
{
unsigned int max_len, bound, total_read;
struct svc_rdma_chunk *chunk, *next;
max_len = rctxt->rc_maxpages << PAGE_SHIFT;
if (!pcl_is_empty(&rctxt->rc_call_pcl)) {
chunk = pcl_first_chunk(&rctxt->rc_call_pcl);
if (chunk->ch_length > max_len)
return false;
bound = chunk->ch_length;
} else {
bound = inline_len;
}
if (pcl_is_empty(&rctxt->rc_read_pcl))
return true;
total_read = 0;
pcl_for_each_chunk(chunk, &rctxt->rc_read_pcl) {
if (chunk->ch_position - total_read > bound)
return false;
if (chunk->ch_length > max_len)
return false;
next = pcl_next_chunk(&rctxt->rc_read_pcl, chunk);
if (!next)
break;
if (chunk->ch_position + chunk->ch_length > next->ch_position)
return false;
total_read += chunk->ch_length;
}
return true;
}
static int pcl_process_region(const struct xdr_buf *xdr,
unsigned int offset, unsigned int length,
int (*actor)(const struct xdr_buf *, void *),
+3
View File
@@ -724,6 +724,9 @@ static int svc_rdma_xdr_decode_req(struct xdr_buf *rq_arg,
rq_arg->head[0].iov_base = rctxt->rc_stream.p;
hdr_len = xdr_stream_pos(&rctxt->rc_stream);
if (!pcl_check_read_chunk_positions(rctxt,
rq_arg->head[0].iov_len - hdr_len))
goto out_inval;
rq_arg->head[0].iov_len -= hdr_len;
rq_arg->len -= hdr_len;
trace_svcrdma_decode_rqst(rctxt, rdma_argp, hdr_len);