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/acpiioctls). - 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/AcpiOsWriteMemoryare invoked byAcpiHwRead/AcpiHwWrite(sys/contrib/dev/acpica/source/components/hardware/hwregs.c:417,517) which do not acquireACPI_MTX_INTERPRETER, andAcpiHwReadis 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 inAcpiOsMapMemory/AcpiOsUnmapMemoryat 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->nextand re-reading*ptrackin 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_ACPICAtrack nodes into a controlled next-pointer β would yield kernel R/W.
Recommended fix
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
sys/contrib/dev/acpica/source/components/hardware/hwregs.c:417,517β callers that do not holdACPI_MTX_INTERPRETER.sys/platform/pc64/x86_64/pmap.c:6203βpmap_mapdevpanics on failure (hence the dead NULL check atOsdMemory.c:92).
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2087 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 1.8 KB | view raw |
| VERDICT.md | verdict | source-trace confirmation | 602 B | β 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
fixedAdded acpi_map_lock; batch build rc=0.
Added acpi_map_lock; batch 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.
No comments yet.