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)
PoC verification
Evidence pack
findings/poc/DF-2308 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- 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 loopwhile (len) { *buf++ = iicbb_readbyte(...); bytes++; len--; }terminates only atlen == 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 perlast; a hung bus yields 0xFF bytes, not fewer bytes. Hence on every successful I2CREAD,count == s->countand the copyout moves exactly the bytes the driver wrote. - pcf (
sys/bus/iicbus/pcf/pcf.c:586-640): its only short-read exit ispcf_wait_byte()failure βpcf_stop(); goto error;returningerror != 0, andiic.c:345-346breaks 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) & 0xffmarker=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 armsnack_thisat the falling edge of the address-ACK clock (iicbb_harness.c fakebb_setscl byte-slot-done branch), one half-clock afteriicbb_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/iic0with a stale unpatched iic.ko re-panicked the kernel exactly as DF-5560 documents (device_get_parent(NULL)at iic.c:289) β capture kept inpanic-setup-df5560-incidental.txt. - DragonFly's
patch(1)assumes-Rnon-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_testablen/a -- false positive; no fix authored or validated per contract (the premise is unreachable with in-tree backends).
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.
No comments yet.