[GH-ISSUE #52] ext2fs: shrinking a mmap'd sparse file panics with "ext2fs_truncate1" #44

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

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

Symptom

On an ext2 scratch volume, stress-mmap trunc panics the kernel on its first
iteration, in every run that reached it (3 of 3):

panic: ext2fs_truncate1

(serial COM2 and VGA "System Panic" box; no backtrace printed). Kernel: mach_kernel.issue43 (cksum 1562297808, built
Oct 15, ffs+hfs+ext2fs+msdos, DIAGNOSTIC). The same test passes on UFS (/tmp)
and on HFS+, and stress-mmap shared mixed private pass on ext2.

After the panic, e2fsck -fn on the volume reports only leaked blocks
(block bitmap differences, wrong free counts); no inode or directory damage.

Reproduce

i386 QEMU guest, ext2 rev 1 volume (1 KiB blocks, 128-byte inodes, features
filetype,sparse_super, made by mke2fs -t ext2 -r 1 -O none,filetype,sparse_super -I 128 -b 1024)
wrapped by vm/ext2_guest.py wrap_volume, mounted with
mount_ext2fs /dev/hd2a /mnt/e. Then:

stress-mmap -n 1 -i 1 -s 1 -d /mnt/e trunc

which does, in one process:

  1. open(O_RDWR|O_CREAT|O_TRUNC), ftruncate(fd, 64 KiB) (sparse, no blocks)
  2. mmap(64 KiB, MAP_SHARED) and write every byte of all 16 pages
  3. ftruncate(fd, 20 KiB) <- panics here

With 1 KiB blocks, pages 3..4 (bytes 12..20 KiB) map through the single
indirect block, which does not exist yet when the truncate starts.

Artifacts: vm/work/fsrepro/ext2-1-panic.{ppm,png,log} (copy of ext2-trunc-n1i1-panic.*),
ext2-mmap-panic.{png,log} (first hit, from stress-mmap -n 3 -i 20 shared mixed private trunc),
ext2-after-panic-fsck.txt.

Where to look

bsd/ext2fs/ext2fs_inode.c, ext2fs_truncate(): the DIAGNOSTIC check after
done: finds newblks[NDADDR+SINGLE] != e2di_blocks[NDADDR+SINGLE], so the
single-indirect pointer changed between the VOP_UPDATE that saved
newblks and the end of the truncate. In that window the old block
pointers and osize are put back and vinvalbuf(V_SAVE) runs. The likeliest
cause is the dirty mapped pages being pushed (pageout through ufs_pageout /
VOP_WRITE / ext2fs_balloc) while the inode looks like the old 64 KiB file:
the push allocates the missing single-indirect block, so the pointer goes from
0 to a new block. ext2fs_vm_flush(..., truncate=1, length) / mapfs_trunc runs
before this and does not seem to stop it, perhaps because the pages below
length are pushed asynchronously, or because a push happens again from
vinvalbuf. Compare ffs_truncate, which passes the same test and also starts with
mapfs_trunc. On a kernel without DIAGNOSTIC, the same race would most likely
leave the inode pointing at a block that was allocated during the truncate,
or leak it.

Found by the stress suite (src/Commands/stress, branch stress-phase3) during the phase 3 filesystem run. Related: #45 (rename races, fixed on fs-races).

Originally created by @raynorpat on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/52 ## Symptom On an ext2 scratch volume, `stress-mmap trunc` panics the kernel on its first iteration, in every run that reached it (3 of 3): ``` panic: ext2fs_truncate1 ``` (serial COM2 and VGA "System Panic" box; no backtrace printed). Kernel: `mach_kernel.issue43` (cksum 1562297808, built Oct 15, ffs+hfs+ext2fs+msdos, DIAGNOSTIC). The same test passes on UFS (/tmp) and on HFS+, and `stress-mmap shared mixed private` pass on ext2. After the panic, `e2fsck -fn` on the volume reports only leaked blocks (block bitmap differences, wrong free counts); no inode or directory damage. ## Reproduce i386 QEMU guest, ext2 rev 1 volume (1 KiB blocks, 128-byte inodes, features `filetype,sparse_super`, made by `mke2fs -t ext2 -r 1 -O none,filetype,sparse_super -I 128 -b 1024`) wrapped by `vm/ext2_guest.py wrap_volume`, mounted with `mount_ext2fs /dev/hd2a /mnt/e`. Then: ``` stress-mmap -n 1 -i 1 -s 1 -d /mnt/e trunc ``` which does, in one process: 1. `open(O_RDWR|O_CREAT|O_TRUNC)`, `ftruncate(fd, 64 KiB)` (sparse, no blocks) 2. `mmap(64 KiB, MAP_SHARED)` and write every byte of all 16 pages 3. `ftruncate(fd, 20 KiB)` <- panics here With 1 KiB blocks, pages 3..4 (bytes 12..20 KiB) map through the single indirect block, which does not exist yet when the truncate starts. Artifacts: `vm/work/fsrepro/ext2-1-panic.{ppm,png,log}` (copy of `ext2-trunc-n1i1-panic.*`), `ext2-mmap-panic.{png,log}` (first hit, from `stress-mmap -n 3 -i 20 shared mixed private trunc`), `ext2-after-panic-fsck.txt`. ## Where to look `bsd/ext2fs/ext2fs_inode.c`, `ext2fs_truncate()`: the DIAGNOSTIC check after `done:` finds `newblks[NDADDR+SINGLE] != e2di_blocks[NDADDR+SINGLE]`, so the single-indirect pointer changed between the `VOP_UPDATE` that saved `newblks` and the end of the truncate. In that window the old block pointers and `osize` are put back and `vinvalbuf(V_SAVE)` runs. The likeliest cause is the dirty mapped pages being pushed (pageout through `ufs_pageout` / `VOP_WRITE` / `ext2fs_balloc`) while the inode looks like the old 64 KiB file: the push allocates the missing single-indirect block, so the pointer goes from 0 to a new block. `ext2fs_vm_flush(..., truncate=1, length)` / `mapfs_trunc` runs before this and does not seem to stop it, perhaps because the pages below `length` are pushed asynchronously, or because a push happens again from `vinvalbuf`. Compare `ffs_truncate`, which passes the same test and also starts with `mapfs_trunc`. On a kernel without DIAGNOSTIC, the same race would most likely leave the inode pointing at a block that was allocated during the truncate, or leak it. Found by the stress suite (`src/Commands/stress`, branch `stress-phase3`) during the phase 3 filesystem run. Related: #45 (rename races, fixed on fs-races).
Author
Owner

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

Fixed in 260033acbb (kernel: Merge filesystem repairs for issues 52-55), on ffs-perf; the ext2fs part is in ext2fs_vnops.c.

Verified on the repair kernel:

  • The original reproducer stress-mmap -n 1 -i 1 -s 1 -d /mnt/e trunc no longer panics.
  • The new focused test vm/ext2_mmap_truncate_test.c (dirty sparse mappings shrunk across direct/indirect block boundaries, partial-page EOF, truncate to zero, zero-filled regrowth) passes at all four lengths.
  • stress-mmap shared mixed private trunc -n 3 -i 20 -s 2 passes on ext2.
  • e2fsck -fn is clean afterwards.

Test notes and focused regressions: vm/filesystem-issues-52-55.md in that commit. Repair kernel cksum 3500498149 (i386, QEMU snapshot); PowerPC not tested.

<!-- gh-comment-id:6206908606 --> @raynorpat commented on GitHub (Oct 16, 2026): Fixed in 260033acbb4c29cf90781f9b16cc028a79785e69 (kernel: Merge filesystem repairs for issues 52-55), on ffs-perf; the ext2fs part is in `ext2fs_vnops.c`. Verified on the repair kernel: - The original reproducer `stress-mmap -n 1 -i 1 -s 1 -d /mnt/e trunc` no longer panics. - The new focused test `vm/ext2_mmap_truncate_test.c` (dirty sparse mappings shrunk across direct/indirect block boundaries, partial-page EOF, truncate to zero, zero-filled regrowth) passes at all four lengths. - `stress-mmap shared mixed private trunc -n 3 -i 20 -s 2` passes on ext2. - `e2fsck -fn` is clean afterwards. Test notes and focused regressions: `vm/filesystem-issues-52-55.md` in that commit. Repair kernel cksum 3500498149 (i386, QEMU snapshot); PowerPC not tested.
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#44
No description provided.