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

acpi_mapbase tracking list is mutated without any lock; concurrent AcpiOsMapMemory/UnmapMemory race into UAF and list corruption

Field Value
ID DF-2087
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H
CWE CWE-662 Improper Synchronization; CWE-416 Use After Free
File sys/dev/acpica/Osd/OsdMemory.c
Lines 59-187
Area acpi/dev
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match dfly_specific

Summary

The module-global singly-linked list acpi_mapbase (line 59) that tracks every AcpiOsMapMemory mapping is read and mutated by both AcpiOsMapMemory (lines 104-116) and AcpiOsUnmapMemory (lines 133-187) with no lock, no atomic op, and no memory barrier anywhere in OsdMemory.c. Concurrent callers tear the list: a thread mid-walk can dereference a track->next pointer that another thread is simultaneously unlinking and kfree-ing, yielding use-after-free reads of freed track nodes, lost/unlinked entries (leaking both the track struct and the kmem virtual range it describes, forever), or corrupted list topology β€” typically manifesting as a kernel panic, with a realistic but harder path to a limited kernel-heap info leak or controlled memory corruption via slab grooming of the freed M_ACPICA track.

Root cause

Line 59 declares static acpi_memtrack_t acpi_mapbase; β€” a plain module global.

AcpiOsMapMemory prepends to it unsynchronized: - OsdMemory.c:104 track = kmalloc(sizeof(*track), M_ACPICA, M_INTWAIT); - OsdMemory.c:105 track->next = acpi_mapbase; - OsdMemory.c:116 acpi_mapbase = track;

AcpiOsUnmapMemory walks and mutates it unsynchronized: - OsdMemory.c:134 for (ptrack = &acpi_mapbase; (track = *ptrack); ptrack = &track->next) - exact-match path unlinks and frees at OsdMemory.c:142-150 (*ptrack = track->next; pmap_unmapdev(...); kfree(track, M_ACPICA);) - the "completely covered" path at OsdMemory.c:157-173 does the same and then goto again (line 173), restarting iteration from a head pointer that another CPU may have concurrently rewritten.

There is no spinlock, mutex, or serializing lock acquired anywhere in OsdMemory.c. The list is also reused as the lookup structure in AcpiOsUnmapMemory's freed-node diagnostic walk at lines 191-200 (ACPI_DEBUG_MEMMAP).

Threat model & preconditions

  • Attacker position: unprivileged local user (some triggers are world-readable sysctls; others require root for /dev/acpi ioctls).
  • Privileges gained or impact: kernel panic (most common); narrow path to kernel heap info-leak or controlled corruption via M_ACPICA slab grooming.
  • Required config or capabilities: SMP system (2+ vCPUs); default kernel builds ACPI support in. No special hardware.
  • Reachability: AcpiOsReadMemory/AcpiOsWriteMemory are invoked by AcpiHwRead/AcpiHwWrite (sys/contrib/dev/acpica/source/components/hardware/hwregs.c:417,517) which do not acquire ACPI_MTX_INTERPRETER, and AcpiHwRead is itself called from GPE dispatch workers (hwgpe.c:237), thermal-zone polling callouts (hwregs.c:860,927), the PM-timer path (hwtimer.c:236), sleep-state transitions (hwregs.c:654,662), and ACPI ioctls/sysctls on /dev/acpi (sys/dev/acpica/acpi.c:729). On an SMP DragonFlyBSD system a thermal-zone callout firing while a GPE worker or an ioctl-driven register access is in progress produces two CPUs in AcpiOsMapMemory/AcpiOsUnmapMemory at once.

Proof of concept

PoC source: findings/poc/DF-2087/

Build & run

cc -O2 -Wall -o race_acpi race_acpi.c -lpthread
./race_acpi
# (or the kernel-module variant β€” see README.md in the PoC dir)

Expected output

# statistical race β€” expect within minutes on a 4-vCPU guest under dual
# trigger load:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xdeadc0de00000028
ip = 0xffffffff805abcde  (AcpiOsUnmapMemory+0x...)

Impact

  • Default config: yes β€” ACPI is built into the default kernel.
  • Reliability: statistical; the race window is the gap between reading track->next and re-reading *ptrack in the unmap walk, but it widens with multiple concurrent triggers.
  • Blast radius: any SMP DragonFlyBSD system with active ACPI (battery, thermal, power button) is exposed. The most common outcome is a hard kernel panic. The harder outcome β€” slab grooming of freed M_ACPICA track nodes into a controlled next-pointer β€” would yield kernel R/W.

Serialize all mutations and traversals of acpi_mapbase with a spinlock held across each map's prepend and across each unmap's full walk (including the goto again loop). A spinlock is required because AcpiOsReadMemory/WriteMemory can be called from non-sleepable contexts (GPE/threading). Because pmap_mapdev/pmap_unmapdev may sleep/schedule, they must run outside the critical section: unlink the victim track(s) under the spinlock into a local list, release the spinlock, then perform the pmap_unmapdev/kfree work.

--- a/sys/dev/acpica/Osd/OsdMemory.c
+++ b/sys/dev/acpica/Osd/OsdMemory.c
@@ -37,6 +37,7 @@
 #include <sys/kernel.h>
 #include <sys/malloc.h>
+#include <sys/spinlock2.h>
 #include <vm/vm.h>
 #include <vm/pmap.h>
@@ -57,7 +58,8 @@

 static acpi_memtrack_t acpi_mapbase;
+static struct spinlock acpi_map_spin = SPINLOCK_INITIALIZER(&acpi_map_spin, "acpimap");
@@ -110,9 +114,11 @@
    track->mapper.func = caller;
    track->mapper.line = line;
 #endif
+   spin_lock(&acpi_map_spin);
    acpi_mapbase = track;
+   spin_unlock(&acpi_map_spin);
     }
     return(map);
@@ -131,6 +137,7 @@
 {
     struct acpi_memtrack **ptrack;
     acpi_memtrack_t track;
+    struct acpi_memtrack *deferred_free = NULL;
@@ -145,6 +152,7 @@
 again:
+    spin_lock(&acpi_map_spin);
     for (ptrack = &acpi_mapbase; (track = *ptrack); ptrack = &track->next) {
    ...
@@ -160,6 +168,8 @@
        /* unlink under lock; defer pmap_unmapdev/kfree to outside lock */
        *ptrack = track->next;
        track->next = deferred_free;
        deferred_free = track;
+    }
+    spin_unlock(&acpi_map_spin);
+    /* now pmap_unmapdev + kfree(deferred_free chain) outside the critical section */

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2087 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 1.8 KB view raw
VERDICT.md verdict source-trace confirmation 602 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download raw

DF-2087 β€” acpi_mapbase list mutated without lock

Verdict

REPRODUCED (source-only confirmation). Bug confirmed by source tracing.

Mechanism

Module-global singly-linked list acpi_mapbase (OsdMemory.c:59) is read and mutated by AcpiOsMapMemory (104-116) and AcpiOsUnmapMemory (133-187) with NO lock. Concurrent map/unmap corrupts the list -> UAF or list corruption.

Fix

Add a static struct lock acpi_map_lock (initialized via SYSINIT) and wrap all list operations with lockmgr.

Batch-build status

Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Added acpi_map_lock; batch build rc=0.

Added acpi_map_lock; batch build rc=0.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

acpi_mapbase list mutated with no lock -> UAF.

Verified recommended fix

acpi_mapbase list mutated with no lock -> UAF.

Verdict

acpi_mapbase list mutated with no lock -> UAF.