vfs_export_lookup() returns an unref'd netcred after dropping NE_LOCK; consumers keep it across the NFS request while DELEXPORT frees it (use-after-free of the authorization credential)
Summary
vfs_export_lookup matches the peer under NE_LOCK, unlocks, and returns the raw struct netcred*; every VFS_CHECKEXP consumer (ufs, tmpfs, hammer2, cd9660, nullfs, ntfs, msdosfs, udf) reads netc_exflags/netc_anon after the unlock β the NFS server holds the credential for the whole request β while vfs_export(MNT_DELEXPORT) kfree()s the netcreds with no refcount. KLD rig on guest modeled the exact hold protocol: 258/492 held netcreds changed identity mid-hold (freed and same-slot-reallocated by the replacement export; exflags flipping between markers). Routine mountd re-export churn racing in-flight unauthenticated-remote NFS requests -> stale/garbage export flags and anonymous credentials (authorization confusion) or crash on unlucky slab reuse. Return-fields-not-object fix drafted (multi-caller change, design-level validated).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5809 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| SHA256SUMS | β | 237 B | β download | |
| VERDICT.md | β | 3.6 KB | β raw | |
| build.sh | β | 173 B | view raw | |
| dmesg.txt | β | 1019 B | view raw | |
| env.txt | β | 218 B | view raw | |
| exploop.c | β | 1.6 KB | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| ncrace.c | β | 4.4 KB | view raw | |
| run.sh | β | 289 B | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-5809 β vfs_export_lookup() returns unref'd netcred; callers use it after NE_UNLOCK (UAF race vs DELEXPORT)
What
vfs_export_lookup() (sys/kern/vfs_subr.c:2297-2342) matches the peer
address in the export radix trees under NE_LOCK, then drops the
lock and returns the raw struct netcred *. Every VFS_CHECKEXP
implementation dereferences it afterwards β e.g.
sys/vfs/ufs/ufs_vfsops.c:246-251:
np = vfs_export_lookup(mp, &ump->um_export, nam);
...
*exflagsp = np->netc_exflags; /* after NE_UNLOCK */
*credanonp = &np->netc_anon; /* kept by the NFS server
for the whole request */
while vfs_export() with MNT_DELEXPORT (mountd re-export, a routine
admin operation) frees those heap netcreds (vfs_free_addrlist β
rn_flush β kfree, vfs_subr.c:2166-2185). There is no refcount and
no lock held by the consumer: use-after-free of the credential object
that NFS uses to authorize the request (garbage exflags/anon cred, or
a crash on unlucky reuse).
Same pattern in tmpfs/hammer2/cd9660/nullfs/ntfs/msdosfs/udf checkexp implementations.
Rig (KLD)
ncrace.cβ kernel module: a looker kthread on a tmpfs mount does exactly what the NFS server does:vfs_export_lookup()β snapshotnetc_exflags/netc_anon.cr_uidβ sleep 1 tick (request lifetime) β re-read. Any change proves the held memory was freed and reused. sysctldebug.ncrace.run= 1/0,debug.ncrace.stat= counters.exploop.cβ root userland: hammersmountctl("/mnt/t", MOUNTCTL_SET_EXPORT, DELEXPORT|EXPORTED)with alternatingex_flagsmarkers (0x04000000 / 0x08000000) and anon uids (65534 / 31337).
Build
cc -O2 -o /tmp/exploop exploop.c # guest
# module:
mkdir -p /root/ncrace && ln -sf /usr/src/sys/vfs/tmpfs/tmpfs.h /root/ncrace/
printf 'KMOD=ncrace\nSRCS=ncrace.c\nSYSDIR=/usr/src/sys\n.include <bsd.kmod.mk>\n' \
> /root/ncrace/Makefile
(cd /root/ncrace && make)
Run
mount -t tmpfs none /mnt/t kldload /root/ncrace/ncrace.ko sysctl debug.ncrace.run=1 /tmp/exploop /mnt/t 10 sysctl debug.ncrace.stat dmesg | grep ncrace sysctl debug.ncrace.run=0 && kldunload ncrace
Expected (stock kernel)
debug.ncrace.stat: lookups=543 holds=492 hits=258 ncrace: UAF-use #1: np=0xfffff80116e73080 exflags 4020100->8020100 anonuid 31337->31337 ...
The held pointer's export-flags word flips from marker A to marker B mid-hold: the netcred was kfree()d by DELEXPORT and the same slab slot reallocated for the replacement export β use-after-free by construction, 52% hit rate with single-threaded contenders.
Files
- ncrace.c, exploop.c rig sources
- dmesg.txt full UAF-use log
- fix.diff direction fix (locked-copy API) β multi-caller change; NOT guest-validated (see VERDICT.md)
DF-5809 β VERDICT
Status: reproduced (258 use-after-free observations in 492 holds under a faithful model of the NFS-server hold window; impact = race UAF on the NFS authorization credential β cred confusion / crash).
Root cause
sys/kern/vfs_subr.c:2310-2334βvfs_export_lookup()performs the radix match underNE_LOCKand releases the lock before returning thestruct netcred *(line 2334NE_UNLOCK, return at 2341).- Consumers:
sys/vfs/ufs/ufs_vfsops.c:246-251(and the identical pattern in tmpfs_vfsops.c:527-532, hammer2_vfsops.c:2848-2851, cd9660_vfsops.c:703-708, null_vfsops.c:341-343, ntfs, msdosfs, udf) readnp->netc_exflagsand export&np->netc_anonβ the NFS server keeps that credential pointer for the duration of the request. - Free side:
sys/kern/vfs_subr.c:2187-2212 vfs_export()with MNT_DELEXPORT βvfs_free_addrlist()(2166-2185) βrn_flushβvfs_free_netcredβkfree(np, M_NETCRED)β under NE_LOCK only. Nothing stops a free between the consumer's unlock and its (millisecond-scale) use.
Production trigger: mountd re-export (mountctl SET_EXPORT /
mount -u -o ...) racing in-flight NFS requests on the same export.
The NFS side is unauthenticated-remote; the free side is an
administrative operation β the race is realistic during routine
re-export churn.
Reproduction (guest DragonFly 6.5-DEVELOPMENT, stock kernel #0)
KLD rig (ncrace.c + exploop.c, see README):
debug.ncrace.stat: lookups=543 holds=492 hits=258 ncrace: UAF-use #1: np=0xfffff80116e73080 exflags 4020100->8020100 anonuid 31337->31337 ... (10 samples logged; two distinct np slab slots cycle) ncrace: looker exiting: lookups=543 holds=492 hits=258
Interpretation: the looker snapshots netc_exflags = marker
0x04000000|MNT_DELEXPORT|MNT_EXPORTED, sleeps one tick (models the NFS
request lifetime β the production window is the same order), then
reads marker 0x08000000β¦ at the same address. The only writer of
marker B at that address is the replacement netcred allocated after
vfs_free_addrlist freed the original β i.e. the consumer used freed
memory. 52% hit rate single-threaded; the rig did not crash the guest
(benign reuse), but nothing prevents a non-netcred reallocation from
landing in the slot, in which case netc_exflags/netc_anon are
attacker-adjacent garbage used as authorization credentials.
Fix direction (fix.diff, not guest-validated)
The fix must be caller-visible: return the fields, not the object.
fix.diff sketches vfs_export_lookup_locked() in vfs_subr.c, which
extracts netc_exflags and copies the embedded anon credential while
still holding NE_LOCK, and converts ufs_checkexp as the exemplar;
every other checkexp implementation needs the same mechanical change.
Guest validation would require the full multi-caller conversion β
deferred; the rig itself demonstrates both the broken protocol (hits
on stock kernel) and the required property (copy under lock β no
post-unlock dereference).
Caveats / honesty
- The rig widens nothing: the 1-tick hold equals or is shorter than a
real NFS request's use of
credanon. - No panic was induced (freed slots were immediately recycled into new netcreds by the export loop itself β the benign-reuse case).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix requires converting every VFS_CHECKEXP caller to the locked-copy API (multi-file change); the vfs_subr.c side is drafted in fix.diff but was not built/validated on the guest within this run - the rig proves both the broken protocol and the property the fix must provide (no post-unlock dereference)
fix.diff; VERDICT.md
Confirmed kernel references
Detail
Exploit chain
root mountd re-export loop (mountctl SET_EXPORT DELEXPORT|EXPORTED) racing the consumer: lookup returns np -> NE_UNLOCK -> consumer holds np across request (ms window) -> DELEXPORT kfrees np -> next export reallocates the slot -> consumer reads replacement/garbage credential; remote unauthenticated NFS peer is the consumer side in production
Evidence (decisive lines)
dmesg.txt (ncrace: UAF-use #1..#10, exflags 4020100->8020100 marker flips, lookups=543 holds=492 hits=258); ncrace.c/exploop.c rig; env.txt
PoC changes
rig built from scratch: kernel module (kthread consumer on a tmpfs export, sysctl-controlled) + root userland export-churn loop with alternating markers; nullfs abandoned (mountctl EINVAL on its root vnode), tmpfs used
Verified recommended fix
return the fields, not the object: extract exflags + snapshot netc_anon while NE_LOCK is held (vfs_export_lookup_locked in fix.diff) and convert all checkexp consumers
Verdict
vfs_export_lookup() (vfs_subr.c:2297-2342) returns a struct netcred* after releasing NE_LOCK; every VFS_CHECKEXP consumer (ufs_vfsops.c:246-251 et al.) reads netc_exflags and keeps &netc_anon across the whole NFS request, while vfs_export(MNT_DELEXPORT) kfree()s those netcreds under the same lock with no refcount. KLD rig modeling the NFS-server hold window observed 258/492 held netcreds change identity mid-hold (freed and slab-reused by the replacement export) - use-after-free by construction. Ceiling: stale/garbage export-flags and anonymous credentials used to authorize NFS requests (credential confusion) or a crash on unlucky reallocation; observed impact in the rig was benign reuse (no panic), hence impact recorded as dos with cred-confusion noted.
No comments yet.