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)
PoC verification
Evidence pack
findings/poc/DF-5683 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
dictfromprop_dictionary_copyin()β never released - vfs_quota.c:362
cmdfromprop_dictionary_get_cstring()(an allocated copy, prop_dictionary_util.c:183-198) β never freed - vfs_quota.c:374
pa_outfromprop_array_create()(+ every item added bycmd_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)
- boot guest with
vfs.quota_enabled=1in /boot/loader.conf cc -O2 -o vq_leak_loop vq_leak_loop.c- 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
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=1boot 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
fixedRebuilt 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
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).
No comments yet.