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)
PoC verification
Evidence pack
findings/poc/DF-5452 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedfix.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)
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.
No comments yet.