β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5809

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5809 Β· 11 files
FileTypeDescriptionSize
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() β†’ snapshot netc_exflags/netc_anon.cr_uid β†’ sleep 1 tick (request lifetime) β†’ re-read. Any change proves the held memory was freed and reused. sysctl debug.ncrace.run = 1/0, debug.ncrace.stat = counters.
  • exploop.c β€” root userland: hammers mountctl("/mnt/t", MOUNTCTL_SET_EXPORT, DELEXPORT|EXPORTED) with alternating ex_flags markers (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)
VERDICT.md
↓ download raw

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 under NE_LOCK and releases the lock before returning the struct netcred * (line 2334 NE_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) read np->netc_exflags and 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_testable
baseline reproduced→ patch + rebuild →patched clean

fix 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
↓ fix.diffper-fix-DF-5809

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.