This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
@@ -0,0 +1,176 @@
# `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.