Shared isab_devclass unit-0 theft: acpi_isab claims the ACPI Processor Container as isab0/isa0, silently renumbering the real PCI-ISA bridge to isab1/isa1 β phantom empty ISA bus at unit 0; 'at isa0' hint misdirection and duplicate-instance double enumeration on hint-compiled kernels
Summary
isab and acpi_isab share the one isab_devclass and isab_attach hardcodes a wired unit-0 child: on QEMU-default firmware (Processor Container with _CID=PNP0A06) acpi_isab attaches first taking isab0/isa0, and the real PIIX3 bridge is silently renumbered to isab1/isa1 - live-observed across three boots (phantom empty isa0 under the CPU container, real bus at isa1). Any unit-0-targeted hint misdirects to the resourceless phantom, and generic 'at isa' hints enumerate on BOTH buses (isahint has no first-bus guard) creating duplicate instances of the same hardware. Negative control proves loader kenv hints are enumeration-inert on GENERIC (resource_query_string walks only the compiled devtab), confining the hazard to hint-compiled custom kernels. First-bus-guard fix proposed. The detach idiom here is the SAFE family variant (bus_generic_detach propagates).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5648 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| trigger.sh | β | 445 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| run.2.log | β | 1.7 KB | view raw | |
| env.txt | β | 150 B | view raw | |
| fix.diff | β | 915 B | view raw | |
| verdict.json | β | 4.3 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-5648 β shared isab_devclass unit-0 theft: ACPI Processor Container preempts isab0/isa0, silently renumbering the real PCI-ISA bridge to isab1/isa1 (phantom empty ISA bus; hint-misdirection/double-enumeration hazard on static-hint kernels)
Finding file: sys/bus/pci/isa_pci.c (pass 2). Severity Info, confidence certain β
the mechanism is live-observed on the audit guest across two independent boots.
How to reproduce (live introspection β no exploit code; the finding is a boot-time
bus-topology/numbering defect, deterministic on any QEMU i440fx rig with default ACPI)
dfbsd-qemu/vm.sh up(guest boots to ssh)dfbsd-qemu/vm.sh run_root './run_root_introspect.sh'(or run the commands intrigger.shdirectly)
Expected output (what constitutes reproduction)
dmesg shows, in this order:
isab0: <ACPI Generic ISA bridge> on acpi0 <- ACPI container stole unit 0 (isab0) isa0: <ISA bus> on isab0 <- phantom ISA bus (zero children) isab1: <PCI-ISA bridge> at device 1.0 on pci0 <- THIS FILE's driver, renumbered 0->1 isa1: <ISA bus> on isab1 <- the REAL ISA bus (vga0, orm0 here)
devinfo shows the real devices (vga0, orm0) under isa1 under isab1@pci0:0:1:0
(Intel PIIX3 0x70008086, matched by isab_probe via class 0x0601,
sys/bus/pci/isa_pci.c:100-102; the devid is also in the compat table at
sys/bus/pci/isa_pci.c:113), while isab0βisa0 under acpi0 (device \_SB_.CPUS,
_HID=ACPI0010 Processor Container, _CID=PNP0A06 matched by acpi_isab_probe,
sys/dev/acpica/acpi_isab.c:93-105) has no children at all.
run.2.log documents the negative-control experiment: a kenv hint
(hint.foo.0.at="isa" in /boot/loader.conf + reboot) creates no device on either
bus on this kernel, because isahint_identify enumerates via resource_query_string
which only walks the compiled-in static devtab (sys/kern/subr_bus.c:2402-2412) β
kenv hints are only honored for point lookups (resource_kenv, subr_bus.c:2300-2325).
The guest was vm.sh reset with-src'd afterwards (config restored).
Impact ceiling
- Default/GENERIC rig (no compiled-in ISA hints): functionally harmless β proven live; the real bus is fully operational at isa1.
- Kernel with static ISA hints compiled in + container firmware:
at isa0-targeted hints misdirect to the resourceless phantom bus, and genericat isahints double-enumerate on BOTH buses (isahint.c:121-129has no first-bus guard, unlike the rescan guard in orm/vga identify) β duplicate device instances for the same physical hardware; shareable legacy IRQs can be registered by both. - No unprivileged-attacker path: every input (ACPI namespace, PCI config space, loader hints) is firmware/boot-config controlled (root-equivalent).
Artifacts
run.logβ decisive dmesg/devinfo/pciconf snapshot (both boots' dmesg visible)run.2.logβ kenv-hint negative-control experiment transcripttrigger.shβ exact introspection commandsenv.txtβ guest uname/kernelfix.diffβ proposed guard (isahint generic-hint first-bus guard)verdict.json/manifest.json
DF-5648 verdict narrative β pass 2 audit of sys/bus/pci/isa_pci.c
FINDING (Info, certain): isab (this file) and acpi_isab share isab_devclass
(declared sys/bus/pci/isa_pci.c:84, registered into by DRIVER_MODULE at
isa_pci.c:86 and by acpi_isab at sys/dev/acpica/acpi_isab.c:89-90).
isab_attach β the attach method wired by THIS file at isa_pci.c:60 and
implemented at sys/bus/isa/isa_common.c:1046-1054 β hardcodes a wired unit:
device_add_child(dev, "isa", 0). acpi_isab_probe (acpi_isab.c:93-105) will
attach an ISA bus to ANY ACPI device whose HID/_CID is PNP0A05/PNP0A06 β
including QEMU's Processor Container \_SB_.CPUS (_HID=ACPI0010, _CID=PNP0A06),
whose only guard is that it must become unit 0 (devclass_get_device(isab_devclass,
0) != dev, acpi_isab.c:99). When ACPI enumeration of _SB.CPUS precedes PCI
enumeration of the PIIX3 (deterministic on the audit rig), the container takes
isab0βisa0, and the REAL PCI-ISA bridge matched by isab_probe (class 0x0601,
isa_pci.c:100-102; devid 0x70008086 also in the compat table at isa_pci.c:113)
is silently renumbered isab1βisa1 by devclass_alloc_unit's "using next available
unit number" path (sys/kern/subr_bus.c:1072-1081, subr_bus.c:1139).
REPRODUCED (status=reproduced, impact=none): observed live on the audit guest across two independent boots β see run.log:
isab0: <ACPI Generic ISA bridge> on acpi0 <- container stole unit 0 isa0: <ISA bus> on isab0 <- phantom, zero children isab1: <PCI-ISA bridge> at device 1.0 on pci0 <- this file's driver isa1: <ISA bus> on isab1 <- real bus (vga0, orm0)
devinfo confirms isab1@pci0:0:1:0 chip=0x70008086 class=0x060100 with children vga0/orm0 under isa1, and an EMPTY isa0 under the container.
Negative control (run.2.log): injected hint.foo.0.at="isa" into /boot/loader.conf,
rebooted β no foo child appears on either bus. Root cause: isahint_identify's
generic loop (sys/bus/isa/isahint.c:121-129) enumerates via resource_query_string,
which walks only the compiled-in static devtab (subr_bus.c:2402-2412); kenv hints
are only consumed by point lookups (resource_kenv, subr_bus.c:2300-2325). The
X86_64_GENERIC kernel compiles in no hints (devtab empty), so on the stock kernel
loader hints are enumeration-inert. This neutralizes the double-enumeration hazard
on GENERIC (good), but on hint-compiled custom kernels the hazard is fully live:
at isa hints would be enumerated on BOTH isa0 (phantom) and isa1 (real), each
BUS_ADD_CHILD renumbering to the next free unit (isahint.c:55, subr_bus.c:1079-1080)
β two device instances for the same physical hardware; shareable legacy IRQs can be
registered by both instances. at isa0-targeted hints would misdirect to the
resourceless phantom bus. orm/vga identify hooks have rescan guards only
(sys/dev/misc/orm/orm.c:88-93), not first-bus guards β rman exclusivity is what
actually prevented double-attach of the ROM/VGA resources on this rig.
WHY INFO / impact=none: every input driving this topology (ACPI namespace contents, PCI config space, loader hints/kernel hints) is firmware- or boot-config-controlled = root-equivalent influence; there is no unprivileged local, remote, or crafted-image attack path. On the default rig the observable consequence is purely cosmetic unit numbering (system is fully functional at isa1 β proven live).
EXPLOIT CHAIN: (empty β no attacker-reachable path; this is a robustness/hardening finding.)
PASS-2 CHECKLIST (file sys/bus/pci/isa_pci.c, 150 LOC) β everything else clean: (a) probe/attach + resource forwarding: probe consumes only fixed-width config reads (pci_get_class/subclass/devid β 8/8/32-bit, isa_pci.c:100-110); attach is the trivial child-add in isa_common.c:1046-1054; all bus_ methods are pure bus_generic_ pass-through to the PCI parent (isa_pci.c:68-73); the driver allocates/claims no resources of its own. (b) detach-veto family (DF-5544/5562/5598): DEVMETHOD(device_detach, bus_generic_detach) at isa_pci.c:61 β subr_bus.c:2872-2877 propagates the first failing child detach and stops; isab owns no softc (driver_t size 0, isa_pci.c:81), no IRQ/IO/DMA to tear down under a vetoed child. NOT an instance of the family β this is the safe idiom. (c) ivar/accessor: no ivar methods in isab; pci accessors used are read-only; no child ivar writes. (The grandchild-ivar cast in isa.c is DF-5641, not here.) (d) config-space-supplied sizes: none β no BAR parsing, no ranges decoding, no length math anywhere in the file; the devid table (isa_pci.c:110-133) is compile-time constants. Also killed: kprintf of 8-bit subclass (isa_pci.c:134-136) is no info leak (config space is user-readable via pciconf anyway); module unload path standard; probe ordering (-10000 vs acpi_isab's 0) is cross-bus and unexploitable; the theoretical devclass-slot TOCTOU in acpi_isab.c:99 cannot corrupt anything because devclass_add_device renumbers collisions safely (subr_bus.c:1072-1081).
FIX VALIDATION: not_testable β impact=none on the stock kernel (negative control proves the hazard path is enumeration-inert on GENERIC), so a fix build has no observable security behavior to flip; fix.diff targets the only consequential variant (double enumeration on hint-compiled kernels) with a first-bus guard in isahint_identify. Recommended complementary hardening (prose only): have acpi_isab refuse Processor Containers (_HID=ACPI0010) unless they declare ISA-enumerable children, or make isab_attach log the renumber loudly at boot.
Guest left CLEAN: vm.sh reset with-src performed after evidence capture
(loader.conf experiment reverted; guest_dirty=0).
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
not_testableFix validation not applicable/not_testable: the finding's impact on the stock kernel is none (negative control proves the hazard path enumeration-inert on GENERIC), so a single-fix kernel build has no observable security behavior to flip; fix.diff (git-apply-able, verified APPLIES-CLEAN) targets the only consequential variant β duplicate device instances from generic 'at isa' hints on hint-compiled kernels β with a first-bus guard in isahint_identify.
fix.diff verified with 'git apply --check' against the pristine tree (sha256 2c2eb54e4e8e17c694337abbc9ebeb60aeb0fd1402544c86131ec6a3f603e93d); no kernel rebuild performed (see fix_verdict).
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: dmesg lines 'isab0: <ACPI Generic ISA bridge> on acpi0' / 'isa0: <ISA bus> on isab0' (phantom, empty) vs 'isab1: <PCI-ISA bridge> at device 1.0 on pci0' / 'isa1: <ISA bus> on isab1' (real, hosts vga0+orm0); devinfo shows isab1@pci0:0:1:0 chip=0x70008086 class=0x060100 with children under isa1 only; run.2.log: hint.foo.0.at="isa" present in kenv after reboot yet no foo child on either bus (resource_query_string walks only the compiled-in devtab) β negative control proving the double-enumeration variant is unreachable on GENERIC.
PoC changes
No PoC code required β evidence class is live introspection. trigger.sh written fresh (no seed existed); added a negative-control kenv-hint injection experiment (run.2.log) that upgraded the double-enumeration claim from speculative to mechanism-proven-inert on GENERIC.
Verified recommended fix
Guard isahint_identify's generic 'at isa' enumeration to the first ISA bus (fix.diff); optionally have acpi_isab refuse Processor Containers or make isab_attach announce the unit renumber.
Verdict
Live-proven on the audit guest across three independent boots: acpi_isab (sharing isab_devclass with this file's isab driver) claims QEMU's ACPI Processor Container \SB.CPUS (_HID=ACPI0010, _CID=PNP0A06) as isab0/isa0, so the real PIIX3 PCI-ISA bridge matched by isab_probe is silently renumbered to isab1/isa1 by devclass_alloc_unit's next-free-unit path, leaving a phantom empty ISA bus at unit 0 and misdirecting unit-0-targeted enumeration. On the stock kernel the consequence is purely cosmetic (real bus fully functional at isa1; negative-control experiment proves loader kenv hints are enumeration-inert on GENERIC, so no double enumeration can occur), and every input driving the topology is firmware/boot-config controlled (root-equivalent) β no unprivileged local, remote, or crafted-image path exists; filed as Info hardening for the unit-numbering/hint-misdirection hazard that is live on hint-compiled custom kernels.
No comments yet.