You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lift the documented single-threaded-write-path assumption.
Every reader and writer of the inode map now holds zm_lock -- the map
pointer is replaced when zlfs_imap_grow expands it, so an unlocked
reader could dereference freed memory: zlfs_read_dinode snapshots the
LBA under the lock, the commit's map-block copy loop and commit_node's
stores take it, the cleaner's pass 2 snapshots entry by entry, and the
remove/rmdir/rename zero-stores are covered (statfs and ialloc already
were).
The commit itself becomes all-or-nothing under held locks: dirty nodes
are snapshotted under one zm_lock hold (count and fill under the same
hold, with a headroom retry, so a node prepended to the list while
malloc slept cannot displace an older one -- say the fsync caller's --
out of the snapshot), each is referenced with an LK_NOWAIT vget (a
dying vnode is mid-reclaim and skipped), and then ALL their vnode
locks are taken with trylocks and held until the dirty flags clear.
Holding the locks across the segment write closes the overlay
use-after-free and the lost-update window (a write slipping between a
node's commit and the bulk flag clear), and keeps every checkpoint a
consistent all-or-nothing set -- no dangling dirents from a
half-committed create. Blocking on a vnode lock while holding
zm_wlock would deadlock against an fsync that holds the vnode and
waits for the commit lock, so contention releases everything and
retries after a short sleep (ten attempts, then EBUSY); the holder
always finishes first, so progress is guaranteed. The fsync caller's
own node recurses through its RWL_DUPOK rrwlock, whose exit
decrements the recursion rather than releasing the caller's hold.
The cleaner's pass 3 gets the same single-hold snapshot treatment: its
count-then-fill raced list growth, and a displaced unlinked-but-open
inode -- the very case pass 3 protects -- could have its zone reset.
doc/zlfs-design.md section 7 and the functional_testing.md gap table
updated; two long-standing latent-UAF rows close.
Copy file name to clipboardExpand all lines: functional_testing.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -51,8 +51,8 @@ size 4096; superblock (SB) zones 0-1, 126 data zones.
51
51
|`statfs` accuracy | IMPROVED (sbcap phase): `f_bfree` = allocatable now (free zones + log-head remainder), `f_files`/`f_ffree` from the real inode map; dead-but-unreclaimed zones still count as used until the cleaner runs (honest for an LFS) | closed enough for bring-up | per-zone byte accounting with the copying cleaner |
52
52
|`zst_live_bytes` semantics | only tested against 0 in the reset loop; not a true byte count | none today; trap for future code | rename or fix when the copying cleaner needs real counts |
53
53
| Inode ceiling (was: single-block map, 512) | format v2 raises the cap to `ZLFS_CKPT_NIMAP * epb` (~256000 at a 4 KB block: ~500 map-block LBAs fit in the checkpoint block) | wide trees fine now; the ~256k cap is structural until the map gets its own indirection | none planned; revisit only if a workload needs more |
54
-
| Commit vs concurrent write on the same file |`zlfs_commit_file_blocks` adoption frees the overlay (`zlfs_dblk_free`) without the vnode lock while a concurrent `write` could be filling a buffer — the documented single-threaded-commit assumption now shields a potential use-after-free, not just a stale flag | none under KERNEL_LOCK today | serialise commit against vnode ops in the concurrency-safe-commit phase|
55
-
|`zlfs_imap_grow` swaps `zm_imap` under `zm_lock` only | readers (`read_dinode`, commit, cleaner pass 2, remove/rmdir/rename) index the map without `zm_lock`; safe today because all accesses are single-expression under the kernel lock and grow has no sleep between copy and install — a latent use-after-free the moment these paths run MP-unlocked. `statfs` now takes `zm_lock` for its scan (sbcap phase) | none under the current single-threaded assumption | take `zm_lock` (or `zm_wlock`) in the remaining map readers as part of the concurrency-safe-commit phase|
54
+
| Commit vs concurrent write on the same file |CLOSED (concurrency phase): commit snapshots dirty nodes under `zm_lock` with vnode references (`LK_NOWAIT` vget skips dying vnodes) and takes ALL their `rrw` locks up front (trylock; contention = release all + retry, holder finishes first) and holds them until the dirty flags clear — closes the overlay UAF, the commit-vs-write lost-update window, and keeps the checkpoint an all-or-nothing set | closed | —|
55
+
|`zlfs_imap_grow` swaps `zm_imap` under `zm_lock` only | readers (`read_dinode`, commit, cleaner pass 2, remove/rmdir/rename) index the map without `zm_lock`; CLOSED (concurrency phase): every reader/writer of `zm_imap` now holds `zm_lock` — `read_dinode`, the commit's map snapshot and `commit_node` stores, cleaner pass 2 (per-entry snapshot), remove/rmdir/rename zero-stores; `statfs`/`ialloc` already did | closed | —|
56
56
| Conventional (non-write-pointer) superblock zones would break the write path | discovery supports them (forward scan), but `zlfs_zones_load` copies `DK_ZONE_WP_INVALID` (~0) write pointers verbatim, so `zm_sb_lba` starts at ~0 and the first commit's superblock write targets a nonsense LBA (pre-existing; flagged by the sbcap review) | RW mount of a device with conventional zones 0-1 fails at the first commit; the QEMU ZNS target has no conventional zones | validate the active SB zone's write pointer at mount; proper conventional-zone append tracking when such a target matters |
0 commit comments