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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5539 Β· 20 files
FileTypeDescriptionSize
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 real iicbb_driver/iicbb_devclass on a fake bus fakebb, exactly like the in-tree DRIVER_MODULE(iicbb, lpbb/viapm/...) declarations (iicbb.c:441-445).
  • iicbb_harness.ko β€” a fakebb device on nexus0 whose iicbb_if methods 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 0xff on 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.diff applied to iicbb.c and only iicbb.ko rebuilt: 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.

VERDICT.md
↓ download raw

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root:dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (patched iicbb.ko only)

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.