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

sys_vquotactl() leaks the copyin'd prop_dictionary, command string, output array and result dictionary on every call β€” unprivileged, linear kernel heap exhaustion

Summary

The vquotactl consumer of prop_dictionary_copyin never releases anything: copyin'd dict, the kmalloc'd command-string copy, the pa_out array plus items, and the output dict created by REASSIGNMENT over the still-unreleased input dict; error paths also skip nlookup_done (namecache leaks). Measured as uid 1001: +1.6-1.9KB live kernel memory per syscall, perfectly linear (2000 calls ~ +1.9MB, InUse never drops), ~250k calls exhaust the 390MB kmem - unprivileged memory-exhaustion DoS on quota-enabled systems (default-off tunable; base-system vquota(8) triggers the same path per invocation). Filed on prop_kern.c reopen as the DF-5674 lead, root cause in vfs_quota.c. Full release-on-all-exits fix compile+boot validated in the combined kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5683 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
build.log β€” 113 B view raw
build.sh β€” 92 B view raw
env.txt β€” 225 B view raw
fix.diff β€” 2.0 KB view raw
fix_build.log β€” 1003 B view raw
fix_combined.diff β€” 2.7 KB view raw
fix_run.log β€” 107 B view raw
run.log β€” 2.2 KB view raw
run.patched.log β€” 1.5 KB view raw
run.sh β€” 180 B view raw
vq_leak.c β€” 1.3 KB view raw
vq_leak.sh β€” 914 B view raw
vq_leak_loop.c β€” 2.0 KB view raw
verdict.json β€” 3.1 KB view raw

DF-5683 β€” sys_vquotactl() leaks the entire prop object graph on every call (unprivileged)

What

