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

nexus_activate_resource() is not idempotent: re-activation of an active MMIO resource double-maps and orphans kernel_map KVA + RW PTEs; no mapping rollback on activation failure

Summary

pmap_mapdev runs BEFORE the idempotent rman_activate_resource and overwrites r_virtual - every re-activation of an already-RF_ACTIVE resource returns success but allocates a fresh kernel_map range + RW PTEs for the same physical window, permanently orphaning the previous mapping (and a failed rman activation strands the fresh one). Guest-verified: 16 alloc/activate/release cycles stranded exactly 16 pages (kernel_map->size +65536, first mapping still dereferenceable after full release); fix shows vaddr1==vaddr2, 0 leaked. Driver-bug amplifier (reachable re-activation wrappers in pci/acpi/exca/pccbb). Guard + rollback fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5450 Β· 12 files
FileTypeDescriptionSize
nxmap.c β€” 4.2 KB view raw
build.sh β€” 81 B view raw
run.sh β€” 155 B view raw
build.log β€” 1.1 KB view raw
run.log β€” 1.5 KB view raw
run.2.log β€” 758 B view raw
fix_run.log β€” 928 B view raw
fix.diff β€” 1.1 KB view raw
fix.combined.diff β€” 1.7 KB view raw
env.txt β€” 687 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
verdict.json β€” 3.3 KB view raw
VERDICT.md
↓ download raw

DF-5450 VERDICT β€” REPRODUCED (KVA + stale-RW-PTE leak)

Finding

nexus_activate_resource() β€” sys/platform/pc64/x86_64/nexus.c:420-451 β€” performs its pmap_mapdev() mapping step unconditionally, before calling the idempotent rman_activate_resource() (nexus.c:450), then overwrites the resource's only mapping handle with rman_set_virtual(r, vaddr) (nexus.c:446). Three consequences:

  1. Re-activation double-maps. bus_activate_resource() on an already RF_ACTIVE MMIO resource returns success (rman idempotence, sys/kern/subr_rman.c:419-424) but pmap_mapdev_attr() (sys/platform/pc64/x86_64/pmap.c:6192-6219) allocates a fresh kernel_map range + RW PTEs on every call. The previous mapping is orphaned: nothing can ever unmap it, because r_virtual was overwritten. nexus_setup_intr() documents the very contract being violated: "We depend here on rman_activate_resource() being idempotent" (nexus.c:513-515).
  2. No rollback on activation failure. If rman_activate_resource() fails (e.g. RF_TIMESHARE share conflict, subr_rman.c:426-438) after the mapping was created, nexus_activate_resource() returns the error leaving the mapping in place; every caller releases the resource without unmapping (nexus.c:410-414 checks only the return code; nexus_release_resource() at nexus.c:475-479 skips deactivate entirely for a resource whose RF_ACTIVE was never set).
  3. Reachable re-activation wrappers (no RF_ACTIVE pre-check anywhere in the chain): - sys/bus/pci/pci.c:4073-4085 β€” re-allocation of an already allocated rid with RF_ACTIVE re-activates unconditionally; - sys/dev/acpica/acpi.c:1163-1169; - sys/dev/pccard/exca/exca.c:862-868; pccbb.c:1188-1194,1339-1348; - any driver calling the public bus_activate_resource() API on an active resource.

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

KLD nxmap.ko (nexus child driver) β€” see run.log / run.2.log: - bus_alloc_resource_any(..., RF_ACTIVE) then a second bus_activate_resource() on the same resource: err=0, and vaddr2 != vaddr1, both mapping the SAME physical page (same_phys=YES). - 16 x alloc/activate/release cycles: kernel_map->size grows by exactly 16 pages (65536 bytes) and never returns; the final vaddr2 sits exactly 0x10000 above the first vaddr1. - After bus_release_resource() of every resource, the first mapping (0xffffffff82601000) is STILL dereferenceable β€” a stale RW PTE mapping physical MMIO 0xE0000000 persists in kernel_pmap forever.

