[PATCH 03/18] SUNRPC: Add helpers to convert xdr_buf byte ranges to scatterlists

From: Chuck Lever

Date: Mon Apr 27 2026 - 09:54:28 EST


From: Chuck Lever <chuck.lever@xxxxxxxxxx>

The crypto/krb5 library accepts data in scatterlist form, but
the GSS-API layer presents RPC payloads as struct xdr_buf.
Bridge that gap with a pair of helper functions:

xdr_buf_to_sg() - populate a caller-supplied scatterlist
array from a byte range
xdr_buf_to_sg_alloc() - populate a caller-supplied inline
scatterlist, chaining to a heap-
allocated overflow for large payloads

The inline array (typically stack-allocated at eight entries)
covers the common case of small RPCs with no heap allocation
on the encrypt/decrypt path. Only buffers spanning many pages
incur a kmalloc for the chained extension.

The segment-walking logic follows the same head, page array,
tail traversal as xdr_process_buf(), but populates a
scatterlist directly rather than invoking a per-segment
callback. sg_next() traversal makes the walker safe for
chained scatterlists. Once subsequent patches reroute all
per-message crypto operations through crypto/krb5,
xdr_process_buf() loses its last callers and is removed.

Signed-off-by: Chuck Lever <chuck.lever@xxxxxxxxxx>
---
include/linux/sunrpc/xdr.h | 15 ++++
net/sunrpc/xdr.c | 199 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 214 insertions(+)

diff --git a/include/linux/sunrpc/xdr.h b/include/linux/sunrpc/xdr.h
index b639a6fafcbc..f82446993fde 100644
--- a/include/linux/sunrpc/xdr.h
+++ b/include/linux/sunrpc/xdr.h
@@ -140,6 +140,21 @@ int xdr_alloc_bvec(struct xdr_buf *buf, gfp_t gfp);
void xdr_free_bvec(struct xdr_buf *buf);
unsigned int xdr_buf_to_bvec(struct bio_vec *bvec, unsigned int bvec_size,
const struct xdr_buf *xdr);
+int xdr_buf_to_sg(const struct xdr_buf *buf, unsigned int offset,
+ unsigned int len, struct scatterlist *sg, unsigned int nsg);
+int xdr_buf_to_sg_alloc(const struct xdr_buf *buf, unsigned int offset,
+ unsigned int len, struct scatterlist *sg_head,
+ unsigned int sg_head_nents,
+ struct scatterlist **sg_overflow, gfp_t gfp);
+
+/*
+ * Inline scatterlist entries for xdr_buf_to_sg_alloc(). Sized to cover the
+ * head kvec, tail kvec, and a few page fragments without any heap allocation.
+ */
+enum {
+ XDR_BUF_TO_SG_NENTS = 8,
+};
+

