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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1109 Β· 10 files
FileTypeDescriptionSize
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
README.md readme reproduce instructions
↓ download 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)

  1. ./build.sh && ./run.sh /dev/iic0 β€” two threads: one issues ioctl(I2CRDWR) in a loop, the other flips nmsgs between the allocation size and an overflow size so a bump lands between the kmalloc and the copyin/loops. A successful race overflows buf (16 B/excess) / usrbufs (8 B/excess).
  2. Requires: an i2c controller so /dev/iic0 exists, 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 β€” snapshot nmsgs into 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.
VERDICT.md verdict full line-by-line trace + fix rationale
↓ download raw

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)

  1. iicioctl (sys/bus/iicbus/iic.c:291) casts data = ap->a_data β€” the raw user ioctl-data pointer; the kernel does not copyin ioctl structs for drivers (device.h:114 a_data is caddr_t user pointer).
  2. case I2CRDWR: (iic.c:~347-369) dereferences d->nmsgs from user memory at six separate points: - :348 buf = kmalloc(sizeof(*d->msgs) * d->nmsgs, ...) reads N1 - :349 usrbufs = kmalloc(sizeof(void*) * d->nmsgs, ...) reads N1 - :350 copyin(d->msgs, buf, sizeof(*d->msgs) * d->nmsgs) reads N2 - :354 for (i = 0; i < d->nmsgs; i++) reads N3 - :361 iicbus_transfer(parent, buf, d->nmsgs) reads N4 - :363 for (i = 0; i < d->nmsgs; i++) reads N5
  3. The intervening kmalloc(M_WAITOK) calls sleep, widening the race window. A racing thread that bumps nmsgs after the allocation (small N1) but before the copyin/loops (large N2..N5) overflows buf (16 B/excess entry β€” struct iic_msg = slave/len/buf) and usrbufs (8 B/excess entry).
  4. The overflowed m->buf/m->len/m->flags are then read from adjacent heap and used as a kmalloc size and copyin/copyout arguments β†’ controlled heap corruption / info leak.
  5. Secondary DoS: nmsgs = UINT32_MAX requests ~24 GB M_WAITOK allocations (16 * UINT32_MAX and 8 * 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_testable

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