177 lines
6.8 KiB
Markdown
177 lines
6.8 KiB
Markdown
# `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.
|