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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5648 Β· 9 files
FileTypeDescriptionSize
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)

  1. dfbsd-qemu/vm.sh up (guest boots to ssh)
  2. dfbsd-qemu/vm.sh run_root './run_root_introspect.sh' (or run the commands in trigger.sh directly)

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 generic at isa hints double-enumerate on BOTH buses (isahint.c:121-129 has 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 transcript
  • trigger.sh β€” exact introspection commands
  • env.txt β€” guest uname/kernel
  • fix.diff β€” proposed guard (isahint generic-hint first-bus guard)
  • verdict.json/manifest.json
VERDICT.md
↓ download raw

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_testable
baseline reproduced→ patch + rebuild →patched clean

Fix 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).
↓ fix.diffper-fix-DF-5648

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.