Impact

Permanent kernel_map KVA exhaustion vector (one roundup(size,PAGE) range per re-activation; MMIO BAR-sized on the pci.c re-alloc path) plus permanent stale RW kernel PTEs mapping device memory after the owning driver has torn down β€” kernel-pmap hygiene degradation compounding with later kmem_alloc aliasing. Same impact family as DF-1074 (deactivate-side truncation), but the activate-side root cause and trigger (any re-activation, no >4GiB BAR required). No data disclosure to userspace; not a privilege escalation. Preconditions: a driver (in-tree wrapper paths exist, none currently double-activate in-tree) or driver-author bug re-activating MMIO. Severity Low.

Fix validation (kernel #1 Oct 5 17:59:42, fix.combined.diff)

fix.diff adds: (a) early return when rman_get_flags(r) & RF_ACTIVE (idempotence for the mapping step), (b) rollback pmap_unmapdev() when rman_activate_resource() fails after a fresh mapping. Patched result (fix_run.log): vaddr1 == vaddr2 on every cycle, kernel_map size unchanged (leaked pages: 0), no orphan mapping after release. Full normal boot and device attach through nexus unchanged.

Kernel refs

sys/platform/pc64/x86_64/nexus.c:427-450, 446, 450, 513-515, 410-414, 475-479 sys/platform/pc64/x86_64/pmap.c:6192-6219 sys/kern/subr_rman.c:419-424, 426-438 sys/bus/pci/pci.c:4073-4085 sys/dev/acpica/acpi.c:1163-1169 sys/dev/pccard/exca/exca.c:862-868

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 (kernel #1): second activation is now a no-op for the mapping (vaddr1==vaddr2), kernel_map->size unchanged over 16 cycles (leaked pages: 0), no orphan mapping after release

findings/poc/DF-5450/fix_run.log (patched-kernel dmesg: leaked pages: 0, stale: N/A (v1 == v2, single mapping))
↓ fix.diffper-fix-DF-5450

Confirmed kernel references

Detail

Exploit chain

No userland chain: trigger is driver-side re-activation (pci.c:4073-4085 re-activates an allocated rid with RF_ACTIVE; acpi/exca/pccbb same pattern; any driver calling bus_activate_resource() on an active resource). Each re-activation permanently leaks roundup(size,PAGE) of kernel_map KVA and a stale RW MMIO PTE set -> long-run KVA exhaustion / kernel-pmap hygiene degradation. Not a data disclosure or privesc.

Evidence (decisive lines)

run.log lines 'err2ndact=0 vaddr1=...2601000 vaddr2=...2611000 same_phys=YES' + 'leaked pages: 16' + 'stale RW PTE still present: YES'; run.2.log single-cycle +1 page; fix_run.log (patched): vaddr1==vaddr2, leaked pages: 0

PoC changes

Added 16-cycle accumulation loop, kernel_map->size accounting, guarded orphan-read (safe on patched kernels); final module builds clean with -Werror

Verified recommended fix

Guard nexus_activate_resource(): if (rman_get_flags(r) & RF_ACTIVE) return rman_activate_resource(r); and pmap_unmapdev() the fresh mapping when rman_activate_resource() fails

Verdict

REPRODUCED on the live guest: nexus_activate_resource() (nexus.c:420-451) maps MMIO with pmap_mapdev() before the idempotent rman_activate_resource() and overwrites r_virtual, so re-activating an active resource returns success but allocates a second kernel_map range + RW PTEs for the same physical window, orphaning the first permanently. 16 alloc/activate/release cycles stranded exactly 16 pages (kernel_map->size +65536, never returned) and left the first mapping dereferenceable after full release (stale RW PTE). Fix (idempotence guard + failure rollback) validated on a rebuilt kernel: single mapping, 0 leaked pages, no orphan PTE.