DF-0847 / characterization.log
DF-0847 โ options-QUOTA kernel characterization log (NON-DEFAULT CONFIG)
========================================================================
The default-kernel verdict (NOT REPRODUCED โ dead code) is in run.log.
This file records the characterization performed on a custom kernel built
with `options QUOTA` to confirm the cited bug is REAL-but-LATENT (not a
phantom) and to validate that fix.diff compiles into a live kernel.
All results below are on a NON-DEFAULT config and do NOT change the
default-kernel verdict (per the audit bright-line rule).
--- Build: options QUOTA kernel (custom config) -------------------------
Config edit: appended `options QUOTA` to sys/config/X86_64_GENERIC.
Build: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC
Result: rc=0, kernel.stripped = 15707848 bytes (valid ELF64).
kern.ver: DragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 12:37:31 UTC 2026
Symbols: ufs_chkdq now 'T' (callers compiled in) โ path is LIVE.
--- Path is LIVE on options-QUOTA kernel --------------------------------
quota_unreachable (run as root; Q_QUOTAON needs caps_priv_check NOQUOTA_WR):
quotactl(Q_QUOTAON, "/mnt/q", uid=0, "/mnt/q/quota.user") -> rc=0 errno=0
PATH LIVE: kernel has `options QUOTA`; the buggy code is reachable.
Contrast with the default #0 kernel (run.log): errno=45 (EOPNOTSUPP).
--- Race harness (dq_race, root-driven) ---------------------------------
KEY FINDING: vfs_quotactl() takes the per-mount mnt_token (UFS is NOT
MNTK_MPSAFE, sys/sys/mount.h:406-417). quotactl on a SINGLE mount is
serialized and CANNOT race. The dquot hash/free-list is GLOBAL across
mounts, so the race is only reachable CROSS-MOUNT. dq_race therefore
splits workers across two quotaon'd UFS mounts (/mnt/q0, /mnt/q1).
Attempt 1 (single mount, 8 threads, 25s, uid 1..4000): survived, 12.2M iters.
Attempt 2 (single mount, 12 threads, 30s, uid 1..50): survived, 14.0M iters.
Attempt 3 (cross-mount, 12 threads, 60s, uid 1..50): survived, 47.6M iters.
No panic observed in any attempt. The race is genuinely narrow (the finding
itself rates CVSS AC:H / confidence 'likely'). Two contributing reasons the
corruption does not manifest as a panic:
1. The list-mutation window (TAILQ_FIRST -> TAILQ_REMOVE / cache-hit
TAILQ_REMOVE) is only a few instructions; the cross-mount concurrency
raises the hit rate but the absolute window is tiny.
2. There are NO INVARIANTS on the dquot hash/free-list integrity
(kern_slaballoc.c INVARIANTS cover malloc chunks, not this TAILQ/LIST),
so a double-unlink corrupts silently and only panics if a subsequent
deref hits the bad pointer โ which may never happen.
This is consistent with a real-but-low-probability concurrency defect.
--- Unprivileged reachability on options-QUOTA --------------------------
Even where the code is live, an UNPRIVILEGED user can only trigger a dquot
cache-miss for their OWN uid (file create/write -> ufs_getinoquota uses
ip->i_uid). That yields a single cached dquot and a negligible race window.
Driving the race needs the privileged Q_GETQUOTA-for-arbitrary-uid path
(caps_priv_check RESTRICTEDROOT, ufs_vfsops.c:123). Realistic unprivileged
impact ceiling: none.
--- fix.diff validation -------------------------------------------------
fix.diff adds `static struct lwkt_token ufs_dq_token` and wraps every
hash/free-list mutation in ufs_dqget/ufs_dqrele/ufs_dqflush in
lwkt_gettoken/lwkt_reltoken (mirroring ext2_ihash_token,
sys/vfs/ext2fs/ext2_ihash.c:67), releasing the token around sleeping calls
(kmalloc M_WAITOK, vn_lock, VOP_READ).
Validation performed:
* `git apply --check -p1 fix.diff` against the audit sys/ tree: PASS.
* Applied to in-guest options-QUOTA source: all 11 hunks applied cleanly.
* Built into a live options-QUOTA kernel: rc=0, kernel.stripped valid ELF.
* Symbol `ufs_dq_token` present in the built kernel.debug
(ffffffff81114880 d ufs_dq_token) => the fix is compiled in.
* Source-traced: every cited unlocked mutation site
(ufs_quota.c:779, 804, 806, 813, 841, 890, 956-966) is now inside a
gettoken/reltoken pair.
Runtime before/after contrast was NOT achievable: the race never produced an
observable panic even on the UNFIXED options-QUOTA kernel (47.6M iters), so
there is no "before" marker to contrast against. This is the textbook
`fix_status: not_testable` case for a latent/unreachable PoC.