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

I2CREAD ioctl leaks uninitialized kernel heap memory to userspace

Summary

I2CREAD allocates kernel buffer with kmalloc(M_WAITOK not zeroed) calls iicbus_read() which may read fewer bytes than requested returning actual count via &count but copyout() copies s->count bytes (requested) instead of count (actual). Uninitialized kernel heap data from tail of buffer copied to userspace leaking stale heap contents. Variable count holding actual read length computed at :344 but never used for copyout. Compare iicread() at :275 which correctly uses bufsize for uiomove. Device node 0600 root:wheel. [VERIFIED FALSE POSITIVE: no in-tree backend returns success with a short fill - zero disclosure possible today; copyout-count hardening one-liner retained]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2308 Β· 17 files
FileTypeDescriptionSize
iicread2308.c β€” 7.4 KB view raw
build.sh β€” 197 B view raw
run.sh β€” 109 B view raw
run1.log β€” 2.2 KB view raw
run2.log β€” 2.2 KB view raw
run3.log β€” 2.2 KB view raw
env.txt β€” 1.4 KB view raw
stock-vs-setup.disasm.txt β€” 2.1 KB view raw
panic-setup-df5560-incidental.txt β€” 742 B view raw
harness.build.log β€” 34.7 KB view raw
iic.build.log β€” 6.9 KB view raw
iicbus.build.log β€” 8.6 KB view raw
groom.build.log β€” 6.6 KB view raw
build.log β€” 58 B view raw
master2308.log β€” 7.3 KB view raw
VERDICT.md β€” 5.3 KB ↓ raw
verdict.json β€” 4.5 KB view raw
VERDICT.md
↓ download raw

DF-2308 β€” VERDICT: not reproduced (false positive: the "short successful read" premise is unreachable in-tree)

Bottom line

The I2CREAD copyout at sys/bus/iicbus/iic.c:347 does use the requested length (s->count) instead of the actual-read length (count set at :344) β€” that part of the claim is accurate source reading. But the leak requires iicbus_read() to return success with a short fill, and no in-tree backend can do that:

  1. iicbb (sys/bus/iicbus/iicbb.c:409-421, the only backend on the guest and the only one reachable without specific PCF8584 parallel-port hardware): the fill loop while (len) { *buf++ = iicbb_readbyte(...); bytes++; len--; } terminates only at len == 0, then *read = bytes; return (0). iicbb_readbyte (iicbb.c:308-330) has no failure mode β€” it samples the (emulated or real) SDA line 8 times and ACKs/NACKs per last; a hung bus yields 0xFF bytes, not fewer bytes. Hence on every successful I2CREAD, count == s->count and the copyout moves exactly the bytes the driver wrote.
  2. pcf (sys/bus/iicbus/pcf/pcf.c:586-640): its only short-read exit is pcf_wait_byte() failure β†’ pcf_stop(); goto error; returning error != 0, and iic.c:345-346 breaks to the epilogue without any copyout on error. (Also unreachable on this guest: PCF parallel-port hardware.)

Live verification (DragonFly 6.5-DEVELOPMENT guest, production KLDs)

Rig: the DF-5539 family harness β€” real in-tree iicbus.ko/iicbb.ko/ iic.ko on a fake GPIO bit-bang parent fakebb0 (nexus), emulated slave ACKs any address byte, read TX pattern 0xc0,0xc1,…. Two disclosed setup patches (the fixes for sibling findings DF-5559/DF-5560) were required to make /dev/iic0 attach and not NULL-deref on first ioctl; the I2CREAD logic under test is stock (verified: only iicbus.c/iic.c attach-path setup hunks applied; module disasm stock-vs-setup.disasm.txt shows the DF-5560 store in iic_attach and nothing else).

