TOCTOU double-fetch of d->nmsgs in I2CRDWR ioctl causes kernel heap buffer overflow
Summary
I2CRDWR ioctl at iic.c:291 casts raw user ioctl data pointer to struct iic_rdwr_data* (device.h:114 a_data is caddr_t user pointer; kernel does NOT copyin ioctl structs for the driver). d->nmsgs (uint32_t, iic.h:56) is re-fetched from user memory SIX separate times across allocation/copyin/loops: :351 buf=kmalloc(sizeof*d->msgs*d->nmsgs) reads N1; :352 usrbufs=kmalloc(sizeof(void*)*d->nmsgs) reads N1; :353 copyin(d->msgs,buf,sizeof*d->msgs*d->nmsgs) reads N2; :357 for(i<d->nmsgs) reads N3; :364 iicbus_transfer(parent,buf,d->nmsgs) reads N4; :366 for(i<d->nmsgs) reads N5. Intervening kmalloc(M_WAITOK) sleeps widen race window. Racing thread bumps nmsgs after allocation but before copyin/loops -> heap overflow into buf (16 bytes per excess entry) and usrbufs (8 bytes per excess entry). m->buf/m->len/m->flags read from adjacent heap then used as kmalloc size and copyin/copyout arguments. Requires root (0600 root:wheel). Race reliable because M_WAITOK sleeps. Also: nmsgs=UINT32_MAX requests ~24GB M_WAITOK allocations = OOM DoS. Fix: snapshot nmsgs into local uint32 once, validate upper bound.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1109 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_iic_toctou.c | trigger-source | racing-threads TOCTOU trigger (documented; needs HW + root) | 2.9 KB | view raw |
| build.sh | build-script | cc -O2 -pthread -o poc_iic_toctou poc_iic_toctou.c | 315 B | view raw |
| run.sh | run-script | ./poc_iic_toctou /dev/iic0 (needs i2c HW + root) | 236 B | view raw |
| fix.diff | suggested-fix | snapshot nmsgs into local uint32 once, cap at 65536, use local at all six sites | 2.0 KB | view raw |
| iic_fix_build.log | build-log | iic.ko rebuilt from patched source under -Werror, rc=0 | 2.7 KB | view raw |
| VERDICT.md | verdict | full line-by-line trace + fix rationale | 3.1 KB | β raw |
| README.md | readme | reproduce instructions | 1.6 KB | β raw |
| env.txt | environment | uname, cc version, kldstat | 383 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1109 β I2C I2CRDWR ioctl TOCTOU double-fetch of d->nmsgs
Verdict (this run)
SOURCE-CONFIRMED, NOT REPRODUCED AT RUNTIME on this guest β the six-fold
re-fetch of d->nmsgs from user memory is traced line-by-line in compiled
module source (iic.ko ships in /boot/kernel), but the I2C driver never
attaches here (no i2c controller, no /dev/iic*). The fix (fix.diff)
applies and compiles (iic.ko rebuilt clean under -Werror).
Additional finding-derived caveat: /dev/iicN is created mode 0600
root:wheel (iic.c:129), so the bug is rootβkernel (no unprivileged
boundary crossing); realistic impact is a root-driven TOCTOU heap overflow plus
an OOM DoS (nmsgs=UINT32_MAX β ~24 GB M_WAITOK allocation).
How to reproduce (HW-equipped host, as root)
./build.sh && ./run.sh /dev/iic0β two threads: one issuesioctl(I2CRDWR)in a loop, the other flipsnmsgsbetween the allocation size and an overflow size so a bump lands between thekmallocand thecopyin/loops. A successful race overflowsbuf(16 B/excess) /usrbufs(8 B/excess).- Requires: an i2c controller so
/dev/iic0exists, and root to open it.
Why not on this guest
No i2c controller hardware; iic.ko not loaded; no /dev/iic*. The trigger
cannot open() a device. Dormant code path, not absent.
Files
poc_iic_toctou.cβ intended racing-threads trigger (documented).fix.diffβ snapshotnmsgsinto a local once, cap at 65536, use local everywhere.iic_fix_build.logβ proof the fix compiles (-Werror, rc=0).VERDICT.mdβ full line-by-line trace.
DF-1109 β VERDICT
Verdict
SOURCE-CONFIRMED (real bug), NOT REPRODUCED AT RUNTIME on this guest.
The TOCTOU double-fetch of d->nmsgs is traced line-by-line in compiled module
source. It does not fire here because the I2C driver never attaches (no i2c
controller, no /dev/iic*). Dormant code path, not a false positive
(iic.ko ships in /boot/kernel). Additional reachability caveat: even with
HW, the device node is 0600 root:wheel so the bug is rootβkernel (no
unprivileged boundary crossing).
Mechanism (source trace)
iicioctl(sys/bus/iicbus/iic.c:291) castsdata = ap->a_dataβ the raw user ioctl-data pointer; the kernel does not copyin ioctl structs for drivers (device.h:114a_dataiscaddr_tuser pointer).case I2CRDWR:(iic.c:~347-369) dereferencesd->nmsgsfrom user memory at six separate points: -:348buf = kmalloc(sizeof(*d->msgs) * d->nmsgs, ...)reads N1 -:349usrbufs = kmalloc(sizeof(void*) * d->nmsgs, ...)reads N1 -:350copyin(d->msgs, buf, sizeof(*d->msgs) * d->nmsgs)reads N2 -:354for (i = 0; i < d->nmsgs; i++)reads N3 -:361iicbus_transfer(parent, buf, d->nmsgs)reads N4 -:363for (i = 0; i < d->nmsgs; i++)reads N5- The intervening
kmalloc(M_WAITOK)calls sleep, widening the race window. A racing thread that bumpsnmsgsafter the allocation (small N1) but before the copyin/loops (large N2..N5) overflowsbuf(16 B/excess entry βstruct iic_msg= slave/len/buf) andusrbufs(8 B/excess entry). - The overflowed
m->buf/m->len/m->flagsare then read from adjacent heap and used as akmallocsize andcopyin/copyoutarguments β controlled heap corruption / info leak. - Secondary DoS:
nmsgs = UINT32_MAXrequests ~24 GBM_WAITOKallocations (16 * UINT32_MAXand8 * UINT32_MAX) β OOM.
Reachability / privilege caveat
/dev/iicN is created mode 0600 UID_ROOT GID_WHEEL (iic.c:129). Opening
it requires root. The bug is therefore a rootβkernel TOCTOU (root corrupting
its own kernel) β no privilege boundary is crossed. This is a valid hard
blocker for any uid=0 claim: there is no unprivileged path to the write.
Why not reproduced here
No i2c controller HW; iic.ko not loaded; /dev/iic* absent. Trigger cannot
open() a device.
Fix
fix.diff snapshots d->nmsgs once into a local uint32_t nmsgs, caps it at
65536 (kills the OOM DoS and bounds the allocation), and uses the local at every
subsequent site (both kmallocs, copyin, both loops, transfer). Supersedes
the finding's proposal (which described the snapshot but did not bound nmsgs).
Fix validation (compile)
Applies (git apply --check clean) and compiles: iic.ko rebuilt from
patched source under -Werror, iic.c compiled clean, rc=0. Runtime
before/after is not_testable (no HW; and even with HW the path is
root-only, so no unprivileged escalation to validate).
Exploit chain
None β the primitive is root-reachable only (no privilege boundary crossed), and unreachable at runtime on this guest regardless.
Fix verification
not_testablecompile validated -Werror
module rebuild rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. iicioctl I2CRDWR re-fetches d->nmsgs 6 times (TOCTOU) -> heap overflow. No i2c HW. Root-only 0600.
No comments yet.