# DF-5539 — VERDICT

**Status: reproduced (leak, Low)** — one byte of uninitialized kernel stack
disclosed to the console / dmesg by `iicbb_print_child()` every time an iicbus
child attaches under iicbb with a parent whose reset returns 0 without filling
`*oldaddr`. Fix validated (patched `iicbb.ko` → deterministic `addr 0x0`).

## Root cause

`sys/bus/iicbus/iicbb.c:164-186`:

```c
u_char oldaddr;                                                    /* :169, uninitialized */
error = IICBB_RESET(device_get_parent(bus), IIC_FASTEST, 0, &oldaddr); /* :173 */
if (error == IIC_ENOADDR) { ... master-only ... }
else {
        IICBB_RESET(device_get_parent(bus), IIC_FASTEST, oldaddr, NULL); /* :179 restore w/ garbage */
        retval += kprintf(" on %s addr 0x%x\n",
                         device_get_nameunit(bus), oldaddr & 0xff);       /* :181-182 LEAK */
}
```

The iicbb_if reset contract has two outcomes: `IIC_ENOADDR` ("no address") or
"success" with `*oldaddr` filled. Audit of all in-tree parents:

* `lpbb_reset` (`sys/dev/misc/lpbb/lpbb.c:182-201`): on the ppbus-busy error
  path (`ppb_request_bus(..., PPB_DONTWAIT)` fails, lpbb.c:186-190) it prints
  "can't allocate ppbus" and **returns 0 (`IIC_NOERR`) without writing
  `*oldaddr`** — iicbb.c:173-185 then reads, prints, and "restores" the
  uninitialized byte. On success it returns `IIC_ENOADDR` (never fills either).
* `viabb_reset` (`sys/dev/powermng/viapm/viapm.c:507-514`): always
  `IIC_ENOADDR`, never fills.
* `cxm_iic_reset` (`sys/dev/video/cxm/cxm_i2c.c:273-296`): always
  `IIC_ENOADDR`, never fills.

Therefore the else-branch at iicbb.c:177-183 **cannot** execute with a written
`oldaddr` on this tree: it is reachable only through lpbb's busy path (or any
out-of-tree parent with the same shape), and every such execution reads
uninitialized kernel stack. This is the iicbb member of the family class filed
this pass for iicsmb.c (DF-5523, uninitialized `bcount`).

## Reachability

* Boot with any iicbb parent present (lpbb on ppbus, viapm 586B, cxm_iic), or
  `kldload` the stack (root) — the leak fires during the attach cascade, once
  per iicbus attach (`sys/kern/subr_bus.c:2004` → `device_print_child`).
* The printed line lands on the console and in the message buffer; `dmesg` is
  readable by unprivileged users (no `dmesg_restrict` on DragonFly), as is
  `/var/log/messages` via syslogd (default `*.notice` kernel facility).
* Trigger privilege: attaching hardware / loading the module requires root or
  boot-time presence; the *disclosure* is then readable by any local user.

## Verification (QEMU guest, DragonFly 6.5-DEVELOPMENT x86_64)

No bit-bang hardware exists in the rig, so a production-code KLD harness
instantiates the stock in-tree `iicbb.ko` under a fake GPIO parent
(`iicbb_reg.ko` + `iicbb_harness.c` — the fake parent implements the full
iicbb_if vtable with an emulated I2C slave; `fakebb_reset` reproduces lpbb's
busy-path contract byte-for-byte: return 0, never write `*oldaddr`).

1. **Leak reproduced**: 8 load/unload cycles per run across 3 spray variants +
   no-spray, **16/16** attach events printed
   `iicbus0: <Philips I2C bus> on iicbb0 addr 0xff` (run.out, dmesg.out).
   The byte is the untouched stack slot content — allocator fill (0xff) on
   this system.
2. **Spray experiment**: the harness pre-sprays ~5 KB of kstack below the
   cascade with 0xa5/0x5a/0x37 before newbus triggers attach
   (`fakebb_identify`). The leaked byte stayed 0xff: the slot is rewritten
   after the spray by the `device_add_child`/kmalloc internals that run
   between identify and print_child at exactly that stack depth. This does
   not weaken the finding — it shows the disclosed byte is whatever the
   kernel's own earlier execution left there (on other systems/paths: pointer
   fragments), i.e. genuinely uninitialized data.
3. **Machine-code proof** (stock_disasm.txt, `/boot/kernel/iicbb.ko`):
   `oldaddr` lives at `-0x21(%rbp)`; the function contains exactly one
   `lea -0x21(%rbp),%rcx` (passing `&oldaddr` to the parent reset) and two
   `movzbl -0x21(%rbp)` reads (restore call + kprintf) — **no instruction
   writes the slot**. With `fakebb_reset` also proven not to write it
   (source + `resets_with_oldaddr=1` counter), the printed byte is
   uninitialized stack by construction.
4. **State machine driven end-to-end** through the real driver (checklist a-d):
   * block write: 8 bytes `de ad be ef 55 aa 12 34` bit-banged out, slave
     shift-register captured them exactly (`slave_rx=deadbeef55aa1234`,
     `write_ok=1`) — byte counter / ACK handling correct;
   * block read: slave pattern `c0..c7` returned intact (`read_ok=1`) —
     last-byte NACK signaling (`iicbb_read`'s `(len==1)?last:0`) correct;
   * NACKed address: `iicbb_start` → `IIC_ENOACK` (=2) with the bus stopped
     afterwards (`bus_started_after=0`) — error path at iicbb.c:359-368
     correct;
   * repeated start mapped to `iicbb_start` (iicbb.c:92): write byte +
     repeated start + read byte round-trip succeeded (`byte=0xc0`);
   * `resets=3 resets_with_oldaddr=1` matches the exact call pattern
     iicbus.c:125 (NULL), iicbb.c:173 (&oldaddr — the leak call),
     iicbb.c:179 (restore, NULL).
5. **Fix validated**: `fix.diff` applied to the guest's `/usr/src` copy of
   iicbb.c, only `iicbb.ko` rebuilt (module — no kernel rebuild), harness
   re-run: output changed from `addr 0xff` to the deterministic `addr 0x0`,
   **zero** occurrences of the uninitialized byte (fix_dmesg.out), state
   machine still green. Patched disassembly (fix_disasm.txt) shows the new
   `movb $0x0,-0x21(%rbp)` prologue store. Stock module restored afterwards.

## Impact ceiling

One byte of uninitialized kernel stack per attach event, readable by any local
user via dmesg/syslog; plus a garbage byte written as the "restored" i2c
address (harmless for in-tree parents — they ignore the addr argument). Not
exploitable beyond information disclosure; severity Low (root/boot-triggered,
1 byte, console medium). No escalation path exists for this class.

## Honest notes / dead ends

* The stack-spray could not claim the specific slot (overwritten by in-cascade
  kmalloc internals) — documented above; the disassembly + 16/16 deterministic
  0xff disclosure carries the proof instead.
* First harness iteration double-faulted on the kstack guard (spray recursion
  too deep) — harness bug, fixed at 24 frames; not a target property.
* Adjacent-file observations made while verifying (out of scope for DF-5539,
  recommend separate review): `iicbus.c:144-146` adds `ic/iic/iicsmb` children
  with plain `device_add_child` (no `struct iicbus_ivar` ivars) while
  `iic.c:115` `iicbus_get_addr()` dereferences `device_get_ivars()` — a NULL
  deref hazard when a child driver probes those children; `iic.c:366-369`
  (I2CRDWR) copies out `IIC_M_RD` buffers even when the transfer failed,
  disclosing fresh uninitialized kmalloc memory.

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