DF-5558 / verdict.json
{ "finding_id": "DF-5558", "status": "reproduced", "reproduced": true, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED on the DragonFly 6.5-DEVELOPMENT guest with production modules. iicioctl(I2CRDWR) (sys/bus/iicbus/iic.c:360,364-369) kmallocs each read-message buffer without M_ZERO and copies every IIC_M_RD buffer to userspace even when iicbus_transfer() failed, disclosing the fresh uninitialized heap chunk. Trigger: I2CRDWR with msgs[0]=1-byte write to a NACKed slave (IIC_ENOACK) and msgs[1]=read of N bytes -- the generic transfer aborts at msg 0, msg 1s buffer is never written, and the copyout loop still runs. 12/12 attempts at N=256/512/1024/2048 returned the error AND fully overwrote the user buffer; the kernel-side groomed M_TEMP marker (DF5558STALEHEAP!+seq+kernel-realm qwords, kmalloc+fill+kfreed via sysctl immediately before the ioctl) came back verbatim (hexdump in leak_sample.txt; +8 shift = allocator chunk header). Setup patches (disclosed, and themselves the fixes for the sibling findings found this pass): iicbus.ko carries the DF-5559 bus_read_ivar fix so the iic child can attach at all, and iic.ko carries the DF-5560 sc->sc_dev fix so the first ioctl does not panic in device_get_parent(NULL); the I2CRDWR logic under test is stock. fix.diff (gate the copyout on error == 0, matching FreeBSD) validated by rebuilding only iic.ko: 0/12 leaks, buffers stay at the 0xAA prefill, success-path reads unchanged. Reachability: /dev/iicN is 0600 root:wheel (iic.c:128-130), so default-config exposure requires root; embedded devfs rulesets exposing the node get an unprivileged repeatable stale-kernel-heap read oracle (KASLR/heap-layout disclosure primitive).", "exploit_chain": "open /dev/iicN (root or devfs-exposed) -> heap-spray kernel M_TEMP objects of the target size class and free them -> ioctl(I2CRDWR, [write-to-absent-slave, read N bytes]) -> ioctl returns IIC_ENOACK but the read buffer now holds the recycled kernel heap chunk -> repeat to harvest pointers/heap contents. No write primitive; info-leak chaining only.", "evidence": "run.log: 12x variant=B ... non-prefill=N marker=YES kptr-qwords=... <== LEAK + RESULT: UNINITIALIZED KERNEL HEAP DISCLOSED TO USERSPACE. leak_sample.txt: hexdump ff ff ff ff ff ff ff ff 41 4c 45 48 45 41 50 21 | seq | size | 5a filler | 0xffff-qb] pattern = the groomed kernel chunk itself. fix_run.log: 0/12, non-prefill=0 on patched iic.ko, sanity read still c0 c1 c2 c3. dmesg.txt: stack attach + BUS_READ_IVAR error=0 v=0 (fix active). panic path that motivated the setup patches: ../DF-5560/panic.txt.", "kernel_refs": [ "sys/bus/iicbus/iic.c:360", "sys/bus/iicbus/iic.c:361-362", "sys/bus/iicbus/iic.c:364", "sys/bus/iicbus/iic.c:366-369", "sys/bus/iicbus/iiconf.c:371-378", "sys/bus/iicbus/iicbb.c:344-367", "sys/bus/iicbus/iic.c:128-130" ], "poc_changes": "No seed PoC existed (pass-2 finding verified in the same run). Authored from scratch: rdwrleak.c (userland I2CRDWR PoC: groomed single- and two-message failing transfers, ungroomed 200-iter hunt, success-path sanity), groom5558.ko (sysctl-driven M_TEMP marker groom + DF-5559 proofs), reusing the DF-5539 fakebb/iicbb harness verbatim. Iterations: (1) marker search had to switch from the full 16-byte magic to the 8-byte tail ALEHEAP! because the allocator chunk header consumes the first 8 bytes (leak appears shifted +8); (2) the device could not be reached on stock modules -- two sibling bugs (DF-5559 probe uninit-stack, DF-5560 sc_dev NULL panic) each block the path and required their fixes as disclosed setup; (3) the ungroomed tight loop shows 0 hits because the chunk recycles the same fds own zeroed contents -- the groom proves cross-source reuse.", "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", "runtime_sec": 4200, "guest_dirty": 0, "build_cmd": "cd /root/df5558 && sh build.sh (harness KLDs + rdwrleak; in-tree iicbus.ko/iic.ko builds logged in fix_build.log)", "run_cmd": "cd /root/df5558 && sh run.sh", "code_hash": "e7930fc6f8528f283324cb6f49d8801ba58e530e09c747745b3a601fc2ab9568", "notes": "Impact ceiling is a size-controlled stale-kernel-heap read oracle (up to 65535 bytes/message, unbounded messages). On this tree the node is 0600 root:wheel; devfs-exposed embedded configs are the unprivileged scenario. Not re-reported here: DF-1109 (nmsgs double-fetch), DF-1110/2309 (usrbufs leak), DF-1111/2310 (ignored copyin/copyout), DF-2308 (I2CREAD path), DF-2312 (nmsgs unbounded). Guest restored (vm.sh reset with-src) after the run.", "recommended_fix": "Gate the read-segment copyout on a successful transfer -- if ((m->flags & IIC_M_RD) && error == 0) copyout(...) -- keeping the kfree loop unconditional (see fix.diff); optionally also allocate with M_ZERO as defense in depth.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to the guests /usr/src copy of sys/bus/iicbus/iic.c (on top of the two disclosed setup patches), only iic.ko rebuilt and reloaded. Baseline stock I2CRDWR logic: 12/12 failing-transfer attempts overwrote the user buffer with the groomed kernel marker (marker=YES, 15-127 kernel-realm qwords). Patched: 0/12 -- non-prefill=0, marker=no, kptr-qwords=0 while the ioctl still returns the transfer error; success-path reads from the ACKing slave unchanged (c0 c1 c2 c3). Leak eliminated, functionality preserved.", "fix_evidence": "fix.diff; fix_build.log; fix_run.log (variant=B ... non-prefill=0 marker=no kptr-qwords=0 x12, groomed attempts done: leak_observed=0, sanity pattern_ok=YES); run.log (baseline 12/12 marker=YES); leak_sample.txt (hexdump of the disclosed chunk)." } |