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

Unbounded attacker-controlled pref_len in _prop_object_copyin causes kernel panic / memory exhaustion (prop_object_copyin_limit dead variable)

Summary

_prop_object_copyin() accepts user-controlled struct plistref whose pref_len field (fully attacker-influenced size_t) used directly as kmalloc size and copyin length with no upper-bound validation. File declares unsigned int prop_object_copyin_limit=65536 at line 383 specifically to bound this input but variable is NEVER READ anywhere in kernel (grep finds only declaration). Unprivileged local user can pass pref_len up to SIZE_MAX triggering: (1) integer overflow pref_len+1 wraps to 0 kmalloc(0) returns ZERO_LENGTH_PTR copyin validates udaddr+SIZE_MAX triggers EFAULT benign but latent; (2) NULL-deref panic for huge pref_len (1TiB) kmalloc takes large-alloc path kmem_slab_alloc returns NULL kmalloc returns NULL even M_WAITOK then copyin(pref_plist NULL pref_len) writes to kernel address 0 page fault panic; (3) limit-panic with repeated large pref_len M_TEMP ks_limit exceeded kmalloc panics at kern_slaballoc.c:877. Reachable via vquotactl syscall #530 (no priv needed only vfs_quota_enabled gate) UDEVPROP ioctl NETBSD_DM_IOCTL TBRIDGE_LOADTEST. Impact: reliable local kernel panic A:H single call. No privilege escalation demonstrated but unbounded copyin also feeds untrusted XML into prop_object.c (DF-2227 High OOB) prop_dictionary.c prop_array.c amplifying parser bugs past 64KB boundary dead variable meant to enforce.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2231 Β· 12 files
FileTypeDescriptionSize
df_poc.c trigger-source parameterised privileged demonstrator via /dev/udev UDEVPROP; accepts hex pref_len + optional mapmb 4.6 KB view raw
build.sh build-script cc -O0 -g -o df_poc df_poc.c 178 B view raw
run.sh run-script sudo ./df_poc <len_hex> [mapmb] 619 B view raw
README.md readme bug, reachability, PoC usage, expected output 2.6 KB ↓ raw
VERDICT.md verdict full narrative: reproduced mechanism, reachability blocker, fix validation 6.7 KB ↓ raw
build.log build-log final clean userspace build 96 B view raw
run.log run-log baseline (#0) bad-behavior markers + reachability notes 3.0 KB view raw
fix_build.log build-log single-fix nativekernel build (NK_DONE rc=0) 5.6 MB ↓ download
fix_run.log run-log patched (#1) good-behavior markers: E2BIG, legit path EIO 940 B view raw
fix_kmem_proof.txt dmesg vmstat -m: temp memuse flat across 1GiB call on #1 (no allocation) 147 B view raw
env.txt environment uname, cc, vfs.quota_enabled, /dev/udev perms, M_TEMP limit 521 B view raw
fix.diff suggested-fix git-apply-able: enforce prop_object_copyin_limit + M_NULLOK + NULL check 1.2 KB view raw
README.md readme bug, reachability, PoC usage, expected output
↓ download raw

DF-2231: _prop_object_copyin() unbounded pref_len in sys/libprop/prop_kern.c

Bug (VERIFIED)

_prop_object_copyin() (sys/libprop/prop_kern.c:386) accepts a user struct plistref whose pref_len (size_t, fully attacker-controlled) is fed directly into kmalloc(pref_len + 1, …) (line 398) and copyin(…, pref_len) (line 399) with no upper-bound validation. The file declares unsigned int prop_object_copyin_limit = 65536; at line 383 specifically to bound this input β€” but it is dead code, never compared against pref_len.

Reachability (default GENERIC guest)

The four kernel callers of prop_*_copyin[_ioctl]() are all privileged on the default guest, so there is no unprivileged trigger: - sys/kern/kern_udev.c:892 β€” UDEVPROP on /dev/udev β€” root:wheel 0600 - sys/kern/vfs_quota.c:346 β€” vquotactl(530) β€” gated by vfs_quota_enabled (default 0, CTLFLAG_RD β†’ root-only; returns EOPNOTSUPP at line 342) - sys/dev/disk/dm/device-mapper.c:267 β€” NETBSD_DM_IOCTL β€” dm module not loaded; /dev/mapper/control is 0640 root:operator - sys/dev/misc/tbridge/tbridge.c:258 β€” TBRIDGE_LOADTEST β€” module not loaded

This PoC therefore runs as root via /dev/udev (the only caller compiled into the base kernel) to prove the code claim, not to demonstrate an unpriv→root escalation. Impact ceiling = privileged memory-exhaustion DoS + dead-code hardening gap.

PoC

cc -O0 -g -o df_poc df_poc.c     # build.sh
sudo ./df_poc <len_hex> [mapmb]  # run.sh ; len_hex=pref_len in hex
  • ./df_poc 0x40000000 β€” 1 GiB, 64-byte user ptr β†’ EFAULT (1 GiB kmalloc OK)
  • ./df_poc 0x18000000 384 β€” 384 MiB, mapped source β†’ EIO (full 384 MiB copyin)
  • ./df_poc 0xffffffffffffffff β€” SIZE_MAX (wrap to 0) β†’ EFAULT
  • ./df_poc 0x40 β€” 64-byte benign control β†’ EIO (legit path, bad XML)

Must run as root (/dev/udev is 0600 root:wheel). On the fixed kernel the oversized cases return errno=7 (E2BIG) with zero allocation; the benign case is unchanged (EIO).

Expected

  • Bug present (#0 baseline): oversized pref_len β†’ EFAULT (kmalloc succeeded, copyin faulted) or EIO (copyin fully succeeded into a huge buffer); guest stays up. Proves unbounded attacker-driven kernel allocation.
  • Fixed (#1): oversized pref_len β†’ E2BIG, no allocation; legit path intact.

Fix

Enforce prop_object_copyin_limit before kmalloc/copyin, plus M_NULLOK + NULL check. See fix.diff (validates clean before/after; git apply clean).

VERDICT.md verdict full narrative: reproduced mechanism, reachability blocker, fix validation
↓ download raw

DF-2231 β€” _prop_object_copyin() unbounded pref_len (prop_kern.c)

Verdict

REPRODUCED β€” the code claim is CONFIRMED: prop_object_copyin_limit (declared at sys/libprop/prop_kern.c:383 precisely to bound the copy-in) is never consulted; _prop_object_copyin() feeds the fully attacker-controlled pref->pref_len (a size_t) straight into kmalloc(pref_len + 1, …) (line 398) and copyin(…, pref_len) (line 399) with no upper-bound check. The proposed fix was VALIDATED on a single-fix kernel (E2BIG, zero allocation; legit path intact).

Impact (honest, on the default GENERIC guest)

Privileged memory-exhaustion DoS + dead-code hardening gap. Impact ceiling, not an unprivileged escalation: - The four kernel callers of prop_*_copyin[_ioctl]() are all privileged on the default guest (verified): - sys/kern/kern_udev.c:892 — UDEVPROP ioctl on /dev/udev — node is root:wheel 0600 (maxx: Permission denied, confirmed). - sys/kern/vfs_quota.c:346 — vquotactl(530) — gated by vfs_quota_enabled which is 0 by default and CTLFLAG_RD (root-only tunable); returns EOPNOTSUPP at vfs_quota.c:342 before reaching the vulnerable call. - sys/dev/disk/dm/device-mapper.c:267 — NETBSD_DM_IOCTL — dm module not loaded on default guest; /dev/mapper/control would be 0640 root:operator. - sys/dev/misc/tbridge/tbridge.c:258 — TBRIDGE_LOADTEST — module not loaded; 0600 root:wheel. - => No unprivileged trigger exists on the default kernel. This is the valid hard blocker for an uid=0 chain (root→kernel is game-over by definition; the bug is a privileged DoS / hardening gap here, not an unpriv→root privesc). - Demonstrated as root via /dev/udev (the only caller compiled into the base kernel): pref_len = 1 GiB → kmalloc(1 GiB) succeeds (EFAULT on the truncated user source); pref_len = 384 MiB with a fully user-mapped source → the kernel fully allocates and copies 384 MiB (EIO from internalize, not EFAULT — copyin ran end-to-end). An attacker-controlled pref_len thus forces unbounded kernel allocations with no validation. - No single-shot panic observed on this guest: copyin() swallows the destination fault (EFAULT) before the unconditional buf[pref_len] = '\0' store at prop_kern.c:404, and the slab allocator's ks_limit precise-recompute (kern_slaballoc.c:867-869) prevents a burst-driven panic("malloc limit exceeded") on a single host (12× 256 MiB parallel burst: all returned EIO, no panic).

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger. A privileged caller (/dev/udev UDEVPROP, root) issues an ioctl whose ap->a_data is a user struct plistref { void *pref_plist; size_t pref_len; } (sys/libprop/plistref.h:43-45).
  2. Sink (no bound). udev_dev_ioctl() (sys/kern/kern_udev.c:892) β†’ prop_dictionary_copyin_ioctl() (prop_kern.c:478) β†’ _prop_object_copyin_ioctl() (prop_kern.c:429, only checks cmd & IOC_IN) β†’ _prop_object_copyin() (prop_kern.c:386). At prop_kern.c:398-399: c buf = kmalloc(pref->pref_len + 1, M_TEMP, M_WAITOK); error = copyin(pref->pref_plist, buf, pref->pref_len); prop_object_copyin_limit (line 383, = 65536) is never compared against pref_len.
  3. Primitive. Attacker-controlled pref_len drives an unbounded kernel allocation + user→kernel copy. Two failure modes observed: - Large pref_len, small user mapping → kmalloc succeeds (huge transient kernel buffer), copyin faults off the user source → EFAULT, kfree. Net: attacker forces a gigabyte-scale transient kernel allocation per call. - pref_len == SIZE_MAX → pref_len + 1 integer-overflows to 0 → kmalloc(0) returns ZERO_LENGTH_PTR ((void *)-8, kern_slaballoc.c:193) → copyin into it faults → EFAULT (no panic: copyin handles the fault before the buf[pref_len]='\0' store at line 404).
  4. Effect. Privileged memory-exhaustion DoS (unbounded per-call allocation) + a latent integer-overflow / dead-code gap. The prop_object_copyin_limit guard was clearly intended (it exists solely for this) but is dead code.

Fix (validated)

Enforce prop_object_copyin_limit against pref_len before kmalloc+ copyin, and make the kmalloc M_NULLOK + NULL-check so a future bypass or genuine kmem exhaustion cannot deref NULL. Full diff in fix.diff:

size_t len = pref->pref_len;
if (len == 0 || len > (size_t)prop_object_copyin_limit)
    return (E2BIG);
buf = kmalloc(len + 1, M_TEMP, M_WAITOK | M_NULLOK);
if (buf == NULL)
    return (ENOMEM);

Fix validation (single-fix kernel, make -j6 nativekernel)

  • Baseline #0 (unpatched, Jul 2 2026, sha256 5dc83dac…): pref_len=1 GiB β†’ EFAULT (1 GiB kmalloc succeeded); pref_len=384 MiB mapped β†’ EIO (full 384 MiB copyin succeeded).
  • Single-fix #1 (Aug 8 16:25:24 2026, sha256 2dbf4696…): all oversized pref_len β†’ errno=7 E2BIG (the fix's guard fires before kmalloc); vmstat -m shows temp memuse flat at 332K before/after a 1 GiB call (only the request counter ticks); benign 64-byte input still reaches internalize and returns EIO (legit path intact).
  • git apply --check: APPLIES CLEAN.

PoC changes

Rewrote df_poc.c from the reviewer draft (which used a stale hand-rolled struct plistref and a syscall-number literal) into a parameterised demonstrator that uses the real <sys/udev.h> UDEVPROP macro and <libprop/plistref.h>, accepts a hex pref_len plus an optional mapmb (to mmap a large source so copyin runs to completion), and documents the four privileged callers + the unprivileged-reachability blocker in its header. Added build.sh/run.sh. Original README claim ("kernel panic") was over-optimistic; the honest impact on this guest is privileged memory-exhaustion DoS (no single-shot panic; see VERDICT.md for why).

Files

  • df_poc.c β€” parameterised privileged demonstrator via /dev/udev UDEVPROP.
  • build.sh / run.sh β€” exact build/run.
  • build.log β€” final clean userspace build.
  • run.log β€” baseline (#0) bad-behavior markers (EFAULT/EIO) + reachability notes.
  • fix_build.log β€” single-fix nativekernel build output (NK_DONE rc=0).
  • fix_run.log β€” patched (#1) good-behavior markers (E2BIG; legit path EIO).
  • fix_kmem_proof.txt β€” vmstat -m showing temp memuse flat across a 1 GiB call on #1.
  • env.txt β€” guest uname, cc, vfs.quota_enabled, /dev/udev perms, M_TEMP limit.
  • fix.diff β€” git apply-able fix (enforce the limit + M_NULLOK + NULL check).
  • manifest.json β€” machine-readable catalog.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Baseline #0 (sha256 5dc83dac..., Jul 2) reproduced: pref_len=1GiB -> EFAULT (1GiB kmalloc succeeded) and pref_len=384MiB mapped -> EIO (full 384MiB copyin). Single-fix kernel #1 (Aug 8 16:25:24, sha256 2dbf4696...) does NOT: all oversized pref_len return errno=7 E2BIG with zero allocation (vmstat -m temp memuse flat at 332K before/after a 1GiB call), and benign 64-byte still reaches internalize and returns EIO (legit path intact). Fix closes the bug; fix_build.log shows NK_DONE rc=0.

baseline #0: df_poc 0x40000000 -> errno=14 EFAULT [1GiB kmalloc OK]; df_poc 0x18000000 384 -> errno=5 EIO [full 384MiB copyin]. patched #1: 0x40000000 -> errno=7 E2BIG; 0x18000000 384 -> errno=7 E2BIG; 0xffffffffffffffff -> errno=7 E2BIG; benign 0x40 -> errno=5 EIO [legit path intact]. vmstat -m temp: 332K before, 332K after 1GiB call on #1.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 16:25:24 UTC 2026 (sha256 2dbf4696590c272e071148088cd67455a705b20e252c8af61545795ff02df235)

Confirmed kernel references

Detail

Exploit chain

none (valid Phase-6 hard blocker: root-only reachability). All four kernel callers of prop_*_copyin_ioctl are privileged on the default GENERIC guest: (1) kern_udev.c:892 UDEVPROP on /dev/udev which is root:wheel 0600 (maxx: Permission denied, confirmed); (2) vfs_quota.c:346 vquotactl(530) gated by vfs_quota_enabled (default 0, root-only tunable) returns EOPNOTSUPP at vfs_quota.c:342 BEFORE reaching the vulnerable call; (3) device-mapper.c:267 NETBSD_DM_IOCTL (dm module NOT loaded; node would be 0640 root:operator); (4) tbridge.c:258 TBRIDGE_LOADTEST (module NOT loaded; 0600 root:wheel). No unprivileged trigger exists, so no privilege boundary to cross and no uid=0 chain (root->kernel game-over). Privileged memory-exhaustion DoS + dead-code hardening gap. Not a memory-corruption write primitive reachable from userspace (destination is freshly kmalloc'd kernel buffer of attacker-chosen SIZE; only 'corruption' is transient over-allocation, freed on the copyin/internalize failure path).

Evidence (decisive lines)

BASELINE #0 (root via /dev/udev): df_poc 0x40000000 (1GiB) -> errno=14 EFAULT [1GiB kmalloc OK]; df_poc 0x18000000 384 (384MiB mapped) -> errno=5 EIO [EIO=full copyin succeeded]; df_poc 0xffffffffffffffff (SIZE_MAX) -> errno=14 EFAULT [pref_len+1 wraps to 0]. maxx unprivileged: open /dev/udev: Permission denied. vfs.quota_enabled: 0. PATCHED #1 (Aug 8 16:25:24): all oversized -> errno=7 E2BIG; benign 0x40 -> errno=5 EIO (legit path intact). vmstat -m temp memuse: 332K before AND after 1GiB call on #1 (no allocation).

PoC changes

Rewrote df_poc.c from reviewer draft (stale hand-rolled struct plistref + syscall-number literal) into parameterised demonstrator using real UDEVPROP macro and , accepting hex pref_len plus optional mapmb (to mmap a large source so copyin runs to completion). Header documents the four privileged callers and the unprivileged-reachability blocker. build.sh/run.sh. Original README 'kernel panic' claim was over-optimistic; corrected to honest privileged-DoS impact.

Verified recommended fix

In sys/libprop/prop_kern.c _prop_object_copyin(), enforce the already-declared bound before kmalloc/copyin: read size_t len = pref->pref_len; if (len == 0 || len > (size_t)prop_object_copyin_limit) return (E2BIG); and make the allocation M_WAITOK|M_NULLOK with a NULL->ENOMEM check so a future bypass or genuine kmem exhaustion cannot deref NULL (the SIZE_MAX case where pref_len+1 wraps to 0 is also closed by the limit check). Supersedes finding proposal (hardens allocation itself). Full git-apply-able diff (validated: git apply --check clean, single-fix kernel built and booted) in findings/poc/DF-2231/fix.diff.

Verdict

REPRODUCED. The code claim is confirmed line-by-line in sys/libprop/prop_kern.c: prop_object_copyin_limit (=65536) is declared at line 383 specifically to bound the copy-in but is NEVER compared against pref_len; _prop_object_copyin() feeds the fully attacker-controlled pref->pref_len (size_t) straight into kmalloc(pref_len+1, M_TEMP, M_WAITOK) (line 398) and copyin(pref_plist, buf, pref_len) (line 399) with no upper-bound check. Demonstrated as root via /dev/udev UDEVPROP (kern_udev.c:892): pref_len=1 GiB -> EFAULT proves kmalloc(1 GiB) succeeded; pref_len=384 MiB with a fully user-mapped source -> EIO (not EFAULT) proves the kernel fully allocated AND populated a 384 MiB buffer per call; pref_len=SIZE_MAX -> pref_len+1 wraps to 0 -> kmalloc(0)=ZERO_LENGTH_PTR -> EFAULT. copyin() swallows the destination fault before the unconditional buf[pref_len]=NUL store; slab allocator ks_limit precise-recompute prevents a burst-driven malloc-limit panic on a single host (12x256MiB parallel burst: all EIO, no panic). Net: attacker-controlled unbounded kernel allocation per call.