[PATCH] io_uring/rsrc: fix folio size overflow in io_vec_fill_bvec()

Ali Ahmet Memis <[email protected]>
Newsgroups org.kernel.vger.stable,org.kernel.vger.io-uring,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
io_vec_fill_bvec() computes the folio size with a plain int 1:

	unsigned long folio_size = 1 << imu->folio_shift;

imu->folio_shift is unsigned int and comes from folio_shift() of the
folio backing the registered buffer, so it can be 32 or more on a 64 bit
kernel. Shifting int 1 that far is undefined, and on x86 and arm64 the
count is taken modulo 32, so a shift of 34 yields 4 rather than 16G.
Every other folio_shift shift in this file already uses 1UL.

The result is that the segment estimate and the fill loop disagree.
io_estimate_bvec_size() sizes the bvec array with the real shift:

	max_segs += (iov[i].iov_len >> shift) + 2;

so a 1M iovec on a 16G folio is charged 2 segments, while
io_vec_fill_bvec() then walks the same iovec in folio_size chunks of 4
bytes and writes res_bvec[bvec_idx] a quarter of a million times, past
the end of the array it was given. src_bvec is advanced once per
iteration as well, so imu->bvec is read past its end at the same time.
validate_fixed_range() only checks that the range is inside the
registered buffer and does not bound the segment count.

Reaching it needs a folio with a shift of at least 32, which means a
gigantic hugetlb page: 16G on arm64 with 64K pages, where
CONT_PMD_SHIFT is 34 and hugetlb_add_hstate(CONT_PMD_SHIFT - PAGE_SHIFT)
registers that size, and likewise on powerpc. x86_64 tops out at 1G, so
a shift of 30, which still fits in int and is unaffected.

Use 1UL, as the rest of the file does.

Fixes: 9ef4cbbcb4ac ("io_uring: add infra for importing vectored reg buffers")
Cc: [email protected]
Signed-off-by: Ali Ahmet Memis <[email protected]>
---
Found by reading, not from a crash. I do not have a machine that can hold
a 16G gigantic page, so I have not run this path with a folio_shift of 34
and the out of bounds write is derived rather than observed. What I did
check in the tree:

  - imu->folio_shift is set from folio_shift() of the coalesced folio in
    io_check_coalesce_buffer(), so it is the real folio order plus
    PAGE_SHIFT and is not clamped anywhere
  - on arm64 with 64K pages ARM64_CONT_PMD_SHIFT is 5 and PMD_SHIFT is 29,
    so CONT_PMD_SHIFT is 34, and arm64_hugetlb_init() calls
    hugetlb_add_hstate(CONT_PMD_SHIFT - PAGE_SHIFT), which is order 18;
    order 18 plus PAGE_SHIFT 16 gives folio_shift 34
  - the four other folio_shift shifts in this file, at the folio_size
    check in io_check_coalesce_buffer(), the bvec fill in
    io_sqe_buffer_register(), and the folio_mask in io_import_fixed(),
    all already use 1UL
  - the only other variable shifts of a plain 1 in io_uring/ are on
    ITER_DEST, ITER_SOURCE and rq_data_dir(), which are 0 or 1

Happy to put together a forced folio_shift reproducer under KASAN if that
would be more useful than the reasoning above.

 io_uring/rsrc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/io_uring/rsrc.c b/io_uring/rsrc.c
index 8d0f2ee24e0c..deb2a844f568 100644
--- a/io_uring/rsrc.c
+++ b/io_uring/rsrc.c
@@ -1477,7 +1477,7 @@ static int io_vec_fill_bvec(int ddir, struct iov_iter *iter,
 				struct iovec *iovec, unsigned nr_iovs,
 				struct iou_vec *vec)
 {
-	unsigned long folio_size = 1 << imu->folio_shift;
+	unsigned long folio_size = 1UL << imu->folio_shift;
 	unsigned long folio_mask = folio_size - 1;
 	struct bio_vec *res_bvec = vec->bvec;
 	size_t total_len = 0;

base-commit: 2d2338c93da79b3bfe4b6099a931d9468d539952
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.