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

iicbus_add_child registered with a 4-arg/int prototype against DragonFly's 5-arg/device_t bus_add_child interface β€” argument+return skew causes an out-of-bounds kernel write in devclass_add_device

Summary

DEVMETHOD binds a FreeBSD-4-era 4-arg int-returning function to DFly's 5-arg device_t interface with no signature check: every dispatch misreads the arguments (name<-order 0 => NULL, unit<-name address truncated negative) and the classless negative-unit child reaches devclass_add_device, writing an 8-byte heap pointer OOB BELOW the devclass device array. Reproduced twice on stock iicbus.ko: the documented identify pattern (device_if.m) self-destructs at kldload - Fatal trap 12 supervisor write at devclass_add_device+0xf6 (fault VA 0xfffffffc13...); same trap from a single healthy-bus BUS_ADD_CHILD. In-tree iic/iicsmb escape only because attach pre-creates their trio so the identify guards never fire - an accident. Root-gated kldload + documented-pattern usage; panic proven, heap corruption plausible with layout control. re-type-to-interface + ivar-allocating add_child fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5632 Β· 27 files
FileTypeDescriptionSize
df5632_rig.c β€” 18.3 KB view raw
dfctl.c β€” 1.9 KB view raw
iicbb_reg.c β€” 491 B view raw
Makefile.rig β€” 229 B ↓ download
Makefile.reg β€” 211 B ↓ download
build.sh β€” 85 B view raw
run_a_stock.sh β€” 1.1 KB view raw
run_b_5559.sh β€” 1017 B view raw
run_c_panic.sh β€” 1006 B view raw
run_d_fixed.sh β€” 1.1 KB view raw
build.log β€” 30.8 KB view raw
run_a2.log β€” 1004 B view raw
run_a2_dmesg.txt β€” 2.4 KB view raw
run_b_5559.log β€” 936 B view raw
run_c_panic_driver.log β€” 518 B view raw
run_d_fixed.log β€” 1.1 KB view raw
run_d_fixed_dmesg.txt β€” 2.0 KB view raw
panic_skew_stock.txt β€” 472 B view raw
panic_skew_addchild.txt β€” 683 B view raw
panic_5635_stock.txt β€” 2.5 KB view raw
fix.diff β€” 5.5 KB view raw
iicbus.c.fixed β€” 7.9 KB ↓ download
env.txt β€” 354 B view raw
README.md β€” 5.5 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 5.1 KB view raw

DF-5632 β€” iicbus_add_child registered with a 4-arg/int prototype against DragonFly's 5-arg/device_t bus_add_child interface β†’ argument+return skew β†’ out-of-bounds kernel write in devclass_add_device

  • File under audit: sys/bus/iicbus/iicbus.c (pass 2)
  • Class: kernel memory corruption (OOB write) via silently type-confused kobj method registration
  • Severity: Medium (trigger = kldload of any identify-based iicbus client driver β€” root-gated; in-tree iic/iicsmb are only masked by an accident of pre-created children)
  • Verdict: REPRODUCED (two independent stock triggers, both ending in Fatal trap 12 at devclass_add_device+0xf6), fix validated

The defect

DragonFly's bus_add_child interface is (sys/kern/bus_if.m:128-134):

METHOD device_t add_child {
    device_t bus;
    device_t parent;
    int order;
    const char *name;
    int unit;
};

Five arguments, returns the new device_t. Every in-tree caller uses (parent, parent, order, name, unit) β€” e.g. nexus.c:251, isahint.c:55, iic.c:109, iicsmb.c:141 β€” and nexus_add_child (sys/platform/pc64/x86_64/nexus.c:90,293) shows the correct implementation shape (5 args, returns child, allocates ivars).

iicbus.c registers a FreeBSD-4-era 4-arg function returning int (iicbus.c:57, :164-171):

