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

Failed MNT_UPDATE remount leaves mp->mnt_data dangling: use-after-free of struct udf_mnt on the live mount (double free at unmount)

Summary

udf_mount (:153-157) falls through to udf_mountfs() on MNT_UPDATE whenever args.fspec != NULL; udf_mountfs then unconditionally overwrites mp->mnt_data with a freshly allocated udf_mnt (:263-272) and, on ANY later failure (bad anchor :283, missing descriptors :329, fsd_part mismatch :334, bad FSD/root FE), bail (:393-403) kfrees that structure WITHOUT restoring the previous pointer. sys_mount's MNT_UPDATE failure path only restores mnt_flag/mnt_kern_flag - the UDF mount stays alive with mnt_data pointing at freed heap. Every subsequent VFS op deriving udfmp from the mount is a UAF: lookup -> udf_vget -> udf_hashlookup derefs the freed chunk's hash_token/hashtbl/hashsz; umount does a second VOP_CLOSE through freed im_devvp, a second vrele, and kfree(udfmp) again (double free), plus a NULL write through im_devvp->v_rdev->si_mountpoint (groomable pointer). A successful remount instead silently leaks the previous udf_mnt + hashtbl + old device vnode forever. Any principal holding the mount privilege (root; delegated cap / vfs.usermount=1) can, by attempting to remount a live UDF filesystem onto a blank/garbage device - a FAILED operation - permanently convert every later access to that filesystem into kernel UAF. VERIFIED stock INVARIANTS: trigger (MNT_UPDATE+fspec=/dev/vn1 zeros) returns EINVAL with mnt_data dangling; `ls /mnt/T25` (never-cached) -> 'panic: assertion count & TOK_COUNTMASK failed in _lwkt_reltokref', stack lwkt_gettoken<-udf_hashlookup<-udf_vget - the freed chunk already reused. Fix validated (reject MNT_UPDATE+fspec EOPNOTSUPP + save/restore mnt_data on bail, kernel #1): trigger gets errno 45, mount stays healthy, lookup succeeds. Escalation beyond panic gated behind mount privilege (uid0 adds nothing).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3164 Β· 11 files
FileTypeDescriptionSize
trigger.c β€” 1.3 KB view raw
run.sh β€” 198 B view raw
build.sh β€” 141 B view raw
craft_base.py β€” 10.9 KB view raw
df3164-base.udf β€” 1.0 MB ↓ download
run.log β€” 2.4 KB view raw
panic_core.txt β€” 694 B view raw
fix_run.log β€” 768 B view raw
fix.diff β€” 893 B view raw
env.txt β€” 668 B view raw
verdict.json β€” 4.6 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, kernel rebuilt (make nativekernel, 5.5 min) and installed. Re-running the byte-identical PoC: the MNT_UPDATE trigger now returns EOPNOTSUPP before udf_mountfs is entered, the UDF mount stays listed and healthy, and the post-failure lookup succeeds without any panic - the UAF is gone.

["findings/poc/DF-3164/fix_run.log: 'errno=45 (Operation not supported)', '/dev/vn0 on /mnt (udf, read-only, local)', 'ls rc=0', 'FS_STILL_USABLE'", 'findings/poc/DF-3164/fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Sep 7 11:10:39 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root/delegated-mount: mount crafted-but-valid UDF image; call mount(2) {MNT_RDONLY|MNT_UPDATE, fspec=} on the live mount (fails EINVAL, mnt_data now dangling); groom the freed ~280-byte chunk (any kernel kmalloc activity; verified reused in practice); trigger via lookup (udf_vget->udf_hashlookup: token/hashtbl fields) or umount (im_devvp->v_rdev->si_mountpoint NULL write, VOP_CLOSE through forged v_op vector, double kfree) -> panic reproduced; weaponization to code execution plausible on the no-KASLR/no-SMEP guest but gated behind mount privilege, so escalation adds nothing - the demonstrated impact is kernel heap UAF + deterministic panic of a machine by mounting untrusted media

Evidence (decisive lines)

['findings/poc/DF-3164/run.log: \'panic: assertion "count & TOK_COUNTMASK" failed in _lwkt_reltokref\' with stack lwkt_gettoken <- udf_hashlookup <- udf_vget', 'findings/poc/DF-3164/panic_core.txt: decisive panic lines', "findings/poc/DF-3164/fix_run.log: errno=45 EOPNOTSUPP on patched kernel, mount stays listed, 'ls /mnt/T25' succeeds (FS_STILL_USABLE)", 'findings/poc/DF-3164/trigger.c: direct mount(2) MNT_UPDATE trigger', 'findings/poc/DF-3164/run.sh: full reproduction script']

PoC changes

mount(8) on DragonFly rejects the 4-arg '-u type special node' form, so the seed idea was replaced with a direct mount(2) trigger (trigger.c) using a userland copy of struct udf_args from sys/vfs/udf/udf_mount.h; run.sh was made self-contained (vnconfig cleanup) after a 'Device busy' iteration; lookup target switched to a never-cached name (T25) so the namecache cannot bypass VOP_LOOKUP.

Verified recommended fix

Reject MNT_UPDATE with a non-NULL fspec in udf_mount (EOPNOTSUPP) and restore mp->mnt_data on every udf_mountfs bail path (save it on entry).

Verdict

A mount(2) call with MNT_UPDATE and a non-NULL fspec on a live UDF mount reaches udf_mountfs, which overwrites mp->mnt_data (udf_vfsops.c:265) and, on any subsequent failure, frees the new udf_mnt at bail (:393-403) without restoring the old pointer; sys_mount's update-failure path (vfs_syscalls.c:408-419) keeps the mount alive, so every later VFS op derived from the mount operates on freed memory. Reproduced on the stock INVARIANTS guest: a lookup after the failed remount panicked in lwkt_gettoken called from udf_hashlookup/udf_vget on the freed struct udf_mnt (chunk already reused - proof of a live UAF primitive, not merely a stale read). Beyond the panic, udf_unmount on the freed structure performs a second VOP_CLOSE, second vrele and a double kfree, and the freed object is groomable, giving a memory-corruption surface behind the mount-privilege boundary (untrusted-media threat model). The companion success-path variant silently leaks the previous udf_mnt, its hash table and an open device vnode.