DF-5392 / verdict.json
{ "finding_id": "DF-5392", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED on DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, QEMU). A KLD harness driving the REAL isa/pnp machinery (kldload -> isa_driver_added -> identify -> pnp_parse_resources -> ISA_ADD_CONFIG -> isa_assign_resources) proved the full chain: (1) pnp_check_quirks()s unbounded PNP_QUIRK_EXTRA_IO appends advance ic_nport by +3 per IO_RANGE tag so pnpparse.cs `ic_nport == ISA_NPORT clamp never fires (17 tags walk 0->51; stored config keeps nport=51 > ISA_NPORT=50); (2) the 18th tag writes ic_port[51], which aliases ic_nmem/ic_nport/ic_nirq/ic_ndrq \u2014 stored config shows nmem=768 nport=66 nirq=1 ndrq=1, every field attacker-set; (3) subsequent tags write attacker-indexed (up to 0x100FE, ~1MB) isa_range slots: run-B (V=0x4000) wrote ~9.6KB of structured garbage ~256KB past the 8640-byte configs[] kmalloc with NO error (silent kernel-heap corruption); (4) run-C (V=0x100FE + 12000 sequential tags) stored nport=36054 and the walk over the stored count ran ~1.6MB out of bounds into a kernel thread-stack guard page: panic: vm_fault fault on stack guard, RIP inside the count consumer \u2014 the same loop shape as pnp_set_config (pnp.c:274). isa_common.c is the anchor: isa_add_config (isa_common.c:938-959) persists these configs with zero count validation and every isa_find_* consumer (isa_common.c:128/192/279/338) iterates them. uid=0 was NOT pursued as local privesc because the trigger is boot-time PnP card data (hardware-supplied), not an unprivileged syscall; the primitive itself is a ring-0 boot-time kernel-heap write with attacker-chosen offsets (~40% byte control per 16-byte slot, unlimited sequential extension) \u2014 arbitrary kernel code execution by crafted hardware. FIX VALIDATED: with fix.diff (isa_add_config count rejection + quirk bound + >= clamps), the same streams load with counts capped at 50, no OOB writes, and the exact baseline-panic stream returns RC=0 with the guest healthy.", "exploit_chain": "crafted ISA-PnP card (or emulated/counterfeit PnP device) supplies resource bytes at boot -> pnp_get_resource_info/pnp_read_resources -> pnp_parse_resources -> IO_RANGE descriptors tagged with SB16 vendor/logical ids so pnp_check_quirks fires PNP_QUIRK_EXTRA_IO (+2 ic_nport per tag, unbounded, pnp.c:338-349) -> after 17 tags ic_nport=51, pnpparse.c:137 `== 50 check bypassed -> tag 18 writes ic_port[51] = {ir_start->ic_nmem, ir_end->ic_nport, ir_size->ic_nirq, ir_align->ic_ndrq} so the attackers endbase becomes ic_nport -> every later tag writes fully attacker-indexed isa_range slots (16-bit start, ~17-bit end, 8-bit size, 8-bit align) plus mirrored quirk copies at +0x400/+0x800, up to index 0x100FE (~1MB) and sequentially beyond -> OOB heap write past the 8640-byte M_DEVBUF configs[] allocation (run-B: silent corruption of mapped kmem; run-C: stored count 36054 -> consumer walk -> stack-guard fault -> kernel panic). isa_add_config (isa_common.c:938-959) persists the poisoned configs unvalidated, feeding isa_find_memory/port/irq/drq and pnp_set_config.", "evidence": "run_a.log (p51 ice nport=51; p61 ice nmem=768 nport=66 nirq=1 ndrq=1; ic_port[51] start=0x300 end=0x42 = live alias of clobbered ic_nmem/ic_nport; slot data at indices 50,64-69 = outside the 50-element array; consumer ran: (null) cant assign resources (port|memory)), run_b.log (POISON_V=0x4000: stored nport=1854, writes ~256KB past allocation, guest survived = silent corruption), run_c.log + panic.txt (POISON_V=0x100FE+12000 tags: stored nport=36054, panic: vm_fault fault on stack guard addr 0xfffff8008dd20000, trace at dump_configs+0x95 = walk over stored poisoned count), fix_run.log (patched kernel: all stored counts = 50, run-C stream RC=0 no panic), fix.diff (git-apply clean)", "kernel_refs": [ "sys/bus/isa/isa_common.c:938-959", "sys/bus/isa/isa_common.c:128-148", "sys/bus/isa/isa_common.c:191-204", "sys/bus/isa/isa_common.c:278-289", "sys/bus/isa/isa_common.c:337-348", "sys/bus/isa/pnp.c:338-349", "sys/bus/isa/pnpparse.c:137-168", "sys/bus/isa/pnpparse.c:425", "sys/bus/isa/pnp.c:274-282" ], "poc_changes": "self-authored harness (no seed): KLD driver on isa bus whose identify feeds crafted PnP byte streams to the real pnp_parse_resources; modules built in-tree under /usr/src/sys/bus/isa/hack_{a,b,c} because bsd.kmod.mk resets CFLAGS (poison parameters passed via wrapper .c files instead of -D); hack_b renamed pnphack2 to coexist with the loaded hack_a; hack_c grew stream[] to 256KB for 12000 sequential tags. Notable: GCC-8 deleted the harness dump loops own i < 64 guard (member-array subscript OOB is UB, so the compiler assumed i<=49) \u2014 the dump itself then walked 1.6MB out and tripped the stack guard, which is precisely the consumer-side OOB the finding describes.", "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 (baseline) / #1: Sat Oct 3 23:39:17 UTC 2026 (fix-validated kernel)", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "(cd /usr/src/sys/bus/isa/hack_a && make obj && make depend && make) # likewise hack_b (POISON_V=0x4000 EXTRA_TAGS=600) and hack_c (POISON_V=0x100FE EXTRA_TAGS=12000)", "run_cmd": "kldload /usr/obj/usr/src/sys/bus/isa/hack_a/pnphack.ko; kldload /usr/obj/usr/src/sys/bus/isa/hack_b/pnphack2.ko; kldload /usr/obj/usr/src/sys/bus/isa/hack_c/pnphack3.ko # C panics on unfixed kernel", "code_hash": "f16b7fcc8465a42a125e235999f3c4604e9fe55f1cc61d8d0cfc53157e723d28", "notes": "impact recorded as panic (reproduced terminal outcome) but the stronger demonstrated primitive is the parse-side OOB WRITE: attacker-chosen index up to ~1MB with ~40% byte control per 16-byte slot, sequentially extendable, at boot before any authentication \u2014 silent heap corruption confirmed in run-B. uid0 chain N/A: no unprivileged-user input path; supply vector is PnP card data (crafted hardware / VM device), same class as the pccard CIS family (DF-5355-5364). Consumers in isa_common.c are incidentally gated by isa_set_resources rid validation (isa_common.c:893-902) which breaks the find_* loops at the array sizes; the unvalidated isa_add_config remains the trust boundary that persists poison for any consumer (pnp_set_config iterates stored counts directly).", "recommended_fix": "Validate ic_nmem/ic_nport/ic_nirq/ic_ndrq in isa_add_config (reject out-of-range), bound PNP_QUIRK_EXTRA_IO appends by ic_nport < ISA_NPORT in pnp_check_quirks, and turn pnpparse.cs seven == ISA_N* clamps (incl. pnp_merge_resources) into >= \u2014 full unified diff in fix.diff.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "Single-fix kernel (nativekernel X86_64_GENERIC + fix.diff, uname #1 Sat Oct 3 23:39:17): baseline behavior GONE. run-A: stored p51/p61 counts all nport=50 (baseline 51 / nmem=768 nport=66 nirq=1 ndrq=1), ic_port[50]+ slots zero (no OOB writes), consumers complete normally. run-C (exact baseline-panic stream POISON_V=0x100FE EXTRA_TAGS=12000): kldload RC=0, guest healthy, no panic. DF-5393 stall segment simultaneously dropped 757ms -> ~17-30ms (ISA_FIND_MAX_TRIES cap).", "fix_evidence": "fix_run.log (patched-kernel dmesg: PNPHACK[p51]: ice[0] ... nport=50, PNPHACK[p61]: ice[0] ... nport=50, callbacks +190001us/+220044us), fix_run_c section of fix_run.log, fix_build.log (kernel build+install), fix_build_a.log", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Oct 3 23:39:17 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64" } |