[GH-ISSUE #41] kernel: ftruncate of a mapped file leaves the pages past EOF mapped (no SIGBUS, stale data) #36

Closed
opened 2026-11-03 22:02:42 -07:00 by gitea-mirror · 1 comment
Owner

Originally created by @raynorpat on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/41

Symptom

After ftruncate() shrinks a file that a process has mapped MAP_SHARED, the process can still read the old contents of the pages past the new end of file, and writes there succeed. No SIGBUS is raised. POSIX requires SIGBUS for a reference to a mapped page that lies wholly beyond EOF.

Reproduce

stress-mmap trunc from the new stress suite (src/Commands/stress, ffs-perf branch). It maps a 16-page file shared, fills it, truncates it to 5 pages, then touches page 7:

mmap/stress-mmap -d /tmp trunc
STRESS mmap trunc FAIL reason=no-sigbus seed=1791946213

A standalone check gave the same result:

  • After ftruncate from 16 pages to 5, reading page 5 or page 7 through the existing shared map returns the old data (0x11 fill).
  • A write to those pages also succeeds.
  • An access to a truly unmapped address does raise SIGBUS, so the test's handler works.

It fails on both kernels tested:

  • the v2 image's master + ffs-fixes kernel (kernel-155.0.0-7);
  • the ffs-perf kernel that also has the MapFS coherency fix (7ed3069ab).

So it is not the MapFS read/write coherency bug.

Where to look

ffs_truncate() in src/kernel-7/bsd/ufs/ffs/ffs_inode.c:

  • When the vnode is mapped (v_vm_info->mapped), the code at lines 307-314 and 350-356 skips vnode_pager_setsize(). It relies on mapfs_trunc() (lines 265-280) instead.
  • mapfs_trunc() deals with the kernel's own MapFS mapping. Nothing appears to shrink the pager object or remove the pages a user mmap has resident beyond the new size. So those pmap entries survive and keep pointing at the old pages, and a later fault in that range is satisfied instead of failing.
  • The other filesystems' setattr/truncate paths (ext2fs, msdosfs, hfs, nfs) probably share the pattern and should be checked the same way.

A fix would likely remove all mappings of pages at or above the new size (pmap/vm_object page removal over the truncated range) and shrink the pager size, even when the vnode is mapped. Faults past EOF would then go to the pager and fail with SIGBUS.

Impact

  • Wrong data: programs see stale data past EOF.
  • Lost writes: writes past EOF go into pages that are no longer backed by the file, so they are silently lost.
  • Disk reuse: freed disk blocks can be reallocated while old page contents are still mapped, so the page cache and the disk disagree.

This is a correctness bug for anything that truncates a file it, or another process, has mapped: databases, logs, dyld caches.

Related: the MapFS coherency fix 7ed3069ab (write() vs mmap).

Originally created by @raynorpat on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/41 ## Symptom After `ftruncate()` shrinks a file that a process has mapped `MAP_SHARED`, the process can still read the old contents of the pages past the new end of file, and writes there succeed. No SIGBUS is raised. POSIX requires SIGBUS for a reference to a mapped page that lies wholly beyond EOF. ## Reproduce `stress-mmap trunc` from the new stress suite (`src/Commands/stress`, ffs-perf branch). It maps a 16-page file shared, fills it, truncates it to 5 pages, then touches page 7: ``` mmap/stress-mmap -d /tmp trunc STRESS mmap trunc FAIL reason=no-sigbus seed=1791946213 ``` A standalone check gave the same result: - After `ftruncate` from 16 pages to 5, reading page 5 or page 7 through the existing shared map returns the old data (`0x11` fill). - A write to those pages also succeeds. - An access to a truly unmapped address does raise SIGBUS, so the test's handler works. It fails on both kernels tested: - the v2 image's master + ffs-fixes kernel (kernel-155.0.0-7); - the ffs-perf kernel that also has the MapFS coherency fix (7ed3069ab). So it is not the MapFS read/write coherency bug. ## Where to look `ffs_truncate()` in `src/kernel-7/bsd/ufs/ffs/ffs_inode.c`: - When the vnode is mapped (`v_vm_info->mapped`), the code at lines 307-314 and 350-356 skips `vnode_pager_setsize()`. It relies on `mapfs_trunc()` (lines 265-280) instead. - `mapfs_trunc()` deals with the kernel's own MapFS mapping. Nothing appears to shrink the pager object or remove the pages a *user* `mmap` has resident beyond the new size. So those pmap entries survive and keep pointing at the old pages, and a later fault in that range is satisfied instead of failing. - The other filesystems' setattr/truncate paths (ext2fs, msdosfs, hfs, nfs) probably share the pattern and should be checked the same way. A fix would likely remove all mappings of pages at or above the new size (pmap/vm_object page removal over the truncated range) and shrink the pager size, even when the vnode is mapped. Faults past EOF would then go to the pager and fail with SIGBUS. ## Impact - **Wrong data:** programs see stale data past EOF. - **Lost writes:** writes past EOF go into pages that are no longer backed by the file, so they are silently lost. - **Disk reuse:** freed disk blocks can be reallocated while old page contents are still mapped, so the page cache and the disk disagree. This is a correctness bug for anything that truncates a file it, or another process, has mapped: databases, logs, `dyld` caches. Related: the MapFS coherency fix 7ed3069ab (`write()` vs `mmap`).
gitea-mirror 2026-11-03 22:02:42 -07:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@raynorpat commented on GitHub (Oct 14, 2026):

Fixed on branch fs-races (commit 76c273754), not yet merged to master.

Root cause: vno_flush() freed the object's pages without pmap_remove_all(), so user mappings kept pointing at the old pages. mapfs_trunc() also skipped the flush entirely when the kernel had no MapFS mapping of the file, and vnode_pagein() zero-filled pages past EOF instead of failing them.

Fix: vno_flush removes pmap mappings before freeing and finds the object through the pager when the kernel has no mapping; mapfs_trunc flushes in the unmapped case; vnode_pagein returns PAGER_ERROR for pages wholly past EOF.

Tested on an i386 QEMU guest (snapshot of the v2 image): stress-mmap -d /tmp trunc is FAIL reason=no-sigbus on the stock kernel every time, and PASS on the fixed kernel (5 of 5). The rest of the stress suite (proc, vm, fs, net, ipc, thread) shows no regressions. Two failures are the same on the stock kernel, so they are not caused by this change: mmap mixed (data mismatch) and thread signal (timeout).

Not checked: ext2fs, msdosfs, hfs and nfs truncate paths. They call mapfs_trunc too, so they pick up the vno_flush change, but I did not test them.

<!-- gh-comment-id:6172418034 --> @raynorpat commented on GitHub (Oct 14, 2026): Fixed on branch `fs-races` (commit 76c273754), not yet merged to master. Root cause: `vno_flush()` freed the object's pages without `pmap_remove_all()`, so user mappings kept pointing at the old pages. `mapfs_trunc()` also skipped the flush entirely when the kernel had no MapFS mapping of the file, and `vnode_pagein()` zero-filled pages past EOF instead of failing them. Fix: `vno_flush` removes pmap mappings before freeing and finds the object through the pager when the kernel has no mapping; `mapfs_trunc` flushes in the unmapped case; `vnode_pagein` returns PAGER_ERROR for pages wholly past EOF. Tested on an i386 QEMU guest (snapshot of the v2 image): `stress-mmap -d /tmp trunc` is `FAIL reason=no-sigbus` on the stock kernel every time, and PASS on the fixed kernel (5 of 5). The rest of the stress suite (proc, vm, fs, net, ipc, thread) shows no regressions. Two failures are the same on the stock kernel, so they are not caused by this change: `mmap mixed` (data mismatch) and `thread signal` (timeout). Not checked: ext2fs, msdosfs, hfs and nfs truncate paths. They call `mapfs_trunc` too, so they pick up the `vno_flush` change, but I did not test them.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
github-starred/RhapsodiOS#36
No description provided.