sys_vquotactl() (sys/kern/vfs_quota.c:328-413) β€” the consumer of prop_dictionary_copyin() from sys/libprop/prop_kern.c β€” never releases any of the objects it creates or copyin'd:

  • vfs_quota.c:346 dict from prop_dictionary_copyin() β€” never released
  • vfs_quota.c:362 cmd from prop_dictionary_get_cstring() (an allocated copy, prop_dictionary_util.c:183-198) β€” never freed
  • vfs_quota.c:374 pa_out from prop_array_create() (+ every item added by cmd_get_usage_all()) β€” never released
  • vfs_quota.c:405 dict = prop_dictionary_create() (reassigns over the still- un-released copyin'd dict) β€” never released
  • vfs_quota.c:351-356 error paths return without nlookup_done(&nd), leaking nlookup root/namecache references per failing call

Reachability

syscall 530 vquotactl(2), no privilege required (there is no priv check anywhere in the function); gated only on the vfs.quota_enabled boot tunable. Base-system vquota(8) ("show"/"sync"/"limit"/"ulim"/"glim") drives it.

Reproduce (baseline, vulnerable kernel)

  1. boot guest with vfs.quota_enabled=1 in /boot/loader.conf
  2. cc -O2 -o vq_leak_loop vq_leak_loop.c
  3. as an unprivileged user: - vmstat -m | grep -i prop (t0) - ./vq_leak_loop / 1000 ; snapshot t1 ; ./vq_leak_loop / 1000 ; snapshot t2

Expected output (vulnerable kernel)

Linear growth of prop malloc types, ~1.6 KB kernel heap per call: t0: propdict 178 / proparay 9 / prop_string 319 t1: propdict 3100 (+2920) / proparay 1009 (+1000) / prop_string 2260 t2: propdict 6030 (+2930) / proparay 1960 (+950) / prop_string 4220 (β‰ˆ +1.9 MB live kernel memory per 1000 unprivileged syscalls, unreclaimable)

Expected output (patched kernel, fix.diff applied)

prop malloc types FLAT across 2000 calls (+2 allocs total).

Files

  • vq_leak_loop.c tight in-process driver (raw syscall 530, hand-crafted XML)
  • vq_leak.sh shell variant driving /sbin/vquota
  • build.sh / run.sh
  • build.log / run.log (baseline) / run.patched.log (fix validation)
  • fix.diff minimal leak fix (git-applyable, sys/kern/vfs_quota.c)
  • fix_combined.diff the combined diff actually built & validated in the guest
  • fix_build.log / fix_run.log
  • VERDICT.md / manifest.json / verdict.json
VERDICT.md
↓ download raw

DF-5683 VERDICT

Status: REPRODUCED (leak, unprivileged) β€” and FIXED by fix.diff (validated on a rebuilt kernel)

Environment

  • Guest: DragonFly 6.5-DEVELOPMENT x86_64, kernel #0: Thu Jul 2 06:02:54 UTC 2026 (baseline) / #1: Sat Oct 10 00:36:54 UTC 2026 (patched, fix applied).
  • vfs.quota_enabled=1 boot tunable (feature is OFF by default; the syscall returns EOPNOTSUPP otherwise β€” same gate DF-2231 documented).
  • Driver runs as unprivileged uid 1001 (maxx).

How it reproduces

vquotactl(2) (syscall 530) with <plist><dict><key>command</key><string>get usage all</string><key>arguments</key><string>x</string></dict></plist> walks exactly the leaking path:

sys_vquotactl (vfs_quota.c:328) -> prop_dictionary_copyin(&pref, &dict) [prop_kern.c:454] -> prop_dictionary_get_cstring -> cmd (kmalloc copy) -> prop_array_create() pa_out; cmd_get_usage_all() fills it -> goto done; dict = prop_dictionary_create() (shadows the copyin'd dict) -> prop_dictionary_copyout(&pref, dict) [prop_kern.c:554] -> return β€” no prop_object_release anywhere, no nlookup_done on the nlookup error paths (vfs_quota.c:351-356).

Baseline measurements (vmstat -m, three checkpoints t0/t1/t2 = 0/1000/2000 calls)

malloc type t0 InUse t1 t2 per-call
propdict 178 3100 6030 +2.9
prop_dictionary 170 3100 6030 +2.9
proparay 9 1009 1960 +1.0
prop_array 9 1009 1960 +1.0
propstng 323 2270 4220 +1.9
prop_string 319 2260 4220 +1.9

β‰ˆ +1.6-1.9 KB live kernel heap per unprivileged syscall, perfectly linear, zero errors, never freed (InUse never drops). With vm.kmem_size β‰ˆ 390 MB, ~250k calls (minutes) exhaust kernel heap.

An equivalent one-shot demo with base-system tooling (as uid 1001): vquota show / β€” each invocation performs exactly one vquotactl round trip.

Fix validation (fix.diff / fix_combined.diff)

Applied to the guest's /usr/src (vfs_quota.c leak fixes: prop_object_release of dict/pa_out/odict on every exit, prop_dictionary_get_cstring_nocopy for cmd, nlookup_done on error paths), make nativekernel KERNCONF=X86_64_GENERIC, installkernel, reboot into kernel #1.

Patched kernel, 2000 root-driven calls (root passes the DF-5684 priv gate and exercises the identical leak path): - proparay 1β†’2, propdict 150β†’152, propstng 314β†’315 (+2 objects total, i.e. per-boot transient, not per-call) vs +9,770 objects on baseline. - LEAK ELIMINATED. Unprivileged callers additionally get EPERM from the companion privilege fix (DF-5684) before any allocation.

Why this is a real finding beyond DF-2231

DF-2231 covers the copyin size handling in sys/libprop/prop_kern.c (unbounded pref_len, dead prop_object_copyin_limit). This finding is the consumer lifetime bug: even a perfectly-sized, fully-parsed dictionary is leaked wholesale by sys_vquotactl on every call, on the success path, with no malformed input required.

kernel_refs

sys/kern/vfs_quota.c:345-346, 351-356, 362, 374, 405-408 sys/libprop/prop_kern.c:454-458 (prop_dictionary_copyin), 554-557 (copyout) sys/libprop/prop_dictionary_util.c:183-198 (get_cstring allocates a copy)

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt kernel with fix_combined.diff (leak fixes + priv gate + copyin error check): 2000 root-driven calls leak +2 objects total vs +9,770 on baseline; unprivileged callers are EPERM-gated before allocation. Leak eliminated, no functional regression for privileged vquota use.

run.patched.log (proparay 1->2, propdict 150->152 over 2000 calls), fix_build.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Oct 10 00:36:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (t0/t1/t2 vmstat -m triples: propdict 178->3100->6030, proparay 9->1009->1960), run.patched.log (flat after fix), fix_build.log (kernel #1 build), VERDICT.md

PoC changes

Original lead referenced 'vfs_quota.c:404 reassigns dict'; confirmed the leak is broader (cmd copy at :362, pa_out at :374, output dict at :405, nlookup state at :351-356). vq_leak.sh shell variant via /sbin/vquota was too slow (fork/exec per call) and the guest vquota CLI differs from the tree (show/limit/ulim, not get usage all) β€” replaced with the in-process syscall driver vq_leak_loop.c.

Verified recommended fix

Release dict/pa_out/odict on every exit path, use prop_dictionary_get_cstring_nocopy for cmd, nlookup_done on error paths

Verdict

REPRODUCED: sys_vquotactl(2) leaks the copyin'd prop_dictionary, the allocated command string, the pa_out array and the output dictionary on EVERY call (plus nlookup state on error paths). Unprivileged uid 1001, vfs.quota_enabled=1 boot tunable. Measured linear leak ~1.6-1.9KB kernel heap per syscall across 6 prop malloc types (2000 calls -> +9,770 live objects / +1.9MB, InUse never drops); vm.kmem_size 390MB exhausts after ~250k calls (minutes). fix.diff (prop_object_release on all exits + get_cstring_nocopy + nlookup_done) validated on a rebuilt kernel: 2000 calls -> +2 objects total. Distinct from DF-2231 (which covers the unbounded pref_len/copyin-size defect in prop_kern.c itself).