Re: [FUSE] Two latent data-integrity defects in the 7.2 iomap read integration — pre-disclosure before large-folio enablement
From: Joanne Koong
Date: Wed Sep 02 2026 - 00:40:44 EST
On Sat, Aug 29, 2026 at 9:32 PM Kanishka De Silva
<kpskanna1915@xxxxxxxxx> wrote:
>
> Hi Miklos and Joanne,
Hi Kanishka,
>
> I am reporting two latent data-integrity defects in the FUSE iomap
> read integration introduced in Linux 7.2. Both defects are unreachable
> in stock 7.2.2 today, but activate the moment partially-uptodate
> folios exist in FUSE — which is precisely what the announced "fuse:
> support large folios" series (Joanne) and any sub-page-blocksize
> invalidation semantics will create. I'm reporting now, before that
> series merges, so the fixes can land alongside or before the
> enablement.
>
> Full report, suggested fixes patch, PoC source, and runtime evidence
> logs are attached as plain text files.
>
> Summary:
>
> The 7.2 series "fuse: use iomap for buffered reads + readahead"
> introduced, for the first time, FUSE read paths where a folio read can
> be a sub-range of that folio (desc.offset > 0 and/or desc.length <
> folio_size). Two legacy helpers were not adapted:
>
> Defect A — fuse_copy_folio() whole-folio zeroing (fs/fuse/dev.c):
> When zeroing is set and count < folio_size, the function calls
> folio_zero_range(folio, 0, size) — zeroing the ENTIRE folio before
> copying the reply at [offset, offset+count). With sub-folio read
> ranges this destroys valid, cached, uptodate data located before the
> requested range in the same folio. Demonstrated end-to-end: 1024 bytes
> of valid cached data silently replaced by zeros.
>
> Suggested fix (one line):
> - folio_zero_range(folio, 0, size);
> + folio_zero_range(folio, offset, size - offset);
I think the fix needs to be folio_zero_range(folio, offset,
ap->descs[i].length); else there's the case where writing to the last
block of a folio and then reading in the entire folio will overwrite
the last block. This is what I had in my local tree, after sashiko
mentioned this a few months ago in [1]:
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -1240,17 +1240,11 @@
* done atomically
*/
int fuse_copy_folio(struct fuse_copy_state *cs, struct folio **foliop,
- unsigned offset, unsigned count, int zeroing)
+ unsigned offset, unsigned count)
{
int err;
struct folio *folio = *foliop;
- size_t size;
-
- if (folio) {
- size = folio_size(folio);
- if (zeroing && count < size)
- folio_zero_range(folio, 0, size);
- }
+ size_t size = folio ? folio_size(folio) : 0;
while (count) {
if (cs->write && cs->pipebufs && folio) {
@@ -1308,10 +1302,25 @@
for (i = 0; i < ap->num_folios && (nbytes || zeroing); i++) {
int err;
+ struct folio *folio = ap->folios[i];
unsigned int offset = ap->descs[i].offset;
- unsigned int count = min(nbytes, ap->descs[i].length);
+ unsigned int length = ap->descs[i].length;
+ unsigned int count = min(nbytes, length);
+
+ /*
+ * The reply may be shorter than what was asked for, in which
+ * case the tail of the requested range needs to be zeroed.
+ * Only [offset, offset + length) may be touched here: the
+ * rest of the folio can hold blocks that are already uptodate
+ * or dirty, and clearing those would lose data.
+ *
+ * Clear the range up front, so that a failed copy leaves
+ * zeroes rather than stale folio contents.
+ */
+ if (folio && zeroing && count < length)
+ folio_zero_range(folio, offset, length);
- err = fuse_copy_folio(cs, &ap->folios[i], offset,
count, zeroing);
+ err = fuse_copy_folio(cs, &ap->folios[i], offset, count);
if (err)
return err;
diff --git a/fs/fuse/dev.h b/fs/fuse/dev.h
--- a/fs/fuse/dev.h
+++ b/fs/fuse/dev.h
@@ -90,7 +90,7 @@
int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size);
int fuse_copy_folio(struct fuse_copy_state *cs, struct folio **foliop,
- unsigned offset, unsigned count, int zeroing);
+ unsigned offset, unsigned count);
void fuse_copy_finish(struct fuse_copy_state *cs);
#ifdef CONFIG_FUSE_IO_URING
diff --git a/fs/fuse/notify.c b/fs/fuse/notify.c
--- a/fs/fuse/notify.c
+++ b/fs/fuse/notify.c
@@ -190,7 +190,7 @@
folio_offset = offset_in_folio(folio, pos);
nr_bytes = min(num, folio_size(folio) - folio_offset);
- err = fuse_copy_folio(cs, &folio, folio_offset, nr_bytes, 0);
+ err = fuse_copy_folio(cs, &folio, folio_offset, nr_bytes);
if (!folio_test_uptodate(folio) && !err && folio_offset == 0 &&
(nr_bytes == folio_size(folio) || file_size == end)) {
folio_zero_segment(folio, nr_bytes, folio_size(folio));
I'll prioritize sending this upstream this week.
>
> Defect B — fuse_send_readpages() ignores descs[0].offset (fs/fuse/file.c):
> The readahead sender computes the FUSE_READ request start as
> folio_pos(ap->folios[0]), ignoring ap->descs[0].offset. If the first
> folio of a batch is prefix-partial, the kernel asks the server for
> data beginning at the folio start instead of the first invalid block.
> The reply is copied at the wrong position — every byte of the batch is
> silently shifted. Both sibling paths handle this correctly:
> - writeback: folio_pos(folio) + offset (fuse_writepage_args_setup)
> - sync read: folio_pos(folio) + off (fuse_do_readfolio)
> The readahead path is the odd one out.
>
> Suggested fix (one line):
> - loff_t pos = folio_pos(ap->folios[0]);
> + loff_t pos = folio_pos(ap->folios[0]) + ap->descs[0].offset;
>
> Reachability today (why latent):
> Stock 7.2.2 cannot construct a partially-uptodate folio in FUSE —
> order-0 folios only, no code path clears per-block uptodate bits, and
> the VFS readahead core only hands freshly-allocated fully-non-uptodate
> folios to ->readahead(). Both defects activate exactly when the
> large-folio series lands or any sub-page invalidation semantics are
> introduced.
>
Even with large folios enabled, this isn't reachable since
page_cache_ra_unbounded() never adds an already-present folio to the
rac. Every folio that ->readahead() sees is freshly allocated / wholly
non-uptodate.
Large folios aren't enabled yet but a patch for that will be sent out
for inclusion in the next (7.4) merge, which would only land after the
above patch for the zeroing lands.
Thanks,
Joanne
[1] https://sashiko.dev/#/patchset/20260717032835.922433-1-lihaofeng%40kylinos.cn