โฌข DragonFlyBSD Kernel Audit
DF-0847 / characterization.log
โ† back to finding โ†“ download raw
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.