proplib type confusion: unvalidated "arguments" object mtx_lock()ed as an array before type check β unprivileged kernel corruption / panic / livelock
| Field | Value |
|---|---|
| ID | DF-2922 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-843 Access of Resource Using Incompatible Type |
| File | sys/kern/vfs_quota.c |
| Lines | 242 (call), 385 (dispatch); sink: prop_array.c:538-546 |
| Area | kern/vfs + libprop |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_vquotactl passes the user-controlled "arguments" prop object to cmd_set_usage_all, which calls prop_array_iterator(args) without any type check. prop_array_iterator() executes mtx_lock(&pa->pa_rwlock) β a real in-kernel sleep mutex at offset 16 β BEFORE the prop_object_is_array() check. The 40-byte struct mtx then aliases the real object's fields: for prop_number its rb_node, for prop_string ps_mutable/ps_size, for the static prop_bool singletons pb_value plus the BSS past them. The __mtx_lock_ex slowpath writes MTX_EXWANTED|MTX_LINKSPIN into the aliased word and stores/reads kernel stack pointers via mtx_exlink; it also runs while cmd_set_usage_all holds the mount's ac_spin (spinlock-held context).
Threat model & preconditions
Any local user (uid 1001 verified) on a system booted with
vfs.quota_enabled=1 (same reachability class as DF-0141): <false/> β
deterministic Fatal trap 12 page fault in the mutex sleep path with a
spinlock held (3/3 fresh boots); <true/> β Fatal trap 9 GPF at the
exlink-enqueue store β a demonstrated write of a kernel stack pointer
through an aliased pointer (corruption primitive); <integer> β
Fatal trap 9 GPF on the exlink read; <string> β silent full-system
kernel livelock (QEMU 102% CPU, OS dead, no console output). Crash
dump captured. uid=0 chain (pool-groom prop_string so the aliased
exlink aims the stack-pointer write at a pooled object's refcount/type)
documented but not completed β the corruption write primitive and
unkillable-livelock are proven.
Proof of contest
VERIFIED (findings/poc/DF-2922/): typeconfuse.c, unprivileged, 5 knockdowns on the stock kernel with full console captures and a crash dump; dict control returns cleanly. fix.diff (type validation + zero-init) validated on a rebuilt guest kernel: all variants 20/20 clean, guest stays up.
Recommended fix
Validate PROP_TYPE_ARRAY before prop_array_iterator + PROP_TYPE_DICTIONARY per item in cmd_set_usage_all (same checks + zero-init in cmd_set_limit/_uid/_gid); harden prop_array_iterator() itself to type-check before taking the lock. Validated fix.diff in findings/poc/DF-2922/.
Timeline
- 2026-09-02 Discovered during pass-2 audit of vfs_quota.c (GLM 5.3); unpriv corruption reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2922 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| typeconfuse.c | β | 3.5 KB | view raw | |
| build.sh | β | 79 B | view raw | |
| run.sh | β | 320 B | view raw | |
| run.log | β | 536 B | view raw | |
| run.patched.log | β | 689 B | view raw | |
| panic.false.txt | β | 27.5 KB | view raw | |
| panic.dump.txt | β | 1.2 KB | view raw | |
| panic.true.txt | β | 947 B | view raw | |
| panic.number.txt | β | 590 B | view raw | |
| livelock-string.txt | β | 627 B | view raw | |
| bss-layout.txt | β | 870 B | view raw | |
| build.log | β | 836 B | view raw | |
| env.txt | β | 488 B | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| verdict.json | β | 5.3 KB | view raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| README.md | β | 3.6 KB | β raw |
DF-2922 β proplib type confusion in sys_vquotactl β kernel memory corruption / panic / livelock
What
sys/kern/vfs_quota.c:385 passes the user-controlled "arguments" proplib
object to cmd_set_usage_all(), which calls prop_array_iterator(args) at
sys/kern/vfs_quota.c:242 without validating that the object is an
array. prop_array_iterator() (sys/libprop/prop_array.c:538-546)
executes mtx_lock(&pa->pa_rwlock) β in-kernel proplib rwlocks are real
sleep mutexes (sys/libprop/prop_object_impl.h:295-299) β before the
prop_object_is_array() type check inside _prop_array_iterator_locked().
For a non-array object the 40-byte struct mtx at offsetof(struct
_prop_array, pa_rwlock) == 16 aliases other fields of the real object:
| "arguments" type | aliased mtx fields | observed result |
|---|---|---|
<dict> (control) |
real mutex at same offset | clean EINVAL-free return (control passes) |
<false/> (static _prop_bool_false) |
pb_value=lock word, gap/past-object=owner/ident |
Fatal trap 12: page fault in strncpy via _mtx_lock_ex β mtx_wait_link (wmesg copy from aliased mtx_ident), panic with 1 spinlocks held β 3/3 fresh boots |
<true/> |
slowpath: mtx_exlink write lands past the bool globals |
Fatal trap 9 (GPF) at _mtx_lock_ex+0xdc movq %r12,(%rax) β the exlink-list write of a kernel stack pointer through the aliased pointer |
<integer>31337</integer> |
pn_link rb_node β mtx fields |
Fatal trap 9 (GPF) at _mtx_lock_ex+0xc4 movq 0x8(%rax),%rcx β read through the aliased exlink chain |
<string> |
ps_mutable low bits = never-zero lock word |
silent full-system livelock: QEMU at 102% CPU, guest OS dead, no console output (the __mtx_lock_ex retry loop spins forever on a heap-pointer lock word while holding the mount's ac_spin) |
All triggered by an unprivileged user (uid 1001) with vfs.quota_enabled=1
(the same reachability already established by DF-0141).
Additionally the fault happens with ac_spin held (cmd_set_usage_all
takes the mount spinlock at vfs_quota.c:228 before calling
prop_array_iterator) β the compounding "sleep/spin under spinlock" defect.
Files
typeconfuse.cβ trigger (mode selectable)build.sh/run.shpanic.false.txt,panic.true.txt,panic.number.txtβ serial console capturespanic.dump.txtβ auto-backtrace from crash dump (debugger_on_panic=0)disasm.txtβ disassembly proving the faulting stores/loads are the__mtx_lock_exexlink-list manipulationbss-layout.txtβnmproof of the_prop_bool_*global layout the aliased mutex overwritesfix.diffβ validated fix (type checks + stack initialization)verdict.json/manifest.json
Build
cc -O -o typeconfuse typeconfuse.c -lprop
Run (on a guest booted with vfs.quota_enabled=1)
# control (returns cleanly): ./typeconfuse /boot dict # each of the following kills the stock kernel deterministically: ./typeconfuse /boot false # fatal trap 12, page fault, spinlock held ./typeconfuse /boot true # fatal trap 9 GPF (write through aliased ptr) ./typeconfuse /boot number # fatal trap 9 GPF (read through aliased ptr) ./typeconfuse /boot string # silent full-system kernel livelock
Expected output (baseline, unpatched)
false: mode=false path=/boot pid=N then the console shows
Fatal user address access from kernel mode from typeconfuse at
ffffffff809d780f / panic: page fault / panic with 1 spinlocks held.
On the patched kernel every variant returns vquotactl returned 22
(EINVAL) and the guest stays up.
DF-2922 VERDICT β REPRODUCED (kernel memory corruption / panic / livelock, unprivileged)
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), vfs.quota_enabled=1 (boot tunable), attacker = uid 1001 (maxx) over ssh.
Root cause (source-proven)
- sys/kern/vfs_quota.c:385
cmd_set_usage_all(nch.mount, args)βargsisprop_dictionary_get(dict, "arguments")(sys/kern/vfs_quota.c:367), an arbitrary proplib object type fully controlled by the caller's plist. - sys/kern/vfs_quota.c:242
prop_array_iterator(args)with no type check. - sys/libprop/prop_array.c:538-546:
_PROP_RWLOCK_RDLOCK(pa->pa_rwlock)(=mtx_lock(), sys/libprop/prop_object_impl.h:297) executes BEFORE theprop_object_is_array()check inside_prop_array_iterator_locked()(prop_array.c:513-536).offsetof(struct _prop_array, pa_rwlock)is 16; the 40-bytestruct mtx(sys/sys/mutex.h:66) then aliases whatever the real object keeps at +16..+56: _prop_number:pn_linkrb_node (sys/libprop/prop_number.c:51)_prop_string:ps_mutable/ps_size/ps_flags(prop_string.c:40-48)_prop_bool:pb_value+ past-object globals (prop_bool.c:35-41;nmshows_prop_bool_false@0xffffffff8179b300,_prop_bool_true@0xffffffff8179b320,_prop_data_pool@0xffffffff8179b338 β the mtx's owner/exlink/shlink/ident fields at +24/+32/+40/+48 land on/around these globals and past them).- The contention slowpath
__mtx_lock_ex(sys/kern/kern_mutex.c:85-200) writesMTX_EXWANTED|MTX_LINKSPINinto the aliased lock word, stores a kernel stack pointer throughmtx->mtx_exlink(list enqueue, kern_mutex.c:153-163), and dereferences the chain β on memory that is not a mutex. - Compounding defect:
cmd_set_usage_allholds the mount'sac_spinspinlock (vfs_quota.c:228) across the call β the sleeping/spinning mutex path then runs with a spinlock held ("panic with 1 spinlocks held").
Reproduction (all unprivileged, 5 independent knockdowns)
false(fresh boot): consoleFatal user address access from kernel mode from typeconfuse at ffffffff809d780f;Fatal trap 12: page fault;fault virtual address = 0x40; backtrace from the crash dump:strncpy β _mtx_lock_ex+0x130(thecallq mtx_wait_linkreturn β the sleep path copiesmtx->mtx_identas the tsleep wchan string);panic with 1 spinlocks held;panic: page fault; dump saved to /var/crash/vmcore.0. Deterministic 3/3 fresh boots.true:Fatal trap 9: general protection fault, RIP_mtx_lock_ex+0xdc = movq %r12,(%rax)β the exlink enqueue writing a kernel stack pointer through the aliasedmtx_exlink.number(31337):Fatal trap 9, RIP_mtx_lock_ex+0xc4 = movq 0x8(%rax),%rcxβ read side of the same exlink walk.string: NO trap β the aliased lock word is the low 32 bits of the heapps_mutablepointer (never 0), so__mtx_lock_ex's retry loop spins forever in kernel mode withac_spinheld: QEMU pinned at 102% CPU, guest OS dead, serial console silent. Full-system livelock from a single unprivileged syscall.- Control
dict: same-shaped object (dictionary rwlock at the same offset), returns cleanly β the plumbing works and only the object TYPE decides the outcome.
Primitive characterization
- write:
mtx->mtx_exlink = link/link->next->prev = linkβ writes a kernel-stack pointer to an address derived from aliased object bytes (forstring, heap bytes the attacker shapes via plist content β write-what(kernel-stack-addr)-where(aliased-ptr) potential with pool grooming). - write:
mtx->mtx_owner = curthreadpast the end of_prop_bool_false(8-byte OOB write on a static global, masked only by linker padding on this build). - sleep/spin-while-holding-ac_spin: unkillable thread / livelock.
- Full uid=0 chain (pool-groom
_prop_string_poolso the aliased exlink points at attacker-shaped heap; the enqueue then writes a stack pointer at a controlled address, e.g. a pooled object's refcount/type) was NOT completed β the corruption primitive itself is proven above; the reliable panic/livelock is the shipped impact.
Fix validation
fix.diff (type validation of "arguments" and per-element dicts + zero
initialization of space/limit) applied to the guest's /usr/src,
make nativekernel, installed, rebooted:
* all four knockdown variants return EINVAL (22) cleanly, guest stays up,
* the leak PoC (DF-2925) then reports ac_limit=0 (initialization fix).
Baseline (unpatched) re-confirmed immediately beforehand in the same
session flow.
References
sys/kern/vfs_quota.c:228,242,249,277,293,314,367,385; sys/libprop/prop_array.c:513-546; sys/libprop/prop_object_impl.h:295-299; sys/sys/mutex.h:66; sys/kern/kern_mutex.c:85-200,209; sys/kern/kern_kmalloc.c:706 (limit-panic policy, cf. DF-2926).
Fix verification
fixedSingle-fix kernel (fix.diff on vfs_quota.c, make nativekernel/installkernel, -Werror clean): baseline knockdowns re-confirmed fatal on #0 immediately prior; on #1 all four variants (false/true/number/string) plus dict control return cleanly, 20/20 repeat of the false variant, guest stays up throughout. Previously-observed bad behavior GONE.
build.log (vfs_quota.o compiled with -Werror, RC=0, install completed); VERDICT.md fix-validation section
Confirmed kernel references
Detail
Exploit chain
unpriv vquotactl("set usage all", arguments=
Evidence (decisive lines)
["panic.false.txt β Fatal trap 12, fault va 0x40, strncpy+0x1f, 'panic with 1 spinlocks held', 3/3 fresh boots", 'panic.dump.txt β crash-dump auto-backtrace: strncpy <- _mtx_lock_ex+0x130 (return addr of callq mtx_wait_link)', 'panic.true.txt β Fatal trap 9 at _mtx_lock_ex+0xdc movq %r12,(%rax) (write through aliased ptr)', 'panic.number.txt β Fatal trap 9 at _mtx_lock_ex+0xc4 movq 0x8(%rax),%rcx (read side)', 'livelock-string.txt β string variant: QEMU 102% CPU, OS dead, zero console output', 'bss-layout.txt β nm proof of _prop_bool_false@0xffffffff8179b300 / _prop_bool_true@0x...b320 / _prop_data_pool@0x...b338 overlaid by the aliased mtx', 'build.log + VERDICT.md β patched kernel #1 (fix.diff, -Werror clean): all variants return cleanly, 20/20, guest up']
PoC changes
Replaced prop_bool_false()/prop_bool_true() (not the userland API) with prop_bool_create(false/true); removed prop_object_is_array() from the userland leak helper; everything else as seeded.
Verified recommended fix
Validate prop_object_type(args)==PROP_TYPE_ARRAY before prop_array_iterator() and PROP_TYPE_DICTIONARY for the set-limit commands and each array element (see validated fix.diff); harden prop_array_iterator() itself to type-check before locking.
Verdict
Unauthenticated-local (uid 1001) type confusion in sys_vquotactl: the user-controlled "arguments" proplib object is passed unvalidated to prop_array_iterator() (vfs_quota.c:242), which mtx_lock()s the object at the _prop_array.pa_rwlock offset BEFORE type-checking it (prop_array.c:538-546). On the stock INVARIANTS kernel the aliased 40-byte struct mtx over non-array memory produces:
No comments yet.