⬢ DragonFlyBSD Kernel Audit
DF-5680 / verdict.json
← back to finding ↓ download raw
{
  "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"
}