β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5558

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5558 Β· 19 files
FileTypeDescriptionSize
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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5558

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).