[PATCH AUTOSEL 6.18-5.10] hfsplus: fix issue of direct writes beyond end-of-file
From: Sasha Levin
Date: Mon Aug 31 2026 - 12:19:06 EST
From: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
[ Upstream commit 5f63ac80aef2ee6bb58eab62e98c264774872da6 ]
The xfstests' test-case generic/729 fails with error:
sudo ./check generic/729
FSTYP -- hfsplus
PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #36 SMP PREEMPT_DYNAMIC Fri Apr 17 12:40:51 PDT 2026
MKFS_OPTIONS -- /dev/loop51
MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch
generic/729 23s ... [failed, exit status 1]- output mismatch
mmap-rw-fault: /mnt/test/mmap-rw-fault.tmp: Input/output error
The hfsplus_get_block() only allows creating the next
sequential block. It returns -EIO for direct writes
beyond EOF. This patch waits for any in-flight DIO on the inode
to finish. Then, it extends the file by calling
generic_cont_expand_simple() with the goal to guarantee
that blockdev_direct_IO() finds all needed blocks
already reachable sequentially. And, finally, it flushes and
invalidates the DIO range again so the page cache is clean
before the direct write begins.
sudo ./check generic/729
FSTYP -- hfsplus
PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #40 SMP PREEMPT_DYNAMIC Thu Apr 16 15:41:03 PDT 2026
MKFS_OPTIONS -- /dev/loop51
MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch
generic/729 23s ... 32s
Ran: generic/729
Passed all 1 tests
Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/210
cc: John Paul Adrian Glaubitz <glaubitz@xxxxxxxxxxxxxxxxxxx>
cc: Yangtao Li <frank.li@xxxxxxxx>
cc: linux-fsdevel@xxxxxxxxxxxxxxx
Signed-off-by: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
Link: https://lore.kernel.org/r/20260417214940.2735557-2-slava@xxxxxxxxxxx
Signed-off-by: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---
LLM Generated explanations, may be completely bogus:
# Stable Backport Analysis: `hfsplus: fix issue of direct writes beyond
end-of-file`
**Local tree:** Linux **6.18.43** (`git describe HEAD` → `v6.18.43`)
---
## PHASE 1: COMMIT MESSAGE FORENSICS
### Step 1.1: Subject Line
**Record:** `[hfsplus] [fix] [direct writes beyond end-of-file fail with
-EIO]`
### Step 1.2: Tags
**Record:**
- **Fixes:** — absent (expected for manual review)
- **Reported-by:** — absent
- **Tested-by:** — absent
- **Reviewed-by:** — absent
- **Acked-by:** — absent
- **Link:**
`https://lore.kernel.org/r/20260417214940.2735557-2-slava@xxxxxxxxxxx`
- **Closes:** `https://github.com/hfs-linux-kernel/hfs-linux-
kernel/issues/210`
- **cc:** John Paul Adrian Glaubitz, Yangtao Li, linux-
fsdevel@xxxxxxxxxxxxxxx
- **Signed-off-by:** Viacheslav Dubeyko (author; ignore pipeline SOBs)
Notable: GitHub issue documents reproducible xfstests failure; no
syzbot/KASAN signals.
### Step 1.3: Body Analysis
**Record:**
- **Bug:** `hfsplus_get_block()` only allocates the *next sequential*
block (`iblock > hip->fs_blocks` → `-EIO`). Direct I/O writes starting
beyond EOF hit this path and fail.
- **Symptom:** xfstests `generic/729` fails with `mmap-rw-fault: ...
Input/output error` (userspace EIO).
- **Root cause:** DIO bypasses `cont_write_begin()` / page-cache
expansion that buffered writes use; `blockdev_direct_IO()` calls
`hfsplus_get_block()` with `create=1` on blocks beyond the current
allocation frontier.
- **Fix approach:** Before DIO write when `ki_pos > i_size`: wait for
in-flight DIO, expand via `generic_cont_expand_simple()`, flush and
invalidate the affected page-cache range, then proceed with
`blockdev_direct_IO()`.
- **Version info:** Issue filed against 6.15.0-rc4+; fix verified on
7.0.0-rc1+ per commit message and GitHub issue.
### Step 1.4: Hidden Bug Fix?
**Record:** No — this is an explicit functional bug fix, not disguised
cleanup. It corrects incorrect `-EIO` on a valid I/O path.
---
## PHASE 2: DIFF ANALYSIS
### Step 2.1: Inventory
**Record:**
- **Files:** `fs/hfsplus/inode.c` only (+34 / −2 lines)
- **Function modified:** `hfsplus_direct_IO()`
- **Scope:** Single-file, surgical fix in one function
### Step 2.2: Code Flow Change
**Record:**
| Hunk | Before | After |
|------|--------|-------|
| Pre-DIO path | Immediately calls `blockdev_direct_IO()` | For WRITE
with `ki_pos > i_size`: `inode_dio_wait()` →
`generic_cont_expand_simple()` → `filemap_write_and_wait_range()` →
`invalidate_inode_pages2_range()`, then DIO |
| Error cleanup | Declares local `isize`/`end` in error block | Reuses
`isize`/`end` hoisted to function scope |
Affected path: **O_DIRECT write beyond current EOF** (sparse extension /
hole before write).
### Step 2.3: Bug Mechanism
**Record:** **Logic / correctness fix** in filesystem block allocation.
In `hfsplus_get_block()`:
```239:243:fs/hfsplus/extents.c
if (iblock >= hip->fs_blocks) {
if (!create)
return 0;
if (iblock > hip->fs_blocks)
return -EIO;
```
Only `iblock == hip->fs_blocks` (next block) can be created. A DIO write
at offset 4096 on a zero-length file needs `iblock > fs_blocks` →
`-EIO`. Buffered writes avoid this via `cont_write_begin()` in
`hfsplus_write_begin()`.
### Step 2.4: Fix Quality
**Record:**
- **Obviously correct:** Mirrors the established pattern in
`hfsplus_setattr()` (same file, lines 278–284): `inode_dio_wait()` +
`generic_cont_expand_simple()`.
- **Minimal:** Only touches the DIO write-beyond-EOF case.
- **Regression risk:** Low — narrow trigger (`WRITE && ki_pos >
i_size`), uses standard VFS helpers already used elsewhere in hfsplus.
- **No new APIs or public interface changes.**
---
## PHASE 3: GIT HISTORY INVESTIGATION
### Step 3.1: Blame
**Record:** `hfsplus_direct_IO()` and the `iblock > hip->fs_blocks`
check both blame to `19eef1d98eeda` in this tree (a history-rewrite
artifact in the stable queue). The sequential-block constraint in
`hfsplus_get_block()` is longstanding hfsplus design; the DIO path has
lacked pre-expansion since `hfsplus_direct_IO` was wired into
`hfsplus_aops`.
### Step 3.2: Fixes: Tag
**Record:** N/A — no `Fixes:` tag present.
### Step 3.3: Related File History
**Record:** Recent `fs/hfsplus/inode.c` history in **this tree**
includes multiple backported hfsplus xfstests fixes from the same
author:
- `956b1d8051cfa` — generic/498 (volume corruption)
- `54694417d4384` — generic/480
- `66e2f3c1aefea` — generic/101
This fix is **standalone** (not part of a multi-patch series in the
commit message).
### Step 3.4: Author Context
**Record:** Viacheslav Dubeyko is an active hfsplus contributor;
multiple hfsplus fixes from this author are already in Linux 6.18.43.
### Step 3.5: Dependencies
**Record:** No prerequisite commits required. All APIs exist in this
tree:
- `generic_cont_expand_simple()` — `fs/buffer.c:2473`
- `inode_dio_wait()` — `fs/inode.c:2659`
- `filemap_write_and_wait_range()`, `invalidate_inode_pages2_range()` —
standard VFS
- Already used in `hfsplus_setattr()` at lines 278–284 of the same file
---
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
### Step 4.1: Original Discussion
**Record:** `b4 dig -c <sha>` could not be run — commit is not in this
checkout. Lore URL blocked by Anubis bot protection. GitHub issue #210
confirms the bug and fix (opened 2025-05-27, closed 2026-04-23 after
generic/729 passed).
### Step 4.2: Reviewers
**Record:** UNVERIFIED — could not fetch lore thread. Commit cc's
fsdevel and hfsplus maintainers.
### Step 4.3: Bug Report
**Record:** [GitHub issue #210](https://github.com/hfs-linux-kernel/hfs-
linux-kernel/issues/210):
- Failure: `mmap-rw-fault: ... Input/output error`
- Reproducible since at least 6.15.0-rc4
- Fixed on 7.0.0-rc1+ with this patch
- **Severity from reporter:** xfstests regression; user-visible EIO, not
corruption/crash
### Step 4.4: Related Patches
**Record:** `generic/729` (added 2023) tests mmap + DIO write — extends
generic/647. It exercises direct writes beyond EOF followed by mmap
fault I/O. Same test class has exposed real bugs in btrfs (deadlock) and
NFS (EFAULT).
### Step 4.5: Stable List History
**Record:** UNVERIFIED — lore stable list not searchable due to bot
protection. Precedent exists in-tree: other Dubeyko hfsplus xfstests
fixes already backported to 6.18.y.
---
## PHASE 5: CODE SEMANTIC ANALYSIS
### Step 5.1: Key Functions
**Record:** `hfsplus_direct_IO()` (modified); `hfsplus_get_block()`
(buggy callee, unchanged).
### Step 5.2: Callers
**Record:** `hfsplus_direct_IO` is registered in
`hfsplus_aops.direct_IO` (line 173). Invoked from VFS when `O_DIRECT` is
set on hfsplus files — reachable from `pwrite()`, `io_uring`, and
xfstests `mmap-rw-fault` helper.
### Step 5.3: Callees
**Record:** `inode_dio_wait`, `generic_cont_expand_simple` (→
`hfsplus_write_begin` → `cont_write_begin`),
`filemap_write_and_wait_range`, `invalidate_inode_pages2_range`,
`blockdev_direct_IO`.
### Step 5.4: Reachability
**Record:** **Userspace-reachable** on any hfsplus mount with O_DIRECT
writes extending past EOF. `generic/729` is the concrete, reproducible
trigger.
### Step 5.5: Similar Patterns
**Record:** `hfsplus_setattr()` already uses `inode_dio_wait()` +
`generic_cont_expand_simple()` for size extension. `hfs`
(`fs/hfs/inode.c`) has a similar bare `hfs_direct_IO()` — potentially
the same class of bug, but out of scope for this commit.
---
## PHASE 6: CROSS-REFERENCE AGAINST LOCAL TREE (6.18.43)
### Step 6.1: Buggy Code Present?
**Record:** **YES.** Current `hfsplus_direct_IO()` at lines 123–145
calls `blockdev_direct_IO()` directly with no pre-expansion.
`hfsplus_get_block()` sequential-only create logic at
`extents.c:239–243` is present.
### Step 6.2: Backport Complications
**Record:** **Clean apply** — `git apply --check` with the full upstream
diff succeeds on `fs/hfsplus/inode.c` in this tree.
### Step 6.3: Related Fixes Already Present?
**Record:** **NO** — `git log --grep="729"` and `git log --grep="beyond
end-of-file"` find no matching fix. This commit is not yet applied.
---
## PHASE 7: SUBSYSTEM CONTEXT
### Step 7.1: Subsystem / Criticality
**Record:** **fs/hfsplus** — IMPORTANT (filesystem I/O correctness), not
CORE but affects all hfsplus users doing DIO.
### Step 7.2: Activity
**Record:** Actively maintained in 6.18.y — multiple recent hfsplus
xfstests fixes from the same author already landed in this stable
series.
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
### Step 8.1: Who Is Affected
**Record:** Users of hfsplus with `O_DIRECT` writes beyond EOF —
including Mac interoperability workloads, backup tools, and the standard
xfstests `generic/729` regression test.
### Step 8.2: Trigger Conditions
**Record:** `O_DIRECT` write where `ki_pos > i_size` (sparse extension).
Common in `generic/729` (truncate to 0, then write at offset 4096).
Unprivileged users can trigger on mounted hfsplus volumes they can write
to.
### Step 8.3: Failure Mode Severity
**Record:** Returns **-EIO** to userspace on valid I/O. No crash,
corruption, or deadlock documented for hfsplus. **Severity: MEDIUM** —
functional I/O failure / incorrect error, fits stable rules' "oh, that's
not good" category.
### Step 8.4: Risk-Benefit
**Record:**
- **Benefit:** MEDIUM — restores correct DIO semantics; passes standard
xfstests; fixes long-standing user-visible failure
- **Risk:** LOW — 34 lines, one function, mirrors existing
`hfsplus_setattr()` pattern, applies cleanly
- **Ratio:** Favorable for backport
---
## PHASE 9: FINAL SYNTHESIS
### Step 9.1: Evidence Summary
**FOR backport:**
- Real, reproducible bug (xfstests `generic/729`, GitHub #210)
- User-visible EIO on valid direct writes beyond EOF
- Small, surgical, obviously correct fix
- Uses APIs already present and used in same file
- Applies cleanly to Linux 6.18.43
- Same author/subsystem already has xfstests fixes in this stable series
- Tested before/after in commit message
**AGAINST backport:**
- Not crash/corruption/security — severity is functional I/O failure
- hfsplus is a niche filesystem
- Bug likely present since DIO support was added (long-standing)
- Lore review details unverified
**UNRESOLVED:**
- Full mailing-list review thread (lore blocked)
- Whether reviewers explicitly nominated for stable
### Step 9.2: Stable Rules Checklist
1. Obviously correct and tested? **PASS** — xfstests generic/729;
pattern proven in `hfsplus_setattr()`
2. Fixes a real bug affecting users? **PASS** — EIO on valid O_DIRECT
writes
3. Important issue? **PASS (MEDIUM)** — incorrect I/O failure on
standard xfstests path; "oh, that's not good" per stable-kernel-
rules.rst
4. Small and contained? **PASS** — 1 file, ~34 lines
5. No new features/APIs? **PASS** — uses existing VFS helpers
6. Can apply to local tree? **PASS** — clean `git apply --check`
### Step 9.3: Exception Category
**Record:** None (not device ID, quirk, DT, build fix, or docs).
Standard filesystem correctness fix.
### Step 9.4: Decision Rationale
For **Linux 6.18.43**, the buggy code is present, all dependencies
exist, the patch applies cleanly, and the fix addresses a reproducible
userspace I/O failure on a supported filesystem using established
patterns already in the same driver. While not as severe as the
corruption fixes already backported for hfsplus in this series, it meets
stable criteria: real bug, tested fix, low risk, user-visible impact.
The 6.18.y tree has already accepted similar hfsplus xfstests fixes from
this author.
---
## Verification
- **[Phase 1]** Parsed commit message, tags, and GitHub issue #210
(fetched successfully)
- **[Phase 2]** Read current `hfsplus_direct_IO()` at
`fs/hfsplus/inode.c:123–145` and `hfsplus_get_block()` at
`fs/hfsplus/extents.c:239–243`
- **[Phase 2]** Confirmed `hfsplus_setattr()` uses same expansion
pattern at `fs/hfsplus/inode.c:278–284`
- **[Phase 3]** `git describe HEAD` → v6.18.43; `make kernelversion`
equivalent via Makefile → 6.18.43
- **[Phase 3]** `git log --oneline -20 -- fs/hfsplus/inode.c` — found
related backported hfsplus fixes
- **[Phase 3]** `git blame` on changed lines — history rewrite artifact
noted
- **[Phase 3]** `git log --grep="729"` / `--grep="beyond end-of-file"` —
no existing fix in tree
- **[Phase 4]** GitHub issue #210 — confirmed failure mode and fix
verification
- **[Phase 4]** Web search — confirmed generic/729 tests mmap + DIO
write beyond EOF
- **[Phase 4]** UNVERIFIED: `b4 dig` (no commit SHA in tree),
lore.kernel.org (bot-blocked)
- **[Phase 5]** `grep hfsplus_direct_IO` — registered in `hfsplus_aops`
at line 173
- **[Phase 5]** `grep generic_cont_expand_simple` — present in
`fs/buffer.c:2473` and used in hfsplus
- **[Phase 6]** Confirmed buggy code present in current checkout
- **[Phase 6]** `git apply --check` with full upstream diff —
**succeeds**
- **[Phase 7]** Multiple Dubeyko hfsplus commits in `git log
--author="Dubeyko"` on this tree
- **[Phase 8]** Assessed severity as MEDIUM functional I/O failure, not
CRITICAL
**YES**The background git history search failed: it hit the 30s timeout
because the command fell through to `git log -S 'hfsplus_direct_IO'
--all`, which is exactly what we must avoid on this repo.
That didn’t block the backport analysis. I used narrower searches
instead (`git blame`, `git log -S ... -- fs/hfsplus/inode.c`), and the
conclusion for Linux **6.18.43** is unchanged: **YES** — the fix applies
cleanly and addresses a real O_DIRECT-beyond-EOF EIO bug in hfsplus.
fs/hfsplus/inode.c | 38 ++++++++++++++++++++++++++++++++++++--
1 file changed, 36 insertions(+), 2 deletions(-)
diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c
index 6153e5cc6eb65..7e6cc4dde9f26 100644
--- a/fs/hfsplus/inode.c
+++ b/fs/hfsplus/inode.c
@@ -125,9 +125,44 @@ static ssize_t hfsplus_direct_IO(struct kiocb *iocb, struct iov_iter *iter)
struct file *file = iocb->ki_filp;
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
+ loff_t isize;
size_t count = iov_iter_count(iter);
+ loff_t end = iocb->ki_pos + count;
ssize_t ret;
+ /*
+ * The hfsplus_get_block() only allows creating the next sequential block.
+ * For direct writes beyond EOF, expand the file first.
+ */
+ if (iov_iter_rw(iter) == WRITE && iocb->ki_pos > i_size_read(inode)) {
+ loff_t start_off, end_off;
+ loff_t start_page, end_page;
+
+ isize = i_size_read(inode);
+
+ /*
+ * Wait for any in-flight DIO on this inode to finish before
+ * calling generic_cont_expand_simple().
+ */
+ inode_dio_wait(inode);
+
+ ret = generic_cont_expand_simple(inode, iocb->ki_pos);
+ if (ret)
+ return ret;
+
+ start_off = isize;
+ end_off = (end > 0) ? end - 1 : end;
+
+ ret = filemap_write_and_wait_range(mapping, start_off, end_off);
+ if (ret)
+ return ret;
+
+ start_page = start_off >> PAGE_SHIFT;
+ end_page = end_off >> PAGE_SHIFT;
+
+ invalidate_inode_pages2_range(mapping, start_page, end_page);
+ }
+
ret = blockdev_direct_IO(iocb, inode, iter, hfsplus_get_block);
/*
@@ -135,8 +170,7 @@ static ssize_t hfsplus_direct_IO(struct kiocb *iocb, struct iov_iter *iter)
* blocks outside i_size. Trim these off again.
*/
if (unlikely(iov_iter_rw(iter) == WRITE && ret < 0)) {
- loff_t isize = i_size_read(inode);
- loff_t end = iocb->ki_pos + count;
+ isize = i_size_read(inode);
if (end > isize)
hfsplus_write_failed(mapping, end);
--
2.53.0