DF-5680 / verdict.json
{ "finding_id": "DF-5680", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "verdict": "Reproduced on the stock guest kernel: the attacker-controlled <data size=\"N\"> plist attribute flows through strtoul (prop_data.c:572, with the ERANGE guard compiled out in-kernel at :573-576) straight into kmalloc(len+1, M_PROP_DATA, M_WAITOK) (prop_data.c:588) with no upper bound. size=2^63 (98-byte plist, one DM ioctl on /dev/mapper/control 0640 root:operator) panics with kmem_slab_alloc(): kernel_map ran out of space! and the exact stack _prop_data_internalize -> _kmalloc -> kmem_slab_alloc (panic.txt), because M_WAITOK lacks M_NULLOK (kern_slaballoc.c:1721-1724); size=8GiB on the 4GiB guest wedges the whole system in the non-interruptible vm_wait(0) retry loop (kern_slaballoc.c:1792-1800) with pagedaemon: WARNING! Continuous paging on the console and ssh dead (hang.console.txt). The same inputs against the same libprop source in userland fail cleanly (malloc NULL -> internalize NULL; run.userland.log), and size=\"-1\"/30-digit-overflow document the missing-ERANGE/negative acceptance gap (errno=0 pass-through in userland for -1). No memory-corruption primitive: every decoder write is bounds-checked (prop_data.c:415-467) and alen!=len (prop_data.c:597) blocks size spoofing, so impact ceiling is panic/wedge at a root/operator entry bar -> Medium. fix.diff (bound len by the remaining XML length) validated by full kernel rebuild + reboot: both kill inputs now internalize to NULL with the guest healthy, and legit size=\"5\" / attribute-free data plists still parse (in-kernel KLD probe + userland rig).", "exploit_chain": "operator/root opens /dev/mapper/control -> ioctl(NETBSD_DM_IOCTL, plistref{<plist><dict><key>a</key><data size=\"N\">QQ==</data></dict></plist>}) -> prop_dictionary_copyin_ioctl (device-mapper.c:267) -> _prop_object_copyin (prop_kern.c:398-411) -> prop_dictionary_internalize -> dict body positions ctx on the value tag (prop_dictionary.c:1347) -> _prop_data_internalize reads size attr (prop_data.c:565), strtoul -> len (prop_data.c:572; ERANGE check compiled out in-kernel :573-576) -> kmalloc(len+1, M_PROP_DATA, M_WAITOK) (prop_data.c:588) -> [N=2^63] kmem_slab_alloc vm_map_findspace fails -> panic kernel_map ran out of space! (kern_slaballoc.c:1723); [N=8GiB>RAM] vm_page_alloc NULL forever -> vm_wait(0) retry loop (kern_slaballoc.c:1797) -> unkillable thread + total memory drain -> guest-wide wedge (pagedaemon continuous paging). Same XML via /dev/udev UDEVPROP (kern_udev.c:892, 0600), quotactl-prop (vfs_quota.c:346) or tbridge on configs exposing them.", "evidence": "panic.txt (serial console: panic + stack _prop_data_internalize+0xa1 -> _kmalloc -> kmem_slab_alloc); hang.console.txt (pagedaemon: WARNING! Continuous paging for 1 minutes, guest unresponsive); run.userland.log (same-source userland differential: huge=ENOMEM-clean, hang=clean, overflow=ERANGE-only-in-userland, neg1=errno-0 acceptance, roundtrip/sizeattr sanity); fixtest.console.txt (patched kernel: both kill inputs return EIO, guest alive); fixtest.module.txt (KLD probe on patched kernel: control/plain -> dict OK data size=5, huge/hang -> NULL \u2014 non-regression proof, isolating DMs own dm_check_version EIO); build.kernel.log (nativekernel rebuild of fix); fix.diff (git-apply-clean).", "kernel_refs": [ "sys/libprop/prop_data.c:543-588", "sys/libprop/prop_data.c:565-579", "sys/libprop/prop_data.c:384-530", "sys/libprop/prop_object_impl.h:258-260", "sys/libprop/prop_kern.c:385-425", "sys/kern/kern_slaballoc.c:863-878", "sys/kern/kern_slaballoc.c:888-891", "sys/kern/kern_slaballoc.c:942-950", "sys/kern/kern_slaballoc.c:1706-1724", "sys/kern/kern_slaballoc.c:1778-1804", "sys/dev/disk/dm/device-mapper.c:258-279", "sys/kern/kern_udev.c:888-892", "sys/kern/vfs_quota.c:346" ], "poc_changes": "No seed pack existed (IDs reserved fresh); trigger written to the established rig pattern: plistref mirror + _IOWR(0xfd,0,plistref) DM ioctl (as DF-5675), userland same-source differential (as DF-5675/DF-5678), KLD probe module (as DF-5678) added during fix validation to disambiguate DMs dm_check_version EIO from internalize failure.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (stock); patched fix kernel #1: Fri Oct 9 23:45:32 UTC 2026", "runtime_sec": 1620, "guest_dirty": 0, "build_cmd": "sh build.sh (guest root: same-source libprop rig from /usr/src/sys/libprop + cc -O2 df5680_userland.c df5680_dmcrash.c; KLD: kmod/Makefile bsd.kmod.mk)", "run_cmd": "sh run.sh (userland battery); kldload dm; ./df5680_dmcrash panic (stock: guest panics, see panic.txt); ./df5680_dmcrash hang (stock: guest wedges, see hang.console.txt); fix cycle: patch /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC, installkernel, reboot, re-run both + kldload kmod/df5680mod.ko", "code_hash": "ceb5d59ef3a0580a678b30580f0743290a22d20582c89433ee64fc76d6511071", "notes": "All kernel plist copyin entry points are 0600/0640 root:operator on stock configs (DM, udev, quotactl, tbridge) -> no unprivileged trigger found; severity Medium accordingly. The len=SIZE_MAX wrap (size=\"-1\" or strtoul saturation with the in-kernel ERANGE elision) reaches kmalloc(0)==ZERO_LENGTH_PTR but is contained by the decoders targsize checks and alen!=len (prop_data.c:597) \u2014 documented as the same root cause, not a separate corruption primitive. Decoder audited state-by-state: no OOB write exists in this file. Negative sweep (equals paths, NOCOPY lifecycle, externalize accounting, empty-element NULL child, KKASSERT reachability) recorded in VERDICT.md.", "recommended_fix": "Reject implausible blob sizes before allocating: after computing len in _prop_data_internalize, return early if len exceeds the number of characters remaining in the XML document (len > strlen(ctx->poic_cp)) \u2014 base64 can never decode to more bytes than its own encoding, so every legitimate plist passes while 2^63/8GiB/SIZE_MAX/strtoul-overflow values are refused before kmalloc.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff applied inside guest /usr/src (never to the read-only sys/ tree), kernel rebuilt (make -j6 nativekernel KERNCONF=X86_64_GENERIC, build.kernel.log) and booted (#1 Fri Oct 9 23:45:32): the size=2^63 input that panicked the stock kernel now internalizes to NULL and the ioctl returns EIO with the guest healthy; the size=8GiB input that wedged the stock kernel (pagedaemon continuous paging, ssh dead) now returns cleanly; non-regression proven in-kernel (KLD probe: control/plain data plists still internalize, data size=5) and in userland against the patched source (roundtrip/sizeattr OK). Guest reset to clean-source snapshot afterwards.", "fix_evidence": "fixtest.console.txt (FIXTEST panic/hang/control on patched kernel), fixtest.module.txt (DF5680MOD: control -> dict OK data size=5; huge/hang -> NULL), build.kernel.log (fix kernel build), fix.diff" } |