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

nexus_alloc_resource() indexes irq_rman[cpuid] with only an INVARIANTS-gated KASSERT; plain bus_alloc_resource() reaches it with cpuid=-1 (panic on INVARIANTS kernels, silent OOB static access on production kernels)

Summary

rm = &irq_rman[cpuid] validated only by a debug assert; the public bus_alloc_resource defaults cpuid to -1 so an explicit-range IRQ allocation reaches nexus with -1: guest-reproduced 'panic: nexus invalid cpuid: -1' on the INVARIANTS build (legacy variant succeeds), while production kernels compute &irq_rman[-1] (80 bytes below the array) feeding rman token/TAILQ ops. In-tree callers survive only via resource_list_alloc's override. Real-bounds-check fix validated (kprintf + NULL return, guest up).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5452 Β· 10 files
FileTypeDescriptionSize
nxirq.c β€” 2.9 KB view raw
build.sh β€” 81 B view raw
run.sh β€” 161 B view raw
build.log β€” 1.1 KB view raw
panic.txt β€” 1.7 KB view raw
fix_run.log β€” 844 B view raw
fix.diff β€” 420 B view raw
env.txt β€” 687 B view raw
VERDICT.md β€” 3.4 KB ↓ raw
verdict.json β€” 3.5 KB view raw
VERDICT.md
↓ download raw

DF-5452 VERDICT β€” REPRODUCED (unguarded irq_rman[cpuid] index; debug-only assert)

Finding

nexus_alloc_resource() β€” sys/platform/pc64/x86_64/nexus.c:346-418 β€” selects rm = &irq_rman[cpuid] for SYS_RES_IRQ where the ONLY validation is KASSERT(cpuid >= 0 && cpuid < ncpus) (nexus.c:377-378). In DragonFly, KASSERT compiles to a no-op unless options INVARIANTS is set (sys/sys/systm.h:94-112). The public driver API bus_alloc_resource() (sys/kern/subr_bus.c:3293-3300) passes cpuid = -1 for every explicit-range allocation. In-tree callers survive because every intermediate bus that uses resource_list_alloc() overrides cpuid with rle->cpuid, and all in-tree IRQ resource setters pass machintr_legacy_intr_cpuid() (pci.c:2815-2816, acpi_resource.c:577-578,599-600, Osd/OsdInterrupt.c:95-96, isahint.c:74-75, isa_common.c:286). A nexus child (or a grandchild behind a passthrough-only bus) that calls bus_alloc_resource(dev, SYS_RES_IRQ, &rid, irq, irq, 1, ...) β€” the obvious API use β€” reaches nexus with cpuid = -1: - INVARIANTS kernels: panic: nexus invalid cpuid: -1; - production kernels without INVARIANTS: rm = &irq_rman[-1] β€” 80 bytes (sizeof(struct rman), measured on the guest) below the array β€” handed to rman_reserve_resource() which takes lwkt_gettoken(rm->rm_slock) and walks/inserts rm->rm_list: an out-of-bounds struct-rman interpretation of unrelated static storage (silent NULL when the preceding BSS is zeroed; wild token/TAILQ reads and writes when it is not).

Reproduction (baseline, kernel #0 Jul 2 2026, INVARIANTS present)

KLD nxirq.ko (nexus child), see panic.txt: - leg 1: bus_alloc_legacy_irq_resource(dev, &rid, 5, RF_SHAREABLE) (subr_bus.c:3302-3309, valid cpuid) β€” SUCCEEDED (res=0xfffff800895d0868); same vector, same rmans. - leg 2: bus_alloc_resource(dev, SYS_RES_IRQ, &rid, 5, 5, 1, RF_SHAREABLE) β€” immediate panic: nexus invalid cpuid: -1 in nexus_alloc_resource, called from bus_alloc_resource (full backtrace in panic.txt).

Impact

Denial of service (clean panic) via any buggy driver on INVARIANTS kernels; uncontrolled out-of-bounds kernel static access on production kernels built without INVARIANTS (a supported configuration; X86_64_GENERIC ships with INVARIANTS, custom/performance kernels routinely omit it). No in-tree trigger today β€” this is a driver-API-boundary hardening defect. Severity Low.

Fix validation (kernel #1 Oct 5 17:59:42)

fix.diff replaces the KASSERT with a real bounds check that returns NULL with a diagnostic. Patched result (fix_run.log): leg 1 still succeeds; leg 2 prints nexus_alloc_resource: invalid cpuid -1 for SYS_RES_IRQ allocation and returns res=0 β€” no panic; module attaches; guest healthy; normal boot with all nexus IRQ allocations unchanged.

Kernel refs

sys/platform/pc64/x86_64/nexus.c:346-398 (index at 377-379) sys/sys/systm.h:94-112 (KASSERT/KKASSERT INVARIANTS-gated) sys/kern/subr_bus.c:3293-3300 (cpuid = -1 default), 3302-3309 sys/kern/subr_rman.c:221-231 (token + TAILQ walk on the bogus rman)

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

fix.combined.diff applied in guest /usr/src, nativekernel rebuilt and booted: leg 2 now returns res=0 with 'nexus_alloc_resource: invalid cpuid -1' diagnostic, no panic, module attaches, full boot with all nexus IRQ allocations unaffected

findings/poc/DF-5452/fix_run.log (patched-kernel dmesg + 'ALIVE' uptime)
↓ fix.diffper-fix-DF-5452

Confirmed kernel references

Detail

Exploit chain

Driver-API-boundary defect: a nexus child (or grandchild behind a passthrough-only bus) using the obvious explicit-range IRQ allocation gets a panic on INVARIANTS kernels and uncontrolled OOB kernel-static access (wild token/TAILQ writes when adjacent statics are non-zero) on non-INVARIANTS production kernels. All in-tree callers currently route cpuid through validated rle->cpuid, so the trigger is a buggy or out-of-tree/KLD driver; no unprivileged-userland path found.

Evidence (decisive lines)

panic.txt: 'panic: nexus invalid cpuid: -1' backtrace nexus_alloc_resource <- bus_alloc_resource <- nxirq_attach, immediately after leg-1 success 'res=0xfffff800895d0868'; fix_run.log: diagnostic + res=0, guest ALIVE on patched kernel

PoC changes

first attempt used vector 60 and tripped ioapic_abi_legacy_intr_cpuid's own range assert (lesson: legacy helpers cap at 15); switched leg 1 to legacy IRQ 5 which allocates cleanly, keeping attribution on nexus

Verified recommended fix

Replace the KASSERT at nexus.c:377 with a real bounds check (cpuid < 0 || cpuid >= ncpus -> kprintf + return NULL)

Verdict

REPRODUCED on the live guest: the plain documented driver API bus_alloc_resource(dev, SYS_RES_IRQ, rid, 5, 5, 1, flags) reaches nexus_alloc_resource with cpuid=-1 (subr_bus.c:3299 default) and the only guard on rm = &irq_rman[cpuid] is an INVARIANTS-gated KASSERT (nexus.c:377-378). On the guest's INVARIANTS kernel it panicked exactly at nexus_alloc_resource ('panic: nexus invalid cpuid: -1') while the same vector allocated fine through bus_alloc_legacy_irq_resource with a valid cpuid. On kernels built without options INVARIANTS the same call computes &irq_rman[-1] (sizeof(struct rman)=80 bytes below the array, measured) and feeds that OOB static storage to rman_reserve_resource()'s lwkt token + TAILQ operations. Fix (real bounds check returning NULL) validated on a rebuilt kernel: diagnostic + graceful NULL, no panic, normal boot unchanged.