The decisive experiment β€” same groom that proved chunk recycling for the sibling I2CRDWR leak (DF-5558, marker returned 12/12 on this same rig): groom5558.ko kmalloc+fill+kfree's an M_TEMP chunk of exactly the I2CREAD size immediately before each ioctl, so iic.c:343's kmalloc hands I2CREAD the stale marker chunk itself. Result over 3 runs, sizes 64/256/512/1024/2048 groomed+ungroomed and 16384 ungroomed (51 attempts per run):

  • rc=0, pattern_bad=0 β€” every byte equals the slave TX pattern (0xc0 + i) & 0xff
  • marker=no (searched "ALEHEAP!" anywhere in the buffer β€” the marker tail that survives the allocator's 8-byte chunk header)
  • kptr-qwords=0 (no 0xffff-prefixed kernel pointers)

i.e. zero bytes of the fresh chunk survive to userspace; there is no oracle to harvest, no partial-fill path to exploit.

Error paths (all leaving the 0xAA-prefilled user buffer 100% untouched): * no-start I2CREAD β†’ EINVAL (iiconf.c:254-259 wrapper gate, !sc->started) * wrong-direction (started byte even) I2CREAD β†’ EINVAL (!(sc->started & LSB) gate at iiconf.c:259) β€” this also covers the after-NACK case (a NACKed START leaves started cleared)

Probe annotations (rig quirks, not kernel behavior)

  • E2 I2CSTART(0x6e) returned rc=0 where the probe predicted ENOACK: the harness arms nack_this at the falling edge of the address-ACK clock (iicbb_harness.c fakebb_setscl byte-slot-done branch), one half-clock after iicbb_ack's rising-edge sample (iicbb.c:271-285 samples at the 9th clock high). A bare I2CSTART therefore ACKs; the NACK surfaces on the following data byte's ACK slot β€” exactly how DF-5539/DF-5558 observed IIC_ENOACK via block writes. Consequence: the after-NACK I2CREAD exercised the wrong-direction EINVAL gate instead of a post-ENOACK state; equally a no-copyout error path.
  • E3 I2CSTART(0x50) then returned EIO-class failure because E2 left the bus started (iicbus_start wrapper refuses when already started, iiconf.c:169-180).

Incidental observations (notes only, not new findings)

  • During rig bring-up, exercising /dev/iic0 with a stale unpatched iic.ko re-panicked the kernel exactly as DF-5560 documents (device_get_parent(NULL) at iic.c:289) β€” capture kept in panic-setup-df5560-incidental.txt.
  • DragonFly's patch(1) assumes -R non-interactively on already-applied diffs, which silently reverted the setup patches in one intermediate round β€” run.sh now uses source-marker guards.

Conclusion

status: not_reproduced, reproduced: 0, impact: none, confidence: certain. The finding is a true statement about a latent API misuse but an incorrect statement about exploitable behavior on this tree. Recommended hardening (not a security fix): make the copyout honor the backend's actual count β€” error = copyout(buf, s->buf, count); β€” so a future backend honoring the short-read contract cannot reintroduce the disclosure. No fix.diff authored (pure false positive per contract).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

n/a -- false positive; no fix authored or validated per contract (the premise is unreachable with in-tree backends).

per-fix-DF-2308

Confirmed kernel references

Detail

Evidence (decisive lines)

run1.log/run2.log/run3.log: 51/51 success attempts per run, all 'rc=0 pattern_bad=0 marker=no kptr-qwords=0' plus error probes 'errno=22 untouched=YES'. VERDICT.md: backend-by-backend code trace (iicbb.c:409-421, pcf.c:586-640, iic.c:345-347, iiconf.c:254-259). stock-vs-setup.disasm.txt + env.txt: stock I2CREAD logic under test on production iic.ko (setup fixes DF-5559/5560 disclosed and disasm-verified). panic-setup-df5560-incidental.txt: rig bring-up re-confirmation of sibling DF-5560.

PoC changes

No seed PoC existed. Authored iicread2308.c from scratch on the DF-5539/DF-5558 production-KLD rig: error-path probes (no-start, after-NACK, wrong-direction) plus groomed/ungroomed success-path I2CREAD at 6 sizes with full-buffer pattern check and marker search. Rig iterations: (1) a stale unpatched iic.ko from an earlier session panicked on the first ioctl (DF-5560 bug, capture kept) -- run.sh now clean-builds and objdump-verifies the setup store before loading; (2) DF patch(1) silently assumes -R on already-applied setup diffs -- switched to source-marker guards; (3) groom KLD build needed Makefile.groom renamed to Makefile and iicbus_if.m copied into the build dir; (4) E2/E3 probe expectations corrected for the harness's ACK-falling-edge NACK arming (documented in VERDICT.md; kernel error-path invariants unaffected).

Verified recommended fix

Hardening (not a security fix): copy out the actual read length -- 'error = copyout(buf, s->buf, count);' at iic.c:347 -- so a future backend honoring the short-read contract cannot disclose uninitialized tail bytes.

Verdict

NOT REPRODUCED -- false positive: the I2CREAD copyout at sys/bus/iicbus/iic.c:347 does use the requested length (s->count) rather than the actual-read length (count from :344), but the claimed disclosure requires an iicbus_read backend that returns success with a short fill, and none exists in-tree. iicbb (iicbb.c:409-421, the only backend on the guest) fills every requested byte unconditionally (while(len) loop, *read=len, return 0; iicbb_readbyte has no failure mode), and pcf (pcf.c:586-640) short-reads only via an error return, which iic.c:345-346 handles by skipping the copyout entirely. Live proof with production modules on the DF-5539 harness rig: 3 runs x 51 success-path I2CREAD attempts at sizes 64..16384, each immediately preceded by the kernel-side M_TEMP groom (kmalloc+fill+kfree of the exact read size, marker DF5558STALEHEAP! + kernel-realm qwords) that DF-5558 used to prove same-size chunk recycling into iic.c kmallocs on this same rig -- every attempt returned rc=0 with pattern_bad=0, marker=no, kptr-qwords=0 (100% of bytes are the slave TX pattern 0xc0+i), i.e. zero bytes of the fresh chunk survive to userspace; all error-path probes returned EINVAL with the 0xAA-prefilled user buffer 100% untouched. The s->count-vs-count mismatch is real latent API misuse worth a hardening one-liner but discloses nothing with any in-tree backend.