# `erofs_vget()` lifecycle violation after `insmntque()` failure ## Status - State: **FIXED / RUNTIME VALIDATION PENDING** - Priority: **P1** - Scope: pre-existing vnode lifecycle issue; not introduced by Pre10 Batch B - Affected feature: inode-to-vnode construction through `erofs_vget()` ## Problem description `repo-pre-10/src/inode.c:431` defines `erofs_vget()`. Before this fix, after allocating a new vnode and attaching an `erofs_node`, the function called `insmntque()` and used this failure branch: ```c vp->v_data = en; en->vnode = vp; en->nid = nid; lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL); error = insmntque(vp, mp); if (error != 0) { free(en, M_EROFS); vp->v_data = NULL; *vpp = NULL; return (error); } ``` The relevant call and failure branch are at `repo-pre-10/src/inode.c:463-468`. On an insertion failure, the FreeBSD `insmntque()` API reclaims and releases the vnode before returning. The subsequent assignment to `vp->v_data` therefore accesses a vnode whose lifetime has ended from the caller's perspective. ## Trigger condition The locally reviewed FreeBSD implementation rejects insertion while the target mount is being torn down. In `/work/dev-freebsd-releng/sys/kern/vfs_subr.c:2303-2306`, the failure condition checks `MNTK_UNMOUNT` and either forced unmount (`MNTK_UNMOUNTF`) or an empty mount vnode list, unless `VV_FORCEINSMQ` applies. The function then returns `EBUSY` at `vfs_subr.c:2315`. The issue can therefore be triggered when vnode creation in `erofs_vget()` races with mount teardown or forced unmount and `insmntque()` rejects the new vnode. ## FreeBSD contract The local FreeBSD kernel source documents the ownership distinction explicitly at `/work/dev-freebsd-releng/sys/kern/vfs_subr.c:2328-2331`: - `insmntque()` reclaims the vnode when insertion fails. - `insmntque1()` leaves vnode cleanup to the caller. `insmntque()` calls `insmntque1_int(vp, mp, true)` at `vfs_subr.c:2333-2337`. In the relevant failure path, `insmntque1_int()` performs all of the following at `vfs_subr.c:2309-2313`: ```c vp->v_data = NULL; vp->v_op = &dead_vnodeops; vgone(vp); vput(vp); ``` Consequently, an error return from `insmntque()` does not preserve caller ownership of a live `vp`. Reading or writing `vp`, including assigning `vp->v_data`, is unsafe after that return. ## Impact The affected path is vnode construction for filesystem objects. Under the mount-teardown race described above, the pre-fix failure branch may write through a released vnode pointer. Likely consequences include a use-after-free, memory corruption, a kernel panic, or corruption of an unrelated vnode if the storage is recycled quickly. The exact manifestation has not been reproduced and must not be treated as confirmed beyond the statically established lifetime violation. The separately allocated `en` object is not released by the kernel failure path. That path clears `vp->v_data` and changes `v_op` to `dead_vnodeops` before calling `vgone()`. The dead vnode reclaim operation is `VOP_NULL`, so the filesystem reclaim callback does not run and cannot release `en`. Caller-side `free(en, M_EROFS)` is therefore required exactly once. ## Implemented fix Pre10 now treats an error return from `insmntque()` as the end of caller ownership of `vp`: ```c error = insmntque(vp, mp); if (error != 0) { free(en, M_EROFS); *vpp = NULL; return (error); } ``` The caller frees only the independently allocated `en` object and does not read, write, reclaim, unlock, or release `vp` after `insmntque()` returns an error. The kernel failure path clears `vp->v_data` and installs `dead_vnodeops` before invoking `vgone()` and `vput()`. Dead vnode reclaim is a no-op, so no vnode cleanup callback owns `en`. This gives `en` exactly one release on the failure path. The successful insertion path, vnode lock state, hash insertion race handling, errno return, and later inode-read cleanup are unchanged. In particular, `vfs_hash_insert()` remains responsible for reclaiming and releasing the losing new vnode when another thread wins the hash race. ## Current analysis and progress - Independently reviewed the Pre10 `erofs_vget()` failure path. - Confirmed the local FreeBSD `insmntque()` and `insmntque1()` ownership contract against kernel source. - Confirmed the teardown/forced-unmount condition that produces `EBUSY`. - Confirmed that `repo-pre-10/src/inode.c` accesses `vp->v_data` after the reclaiming API returns an error. - Compared the cleanup pattern with the local FreeBSD ext2fs, msdosfs, UDF, pseudofs, and p9fs vnode construction paths. They likewise avoid touching the vnode after `insmntque()` fails and separately dispose caller-owned node state where required. - Implemented the minimal Pre10 source fix by removing the post-failure `vp->v_data` assignment. - Completed static ownership, lock, hash-race, reclaim, and error-path review. - No QEMU run, runtime reproduction, dedicated race test, or feature test has been attempted. The statically proven lifetime violation is fixed. Runtime validation remains open because the triggering teardown race is not covered by ordinary smoke testing. ## Candidate fix boundaries The implemented correction preserves the existing choice of `insmntque()` and treats an error return as transferring vnode cleanup entirely to the kernel. The caller releases only independently owned `en`, clears `*vpp`, and returns without any further access to or release of `vp`. An alternative would be to deliberately switch to `insmntque1()` and implement the complete caller-owned failure cleanup required by that API. This is a wider lifecycle change and should not be selected merely to mirror naming or control flow. The wider `insmntque1()` alternative was rejected because it would require new caller-owned vnode cleanup without providing any benefit for this path. ## Required validation Before marking runtime validation complete: 1. Build the EROFS module with the supported FreeBSD source tree. 2. Run a dedicated mount/unmount race test that repeatedly creates or looks up previously uncached vnodes while normal and forced unmount are attempted. 3. Use kernel diagnostics appropriate for detecting stale vnode access, memory corruption, lock misuse, and double release. 4. Re-run the ordinary mount, lookup, read, and unmount smoke coverage after the race test passes. The dedicated unmount race test is required because ordinary static checks and single-threaded smoke tests do not exercise the failing `insmntque()` branch. ## Relationship to Pre10 Batch B This issue predates Batch B. Batch B extracted `erofs_fill_vnode()` only for the successful post-`erofs_read_inode()` field setup and did not modify the `insmntque()` call or its failure branch. Independent review found no Batch B regression in this path. The minimal ownership fix is included in Pre10. The dedicated teardown race test remains deferred and cannot be replaced by the planned ordinary smoke test.