[PATCH AUTOSEL 6.18-6.6] fuse: zero the partial EOF page when extending a file
From: Sasha Levin
Date: Mon Oct 19 2026 - 10:27:14 EST
From: Jimmy Zuber <jamz@xxxxxxxxxx>
[ Upstream commit 34b5c4a6e4fb9dbb3f9d87f3b0fb0372105c8302 ]
Extending a fuse file past a non-page-aligned EOF does not zero the tail of
the old last page. When that page is cached and has been mmap-dirtied beyond
the old EOF, the now in-bounds tail is served to later reads as stale data
rather than zeros, which violates POSIX file-extension semantics.
Some file systems get this zeroing automatically at writeback time
(block_write_full_folio() / iomap_writeback_handle_eof() zero the tail of the
folio straddling i_size). A non-writeback caching fuse file system uses neither
path, so it has to zero the tail itself from the size-extending paths, like
XFS (xfs_file_write_zero_eof()) and ext4 (ext4_block_zero_eof()) do.
Call truncate_pagecache_range() over the newly-exposed range up front from the
three paths that extend a file, before the new size is published:
- a buffered write whose position is past the old EOF (fuse_perform_write());
- a size-extending setattr/truncate (fuse_do_setattr());
- a size-extending fallocate (fuse_file_fallocate()).
This unmaps the stale mappings and zeroes the partial tail of the old EOF
folio, so a later read returns zeros. Truncating [old EOF, write start) before
a buffered write keeps the dropped range disjoint from the written data, so a
write that lands inside the old EOF folio is preserved.
writeback_cache connections are unaffected, as their writes go through
iomap_file_buffered_write(), which zeroes post-EOF folios. The bug is
observable on a non-writeback_cache server that returns FOPEN_KEEP_CACHE on
writable files (without FOPEN_DIRECT_IO), and is caught by the new
write_extend_eof fuse selftest.
Signed-off-by: Jimmy Zuber <jamz@xxxxxxxxxx>
Signed-off-by: Miklos Szeredi <mszeredi@xxxxxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---
LLM Generated explanations, may be completely bogus:
The local tree is **6.12.111** (`pending-6.12`). The buggy FUSE size-
extend paths are present here; the fix is not. This is a small
POSIX/data-integrity fix that belongs in this tree.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
**Step 1.1: PARSE THE SUBJECT LINE**
Record: **[fuse]** **[zero]** zero the partial EOF page when extending a
file. “Zero” here is a correctness fix (fill a hole that should read as
zeros), not a feature.
**Step 1.2: PARSE ALL COMMIT MESSAGE TAGS**
Record:
- Signed-off-by: Jimmy Zuber \<jamz@xxxxxxxxxx\> (author)
- Signed-off-by: Miklos Szeredi \<mszeredi@xxxxxxxxxx\> (FUSE
maintainer; applied the patch)
- No Fixes:, Reported-by:, Tested-by:, Reviewed-by:, Link:, or Cc:
stable (absence of Fixes/Cc: stable is expected for this pipeline)
- Ignore any “[Upstream commit …]” / Sasha Levin SOB if present —
candidate-prep only
Notable: maintainer SOB. Cover letter (v4 0/2) also names testing under
UML plus libfuse `passthrough_ll`. Recipients included Jan Kara, Joanne
Koong, Brian Foster (author of the analogous ext4/XFS EOF-zeroing work).
**Step 1.3: ANALYZE THE COMMIT BODY TEXT**
Record:
- **Bug:** Extending a FUSE file past a non-page-aligned EOF does not
zero the tail of the old last page.
- **Symptom:** If that page is cached and mmap-dirtied past the old EOF,
later reads of the newly in-bounds tail return stale bytes instead of
zeros (POSIX file-extension violation).
- **Root cause:** Writeback-style filesystems get this from
`block_write_full_folio()` / `iomap_writeback_handle_eof()`. A non-
writeback caching FUSE fs uses neither, so it must zero on extend,
like XFS/ext4.
- **Fix:** `truncate_pagecache_range()` over the newly exposed range on
write, setattr/truncate, and fallocate, before publishing the new size
(write/fallocate; setattr still publishes `i_size` first, as it
already did).
- **Scope note:** writeback_cache writes are claimed unaffected on
mainline because they go through `iomap_file_buffered_write()`.
Observable on non-writeback_cache + `FOPEN_KEEP_CACHE` without
`FOPEN_DIRECT_IO`. Caught by `write_extend_eof` selftest.
- **Version info:** none in the message. Cover letter base-commit
`7d87a5a284bb34edb3f4e7e312ef403b3385a7b7` is post-6.12.
**Step 1.4: DETECT HIDDEN BUG FIXES**
Record: Not hidden. Explicit POSIX/data-integrity bug with a regression
test. v1 originally zeroed from `fuse_write_update_attr()` after the
fact; v3/v4 moved zeroing to the extend paths so a write inside the old
EOF folio is preserved.
---
## PHASE 2: DIFF ANALYSIS - LINE BY LINE
**Step 2.1: INVENTORY THE CHANGES**
Record:
- `fs/fuse/dir.c`: +3 in `fuse_do_setattr()`
- `fs/fuse/file.c`: +4 in `fuse_perform_write()`, +5 in
`fuse_file_fallocate()`
- **~12 insertions, 0 deletions.** Single-subsystem surgical fix (FUSE
only). Companion selftest (series 2/2) is not part of this commit.
**Step 2.2: UNDERSTAND THE CODE FLOW CHANGE**
Record:
- **`fuse_perform_write()`:** Snapshot `old_size`. If `pos > old_size`,
`truncate_pagecache_range(inode, old_size, pos-1)` before filling
pages. Hole `[old EOF, write start)` is unmapped/zeroed; the write
range itself is left intact.
- **`fuse_do_setattr()`:** On size change, if extending, range-truncate
`[oldsize, newsize-1]` then the existing `truncate_pagecache()` +
`invalidate_inode_pages2()`.
- **`fuse_file_fallocate()`:** If not `FALLOC_FL_KEEP_SIZE` and
`offset+length > oldsize`, range-truncate `[oldsize, offset+length-1]`
before `fuse_write_update_attr()` publishes the new size. Punch/zero
still has its own existing range truncate.
**Step 2.3: IDENTIFY THE BUG MECHANISM**
Record: **Logic/correctness + memory-contents (stale page-cache data).**
mmap may dirty the last page past `i_size`. Extending makes that tail
in-bounds. Without zeroing, reads return mmap garbage instead of zeros.
`truncate_pagecache_range()` unmaps full pages in the hole and
`truncate_inode_partial_folio()` / `folio_zero_range()` zeros the
partial old-EOF folio (`mm/truncate.c`).
**Step 2.4: ASSESS THE FIX QUALITY**
Record: Minimal, matches XFS/ext4. Uses a long-standing helper. v4
dropped a custom `fuse_zero_partial_eof_folio()` after Miklos/Jan noted
`folio_mkclean()`/`folio_mark_dirty()` is a buffer-head concern;
`truncate_pagecache_range()` already zeros. Regression risk is low:
extra truncate of a hole that must read as zeros. Failed write after
hole-zeroing only clears post-EOF cache, which is still past `i_size`.
Does not add APIs or features.
---
## PHASE 3: GIT HISTORY INVESTIGATION
**Step 3.1: BLAME THE CHANGED LINES**
Record:
- setattr size-change block: `8373200b124d03` (“fuse: Trust kernel
i_size only”, 2014) wrapping `truncate_pagecache` from
`7caef26767c172` (2013). Comment around the invalidate goes back to
Miklos 2008 (`3be5a52b30aa5c`).
- `fuse_perform_write()`: writethrough extend path since at least
`ea9b9907b82a09` (2008); `FUSE_I_SIZE_UNSTABLE` from `06a7c3c2781409`
(2013).
- fallocate extend: `bee6c307800bbb` / `b0aa7606521790` (2013).
Bug is old, long predating 6.12.
**Step 3.2: FOLLOW THE FIXES: TAG**
Record: No Fixes: tag. Introducing commit is “FUSE never zeroed this on
extend,” not a recent regression.
**Step 3.3: CHECK FILE HISTORY FOR RELATED CHANGES**
Record: Recent 6.12 fuse work is lock leaks, DIO cache, virtiofs UAF —
unrelated. This is standalone, not a prep patch. Series 2/2 is a
selftest only.
**Step 3.4: CHECK THE AUTHOR'S OTHER COMMITS**
Record: Jimmy Zuber has no other fuse commits in this tree. Applied by
Miklos Szeredi (maintainer). Analogous ext4 fix `b6ce2dbe984bc` (“ext4:
partial zero eof block on unaligned inode size extension”) **is already
in this tree**.
**Step 3.5: CHECK FOR DEPENDENT/PREREQUISITE COMMITS**
Record: Added calls are only `i_size_read()` +
`truncate_pagecache_range()`, both present. No new helpers. The mainline
hunk context mentions `fuse_folios_alloc`; this tree still uses
`fuse_pages_alloc` / `ap->pages`, so the **write hunk may need a trivial
context backport**. setattr and fallocate hunks match 6.12. Standalone:
yes.
---
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
**Step 4.1: FIND THE ORIGINAL PATCH DISCUSSION**
Record: Could not `b4 dig -c <sha>` (commit not in this checkout).
Retrieved thread via msgid `20260824143052.1546163-2-jamz@xxxxxxxxxx`.
- Lore/LWN cover: `[PATCH v4 0/2] fuse: zero the partial EOF page when
extending a file`
- Openwall: https://lists.openwall.net/linux-kernel/2026/08/24/1447
(1/2), 1443 (2/2)
- Revisions: v1 (2026-07-31) zeroed from `fuse_write_update_attr()`
(would clobber in-page extending writes); v2/v3 moved zeroing to
extend paths; v4 dropped the helper per Miklos/Jan. Applied version is
v4.
- Miklos reply 2026-08-24: **“Applied, thanks.”** No NAK, no stable
objection.
**Step 4.2: CHECK WHO REVIEWED THE PATCH**
Record: To: Miklos Szeredi, Shuah Khan. Cc: fuse-devel, linux-kselftest,
Jan Kara, Joanne Koong, Brian Foster. Maintainer applied v4 after
review-driven simplification.
**Step 4.3: SEARCH FOR THE BUG REPORT**
Record: No syzbot/bugzilla. Author reproduced vs libfuse
`passthrough_ll` and added a selftest that fails without the fix. Same
mmap-beyond-EOF pattern as the ext4 case (`xfs_io`
pwrite/mmap/mwrite/truncate).
**Step 4.4: CHECK FOR RELATED PATCHES AND SERIES**
Record: 2-patch series. 2/2 is `write_extend_eof_test.c` only. Kernel
fix does not depend on it. 6.12 has no
`tools/testing/selftests/filesystems/fuse/` (no need to backport the
test).
**Step 4.5: CHECK STABLE MAILING LIST HISTORY**
Record: **UNVERIFIED** — lore.kernel.org returned 403 (Anubis). Patch is
from Aug 2026; no evidence it was rejected for stable. Absence of Cc:
stable is not a negative signal here.
---
## PHASE 5: CODE SEMANTIC ANALYSIS
**Step 5.1: IDENTIFY KEY FUNCTIONS IN THE DIFF**
Record: `fuse_perform_write()`, `fuse_do_setattr()`,
`fuse_file_fallocate()`. Helper used: `truncate_pagecache_range()`.
**Step 5.2: TRACE CALLERS**
Record:
- `fuse_perform_write()` ← `fuse_cache_write_iter()` (under
`inode_lock`) ← `fuse_file_write_iter()` (`.write_iter`). Userspace
`write`/`pwrite` on cached FUSE files. Direct-IO and DAX do not use
this function.
- `fuse_do_setattr()` ← `fuse_setattr()` / open `O_TRUNC` path.
Userspace `ftruncate`/`truncate`.
- `fuse_file_fallocate()` ← `.fallocate`. Userspace `fallocate()`.
**Step 5.3: TRACE CALLEES**
Record: `truncate_pagecache_range()` → optional `unmap_mapping_range()`
for full pages in the hole → `truncate_inode_pages_range()` →
`truncate_inode_partial_folio()` / `folio_zero_range()` for the partial
EOF folio. Confirmed in `mm/truncate.c` in this tree.
**Step 5.4: FOLLOW THE CALL CHAIN**
Record: Reachable from ordinary syscalls on a common FUSE mount. Default
no-open flags are `FOPEN_KEEP_CACHE` (`fs/fuse/file.c` ~line 151). Non-
writeback is the usual libfuse default. Unprivileged apps that mmap then
extend trigger it.
**Step 5.5: SEARCH FOR SIMILAR PATTERNS**
Record: Same class as ext4 `ext4_zero_partial_blocks()` on unaligned
extend (already in this tree) and XFS `xfs_file_write_zero_eof()`.
Remaining FUSE holes not covered here: `copy_file_range` (truncates only
the copied range), getattr-driven remote size growth in
`fuse_change_attributes()`. Incomplete coverage is not a reason to skip
the three primary paths.
---
## PHASE 6: CROSS-REFERENCING AGAINST THE LOCAL TREE
**Step 6.1: DOES THE BUGGY CODE EXIST IN THIS TREE?**
Record: **Yes.**
- `fuse_do_setattr()`: `fs/fuse/dir.c` ~2061–2065 — size change →
`truncate_pagecache` + `invalidate_inode_pages2` only (no hole zero on
extend). Dirty mmap pages can survive invalidate.
- `fuse_perform_write()`: `fs/fuse/file.c` ~1294–1305 — no
`truncate_pagecache_range`.
- `fuse_file_fallocate()`: `fs/fuse/file.c` ~3223–3230 — extend updates
`i_size` only; range truncate is punch/zero only.
Introduced years before 6.12 (2008–2014). `git log --grep="zero the
partial EOF"` on `fs/fuse/` is empty here.
**Step 6.2: CHECK FOR BACKPORT COMPLICATIONS**
Record: **Minor context only.** setattr/fallocate match. Write function
exists with the same `pos` / `FUSE_I_SIZE_UNSTABLE` shape; later in the
function 6.12 still uses pages, not folios. No semantic rework required.
**Step 6.3: CHECK IF RELATED FIXES ARE ALREADY HERE**
Record: This fix is not present. No earlier FUSE EOF-tail zeroing. ext4
analog is present (`b6ce2dbe984bc`).
Note: the commit’s “writeback_cache uses `iomap_file_buffered_write()`”
is mainline. **6.12 writeback still uses `generic_file_write_iter` →
`fuse_write_begin`/`fuse_write_end`.** That does not remove the
writethrough bug this patch fixes (`fuse_cache_write_iter` non-wb path
still calls `fuse_perform_write()`). setattr/fallocate changes still
apply to both modes.
---
## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT
**Step 7.1: IDENTIFY THE SUBSYSTEM AND ITS CRITICALITY**
Record: **fs/fuse — IMPORTANT.** Not mm/VFS-core, but FUSE is widely
used (sshfs, fuse-overlayfs, ntfs-3g, virtiofs, container/user mounts).
Data served from the page cache is wrong.
**Step 7.2: ASSESS SUBSYSTEM ACTIVITY**
Record: Actively maintained (Miklos). This is a long-standing hole, not
churn from a brand-new API.
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
**Step 8.1: DETERMINE WHO IS AFFECTED**
Record: **FUSE users with page cache kept and without writeback_cache**
(common). Config-specific (`CONFIG_FUSE_FS`), not a single obscure
driver.
**Step 8.2: DETERMINE THE TRIGGER CONDITIONS**
Record: Mid-page EOF + mmap store past EOF (legal on the last page) +
extend via `pwrite`/`ftruncate`/`fallocate`. Not every write; well-known
and reproducible (selftest + libfuse passthrough). Unprivileged if the
user can mmap/write the file.
**Step 8.3: DETERMINE THE FAILURE MODE SEVERITY**
Record: **Stale data returned as file contents / POSIX hole not zero —
HIGH (data integrity).** Not a panic. Same class stable already took for
ext4.
**Step 8.4: CALCULATE RISK-BENEFIT RATIO**
Record: **Benefit high** (wrong reads on a common FUSE config). **Risk
low** (~12 lines, existing helper, maintainer-applied, selftest). Ratio
strongly favors backport.
---
## PHASE 9: FINAL SYNTHESIS
**Step 9.1: COMPILE THE EVIDENCE**
For:
- Real POSIX/data-integrity bug; selftest fails without the fix
- Small, surgical, no new API
- Buggy code present in 6.12 for years
- Same pattern as ext4, already backported here
- Maintainer applied v4 after review
- Default `FOPEN_KEEP_CACHE`; userspace-reachable
- Failed-write side effect is only clearing post-EOF cache
Against:
- Trigger needs mmap-past-EOF then extend (not every I/O)
- Does not cover `copy_file_range` / getattr size updates
- Write hunk may need a one-line context tweak (pages vs folios)
- No explicit Cc: stable (expected)
Unresolved:
- Exact mainline SHA (not in this tree; git.kernel.org blocked)
- stable@ thread (lore 403)
- Follow-ups after Miklos applied (none seen in the 4-message v4 thread)
**Step 9.2: APPLY THE STABLE RULES CHECKLIST**
1. Obviously correct and tested? **PASS** — helper is standard; UML
selftest; maintainer applied
2. Fixes a real bug that affects users? **PASS** — stale reads vs zeros
3. Important (crash/security/corruption/deadlock)? **PASS** — data
integrity / POSIX hole semantics
4. Small and contained? **PASS** — 12 lines, two files, three call sites
5. No new features or APIs? **PASS**
6. Can apply to the local tree? **PASS** — logic applies; write hunk may
need trivial context
**Step 9.3: CHECK FOR EXCEPTION CATEGORIES**
Record: Not a device-id/quirk/DT/build/docs exception. Qualifies as a
normal important bugfix.
**Step 9.4: MAKE YOUR DECISION**
Backport. The bug exists in this 6.12 tree, the fix is small and already
the form the maintainer wanted, and the user-visible failure is wrong
file data.
---
### What the commit solves for stable users
On cached, non-writeback FUSE, extending a file (write past EOF,
`ftruncate` up, `fallocate` without `KEEP_SIZE`) leaves mmap-dirtied
bytes in the old last page. Those bytes become part of the file and are
returned to `read` instead of zeros. That is a data-integrity / POSIX
violation. 6.12’s three extend paths match the buggy mainline code.
### Risk vs benefit
Benefit is preventing stale data on a common FUSE configuration. Risk is
a few `truncate_pagecache_range()` calls on a hole that must be zeros
anyway. Companion selftest is optional and this tree lacks the fuse
selftest directory.
---
### Verification
- **[Phase 1]** Tags: author Jimmy Zuber; Miklos Szeredi SOB; no
Fixes/Reported-by/Cc: stable
- **[Phase 1]** Cover letter v4: msgid
`20260824143052.1546163-1-jamz@xxxxxxxxxx`; tested UML + libfuse
passthrough_ll
- **[Phase 2]** Diff: +3 dir.c setattr, +4 perform_write, +5 fallocate;
12 lines
- **[Phase 2]** `truncate_pagecache_range()` /
`truncate_inode_partial_folio()` / `folio_zero_range()` read in
`mm/truncate.c`
- **[Phase 3]** blame: setattr guard `8373200b124d03` (2014); write path
`ea9b9907b82a09` (2008); fallocate `bee6c307800bbb` (2013)
- **[Phase 3]** No Fixes: tag; `git log --grep="zero the partial EOF" --
fs/fuse/` empty in this tree
- **[Phase 3]** ext4 analog `b6ce2dbe984bc` is an ancestor of HEAD
- **[Phase 4]** `b4 mbox` msgid
`20260824143052.1546163-2-jamz@xxxxxxxxxx`: 4 messages; Miklos
“Applied, thanks.”
- **[Phase 4]** v1→v4: moved zeroing off `fuse_write_update_attr`;
dropped custom helper
- **[Phase 4]** lore.kernel.org search 403 — UNVERIFIED stable@
discussion
- **[Phase 5]** Callers: `fuse_file_write_iter` →
`fuse_cache_write_iter` → `fuse_perform_write` under `inode_lock`;
setattr; `.fallocate`
- **[Phase 5]** Default `FOPEN_KEEP_CACHE` at `fs/fuse/file.c:151`
- **[Phase 6]** `git describe HEAD` → `p-6.12-3-gc504cf0baa3d8`; `make
kernelversion` → 6.12.111
- **[Phase 6]** Buggy snippets present at dir.c:2061–2065,
file.c:1294–1305, file.c:3223–3230
- **[Phase 6]** 6.12 uses `fuse_pages_alloc`, not `fuse_folios_alloc` —
minor apply context
- **[Phase 6]** 6.12 writeback_cache uses `fuse_write_begin`, not
`iomap_file_buffered_write` (commit text is mainline-specific)
- **[Phase 8]** Failure mode: stale page-cache data on read after
extend; severity HIGH
- **UNVERIFIED:** exact mainline SHA; post-apply follow-ups beyond the
v4 thread; copy_file_range/getattr as extra bugs (not used to vote NO)
**YES**
fs/fuse/dir.c | 3 +++
fs/fuse/file.c | 9 +++++++++
2 files changed, 12 insertions(+)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a013fad843b2c..9e20bc9dbdac4 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2095,6 +2095,9 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
*/
if ((is_truncate || !is_wb) &&
S_ISREG(inode->i_mode) && oldsize != outarg.attr.size) {
+ if (outarg.attr.size > oldsize)
+ truncate_pagecache_range(inode, oldsize,
+ outarg.attr.size - 1);
truncate_pagecache(inode, outarg.attr.size);
invalidate_inode_pages2(mapping);
}
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 0812eee881519..a7a3ac5b34f42 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1290,9 +1290,13 @@ static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii)
struct fuse_conn *fc = get_fuse_conn(inode);
struct fuse_inode *fi = get_fuse_inode(inode);
loff_t pos = iocb->ki_pos;
+ loff_t old_size = i_size_read(inode);
int err = 0;
ssize_t res = 0;
+ if (pos > old_size)
+ truncate_pagecache_range(inode, old_size, pos - 1);
+
if (inode->i_size < pos + iov_iter_count(ii))
set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state);
@@ -2952,6 +2956,11 @@ static long fuse_file_fallocate(struct file *file, int mode, loff_t offset,
/* we could have extended the file */
if (!(mode & FALLOC_FL_KEEP_SIZE)) {
+ loff_t oldsize = i_size_read(inode);
+
+ if (offset + length > oldsize)
+ truncate_pagecache_range(inode, oldsize,
+ offset + length - 1);
if (fuse_write_update_attr(inode, offset + length, length))
file_update_time(file);
}
--
2.53.0