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

isa_alloc_resource/isa_release_resource cast grandchild ivars via DEVTOISA unconditionally β€” type-confused (often NULL) resource-list pointer passed through, safe today only because resource_list_alloc/release early-forward on passthrough

Summary

passthrough is computed but the DEVTOISA cast runs regardless: for grandchild callers (psm0 on ISA-attached atkbdc0, ata channels, pccbb cards) rl is type-confused or NULL, handed to resource_list_alloc/release whose early-forward-on-passthrough is the ONLY guard. Live-verified: hinted atkbdc0 onto ISA -> psmattach executed the exact passthrough call with atkbdc_device_t ivars; IRQ 12 allocated, guest healthy - the confused pointer is computed on every such boot and never dereferenced. One refactor hoisting resource_list_find out of the !passthrough guard (as pre-fix FreeBSD once had) turns every ISA PS/2-mouse boot into a NULL/wild deref. guard-the-cast fix mirrors FreeBSD's restructuring.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5641 Β· 13 files
FileTypeDescriptionSize
README.md β€” 4.8 KB ↓ raw
VERDICT.md β€” 7.3 KB ↓ raw
introspect.sh β€” 1.4 KB view raw
build.sh β€” 131 B view raw
run.sh β€” 215 B view raw
trigger_hints.txt β€” 115 B view raw
devinfo.hinted.txt β€” 403 B view raw
dmesg.hinted.txt β€” 12.0 KB view raw
env.txt β€” 288 B view raw
run.log β€” 1.8 KB view raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 5.0 KB view raw

DF-5641 β€” isa_alloc_resource/isa_release_resource cast grandchild ivars via DEVTOISA (latent type confusion, dead-cast today)

Target file: sys/bus/isa/x86_64/isa.c (pass 2) Severity: Info (defense-in-depth) Β· no runtime impact demonstrated β€” and this pack proves the confused pointer is computed on a live boot and never dereferenced.

Claim

isa_alloc_resource() (sys/bus/isa/x86_64/isa.c:76-124) and isa_release_resource() (isa.c:126-134) unconditionally do

struct isa_device* idev = DEVTOISA(child);          /* isa.c:87, :130 */
struct resource_list *rl = &idev->id_resources;

even when passthrough is true (device_get_parent(child) != bus, isa.c:85). For a passthrough (grandchild) caller the child's ivars are not a struct isa_device β€” they are the sub-bus's own ivar type (or NULL), so rl is a type-confused / NULL pointer. It is handed to resource_list_alloc()/resource_list_release(), which early-forward on passthrough without touching rl (sys/kern/subr_bus.c:2683-2687 and :2729-2732). That subr_bus.c implementation detail is the only reason every DragonFly x86_64 boot with a PS/2 mouse does not panic in resource_list_find() (subr_bus.c:2652 SLIST_FOREACH dereference).

Live demonstration (guest)

Standard QEMU guest, kernel X86_64_GENERIC (see env.txt). One configuration change β€” loader hints that attach atkbdc0 to the ISA bus (the in-tree DRIVER_MODULE(atkbdc, isa, …) attachment, sys/dev/misc/atkbdc_layer/atkbdc_isa.c:401):

hint.atkbdc.0.at="isa"      # isahint.c:121-129 creates atkbdc0 on the isa bus
hint.atkbdc.0.port="0x60"
hint.atkbdc.0.irq="1"
hint.psm.0.at="atkbdc0"     # atkbdc_isa.c:290-295 adds psm0 with atkbdc_device_t ivars
hint.psm.0.irq="12"

