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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2922 Β· 17 files
FileTypeDescriptionSize
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.sh
  • panic.false.txt, panic.true.txt, panic.number.txt β€” serial console captures
  • panic.dump.txt β€” auto-backtrace from crash dump (debugger_on_panic=0)
  • disasm.txt β€” disassembly proving the faulting stores/loads are the __mtx_lock_ex exlink-list manipulation
  • bss-layout.txt β€” nm proof of the _prop_bool_* global layout the aliased mutex overwrites
  • fix.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.

VERDICT.md
↓ download raw

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) β€” args is prop_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 the prop_object_is_array() check inside _prop_array_iterator_locked() (prop_array.c:513-536). offsetof(struct _prop_array, pa_rwlock) is 16; the 40-byte struct mtx (sys/sys/mutex.h:66) then aliases whatever the real object keeps at +16..+56:
  • _prop_number: pn_link rb_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; nm shows _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) writes MTX_EXWANTED|MTX_LINKSPIN into the aliased lock word, stores a kernel stack pointer through mtx->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_all holds the mount's ac_spin spinlock (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)

  1. false (fresh boot): console Fatal 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 (the callq mtx_wait_link return β€” the sleep path copies mtx->mtx_ident as the tsleep wchan string); panic with 1 spinlocks held; panic: page fault; dump saved to /var/crash/vmcore.0. Deterministic 3/3 fresh boots.
  2. 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 aliased mtx_exlink.
  3. number (31337): Fatal trap 9, RIP _mtx_lock_ex+0xc4 = movq 0x8(%rax),%rcx β€” read side of the same exlink walk.
  4. string: NO trap β€” the aliased lock word is the low 32 bits of the heap ps_mutable pointer (never 0), so __mtx_lock_ex's retry loop spins forever in kernel mode with ac_spin held: QEMU pinned at 102% CPU, guest OS dead, serial console silent. Full-system livelock from a single unprivileged syscall.
  5. 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 (for string, heap bytes the attacker shapes via plist content β†’ write-what(kernel-stack-addr)-where(aliased-ptr) potential with pool grooming).
  • write: mtx->mtx_owner = curthread past 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_pool so 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 3 13:57:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unpriv vquotactl("set usage all", arguments=) -> prop_dictionary_copyin internalizes the attacker plist -> cmd_set_usage_all (holds mount ac_spin) -> prop_array_iterator -> mtx_lock on aliased fields of attacker-typed object -> __mtx_lock_ex slowpath writes MTX_EXWANTED|MTX_LINKSPIN into aliased word, stores/reads kernel stack pointers via mtx_exlink -> fatal trap (12/9) or infinite kernel spin with ac_spin held. Escalation potential (not completed): shape the aliased bytes via a prop_string so the exlink-derived write lands on a pooled object's refcount/type.

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: -> Fatal trap 12 page fault in the mutex sleep path (strncpy of the aliased mtx_ident as tsleep wchan) with ac_spin held ('panic with 1 spinlocks held'), deterministic 3/3 fresh boots; -> Fatal trap 9 GPF at the exlink-enqueue store movq %r12,(%rax) (writes a kernel stack pointer through the aliased pointer, i.e. a demonstrated corruption write primitive); -> Fatal trap 9 GPF on the exlink read (aliased prop_number rb_node); -> silent full-system kernel livelock (QEMU pinned 102% CPU, OS dead, no console output) because the aliased lock word is a heap pointer that never clears. Dict control returns cleanly. A crash dump was captured and the auto-backtrace confirms the _mtx_lock_ex -> mtx_wait_link -> strncpy chain. Full uid=0 chain (grooming the string/number pools so the aliased exlink aims the stack-pointer write at a controlled address) was not completed; the corruption primitive itself is proven. Fix validated: with type checks added in vfs_quota.c, the same single-fix kernel survives all four knockdown variants (20/20 clean returns) and the guest stays up.