[GH-ISSUE #59] kernel: mapped-file shrink then regrow loses retained data on UFS and resurrects stale data on msdosfs #54

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

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

Symptom

A file that is written through a MAP_SHARED mapping, shrunk with ftruncate while still mapped, unmapped, then grown back with ftruncate comes back wrong when read with read():

Filesystem First wrong byte What is wrong
UFS offset 0, below the truncation point Data loss: the dirty mapped data that the truncate should have kept is gone
FAT16 (msdosfs) offset 20480, the old EOF Stale data: the old contents past the truncated EOF reappear where the regrown file must read zeros

Both reproduce the same way on the unmodified issue-43 kernel (mach_kernel.issue43) and on the #52–#55 repair kernel (260033acb, cksum 3500498149). They are older than those fixes. The same sequence done with write() instead of a mapping passes on FAT16. ext2fs passes it after #52's fix.

Reproduce

vm/ext2_mmap_truncate_test.c (added in 260033acb), on a scratch file of the filesystem under test:

cc -O -o t vm/ext2_mmap_truncate_test.c
./t /mnt/u/t     # UFS scratch mount  -> "regrowth mismatch at 0, length=20480"
./t /mnt/m/t     # FAT16 mount        -> "regrowth mismatch at 20480, length=20480"

The test does this:

  1. Create the file and ftruncate it to 64 KiB (sparse).
  2. mmap it MAP_SHARED and write a pattern to every byte.
  3. ftruncate to length (20480 in the failing case) while still mapped.
  4. Check that the mapping still shows the pattern below length. This step passes.
  5. munmap, then ftruncate back to 64 KiB, fsync, close.
  6. Reopen and read() everything. Bytes below length must hold the pattern, bytes from length up must be zero.

Logs: vm/work/issues52-55/res/ufs-regrow*.log, msdos-regrow-baseline.log, msdos-truncate.log, msdos-write-regrow.log (the passing write() variant).

Where to look

  • UFS, data below the cut lost: ffs_truncate → mapfs_trunc / vnode_pager_setsize. The truncate must keep, and later write back, dirty mapped pages below the new EOF. Something discards them instead. Possibilities:
    • the whole object's pages are invalidated rather than only those past length;
    • the dirty bit is lost when the pages are flushed before the block release;
    • the pages are never pushed before munmap/fsync.
      Compare with ext2fs after #52's fix, which passes this test.
  • FAT16, stale data past EOF: msdosfs detrunc and the regrowth path in msdosfs_vnops.c (deextend). When the file grows again, the clusters, or the cached pages past the old EOF, must read as zero. The write() path zeroes them; the mapped path leaves the old contents, either in the VM object's cached pages or in clusters that were not cleared.

Found by the #52–#55 repair work (see vm/filesystem-issues-52-55.md in 260033acb). Related: #41 (truncate of a mapped file), #52, #55.

Originally created by @raynorpat on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/59 ## Symptom A file that is written through a `MAP_SHARED` mapping, shrunk with `ftruncate` while still mapped, unmapped, then grown back with `ftruncate` comes back wrong when read with `read()`: | Filesystem | First wrong byte | What is wrong | |---|---|---| | UFS | offset **0**, below the truncation point | **Data loss:** the dirty mapped data that the truncate should have kept is gone | | FAT16 (msdosfs) | offset **20480**, the old EOF | **Stale data:** the old contents past the truncated EOF reappear where the regrown file must read zeros | Both reproduce the same way on the unmodified `issue-43` kernel (`mach_kernel.issue43`) and on the #52–#55 repair kernel (260033acb, cksum 3500498149). They are older than those fixes. The same sequence done with `write()` instead of a mapping passes on FAT16. ext2fs passes it after #52's fix. ## Reproduce `vm/ext2_mmap_truncate_test.c` (added in 260033acb), on a scratch file of the filesystem under test: ``` cc -O -o t vm/ext2_mmap_truncate_test.c ./t /mnt/u/t # UFS scratch mount -> "regrowth mismatch at 0, length=20480" ./t /mnt/m/t # FAT16 mount -> "regrowth mismatch at 20480, length=20480" ``` The test does this: 1. Create the file and `ftruncate` it to 64 KiB (sparse). 2. `mmap` it `MAP_SHARED` and write a pattern to every byte. 3. `ftruncate` to `length` (20480 in the failing case) while still mapped. 4. Check that the mapping still shows the pattern below `length`. This step passes. 5. `munmap`, then `ftruncate` back to 64 KiB, `fsync`, `close`. 6. Reopen and `read()` everything. Bytes below `length` must hold the pattern, bytes from `length` up must be zero. Logs: `vm/work/issues52-55/res/ufs-regrow*.log`, `msdos-regrow-baseline.log`, `msdos-truncate.log`, `msdos-write-regrow.log` (the passing `write()` variant). ## Where to look - **UFS, data below the cut lost:** `ffs_truncate` → `mapfs_trunc` / `vnode_pager_setsize`. The truncate must keep, and later write back, dirty mapped pages below the new EOF. Something discards them instead. Possibilities: - the whole object's pages are invalidated rather than only those past `length`; - the dirty bit is lost when the pages are flushed before the block release; - the pages are never pushed before `munmap`/`fsync`. Compare with ext2fs after #52's fix, which passes this test. - **FAT16, stale data past EOF:** msdosfs `detrunc` and the regrowth path in `msdosfs_vnops.c` (`deextend`). When the file grows again, the clusters, or the cached pages past the old EOF, must read as zero. The `write()` path zeroes them; the mapped path leaves the old contents, either in the VM object's cached pages or in clusters that were not cleared. Found by the #52–#55 repair work (see `vm/filesystem-issues-52-55.md` in 260033acb). Related: #41 (truncate of a mapped file), #52, #55.
Author
Owner

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

Fixed on ffs-perf (merge d6aad145f of branch issue-59).

  • b0a796ecd, stale data past the cut (FAT16, and UFS too). The not-kernel-mapped branch of mapfs_trunc (kern/mapfs.c ~639) freed the pages past round_page(length), but left the page holding the new end resident, still carrying what the mapping wrote past the cut. A new vno_flush_eof() writes that page back, stopping at the new EOF, and then frees it.
  • 2939f6a31, UFS lost the data below the cut. ffs_truncate called vnode_uncache (ffs_inode.c :311 and :342), which made the VM object non-persistent. vm_object_terminate then freed the dirty mapped pages without writing them back. Those calls now run only without MACH_NBC, like ffs_write.
  • 44d06b87d: a stress mmap regrow test.

Two existing ext2 deadlocks surfaced under the stress suite's concurrent mmap load, while verifying this fix. Both are fixed in the same merge:

  • 6d3b2c1a2: ext2fs_vget_internal kept the inode-table block busy across getnewvnode. A recycled vnode's inactive was waiting on that same block.
  • 631eb5071: vnode_pagein set nfspagereq only after vn_lock, so a pusher holding the vnode waited forever on the busy page.

Verified on a kernel with these fixes plus a rebuilt driverkit (cksum 1566268189):

  • vm/ext2_mmap_truncate_test.c at all 4 lengths, and 28 regrow variants, pass on UFS, FAT16 and ext2.
  • The #41, #52 and #55 reproducers and make check-mmap pass.
  • The suite's concurrent mmap/runtests -n 4 passes on UFS and FAT16, and passes on ext2 in 3 of 4 runs, with no hangs.
  • fsck -n, fsck_msdos -n and e2fsck -fn are clean.

Still open, tracked separately:

  • An ext2 mmap sync read-back failure (zeros after offset 0x36) seen once under concurrent load.
  • Data written through a mapping and never msync'd is lost across sync; umount; mount on UFS and FAT16. This happens on the unmodified kernel too.
<!-- gh-comment-id:6221357539 --> @raynorpat commented on GitHub (Oct 16, 2026): Fixed on ffs-perf (merge d6aad145f of branch issue-59). - **b0a796ecd, stale data past the cut (FAT16, and UFS too).** The not-kernel-mapped branch of `mapfs_trunc` (`kern/mapfs.c` ~639) freed the pages past `round_page(length)`, but left the page holding the new end resident, still carrying what the mapping wrote past the cut. A new `vno_flush_eof()` writes that page back, stopping at the new EOF, and then frees it. - **2939f6a31, UFS lost the data below the cut.** `ffs_truncate` called `vnode_uncache` (`ffs_inode.c` :311 and :342), which made the VM object non-persistent. `vm_object_terminate` then freed the dirty mapped pages without writing them back. Those calls now run only without MACH_NBC, like `ffs_write`. - **44d06b87d:** a stress `mmap regrow` test. Two existing ext2 deadlocks surfaced under the stress suite's concurrent mmap load, while verifying this fix. Both are fixed in the same merge: - **6d3b2c1a2:** `ext2fs_vget_internal` kept the inode-table block busy across `getnewvnode`. A recycled vnode's inactive was waiting on that same block. - **631eb5071:** `vnode_pagein` set `nfspagereq` only after `vn_lock`, so a pusher holding the vnode waited forever on the busy page. **Verified** on a kernel with these fixes plus a rebuilt driverkit (cksum 1566268189): - `vm/ext2_mmap_truncate_test.c` at all 4 lengths, and 28 regrow variants, pass on UFS, FAT16 and ext2. - The #41, #52 and #55 reproducers and `make check-mmap` pass. - The suite's concurrent `mmap/runtests -n 4` passes on UFS and FAT16, and passes on ext2 in 3 of 4 runs, with no hangs. - `fsck -n`, `fsck_msdos -n` and `e2fsck -fn` are clean. **Still open, tracked separately:** - An ext2 `mmap sync` read-back failure (zeros after offset 0x36) seen once under concurrent load. - Data written through a mapping and never msync'd is lost across `sync; umount; mount` on UFS and FAT16. This happens on the unmodified kernel too.
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#54
No description provided.