iicbb_print_child discloses an uninitialized kernel stack byte (and 'restores' it as the bus address) whenever the parent reset returns non-IIC_ENOADDR β reachable in-tree via lpbb's ppbus-busy path
Summary
oldaddr is declared uninitialized, passed &oldaddr to the parent reset, and on any return != IIC_ENOADDR printed to console/dmesg and written back as the restored address - but no in-tree parent can reach that branch with oldaddr written (lpbb busy path returns 0 without touching it; viapm/cxm always return ENOADDR). Every execution of the branch discloses a kernel stack byte to any local user via dmesg (0xff allocator fill observed 16/16 events; can hold pointer fragments elsewhere) - the iicbb member of the uninit-byte family class (cf. DF-5523). Production-code KLD harness with bit-level emulated I2C slave proved it and exercised the transfer state machine green; init+guard fix validated (deterministic addr 0x0). One-byte boot-time leak, no escalation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5539 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 7.2 KB | β raw | |
| iicbb_harness.c | β | 11.9 KB | view raw | |
| iicbb_reg.c | β | 803 B | view raw | |
| spray_conf.h | β | 122 B | view raw | |
| Makefile | β | 346 B | β download | |
| Makefile.reg | β | 189 B | β download | |
| build.sh | β | 154 B | view raw | |
| run.sh | β | 187 B | view raw | |
| build.log | β | 5.5 KB | view raw | |
| run.out | β | 4.8 KB | view raw | |
| dmesg.out | β | 10.7 KB | view raw | |
| fix_dmesg.out | β | 651 B | view raw | |
| stock_disasm.txt | β | 3.9 KB | view raw | |
| fix_disasm.txt | β | 4.0 KB | view raw | |
| fix_build.log | β | 153 B | view raw | |
| env.txt | β | 451 B | view raw | |
| fix.diff | β | 514 B | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-5539 β iicbb_print_child() discloses an uninitialized kernel stack byte
sys/bus/iicbus/iicbb.c:164-186
What
iicbb_print_child() declares u_char oldaddr; (iicbb.c:169) uninitialized,
hands &oldaddr to the parent driver's iicbb_reset method (iicbb.c:173), and β
whenever that call does not return IIC_ENOADDR β prints the byte to the
console and writes it back as the "restored" bus address (iicbb.c:179-182):
iicbus0: <Philips I2C bus> on iicbb0 addr 0xff
No in-tree iicbb parent ever fulfills the "return 0 and fill *oldaddr"
contract: lpbb_reset returns IIC_ENOADDR on success and returns 0 without
writing *oldaddr on its ppbus-busy path (sys/dev/misc/lpbb/lpbb.c:186-190);
viabb_reset and cxm_iic_reset always return IIC_ENOADDR. So the only way
the else-branch (iicbb.c:177-183) can execute in-tree is with oldaddr never
written β every execution of those lines reads uninitialized kernel stack.
How to reproduce (production-code KLD harness β no bit-bang hardware)
The guest has no GPIO/ppbus bit-bang hardware, so the harness instantiates the
real, unmodified in-tree iicbb.ko under a fake GPIO parent:
iicbb_reg.koβ registers the realiicbb_driver/iicbb_devclasson a fake busfakebb, exactly like the in-treeDRIVER_MODULE(iicbb, lpbb/viapm/...)declarations (iicbb.c:441-445).iicbb_harness.koβ afakebbdevice onnexus0whoseiicbb_ifmethods emulate the GPIO pins plus a wired I2C slave (START/STOP detection, SCL-edge sampling, ACK/NACK injection, TX pattern generator).fakebb_reset()faithfully reproduces lpbb's ppbus-busy contract violation: returns 0, never writes*oldaddr.
On load, newbus walks identify β probe β attach (sys/kern/subr_bus.c:3007-3013,
print at subr_bus.c:2004), the cascade fakebb0 β iicbb0 β iicbus0 runs inside
fakebb_attach, and iicbb_print_child prints the uninitialized byte. The
harness then drives the real transfer state machine through the public iicbus
API (write-echo, read-pattern, NACK β IIC_ENOACK + bus-stopped,
repeated-start read) to demonstrate the driver is exercised end-to-end.
Build (on the DragonFly guest, as root, in /root/df5539)
./build.sh # builds iicbb_reg.ko + 4 harness variants
# (a5/5a/37: stack pre-sprayed; none: no spray)
Run
kldload /boot/kernel/iicbus.ko kldload /boot/kernel/iicbb.ko kldload /usr/obj/root/df5539/build_reg/iicbb_reg.ko kldload /usr/obj/root/df5539/build_a5/iicbb_harness.ko # etc. per variant dmesg | grep 'on iicbb0 addr' ./run.sh # does all of the above + 8 load/unload cycles
Expected
- Stock kernel/modules:
iicbus0: <Philips I2C bus> on iicbb0 addr 0xffon every attach (16/16 observed; value is stack junk β allocator fill on this system, see VERDICT.md for the spray experiment and the disassembly proof that the slot is never written). - State machine green:
write ok=1 read_ok=1,nacked-write: error=2 (IIC_ENOACK=2) bus_started_after=0,repstart-read: byte=0xc0. - With
fix.diffapplied to iicbb.c and onlyiicbb.korebuilt:addr 0x0(deterministic, no uninitialized data), state machine unchanged.
Fix
See fix.diff (also inline in the finding): initialize oldaddr = 0 and take
the else-branch only on error == IIC_NOERR.
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:
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 returnsIIC_ENOADDR(never fills either).viabb_reset(sys/dev/powermng/viapm/viapm.c:507-514): alwaysIIC_ENOADDR, never fills.cxm_iic_reset(sys/dev/video/cxm/cxm_i2c.c:273-296): alwaysIIC_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
kldloadthe 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;
dmesgis readable by unprivileged users (nodmesg_restricton DragonFly), as is/var/log/messagesvia syslogd (default*.noticekernel 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).
- 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. - 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 thedevice_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. - Machine-code proof (stock_disasm.txt,
/boot/kernel/iicbb.ko):oldaddrlives at-0x21(%rbp); the function contains exactly onelea -0x21(%rbp),%rcx(passing&oldaddrto the parent reset) and twomovzbl -0x21(%rbp)reads (restore call + kprintf) β no instruction writes the slot. Withfakebb_resetalso proven not to write it (source +resets_with_oldaddr=1counter), the printed byte is uninitialized stack by construction. - State machine driven end-to-end through the real driver (checklist a-d):
* block write: 8 bytes
de ad be ef 55 aa 12 34bit-banged out, slave shift-register captured them exactly (slave_rx=deadbeef55aa1234,write_ok=1) β byte counter / ACK handling correct; * block read: slave patternc0..c7returned 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 toiicbb_start(iicbb.c:92): write byte + repeated start + read byte round-trip succeeded (byte=0xc0); *resets=3 resets_with_oldaddr=1matches the exact call pattern iicbus.c:125 (NULL), iicbb.c:173 (&oldaddr β the leak call), iicbb.c:179 (restore, NULL). - Fix validated:
fix.diffapplied to the guest's/usr/srccopy of iicbb.c, onlyiicbb.korebuilt (module β no kernel rebuild), harness re-run: output changed fromaddr 0xffto the deterministicaddr 0x0, zero occurrences of the uninitialized byte (fix_dmesg.out), state machine still green. Patched disassembly (fix_disasm.txt) shows the newmovb $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-146addsic/iic/iicsmbchildren with plaindevice_add_child(nostruct iicbus_ivarivars) whileiic.c:115iicbus_get_addr()dereferencesdevice_get_ivars()β a NULL deref hazard when a child driver probes those children;iic.c:366-369(I2CRDWR) copies outIIC_M_RDbuffers 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.
Fix verification
fixedfix.diff (initialize oldaddr + gate the else-branch on error == IIC_NOERR) applied to the guest's /usr/src copy of sys/bus/iicbus/iicbb.c and only iicbb.ko rebuilt (module, no kernel rebuild). Baseline stock module: 16/16 'addr 0xff' uninitialized disclosures. Patched module: deterministic 'addr 0x0', zero occurrences of the uninitialized byte, transfer state machine unchanged (write_ok=1 read_ok=1). Patched disassembly adds the initializing 'movb $0x0,-0x21(%rbp)' store. Guest /usr/src restored to pristine and stock module reloaded afterwards.
fix.diff; fix_build.log; fix_dmesg.out ('iicbus0: <Philips I2C bus> on iicbb0 addr 0x0', zero 'addr 0xff'); fix_disasm.txt line 16 (movb $0x0,-0x21(%rbp)); dmesg.out (baseline 16x addr 0xff).
Confirmed kernel references
Detail
Evidence (decisive lines)
run.out + dmesg.out: 16x 'iicbus0: <Philips I2C bus> on iicbb0 addr 0xff' and green [fakebb] state-machine markers (write ok=1 read_ok=1, nacked error=2 IIC_ENOACK bus_started_after=0, repstart byte=0xc0, resets_with_oldaddr=1). stock_disasm.txt: iicbb_print_child has NO write to -0x21(%rbp) (only lea at +0x3d and movzbl reads at +0x69/+0xb1). fix_disasm.txt: patched prologue adds 'movb $0x0,-0x21(%rbp)'. fix_dmesg.out: 'addr 0x0', zero occurrences of the uninitialized byte.
PoC changes
No seed PoC existed (pass-2 finding verified in the same run). Authored the family-standard production-code KLD harness from scratch: iicbb_reg.ko registers the real iicbb_driver on a fake 'fakebb' bus; iicbb_harness.ko instantiates fakebb0 on nexus0 with a full iicbb_if vtable (bit-level emulated slave: START/STOP edge detection, SCL-edge sampling, ACK/NACK injection, TX pattern) whose reset returns 0 without writing *oldaddr (lpbb busy-path faithful), pre-sprays the kstack with a compile-time pattern, and drives the real iicbus API. Iterations: (1) kmod.mk clobbers CFLAGS so -D spray flags were moved to a generated spray_conf.h; (2) 64-frame spray hit the kstack guard page (double fault) -- reduced to 24 frames; (3) module-registration ordering inside one .ko made the attach cascade lag fakebb_attach -- split the (fakebb,iicbb) DRIVER_MODULE into a separate preload KLD.
Verified recommended fix
Initialize 'u_char oldaddr = 0' in iicbb_print_child and enter the address-print/restore branch only when the parent reset returns IIC_NOERR (see fix.diff).
Verdict
iicbb_print_child() (sys/bus/iicbus/iicbb.c:164-186) reads an uninitialized stack byte 'oldaddr' (declared :169, address passed to the parent reset at :173) whenever the parent's iicbb_reset returns anything other than IIC_ENOADDR, prints it to console/dmesg (:181-182) and writes it back as the restored bus address (:179). No in-tree parent can reach that branch with oldaddr written: lpbb_reset's ppbus-busy path returns 0 without touching *oldaddr (lpbb.c:186-190), viabb/cxm always return IIC_ENOADDR. Reproduced on the DragonFly guest with a production-code KLD harness that loads the stock in-tree iicbb.ko under a fake GPIO parent (emulated I2C slave, faithful lpbb busy-path reset): 16/16 attach events printed 'on iicbb0 addr 0xff' -- the untouched stack slot. Machine-code proof: the stock iicbb.ko disassembly of iicbb_print_child contains no write to -0x21(%rbp), only the lea passing &oldaddr and two reads. The same harness drove the full transfer state machine green (write echo, read pattern, NACK->IIC_ENOACK with bus stopped, repeated-start), so the checklist (a)-(d) was exercised live. fix.diff (init oldaddr=0 + else-branch only on IIC_NOERR) validated by rebuilding only iicbb.ko: output becomes deterministic 'addr 0x0' with zero uninitialized bytes and an unchanged state machine.
No comments yet.