⬢ DragonFlyBSD Kernel Audit
DF-2316 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2316",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Reproduced with byte attribution on the QEMU guest through the verbatim TU-compiled pnpparse.c (prep.sh sha256 gate 4b13638c... = pristine audit tree; pnp.c stays the stock in-build unit). pnp_parse_desc() dereferences fixed descriptor offsets with only the card-declared len >= l checked upstream: scenario 20 proves the exact read set per tag with markers res[i]=i+1 and declared len 0 (COMPAT 4, IRQ 2, DMA 1, IO_RANGE 7, IO_FIXED 3, MEM_RANGE 9, MEM32_RANGE 17, MEM32_FIXED 9 bytes; MEM32_RANGE stored ir_size=0x11100f0e = I32(res+13), offsets 0..16 all past a len-0 descriptor; declared-16 case shows the 1-byte over-read). Scenario 21 proves the read crosses the heap chunk end (MEM32_RANGE len 0 at a 512B kmalloc chunk end, adjacent canary chunk: ir_size=0x21595241 == I32(c2+9), text ARY! from DF2316CANARY!, 3/3 runs). Scenario 23.2 drives the REAL verbatim pnp_parse_resources() with a crafted card stream whose last descriptor ends exactly at a 1024B chunk end: stored config ir_size=0x33324644 == I32(c2+13), i.e. 17 canary bytes read fully past the allocation via the same path a malicious card drives at boot. Scenario 23.3 shows the leak side end to end: a freed 1024B M_TEMP chunk re-allocated as the card stream buffer (pnp_read_bytes growth pattern) delivers its prior contents (kernel heap residue) into the stored isa_config (ir_size=0x44504145 from residue bytes EAPD), reachable via id_configs/devinfo/bootverbose prints. Scenario 22 proves the conditional 1-byte OOB WRITE of the same root cause: IO_RANGE len 0 flips an adjacent-chunk canary byte from 0x00 to 0x01 (pnpparse.c:163-166 res[5]=1). Scenario 25 proves the page-boundary DoS: with the descriptor at a page edge and the following pages kmem_freed (vm_map entries removed; the harness documents why a bare pmap_kremove is survivable - trap_pfault re-materializes the PTE while the entry exists), the baseline kernel takes Fatal trap 12, page fault while in kernel mode, fault virtual address = res+16 inside the I32(res+13) null-check read, supervisor read data, page not present, backtrace pnp_parse_desc()+0x685, panic: page fault, crash dump written, guest wedged. Fix validated on a rebuilt kernel (fix.diff: one per-handler declared-length guard, 8/8 hunks): truncated tags are skipped with a single bogus-length console line, canaries stay untouched, no residue reaches stored configs, scenario 25 no longer panics, and the well-formed 27-field config vector is bit-identical baseline vs fixed. Privesc: none - boot-time hardware attacker only (malicious ISA-PnP card / VM firmware, AV:P), the primitive is a bounded 17-byte read into resource fields plus a fixed-value 1-byte write; no local unprivileged or remote trigger, no uid=0 chain (see VERDICT.md). FreeBSD head sys/isa/pnpparse.c carries the identical unguarded handlers.",
  "exploit_chain": "malicious ISA-PnP card / VM firmware sends a resource stream whose final descriptor declares a truncated length (e.g. MEMORY32_RANGE tag 0x85 with declared len 0, or 16 for a 1-byte over-read) sized so the descriptor lands at the end of the 1024-byte M_TEMP stream buffer -> pnp_read_resources/pnp_create_devices (pnp.c:644/:649) -> pnp_parse_resources (pnpparse.c:538) -> pnp_parse_desc reads res[0..16] past the descriptor: within the chunk tail = uninitialized heap residue leak into ir_start/ir_end/ir_size/ir_align stored on the device (devinfo/dmesg visible, ~17 heap bytes per descriptor, repeatable); past the chunk end at an allocation edge = adjacent heap disclosure and, when the next page is unmapped, Fatal trap 12 panic (DoS); IO_RANGE variant additionally writes 0x01 one byte past the descriptor when adjacent bytes permit. No further escalation: read-only ceiling plus a fixed-value 1-byte write, boot-time reach only.",
  "evidence": "run.log PART2: sweep MEM32_RANGE declen=0 need=17 TRUNCATED 17 bytes read past a len-0 descriptor attrib=PASS with f[3]=0x11100f0e; canrd ir_size=0x21595241 == I32(c2+9) 3 runs; canwr c2 before cc cc 00 7f after cc cc 01 7f OOB WRITE PROVEN; e2e2 ir_size=0x33324644 == I32(c2+13) 17 canary bytes past the 1024B allocation through the REAL parse entry; e2e3 ir_size=0x44504145 == I32(st+16) heap residue into resource config. panic.txt: Fatal trap 12 fault virtual address 0xfffff80118371003 (= res+16) supervisor read data page not present, pnp_parse_desc()+0x685, panic: page fault. fix_run.log: bogus memory32 range length 0, no fault, nmem=0 configs, canaries intact, 27/27 well-formed fields bit-identical",
  "kernel_refs": [
    "sys/bus/isa/pnpparse.c:60-340",
    "sys/bus/isa/pnpparse.c:95",
    "sys/bus/isa/pnpparse.c:105",
    "sys/bus/isa/pnpparse.c:123",
    "sys/bus/isa/pnpparse.c:141-167",
    "sys/bus/isa/pnpparse.c:178-198",
    "sys/bus/isa/pnpparse.c:242-266",
    "sys/bus/isa/pnpparse.c:278-299",
    "sys/bus/isa/pnpparse.c:308-326",
    "sys/bus/isa/pnpparse.c:433-543",
    "sys/bus/isa/pnpparse.c:575-601",
    "sys/bus/isa/pnp.c:493-526",
    "sys/bus/isa/pnp.c:536-581",
    "sys/bus/isa/pnp.c:406-480",
    "sys/platform/pc64/x86_64/trap.c:855-878"
  ],
  "poc_changes": "no seed PoC existed for this finding; extended the PNPH verbatim-interposition kernel harness from the DF-2314/2315 run to TU-compile the VERBATIM pnpparse.c instead of pnp.c (stock pnp.c restored to the build, stock pnpparse.c unit dropped, same sha256 gate pattern). Rig fixes during bring-up: prep.sh initially omitted installing the harness TU; a /* inside a comment tripped -Werror; the IRQ_FORMAT sweep tag byte was 0x10 (LOGICAL_DEVICE) instead of 0x20; the well-formed reference stream was missing the info byte of the 17/9-byte memory descriptors; and the page-boundary scenario first used pmap_kremove alone, which the kernel survives because trap_pfault re-materializes the PTE while the vm_map entry exists - switched to kmem_free of the tail pages so the entries are gone and the fault is fatal. Two cosmetic rig quirks remain documented: the SC20 userland summary vector mixes the MEM32_RANGE header with MEM32_FIXED field data (kernel dmesg lines are per-tag correct), and o_child prints (null) for unnamed isa children.",
  "attempts": 6,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Sun Oct 18 17:09:41 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/PNPH  x86_64",
  "runtime_sec": 4980,
  "guest_dirty": 0,
  "build_cmd": "sh prep.sh && sh build.sh (cd /usr/src && make -j6 nativekernel KERNCONF=PNPH && make installkernel KERNCONF=PNPH)",
  "run_cmd": "sh run.sh (pnphctl 9 20 21 22 23 1 23 2 23 3 24 26); sh run2.sh (pnphctl 25 -> baseline panic); fixed-kernel A/B re-run of all",
  "code_hash": "e5647c749b098fa9f4be6a5c5949c8b96559d30628940f64cc97242e43528f64",
  "notes": "impact=panic is the strongest demonstrated manifestation (Fatal trap 12 at the attributed offset res+16, backtrace inside pnp_parse_desc, panic.txt); the leak side is co-demonstrated (canary + heap residue into stored resource configs, 3/3 stable runs, leak_sample.txt) and matches the kernleak bucket. In the stock boot path the stream buffer is a kmalloc zone chunk whose neighbor is usually mapped, so the routine manifestation is the residue leak and the 1-byte write; the fault requires the buffer to abut unmapped KVA, demonstrated deterministically by removing the vm_map entries. Privesc explicitly closed: boot-time hardware attacker (AV:P) only, bounded read into integer resource fields plus a fixed-value conditional 1-byte write; no local unprivileged trigger; no uid=0 chain. FreeBSD head carries the same unguarded handlers. Guest returned via vm.sh reset with-src.",
  "recommended_fix": "add a per-handler declared-length guard before the first fixed-offset dereference in pnp_parse_desc: if (len < N) { log and skip } with N = 4, 2, 1, 7, 3, 9, 17, 9 for COMPAT_DEVICE, IRQ_FORMAT, DMA_FORMAT, IO_RANGE, IO_FIXED, MEMORY_RANGE, MEMORY32_RANGE, MEMORY32_FIXED (fix.diff)",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Sun Oct 18 17:35:42 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/PNPH  x86_64",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff applied to guest /usr/src (8/8 hunks), kernel rebuilt and booted: all truncated-tag OOB behavior gone (no canary reads, no canary write, no residue in stored configs, scenario 25 no panic - guard logs bogus memory32 range length 0 and skips), and the well-formed scenario-24 config vector is bit-identical to baseline (27/27 fields).",
  "fix_evidence": "fix_run.log: sweep truncated cases f[]=0 MISMATCH vs controls PASS; canrd ir_size=0 nmem=0; canwr not written, canary stays 00; e2e1/2/3 nmem=0 stored configs empty; pgflt: bogus memory32 range length 0 then UNEXPECTED no-fault rc=0, guest up; well field vector diff vs baseline: BIT-IDENTICAL 27/27"
}