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)
PoC verification
Evidence pack
findings/poc/DF-5632 Β· 27 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 =
kldloadof any identify-based iicbus client driver β root-gated; in-treeiic/iicsmbare only masked by an accident of pre-created children) - Verdict: REPRODUCED (two independent stock triggers, both ending in
Fatal trap 12atdevclass_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:
- Module-load trigger (
panic_skew_stock.txt): loading the rig module registersdf5632k_driveron 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 = thekldload. 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. - API-contract trigger (
panic_skew_addchild.txt): with the bus up and healthy (STAT: nchildren=3, iicbus0state=3attached), oneBUS_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_CHILDreturned 0xfffff801175bc9c0 (a real device_t) and the child list showschild[3] devclass=df5632c unit=0β the requested rendezvous, created correctly;- no panic at any point; module load, ioctls, cycling and unload all clean.
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β theparentdevice_t (truncated),nameβ the integerorder(0 β NULL),unitβ thenamestring-literal address (truncated, negative),- return value
0βNULLfor 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'sdf5632kdriver uses the verbatimiic_identifypattern (iic.c:104-110); its registration on the attached iicbus0 firesBUS_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 attachedstate=3), then ONEBUS_ADD_CHILD(iicbus0, iicbus0, 0, "df5632c", -1)(what every identify method does) β identicalFatal trap 12atdevclass_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_CHILDreturned 0xfffff801175bc9c0 (real device_t, not NULL) andchild[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
fixedWith 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)
Confirmed kernel references
- sys/bus/iicbus/iicbus.c:57
- sys/bus/iicbus/iicbus.c:66
- sys/bus/iicbus/iicbus.c:164-171
- sys/kern/bus_if.m:128-134
- sys/kern/subr_bus.c:devclass_add_device
- sys/kern/subr_bus.c:devclass_alloc_unit
- sys/bus/iicbus/iic.c:104-110
- sys/bus/iicbus/iicsmb.c:137-142
- sys/platform/pc64/x86_64/nexus.c:90,293
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.
No comments yet.