psm0 is then a grandchild of the isa bus. psmattach() (sys/dev/misc/psm/psm.c:2014) calls bus_alloc_legacy_irq_resource(dev=psm0, …, irq=12, RF_ACTIVE) (sys/kern/subr_bus.c:3302-3308), which dispatches to atkbdc's bus_generic_alloc_resource (atkbdc_isa.c:83) and then to isa_alloc_resource(bus=isa0, child=psm0) with:

  • passthrough == 1 (psm0's parent is atkbdc0),
  • psm0's ivars = atkbdc_device_t * (atkbdc_isa.c:231-232) β€” cast to struct isa_device * at isa.c:87. rl = the atkbdc_device_t object reinterpreted as an SLIST head (id_resources is the first member, isa_common.h:53-54).

Observed (dmesg.hinted.txt, devinfo.hinted.txt, env.txt):

atkbdc0: <Keyboard controller (i8042)> at port 0x60,0x64 irq 1 on isa0
psm0: <PS/2 Mouse> irq 12 on atkbdc0
interrupt uses mplock: psm0
psm0: model IntelliMouse Explorer, device ID 4

vmstat -i lists psm0 as a registered interrupt source β†’ sc->intr != NULL at psm.c:2015 β†’ the full chain psm.c:2014 β†’ subr_bus.c:3302 β†’ atkbdc_isa.c:83 β†’ isa.c:76-124 (type-confused rl computed) β†’ subr_bus.c:2683 (early forward, rl untouched) β†’ nexus completed, the guest booted and stayed up. The confused pointer is used on every such boot and survives only by the subr_bus.c passthrough convention.

Reproduce

  1. ./introspect.sh (from the host) β€” or manually: append the five hints above to the guest's /boot/loader.conf.local, reboot, then devinfo | grep -A3 atkbdc0 and vmstat -i | grep psm0.
  2. Expected: tree shows isa0 β†’ atkbdc0 β†’ psm0, psm0 registered as an interrupt, guest healthy (no panic) β€” the type-confused passthrough call executed and was harmless.
  3. Remove the hints and reboot to restore (guest was left in the restored state; guest_dirty: 0).

Why Info and not higher

No dereference of the confused rl exists on any reachable path today: the only deref site (resource_list_find, isa.c:92) is inside if (!passthrough && !isdefault), and the two subr_bus.c helpers early-return on passthrough before touching rl. All inputs to this file come from in-tree drivers (kernel-internal bus methods at probe/attach/kldload); no user, hint, or loader value reaches the non-default arithmetic here (hints enter through isa_set_resource, isa_common.c:893-902, bounds-checked, and are read back via resource_list_alloc, never via this file's checks). The finding documents a one-edit-away landmine: hoisting the resource_list_find call (as pre-fix FreeBSD once did) or adding any rl use outside the passthrough guard turns every PS/2-mouse boot into a NULL/wild dereference. FreeBSD restructured exactly this code to go through BUS_GET_RESOURCE_LIST() (and gave isa a bus_get_resource_list method).

Fix

fix.diff β€” compute rl only for direct children (!passthrough), where isa_add_child() (isa_common.c:527-533) guarantees struct isa_device ivars. Fix not applied to sys/ (read-only audit); not kernel-rebuilt for this Info-severity hardening (no misbehavior exists to diff β€” baseline observation itself is the proof).

VERDICT.md
↓ download raw

DF-5641 VERDICT β€” reproduced (code-path demonstration), impact none

File: sys/bus/isa/x86_64/isa.c (pass 2, GLM 5.3) Finding class: latent type confusion / fragile-coupling hardening (Info) Status: reproduced (the observable claim β€” a passthrough call with foreign ivars executes at boot and survives only via the subr_bus.c passthrough early-forward) Β· impact: none (no dereference on any reachable path) Β· confidence: certain

Root cause (path:line)

  • isa_alloc_resource() computes passthrough = (device_get_parent(child) != bus) at sys/bus/isa/x86_64/isa.c:85 β€” then ignores it when casting: idev = DEVTOISA(child); rl = &idev->id_resources; at isa.c:87-88.
  • isa_release_resource() repeats the pattern at isa.c:130-131.
  • DEVTOISA(dev) is ((struct isa_device *)device_get_ivars(dev)) (sys/bus/isa/isa_common.h:64). For a grandchild caller this is the sub-bus's ivar type or NULL β€” never a struct isa_device.
  • The confused rl is passed to resource_list_alloc() (isa.c:122) / resource_list_release() (isa.c:133). Both compute the same passthrough flag and return early without touching rl when it is set: sys/kern/subr_bus.c:2680-2687 and :2726-2732. This is the only guard.
  • Direct isa children always have proper ivars: isa_add_child() allocates and installs struct isa_device (sys/bus/isa/isa_common.c:527-533), and the isa bus has no other child-creation path (isa_bus_device is used only for isa_probe_children, sys/platform/pc64/x86_64/autoconf.c:161).

Why the confused path is real and exercised

Chain proven by source and executed live on the audit guest:

  1. DRIVER_MODULE(atkbdc, isa, …) β€” sys/dev/misc/atkbdc_layer/atkbdc_isa.c:401.
  2. atkbdc's bus method table registers bus_alloc_resource = bus_generic_alloc_resource (atkbdc_isa.c:83) β€” pure upward forwarder (sys/kern/subr_bus.c:3295-3301).
  3. atkbdc children get atkbdc_device_t ivars (atkbdc_isa.c:218-232) β€” not struct isa_device.
  4. psmattach() unconditionally allocates its IRQ via bus_alloc_legacy_irq_resource(dev, …) β€” sys/dev/misc/psm/psm.c:2014, which calls BUS_ALLOC_RESOURCE(dev->parent, dev, …) (sys/kern/subr_bus.c:3302-3308).
  5. β‡’ isa_alloc_resource(bus=isa0, child=psm0) with passthrough == 1 and DEVTOISA(psm0) casting atkbdc_device_t* to struct isa_device*. id_resources is the first struct member (isa_common.h:53-54), so rl aliases the atkbdc ivar object itself, reinterpreted as an SLIST head.

Same shape exists for other isa sub-buses (ata channels under ata-isa: sys/dev/disk/nata/ata-isa.c has no bus_alloc_resource method; CardBus cards under pccbb_isa: sys/dev/pccard/pccbb/pccbb_isa.c:185-186 forwards up).

Live verification (audit guest, QEMU/KVM)

  • Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026, X86_64_GENERIC (env.txt).
  • Baseline boot: ACPI claims atkbdc0 (acpi0), so the default config does not exercise the isa variant β€” noted honestly.
  • Test boot: loader hints hint.atkbdc.0.at="isa", port 0x60, irq 1, hint.psm.0.at="atkbdc0", irq 12 (see trigger_hints.txt / introspect.sh). isahint (sys/bus/isa/isahint.c:121-129) creates atkbdc0 on the isa bus; atkbdc_attach (atkbdc_isa.c:290-295) creates psm0 with atkbdc_device_t ivars; psmattach runs the chain above.
  • Observed (dmesg.hinted.txt): atkbdc0: <Keyboard controller (i8042)> at port 0x60,0x64 irq 1 on isa0 psm0: <PS/2 Mouse> irq 12 on atkbdc0 interrupt uses mplock: psm0 / psm0: model IntelliMouse Explorer, device ID 4
  • vmstat -i lists psm0/atkbd0 as registered interrupts (env.txt) β‡’ the allocation succeeded β‡’ resource_list_alloc took the passthrough branch (subr_bus.c:2683-2687) and never dereferenced the confused rl. Guest stayed up; no panic.
  • Tree captured in devinfo.hinted.txt: nexus0 β†’ legacy0 β†’ pcib0 β†’ pci0 β†’ isab0 β†’ isa0 β†’ atkbdc0 β†’ {atkbd0, psm0}.
  • Restored afterwards (hints removed, acpi0 back, atkbdc0 on acpi0) β€” guest_dirty = 0.

Why impact is "none" (candidates killed, with citations)

  • Deref of confused rl: impossible today β€” sole deref site resource_list_find(rl,…) sits inside if (!passthrough && !isdefault) (isa.c:91-92); both subr_bus.c consumers early-forward on passthrough (subr_bus.c:2683-2687, 2729-2732). Proven live above.
  • machintr_legacy_intr_cpuid(start) (isa.c:100): ICU implementation ignores its argument (sys/platform/pc64/icu/icu_abi.c:399-402); IO-APIC implementation bounds-checks with KKASSERT(irq >= 0 && irq < legacy_irq_max) before indexing (sys/platform/pc64/apic/ioapic_abi.c:1122-1125); the argument is truncated u_longβ†’int and is always an in-tree driver constant on this path (hints reach IRQ cpuid via isahint.c:73-75 / isa_set_resource instead, never through isa.c:100).
  • rid bounds: non-default path fully bounds-checked per type (isa.c:94-115, ISA_NIRQ/ISA_NDRQ/ISA_NMEM/NPORT from isavar.h:63-66); the default (0,~0) path never indexes anything β€” it only reads rles that were created through the checked isa_set_resource (isa_common.c:893-902) or this file's checked resource_list_add.
  • isa_setup_intr trailing NULL (isa.c:148): that parameter is desc (nexus.c:498-501), not cpuid; nexus derives the CPU from rman_get_cpuid(irq) and uses device_get_nameunit(child) when desc is NULL (sys/platform/pc64/x86_64/nexus.c:522-536). Intentional.
  • User/hint/loader reachability: none β€” all four functions are bus methods invoked only by in-tree drivers at probe/attach (boot) or isa_driver_added (kldload, root-only, serialized); no copyin/ioctl path exists, and no file-hint value flows into this file's arithmetic.
  • Refcount/lifetime/locks: the file holds no refs and takes no locks; its callers run in serialized boot/kld contexts.

Exploit chain

None β€” no memory misbehavior exists to exploit; the finding is a one-edit-away hazard, not a bug with a primitive.

Fix

fix.diff (authored post-verification, never applied to the read-only sys/ tree): compute rl from child ivars only when !passthrough; pass NULL into the resource_list helpers for passthrough (they ignore it there). Not kernel-rebuilt: Info severity, no runtime misbehavior to diff β€” the baseline run itself demonstrates the code path, and the change only narrows when the cast happens (no behavioral delta for any in-tree caller).

Verdict bottom line

The unconditional DEVTOISA(child) in isa_alloc_resource/isa_release_resource produces a type-confused (sometimes NULL) resource-list pointer for every grandchild allocation, including every ISA-attached PS/2 mouse boot; it is harmless today solely because resource_list_alloc/resource_list_release forward on passthrough before touching rl. Guard the cast (fix.diff) or restructure the way FreeBSD did (BUS_GET_RESOURCE_LIST + an isa bus_get_resource_list method). File as hardening (Info); the rest of sys/bus/isa/x86_64/isa.c is clean under the pass-2 checklist.

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.diff authored post-verification and git-apply --check-verified against pristine sys/, but not kernel-rebuilt: Info-severity hardening with no runtime misbehavior to diff (the baseline run itself demonstrates the path executes harmlessly); the change only narrows when the ivars cast happens and alters no behavior for any in-tree caller.

findings/poc/DF-5641/fix.diff (git apply --check clean); VERDICT.md 'Fix' section
↓ fix.diffper-fix-DF-5641

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log [2]-[5] and VERDICT.md: devinfo.hinted.txt shows nexus0->legacy0->pcib0->pci0->isab0->isa0->atkbdc0->psm0 (psm0 = isa grandchild with atkbdc_isa.c:231-232 atkbdc_device_t ivars); dmesg.hinted.txt 'psm0: <PS/2 Mouse> irq 12 on atkbdc0'; env.txt vmstat -i lists psm0 as a registered interrupt (chain through isa.c:76-124 completed); guest stayed up β€” proving the type-confused rl at isa.c:87 was computed and never dereferenced (subr_bus.c:2683 early-forward). Guest restored afterwards (acpi0 back).

PoC changes

not a seeded-code PoC: the trigger is a loader-hint configuration (trigger_hints.txt) driving an in-tree code path; no source had to be corrected. Two restore attempts stumbled on grep -v exit semantics (all file lines were hint lines -> empty output -> exit 1 -> '&& mv' skipped); final restore used truncate.

Verified recommended fix

Compute the resource-list pointer only for direct ISA children (rl = &DEVTOISA(child)->id_resources when !passthrough, NULL otherwise) in isa_alloc_resource/isa_release_resource, so grandchild ivars are never cast to struct isa_device.

Verdict

isa_alloc_resource()/isa_release_resource() (sys/bus/isa/x86_64/isa.c:87-88,130-131) cast the child's ivars to struct isa_device via DEVTOISA() unconditionally β€” including passthrough (grandchild) requests whose ivars belong to a sub-bus (e.g. atkbdc_device_t for psm0 on an ISA-attached atkbdc0) or are NULL β€” producing a type-confused/NULL resource-list pointer that is passed into resource_list_alloc()/resource_list_release(). Demonstrated live on the audit guest: with atkbdc0 hinted onto the ISA bus, psm0's psmattach() (psm.c:2014) allocation walked psm->atkbdc(bus_generic_alloc_resource)->isa_alloc_resource with passthrough=1 and a type-confused rl, and succeeded only because both helpers early-forward on passthrough without touching rl (subr_bus.c:2683-2687, 2726-2732). No dereference of the confused pointer exists on any reachable path today, so impact is none; the hazard is one refactor away (hoisting resource_list_find out of the !passthrough guard, as pre-fix FreeBSD once had, turns every ISA PS/2-mouse boot into a NULL/wild deref). Fix: compute rl only for direct children (fix.diff), or restructure FreeBSD-style via BUS_GET_RESOURCE_LIST plus an isa bus_get_resource_list method (DF's generic default returns NULL β€” subr_bus.c:3000-3004 β€” so the isa method is required).