static int iicbus_add_child(device_t dev, int order, const char *name, int unit);
...
DEVMETHOD(bus_add_child, iicbus_add_child),          /* iicbus.c:66 */
...
static int
iicbus_add_child(device_t dev, int order, const char *name, int unit)
{
    device_add_child_ordered(dev, order, name, unit);
    bus_generic_attach(dev);
    return (0);
}

DEVMETHOD stores the raw function pointer (no compile-time signature check), so the kobj dispatcher calls it with 5 register arguments and interprets EAX as a device_t. On x86-64 the callee therefore reads:

iicbus_add_child param actually receives
order the parent device_t, truncated to int
name the order int (0 β†’ (char *)0 = NULL)
unit the name string literal address, truncated to int
return 0 β†’ NULL handed to every caller expecting the child

and then calls device_add_child_ordered(dev, (int)parent, NULL, (int)name_addr).

Why it corrupts memory

make_device(parent, NULL, garbage_unit) (subr_bus.c) creates a classless child with unit = (int)(intptr_t)"name" β€” a low-32-bits kernel rodata address, in practice a negative int. When that ghost is probed (by the bus_generic_attach(dev) inside iicbus_add_child itself, or by the bus_generic_driver_added probe loop), device_probe_child() β†’ device_set_devclass(child, driver->name) β†’ devclass_add_device() runs:

  • devclass_alloc_unit() (subr_bus.c:~2460): a negative wired unit skips both the collision branch (unit >= 0 && ...) and the table-extension branch (unit >= dc->maxunit) and is returned unchanged;
  • devclass_add_device() (subr_bus.c:~2420): dc->devices[dev->unit] = dev β€” an 8-byte write of a kernel heap pointer below the device array.

Reproduction (guest, stock /boot/kernel/iicbus.ko)

Rig: df5632_rig.c β€” fake GPIO bridge under nexus hosting the production chain fakebb0 β†’ iicbb0 (iicbb.ko) β†’ iicbus0 (iicbus.ko), plus a client driver df5632k whose identify is a verbatim copy of the iic.c:104-110 pattern. Two independent triggers:

  1. Module-load trigger (panic_skew_stock.txt): loading the rig module registers df5632k_driver on the attached iicbus0 β†’ bus_generic_driver_added β†’ df5632k_identify β†’ BUS_ADD_CHILD(iicbus0, iicbus0, 0, "df5632k", -1) β†’ skew β†’ ghost child β†’ probe β†’ Fatal trap 12 ... supervisor write data, page not present, fault virtual address = 0xfffffffc131cb558, Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1), current process = the kldload. This is exactly what happens to any driver author who follows the documented identify pattern (device_if.m:99-101) for a new iicbus client.
  2. API-contract trigger (panic_skew_addchild.txt): with the bus up and healthy (STAT: nchildren=3, iicbus0 state=3 attached), one BUS_ADD_CHILD(bus, bus, 0, "df5632c", -1) β€” the call every identify method makes β€” panics identically (fault virtual address = 0xfffffffc131cc228, same stopped PC).

The only reason the in-tree iic.ko/iicsmb.ko do not hit this today is that iicbus_attach() pre-creates the ic/iic/iicsmb trio (iicbus.c:144-146), so those drivers' identify guards (device_find_child(...) == NULL) never fire β€” an accident, not a protection: any client whose rendezvous child is missing (deleted child, out-of-tree driver, future driver, DF-5633's ghost-populated devclass corner cases) takes the OOB write.

Fix (validated)

fix.diff re-types iicbus_add_child to the interface contract (static device_t iicbus_add_child(device_t bus, device_t parent, int order, const char *name, int unit)), returns the created child (and allocates its struct iicbus_ivar, see DF-5634), and drops the redundant bus_generic_attach() (the bus_generic_driver_added probe loop already runs after identify; nexus_add_child does not re-attach either).

With only iicbus.ko rebuilt from this diff (run_d_fixed.log):

  • dfctl addchild: BUS_ADD_CHILD returned 0xfffff801175bc9c0 (a real device_t) and the child list shows child[3] devclass=df5632c unit=0 β€” the requested rendezvous, created correctly;
  • no panic at any point; module load, ioctls, cycling and unload all clean.
