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)
PoC verification
Evidence pack
findings/poc/DF-3164 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedfix.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']
Confirmed kernel references
Detail
Exploit chain
root/delegated-mount: mount crafted-but-valid UDF image; call mount(2) {MNT_RDONLY|MNT_UPDATE, fspec=
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.
No comments yet.