DF-2774 / verdict.json
{ "finding_id": "DF-2774", "status": "reproduced", "reproduced": 1, "impact": "none", "confidence": "certain", "verdict": "Reproduced deterministically on the stock INVARIANTS kernel with vfs.quota_enabled=1: vp->v_pfsmp is a raw, never-cleared pointer to the nullfs struct mount (vfs_cache.c:1386-1389); after unmount, mountlist_exists() (vfs_mount.c:692-708) validates it by pointer-compare only, so a fresh mount reusing the freed M_MOUNT chunk makes the guard pass and VFS accounting/enforcement operates on the wrong, unrelated mount โ an append through the unquota'd lower tmpfs failed with EDQUOT under an unrelated mount's 10-byte limit, and an O_TRUNC through the lower path drove that mount's usage to 2^64-8192. A concurrent-unmount variant is a struct mount UAF (stale mnt_op read; vfs_stdaccount spin_lock + ac_bytes += delta on freed memory), bounded in practice by M_MOUNT zone type-stability. Default configs (quota_enabled=0) reduce VFS_ACCOUNT to a benign NULL check. fix.diff (mountlist_hold + per-mount cookie + held vq_vptomp) built as kernel #1 in-guest and eliminates both manifestations while preserving alias accounting; no uid0 path (logic-corruption class, not a control-flow primitive).", "exploit_chain": "unpriv user file activity on a nullfs-aliased lower fs -> admin unmounts nullfs (v_pfsmp goes stale) -> next mount reuses the freed struct mount address -> mountlist_exists(stale)==1 -> vq_vptomp returns wrong mount -> vq_write_ok/VFS_ACCOUNT enforce/charge the wrong mount (demonstrated: EDQUOT on unquota'd tmpfs + negative usage). Race variant: unmount+free lands between exists() and use -> vfs_stdaccount writes freed mount (limited by zone type-stability).", "evidence": [ "findings/poc/DF-2774/run.log โ decisive baseline: 'dd: stdout: Disc quota exceeded' (TEST1) and 'total: 18446744073709543424, limit = 10' (TEST2)", "findings/poc/DF-2774/run.fix.log โ patched kernel #1: TEST1_RC=0, alias2 stays 'total: 0, limit = 10'", "findings/poc/DF-2774/fix.diff โ 7-file git-apply-able fix (mountlist_hold, mnt_cookie, v_pfsmp_cookie, held vq_vptomp)", "findings/poc/DF-2774/VERDICT.md โ full narrative with path:line for every claim" ], "kernel_refs": [ "sys/kern/vfs_mount.c:692-708", "sys/kern/vfs_mount.c:399-405", "sys/kern/vfs_cache.c:1386-1389", "sys/kern/vfs_quota.c:421-433", "sys/kern/vfs_quota.c:149-172", "sys/kern/vfs_vnops.c:325-326", "sys/kern/vfs_vopops.c:487-496", "sys/kern/vfs_syscalls.c:1040-1117" ], "poc_changes": "Seed sketch rewritten entirely: DFly dd rejects the '2>/dev/null'-style operand placement used in the draft and vquota syntax is 'limit/show' not 'set limit/get usage'; crucially the victim file must be created/resolved THROUGH the nullfs path first (an already-resolved ncp never re-runs _cache_setvp, so v_pfsmp is never set if the lower path resolves it first) โ first two runs failed for those reasons.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline) / #1: Tue Sep 1 02:11:26 UTC 2026 (patched)", "runtime_sec": 1140, "guest_dirty": 1, "build_cmd": "cd /usr/src && patch -p1 < /root/fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC", "run_cmd": "sh /root/poc.sh (as root; file activity via 'su -m qa', vfs.quota_enabled=1 in /boot/loader.conf)", "code_hash": "ac6712ff03ac2b3179fe1d256686c62e88428f09e50b830022e41d3b0dc1eccc", "notes": "impact field: no memory-safety manifestation demonstrated (enum has no 'logic-corruption' class); demonstrated effects are cross-mount quota enforcement (EDQUOT on unquota'd fs) and accounting corruption (2^64-8192 usage). UAF-write variant is race-bound and bounded by M_MOUNT zone type-stability + DFly slab retaining freed-chunk contents. vfs.usermount is irrelevant here: the unprivileged side only opens/truncates files; mount/unmount is privileged admin activity that arms the stale pointer persistently.", "recommended_fix": "Replace the refcount-free mountlist_exists() probe in vq_vptomp() with mountlist_hold() (a real-hold lookup) plus a per-mount generation cookie (mnt_cookie/v_pfsmp_cookie) so a stale v_pfsmp can neither be used after free nor validate against a different mount that reused the address; callers mount_drop() after accounting.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 02:11:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Built fix.diff in-guest (make -j6 nativekernel, 362s), installed, rebooted into kernel #1, re-ran the identical poc.sh: baseline EDQUOT denial and negative-usage charge are gone (TEST1_RC=0, alias2 total: 0) while legitimate alias-path accounting still charges the nullfs mount 4096. Behavior change is exactly the bug disappearing.", "fix_evidence": [ "findings/poc/DF-2774/run.fix.log", "findings/poc/DF-2774/build.log (head of in-guest build.log)", "findings/poc/DF-2774/fix.diff" ] } |