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)
PoC verification
Evidence pack
findings/poc/DF-2231 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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) orEIO(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).
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)
- Trigger. A privileged caller (
/dev/udevUDEVPROP, root) issues an ioctl whoseap->a_datais a userstruct plistref{ void *pref_plist; size_t pref_len; }(sys/libprop/plistref.h:43-45). - 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 checkscmd & IOC_IN) β_prop_object_copyin()(prop_kern.c:386). Atprop_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 againstpref_len. - Primitive. Attacker-controlled
pref_lendrives an unbounded kernel allocation + userβkernel copy. Two failure modes observed: - Largepref_len, small user mapping βkmallocsucceeds (huge transient kernel buffer),copyinfaults off the user source βEFAULT,kfree. Net: attacker forces a gigabyte-scale transient kernel allocation per call. -pref_len == SIZE_MAXβpref_len + 1integer-overflows to0βkmalloc(0)returnsZERO_LENGTH_PTR((void *)-8,kern_slaballoc.c:193) βcopyininto it faults βEFAULT(no panic: copyin handles the fault before thebuf[pref_len]='\0'store at line 404). - Effect. Privileged memory-exhaustion DoS (unbounded per-call allocation)
+ a latent integer-overflow / dead-code gap. The
prop_object_copyin_limitguard 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, sha2565dc83dacβ¦):pref_len=1 GiBβEFAULT(1 GiB kmalloc succeeded);pref_len=384 MiBmapped βEIO(full 384 MiB copyin succeeded). - Single-fix
#1(Aug 8 16:25:24 2026, sha2562dbf4696β¦): all oversizedpref_lenβ errno=7E2BIG(the fix's guard fires beforekmalloc);vmstat -mshowstempmemuse flat at332Kbefore/after a 1 GiB call (only the request counter ticks); benign 64-byte input still reachesinternalizeand returnsEIO(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-fixnativekernelbuild output (NK_DONE rc=0).fix_run.logβ patched (#1) good-behavior markers (E2BIG; legit path EIO).fix_kmem_proof.txtβvmstat -mshowingtempmemuse flat across a 1 GiB call on #1.env.txtβ guestuname,cc,vfs.quota_enabled,/dev/udevperms, M_TEMP limit.fix.diffβgit apply-able fix (enforce the limit + M_NULLOK + NULL check).manifest.jsonβ machine-readable catalog.
Fix verification
fixedVALIDATED. 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.
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
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.
No comments yet.