[PATCH v2] spi: fix zero-length scatterlist entries for vmalloc buffers
From: Chunfa Zhang
Date: Fri Oct 30 2026 - 03:42:50 EST
spi_map_buf_attrs() sizes the sg table with
DIV_ROUND_UP(len + offset_in_page(buf), desc_len)
but fills the entries with
min(desc_len, min(len, PAGE_SIZE - offset_in_page(buf)))
Those two expressions only agree when a page boundary coincides with a
chunk boundary. On 64K pages with a controller whose max_seg_size is
smaller than PAGE_SIZE, a buffer that is not page aligned produces a
table that is one or more entries too large, and the trailing entries are
filled with length 0.
dma_map_sgtable() maps every entry up to sgt->orig_nents, so such an
entry reaches dma_direct_map_sg() -> dma_direct_map_page(size = 0).
dma_capable() computes end = addr + size - 1, i.e. addr - 1 for a zero
sized mapping, which is false for any buffer above the device DMA mask,
and swiotlb_map() then calls swiotlb_tbl_map_single(size = 0) ->
swiotlb_do_find_slots(), where
BUG_ON(!nslots);
fires because nr_slots(0) is 0. On 4K pages desc_len always equals
PAGE_SIZE and the page boundary absorbs the offset, which is why this has
only been observed after switching to 64K pages.
Fix the entry count to match the way the loop consumes the buffer.
Signed-off-by: Chunfa Zhang <zcf1810@xxxxxxx>
---
Changes in v2:
- Drop the zero-length early return. A zero-length transfer has to fail
with -EINVAL from sg_alloc_table() so that spi_map_buf_attrs() returns
an error to the caller; returning 0 there would report a successful
mapping that does not exist and would break the KUnit error-path
coverage that relies on the zero length transfer failing.
The sg table sizing change is unchanged from v1. It was validated against
the fill loop with 20000 random combinations of (len, offset_in_page,
PAGE_SIZE in {4K,16K,64K}, max_seg_size): no zero-length entries are
produced and the segment lengths always sum to len. Not boot-tested on
hardware; the reporter hits the BUG on a 6.1.158 arm64 board with 64K
pages.
drivers/spi/spi.c | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/drivers/spi/spi.c b/drivers/spi/spi.c
index 40cda00..1b87fb2 100644
--- a/drivers/spi/spi.c
+++ b/drivers/spi/spi.c
@@ -1349,8 +1349,24 @@ static int spi_map_buf_attrs(struct spi_controller *ctlr, struct device *dev,
int i, ret;
if (vmalloced_buf || kmap_buf) {
- desc_len = min_t(unsigned long, max_seg_size, PAGE_SIZE);
- sgs = DIV_ROUND_UP(len + offset_in_page(buf), desc_len);
+ size_t off = offset_in_page(buf);
+ size_t first = min_t(size_t, len, PAGE_SIZE - off);
+ size_t rest;
+
+ desc_len = min_t(size_t, max_seg_size, PAGE_SIZE);
+ /*
+ * The buffer is first split at page boundaries, then every
+ * piece is split into desc_len chunks. Sizing the table with
+ * DIV_ROUND_UP(len + off, desc_len) does not model that and
+ * leaves zero-length entries whenever a page boundary does
+ * not coincide with a chunk boundary (which trips
+ * BUG_ON(!nslots) in swiotlb on 64K pages).
+ */
+ sgs = DIV_ROUND_UP(first, desc_len);
+ rest = len - first;
+ sgs += (rest / PAGE_SIZE) * DIV_ROUND_UP(PAGE_SIZE, desc_len);
+ if (rest % PAGE_SIZE)
+ sgs += DIV_ROUND_UP(rest % PAGE_SIZE, desc_len);
} else if (virt_addr_valid(buf)) {
desc_len = min_t(size_t, max_seg_size, ctlr->max_dma_len);
sgs = DIV_ROUND_UP(len, desc_len);
--
2.25.1