I2CRDWR copies out IIC_M_RD buffers after a FAILED transfer β uninitialized kernel heap disclosed to userspace
Summary
Read-message buffers are kmalloc'd without M_ZERO, never filled, and copied back unconditionally: a NACKing/absent slave aborts the transfer at the first message yet the caller receives the raw fresh heap chunk - a repeatable size-controlled stale-kernel-heap read oracle (up to 64KB/message) despite the error return. The DFly copy dropped FreeBSD's '&& error == 0' gate. Reproduced 12/12 at N=256..2048 with groomed M_TEMP marker chunks and 15-127 kernel-realm pointer qwords per read (I2CRDWR logic under test stock). Gate-on-success fix validated (0/12, success-path reads unchanged). Root-gated on default config (0600); devfs-exposed embedded configs give unpriv reach.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5558 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rdwrleak.c | β | 8.0 KB | view raw | |
| groom5558.c | β | 9.2 KB | view raw | |
| iicbb_harness.c | β | 11.9 KB | view raw | |
| iicbb_reg.c | β | 803 B | view raw | |
| Makefile | β | 346 B | β download | |
| Makefile.reg | β | 189 B | β download | |
| Makefile.groom | β | 184 B | β download | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 192 B | view raw | |
| build.log | β | 2.7 KB | view raw | |
| run.log | β | 3.2 KB | view raw | |
| run2-full.log | β | 11.8 KB | view raw | |
| fix_run.log | β | 2.2 KB | view raw | |
| leak_sample.txt | β | 5.0 KB | view raw | |
| dmesg.txt | β | 960 B | view raw | |
| env.txt | β | 541 B | view raw | |
| fix.diff | β | 426 B | view raw | |
| fix_build.log | β | 6.7 KB | view raw | |
| verdict.json | β | 5.7 KB | view raw |
Fix verification
fixedfix.diff applied to the guest's /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.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).
Confirmed kernel references
Detail
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 (decisive lines)
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.
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 fd's own zeroed contents -- the groom proves cross-source reuse.
Verified 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.
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 1's 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+kfree'd 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).
No comments yet.