VERDICT.md
↓ download raw

DF-5632 VERDICT β€” REPRODUCED (OOB kernel write via ABI-skewed bus_add_child), fix validated

Root cause (path:line)

iicbus_add_child (sys/bus/iicbus/iicbus.c:57,164-171) is declared and registered (DEVMETHOD(bus_add_child, iicbus_add_child), iicbus.c:66) with a FreeBSD-legacy 4-argument, int-returning prototype. DragonFly's bus_add_child interface (sys/kern/bus_if.m:128-134) is 5-argument, device_t-returning (bus, parent, order, name, unit). DEVMETHOD performs no signature check (sys/sys/bus.h), so every dispatch through the iicbus driver skews the arguments:

  • order ← the parent device_t (truncated),
  • name ← the integer order (0 β†’ NULL),
  • unit ← the name string-literal address (truncated, negative),
  • return value 0 β†’ NULL for every caller expecting the new child (iic.c:109, iicsmb.c:141, iicbus.c's own callers-to-be).

The skewed device_add_child_ordered(dev, order, name, unit) creates a classless child with a negative wired unit. The first probe of that child (from iicbus_add_child's own bus_generic_attach(dev), or the bus_generic_driver_added probe loop) reaches devclass_add_device β†’ devclass_alloc_unit, where a negative wired unit skips both the collision check and the table extension (sys/kern/subr_bus.c, devclass_alloc_unit), and then

dc->devices[dev->unit] = dev;    /* devclass_add_device */

writes an 8-byte kernel heap pointer out of bounds, below the devclass device array.

Evidence (guest, DragonFly 6.5-DEVELOPMENT, stock /boot/kernel/iicbus.ko)

  • panic_skew_stock.txt β€” trigger 1 (module load): rig's df5632k driver uses the verbatim iic_identify pattern (iic.c:104-110); its registration on the attached iicbus0 fires BUS_ADD_CHILD β†’ Fatal trap 12, supervisor write data, page not present, fault virtual address = 0xfffffffc131cb558, Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1), current process = kldload. Full chain on serial console: fakebb0 β†’ iicbb0 β†’ iicbus0 (attached, trio created) β†’ rig attach done β†’ panic at df5632k driver registration.
  • panic_skew_addchild.txt β€” trigger 2 (API contract): bus verified healthy (STAT: nchildren=3, iicbus0 attached state=3), then ONE BUS_ADD_CHILD(iicbus0, iicbus0, 0, "df5632c", -1) (what every identify method does) β†’ identical Fatal trap 12 at devclass_add_device+0xf6, fault virtual address = 0xfffffffc131cc228, current process = dfctl.
  • Fault VAs (0xfffffffc…) = dc->devices + 8*(negative int) β€” the negative-index arithmetic exactly as derived from the skew.

Reachability honesty: the trigger is kldload of an iicbus client driver (root-gated), which is why this is Medium and not High. In-tree iic.ko and iicsmb.ko escape only because iicbus_attach pre-creates their rendezvous children, so their identify guards never call BUS_ADD_CHILD on an iicbus. Any out-of-tree or future in-tree client following the documented pattern (sys/kern/device_if.m:99-101: "Using generic_bus_identify() will simply call BUS_ADD_CHILD using the driver name") takes the OOB write at load.

Fix validation (fix.diff, module-only rebuild of iicbus.ko)

run_d_fixed.log / run_d_fixed_dmesg.txt:

  • dfctl addchild β†’ BUS_ADD_CHILD returned 0xfffff801175bc9c0 (real device_t, not NULL) and child[3] devclass=df5632c unit=0 driver=(none) β€” the rendezvous is created with the requested name/unit;
  • no panic at load, under addchild, under 2 detach/reattach cycles, or at unload (RUN-D-DONE, guest healthy).

Distinction from the family findings

  • DF-5559 (iic.c) is the consumer of the missing ivar plumbing; this finding is the provider-side ABI break. Its fix (read/write_ivar) does not touch bus_add_child.
  • DF-5563 (iiconf.c) is the waiter/teardown protocol; unrelated to add_child.
  • DF-5633 (same file) is attach-time child duplication; DF-5634 is the ivar allocation; DF-5635 is detach error propagation. One coherent fix.diff covers all four (each is independently validated).

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

With only iicbus.ko rebuilt from fix.diff, BUS_ADD_CHILD returns a real device_t (0xfffff801175bc9c0) and creates the requested rendezvous child (devclass=df5632c unit=0); the previous Fatal-trap-12 OOB write at devclass_add_device+0xf2/0xf6 is gone under both triggers' equivalents; load, addchild, 2 detach/reattach cycles and unload all clean, guest healthy.

run_d_fixed.log ('dfctl addchild: kernel rc=0 v0=0xfffff801175bc9c0'), run_d_fixed_dmesg.txt ('child[3] devclass=df5632c unit=0', 'RUN-D-DONE', no trap)
↓ fix.diffper-fix-DF-5632

Confirmed kernel references

Detail

Exploit chain

root: kldload an iicbus client driver whose identify follows the documented device_if.m pattern (iic.c:104-110 verbatim) -> BUS_ADD_CHILD(iicbusN,...) dispatch into skewed iicbus_add_child -> ghost child with negative wired unit -> first probe -> devclass_alloc_unit passes negative unit through -> devclass_add_device writes device_t OOB below dc->devices -> kernel memory corruption / Fatal trap 12. Trigger is root-gated (kldload); in-tree iic/iicsmb are masked only by the pre-created trio.

Evidence (decisive lines)

panic_skew_stock.txt (trigger 1: kldload, fault VA 0xfffffffc131cb558, Stopped at devclass_add_device+0xf6); panic_skew_addchild.txt (trigger 2: BUS_ADD_CHILD ioctl on a verified-healthy bus, fault VA 0xfffffffc131cc228, same PC); run_d_fixed.log + run_d_fixed_dmesg.txt (fixed: BUS_ADD_CHILD returned 0xfffff801175bc9c0, child devclass=df5632c unit=0, no panic); df5632_rig.c (rig: fakebb0->iicbb0->iicbus0 production chain + df5632k identify client)

PoC changes

Rig adapted from the DF-5563 fakebb harness (same GPIO emulator): added the df5632k identify-based client driver (verbatim iic_identify pattern), an hw.df5632.identify_add gate so non-skew tests can load cleanly on stock, and /dev/df5632 ioctls driving production newbus APIs (BUS_ADD_CHILD, device_detach, device_probe_and_attach, BUS_READ/WRITE_IVAR, iicbus_request_bus).

Verified recommended fix

Re-type iicbus_add_child to the DFly bus_add_child contract: static device_t iicbus_add_child(device_t bus, device_t parent, int order, const char *name, int unit) { child = device_add_child_ordered(parent, order, name, unit); allocate struct iicbus_ivar; device_set_ivars(child, ivar); return child; } -- and drop the redundant bus_generic_attach() call (see fix.diff).

Verdict

REPRODUCED on stock iicbus.ko via two independent triggers (module-load of an identify-pattern client driver, and a direct BUS_ADD_CHILD ioctl): the 4-arg/int iicbus_add_child registered against DragonFly's 5-arg/device_t bus_add_child interface misreads every dispatched argument (order<-parent pointer, name<-order int, unit<-name string address) and returns NULL to callers expecting the new child; the classless negative-unit child it creates reaches devclass_add_device, whose dc->devices[dev->unit]=dev is an out-of-bounds 8-byte kernel write below the devclass device array (Fatal trap 12, supervisor write, devclass_add_device+0xf6, fault VA 0xfffffffc13...). With fix.diff (correct 5-arg prototype returning the child + ivar allocation) BUS_ADD_CHILD returns a real device_t and creates the rendezvous correctly; no panic anywhere.