static inline __be32 *xdr_encode_array(__be32 *p, const void *s, unsigned int len)
{
diff --git a/net/sunrpc/xdr.c b/net/sunrpc/xdr.c
index e83d5d0be78b..516833b4c114 100644
--- a/net/sunrpc/xdr.c
+++ b/net/sunrpc/xdr.c
@@ -187,6 +187,205 @@ unsigned int xdr_buf_to_bvec(struct bio_vec *bvec, unsigned int bvec_size,
}
EXPORT_SYMBOL_GPL(xdr_buf_to_bvec);

+/**
+ * xdr_buf_to_sg - Populate a scatterlist from an xdr_buf range
+ * @buf: xdr_buf to map
+ * @offset: starting byte offset within @buf
+ * @len: number of bytes to cover
+ * @sg: scatterlist array initialized with sg_init_table()
+ * @nsg: number of entries available in @sg
+ *
+ * @sg is traversed with sg_next(), so callers may pass a list
+ * assembled with sg_chain().
+ *
+ * Return: on success, the number of scatterlist entries used; the
+ * last used entry is marked with sg_mark_end(). On failure, a
+ * negative errno.
+ */
+int xdr_buf_to_sg(const struct xdr_buf *buf, unsigned int offset,
+ unsigned int len, struct scatterlist *sg, unsigned int nsg)
+{
+ unsigned int page_len, thislen, page_offset;
+ struct scatterlist *cur = sg, *prev = NULL;
+ int nents = 0;
+ int i;
+
+ if (len == 0)
+ return 0;
+
+ if (offset >= buf->head[0].iov_len) {
+ offset -= buf->head[0].iov_len;
+ } else {
+ thislen = min_t(unsigned int,
+ buf->head[0].iov_len - offset, len);
+ if (nents >= nsg)
+ return -ENOSPC;
+ sg_set_buf(cur, buf->head[0].iov_base + offset,
+ thislen);
+ prev = cur;
+ cur = sg_next(cur);
+ nents++;
+ len -= thislen;
+ offset = 0;
+ }
+ if (len == 0)
+ goto done;
+
+ if (offset >= buf->page_len) {
+ offset -= buf->page_len;
+ } else {
+ page_len = min(buf->page_len - offset, len);
+ len -= page_len;
+ page_offset = (offset + buf->page_base) & (PAGE_SIZE - 1);
+ i = (offset + buf->page_base) >> PAGE_SHIFT;
+ thislen = PAGE_SIZE - page_offset;
+ do {
+ if (thislen > page_len)
+ thislen = page_len;
+ if (nents >= nsg)
+ return -ENOSPC;
+ sg_set_page(cur, buf->pages[i],
+ thislen, page_offset);
+ prev = cur;
+ cur = sg_next(cur);
+ nents++;
+ page_len -= thislen;
+ i++;
+ page_offset = 0;
+ thislen = PAGE_SIZE;
+ } while (page_len != 0);
+ offset = 0;
+ }
+ if (len == 0)
+ goto done;
+
+ if (offset < buf->tail[0].iov_len) {
+ thislen = min_t(unsigned int,
+ buf->tail[0].iov_len - offset, len);
+ if (nents >= nsg)
+ return -ENOSPC;
+ sg_set_buf(cur, buf->tail[0].iov_base + offset,
+ thislen);
+ prev = cur;
+ nents++;
+ len -= thislen;
+ }
+ if (len != 0)
+ return -EINVAL;
+
+done:
+ if (prev)
+ sg_mark_end(prev);
+ return nents;
+}
+EXPORT_SYMBOL_GPL(xdr_buf_to_sg);
+
+/*
+ * Count the scatterlist entries needed to cover [offset, offset + len)
+ * within @buf. Mirrors the walk in xdr_buf_to_sg() so the caller can
+ * size an allocation that matches the requested sub-range rather than
+ * the full xdr_buf.
+ */
+static unsigned int xdr_buf_sg_nents(const struct xdr_buf *buf,
+ unsigned int offset, unsigned int len)
+{
+ unsigned int nsg = 0, thislen, page_offset;
+
+ if (len == 0)
+ return 0;
+
+ if (offset < buf->head[0].iov_len) {
+ thislen = min_t(unsigned int,
+ buf->head[0].iov_len - offset, len);
+ nsg++;
+ len -= thislen;
+ offset = 0;
+ } else {
+ offset -= buf->head[0].iov_len;
+ }
+ if (len == 0)
+ return nsg;
+
+ if (offset < buf->page_len) {
+ thislen = min(buf->page_len - offset, len);
+ page_offset = (offset + buf->page_base) & (PAGE_SIZE - 1);
+ nsg += DIV_ROUND_UP(page_offset + thislen, PAGE_SIZE);
+ len -= thislen;
+ offset = 0;
+ } else {
+ offset -= buf->page_len;
+ }
+ if (len == 0)
+ return nsg;
+
+ if (offset < buf->tail[0].iov_len)
+ nsg++;
+ return nsg;
+}
+
+/**
+ * xdr_buf_to_sg_alloc - Populate a scatterlist for an xdr_buf range
+ * @buf: xdr_buf to map
+ * @offset: starting byte offset within @buf
+ * @len: number of bytes to cover
+ * @sg_head: caller-provided scatterlist array (typically stack-allocated)
+ * @sg_head_nents: number of entries in @sg_head
+ * @sg_overflow: OUT: chained extension, or NULL when @sg_head sufficed
+ * @gfp: memory allocation flags for overflow
+ *
+ * Populates @sg_head directly when the xdr_buf fits. When more
+ * entries are needed, an overflow scatterlist is allocated and
+ * chained from @sg_head so that the result is traversable with
+ * sg_next().
+ *
+ * Return: on success, the number of populated scatterlist entries
+ * (counting only data entries, not chain entries). @sg_head is
+ * the head of the resulting list. Caller must kfree @sg_overflow
+ * when done. On failure, a negative errno.
+ */
+int xdr_buf_to_sg_alloc(const struct xdr_buf *buf, unsigned int offset,
+ unsigned int len, struct scatterlist *sg_head,
+ unsigned int sg_head_nents,
+ struct scatterlist **sg_overflow, gfp_t gfp)
+{
+ unsigned int nsg;
+ int ret;
+
+ *sg_overflow = NULL;
+ if (len == 0)
+ return 0;
+
+ nsg = xdr_buf_sg_nents(buf, offset, len);
+ if (nsg == 0)
+ return -EINVAL;
+
+ if (nsg <= sg_head_nents) {
+ sg_init_table(sg_head, nsg);
+ } else {
+ /* +1 replaces the slot sg_chain() consumes as the link. */
+ unsigned int overflow_nents = nsg - sg_head_nents + 1;
+ struct scatterlist *overflow;
+
+ overflow = kmalloc_array(overflow_nents, sizeof(*overflow),
+ gfp);
+ if (!overflow)
+ return -ENOMEM;
+
+ sg_init_table(sg_head, sg_head_nents);
+ sg_init_table(overflow, overflow_nents);
+ sg_chain(sg_head, sg_head_nents, overflow);
+ *sg_overflow = overflow;
+ }
+
+ ret = xdr_buf_to_sg(buf, offset, len, sg_head, nsg);
+ if (ret < 0) {
+ kfree(*sg_overflow);
+ *sg_overflow = NULL;
+ }
+ return ret;
+}
+EXPORT_SYMBOL_GPL(xdr_buf_to_sg_alloc);
+
/**
* xdr_inline_pages - Prepare receive buffer for a large reply
* @xdr: xdr_buf into which reply will be placed

--
2.53.0