@samitouri / QOSamiQemu / commits / 4adf36d940

fuse: Explicitly handle non-grow post-EOF accesses

When reading to / writing from non-growable exports, we cap the I/O size by `offset - blk_len`. This will underflow for accesses that are completely past the disk end. Check and handle that case explicitly. This is also enough to ensure that `offset + size` will not overflow; blk_len is int64_t, offset is uint32_t, `offset < blk_len`, so from `INT64_MAX + UINT32_MAX < UINT64_MAX` it follows that `offset + size` cannot overflow. Just one catch: We have to allow write accesses to growable exports past the EOF, so then we cannot rely on `offset < blk_len`, but have to verify explicitly that `offset + size` does not overflow. The negative consequences of not having this commit are luckily limited because blk_pread() and blk_pwrite() will reject post-EOF requests anyway, so a `size` underflow post-EOF will just result in an I/O error. So: - Post-EOF reads will incorrectly result in I/O errors instead of just 0-length reads. We will also attempt to allocate a very large buffer, which is wrong and not good, but not terrible. - Post-EOF writes on non-growable exports will result in I/O errors instead of 0-length writes (which generally indicate ENOSPC). - Post-EOF writes on growable exports can theoretically overflow on EOF and truncate the export down to a much too small size, but in practice, FUSE will never send an offset greater than signed INT_MAX, preventing a uint64_t overflow. (fuse_write_args_fill() in the kernel uses loff_t for the offset, which is signed.) Signed-off-by: Hanna Czenczek <hreitz@redhat.com> Message-ID: <20260309150856.26800-15-hreitz@redhat.com> Reviewed-by: Kevin Wolf <kwolf@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com>

Hanna Czenczek committed Mar 9, 2026 at 16:08 UTC 4adf36d940af0e69fab80ef2ac80020c7576a130
3 files changed +59 -6
block/export/fuse.c
+19 -1
@@ -657,6 +657,16 @@ static void fuse_read(fuse_req_t req, fuse_ino_t inode,
657 return;
658 }
659
660 + if (offset >= blk_len) {
661 + /*
662 + * Technically libfuse does not allow returning a zero error code for
663 + * read requests, but in practice this is a 0-length read (and a future
664 + * commit will change this code anyway)
665 + */
666 + fuse_reply_err(req, 0);
667 + return;
668 + }
669 +
670 if (offset + size > blk_len) {
671 size = blk_len - offset;
672 }
@@ -717,7 +727,15 @@ static void fuse_write(fuse_req_t req, fuse_ino_t inode, const char *buf,
727 return;
728 }
729
720 - if (offset + size > blk_len) {
730 + if (offset >= blk_len && !exp->growable) {
731 + fuse_reply_write(req, 0);
732 + return;
733 + }
734 +
735 + if (offset + size < offset) {
736 + fuse_reply_err(req, EINVAL);
737 + return;
738 + } else if (offset + size > blk_len) {
739 if (exp->growable) {
740 ret = fuse_do_truncate(exp, offset + size, true, PREALLOC_MODE_OFF);
741 if (ret < 0) {
tests/qemu-iotests/308
+30 -5
@@ -300,16 +300,34 @@ dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len \
300 conv=notrunc 2>&1 \
301 | _filter_testdir | _filter_imgfmt
302
303 +# And one really squarely post-EOF write
304 +dd if=/dev/zero of="$EXT_MP" bs=1 count=1 seek=$((orig_len + 32 * 1024)) \
305 + conv=notrunc 2>&1 \
306 + | _filter_testdir | _filter_imgfmt
307 +
308 +# Half-post-EOF reads
309 +dd if="$EXT_MP" of=/dev/null bs=1 count=64k skip=$((orig_len - 32 * 1024)) \
310 + 2>&1 | _filter_testdir | _filter_imgfmt
311 +
312 +# And one really squarely post-EOF read
313 +dd if="$EXT_MP" of=/dev/null bs=1 count=1 skip=$((orig_len + 32 * 1024)) \
314 + 2>&1 | _filter_testdir | _filter_imgfmt
315 +
316 echo
317 echo '--- Resize export ---'
318
319 # But we can truncate it explicitly; even with fallocate
307 -fallocate -o "$orig_len" -l 64k "$EXT_MP"
320 +# (Make sure we extend it to a length not divisible by 128k, we need that below)
321 +bs=$((128 * 1024))
322 +extend_to=$(((orig_len + bs - 1) / bs * bs + bs / 2))
323 +extend_by=$((extend_to - orig_len))
324 +
325 +fallocate -o "$orig_len" -l $extend_by "$EXT_MP"
326
327 new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
310 -if [ "$new_len" != "$((orig_len + 65536))" ]; then
328 +if [ "$new_len" != "$extend_to" ]; then
329 echo 'ERROR: Unexpected post-truncate image size:'
312 - echo "$new_len != $((orig_len + 65536))"
330 + echo "$new_len != $extend_to"
331 else
332 echo 'OK: Post-truncate image size is as expected'
333 fi
@@ -322,6 +340,13 @@ else
340 echo "$orig_disk_usage => $new_disk_usage"
341 fi
342
343 +# Use this opportunity to test a read access across the (now no longer so much
344 +# aligned) EOF. dd can only do requests with a length of its block size, and
345 +# all of its seek/skip values are in bs units, so it is hard to do a request
346 +# across the EOF if the EOF is at a power of two (64M).
347 +dd if="$EXT_MP" of=/dev/null bs=$bs count=2 skip=$((extend_to / bs)) \
348 + 2>&1 | _filter_testdir | _filter_imgfmt
349 +
350 echo
351 echo '--- Try growing growable export ---'
352
@@ -338,9 +363,9 @@ dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len conv=notrunc 2>&1 \
363 | _filter_testdir | _filter_imgfmt
364
365 new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
341 -if [ "$new_len" != "$((orig_len + 131072))" ]; then
366 +if [ "$new_len" != "$((extend_to + 65536))" ]; then
367 echo 'ERROR: Unexpected post-grow image size:'
343 - echo "$new_len != $((orig_len + 131072))"
368 + echo "$new_len != $((extend_to + 65536))"
369 else
370 echo 'OK: Post-grow image size is as expected'
371 fi
tests/qemu-iotests/308.out
+10
@@ -134,11 +134,21 @@ wrote 65536/65536 bytes at offset 1048576
134 dd: error writing 'TEST_DIR/t.IMGFMT.fuse': No space left on device
135 1+0 records in
136 0+0 records out
137 +dd: error writing 'TEST_DIR/t.IMGFMT.fuse': No space left on device
138 +1+0 records in
139 +0+0 records out
140 +32768+0 records in
141 +32768+0 records out
142 +dd: TEST_DIR/t.IMGFMT.fuse: cannot skip to specified offset
143 +0+0 records in
144 +0+0 records out
145
146 --- Resize export ---
147 (OK: Lengths of export and original are the same)
148 OK: Post-truncate image size is as expected
149 OK: Disk usage grew with fallocate
150 +0+1 records in
151 +0+1 records out
152
153 --- Try growing growable export ---
154 {'execute': 'block-export-del',