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

AcpiOsAcquireLock implements one ACPICA lock as two disjoint primitives (idle thread β†’ spinlock, everyone else β†’ lockmgr+crit): no mutual exclusion between the two holder classes across CPUs

Summary

The acquire/release pick the primitive by caller identity - the idle thread takes only the spinlock, everyone else only lockmgr+crit, so an idle holder on CPU A and a normal holder on CPU B can be inside the SAME ACPICA critical section simultaneously. Live via acpi_cst_idle's AcpiWriteBitRegister on the C3 BM/ARB paths (idle side) racing CST re-evaluation/RLD writes/S3 resume status clears (normal side) - interleaved RMWs lose updates (missed BM_STS clears, C3 with arbitration enabled, mangled resume clears). Platform-state corruption/DoS only (no kernel memory written by either side). QEMU guest lacks C2/C3 so preconditions absent (probe in pack). Serialize-both-classes-on-the-slock fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4890 Β· 9 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 7.0 KB ↓ raw
precond_probe.sh β€” 1.3 KB view raw
race_trigger.sh β€” 1.0 KB view raw
run.log β€” 1.0 KB view raw
env.txt β€” 1.0 KB view raw
fix.diff β€” 1.8 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.3 KB view raw

DF-4890 β€” AcpiOsAcquireLock: idle-thread and normal-thread callers use

disjoint locking primitives (spinlock vs lockmgr) β€” no mutual exclusion

between them

Files audited in this batch

What the bug is (one paragraph)

AcpiOsAcquireLock() (sys/dev/acpica/Osd/OsdSynch.c:374-403) picks the underlying primitive based on WHO the caller is:

  • idle thread (gd_curthread == &gd_idlethread): spin_lock(&Spin->slock) (OsdSynch.c:383-384)
  • any normal thread: lockmgr(&Spin->lock, LK_EXCLUSIVE) + crit_enter() (OsdSynch.c:385-387) β€” slock is never touched

The two primitives have nothing to do with each other: a holder on the idle-thread path and a holder on the normal-thread path can be inside the "same" ACPICA critical section simultaneously on different CPUs. ACPICA's contract for AcpiOsAcquireLock is a single exclusive lock per handle (ACPI OSPM/OVL layer). The result is a data race on whatever the lock protects.

What the lock protects

In this ACPICA version the OSL spinlocks created by AcpiUtMutexInitialize (sys/contrib/dev/acpica/source/components/utilities/utmutex.c:206-218) are AcpiGbl_GpeLock, AcpiGbl_HardwareLock, AcpiGbl_ReferenceCountLock. Only AcpiGbl_HardwareLock is reachable from the idle thread:

cpu_idle_hook = acpi_cst_idle                 (acpi_cpu_cstate.c:786)
acpi_cst_idle: C3 with BM_STS flag ->
    AcpiReadBitRegister(BUS_MASTER_STATUS)    (acpi_cpu_cstate.c:933)
    AcpiWriteBitRegister(BUS_MASTER_STATUS,1) (acpi_cpu_cstate.c:935)
C3 preamble ARB ->
    AcpiWriteBitRegister(ARB_DISABLE, 1|0)    (acpi_cpu_cstate.c:958,977)
    -> AcpiWriteBitRegister takes AcpiGbl_HardwareLock around a
       read-modify-write of PM1_STS / PM2_CNT
       (hwxface.c:411 `AcpiOsAcquireLock(AcpiGbl_HardwareLock)` .. :471)

Normal-thread takers of the same lock: AcpiWriteBitRegister() from non-idle contexts β€” e.g. acpi_cpu_cstate.c:668 and :810 (BUS_MASTER_RLD writes in CST probe/attach/notify paths), and ACPICA's AcpiHwClearAcuiStatus() (hwregs.c:574) on sleep-entry/exit (AcpiEnableSubsystem, resume path).

Both sides do masked read-modify-write of PM1/PM2 registers under the "lock"; interleaving loses updates (lost BM_STS clear, lost ARB_DISABLE set/clear, lost BM_RLD write, mangled PM1 status clears during resume).

How to reproduce (real hardware)

Preconditions: multi-CPU system, ACPI _CST advertising C3 (or P_BLK C3) with BM/ARB preamble, and a concurrent normal-thread bit-register write (CST re-evaluation via hw.acpi.cpu.cx_lowest/notify, or S3 resume's AcpiHwClearAcuiStatus).

  1. sysctl hw.ncpu >= 2, sysctl hw.acpi.cpu0.cx_supported shows C2/C3.
  2. sysctl hw.acpi.cpu.cx_lowest=C3 so idle threads take the C3 path.
  3. Generate normal-thread AcpiWriteBitRegister traffic while CPUs idle (race_trigger.sh hammers CST notify/RLD paths).
  4. Observe with an instrumented kernel (kprintf in both branches keyed on Spin == AcpiGbl_HardwareLock) β€” on stock kernels the lost update is silent; the visible symptoms are missed wake events / wrong sleep transitions / C3 entered with arbitration disabled.

On this QEMU guest

precond_probe.sh output is in env.txt: 6 CPUs, cpu_cst0..5 attached, but cx_supported: C1/0 only β€” QEMU's DSDT/FADT exposes no C2/C3, so the idle-thread side of the race (C3 BM/ARB register writes) never executes in this guest. The race therefore cannot be hosted here; see VERDICT.md.

Fix

fix.diff: make the normal-thread path take the same spinlock for the critical section (lockmgr stays as the sleepable contention lock; the HardwareLock sections are short, non-blocking register RMWs β€” verified at hwxface.c:411-471 and hwregs.c:574-581).

VERDICT.md
↓ download raw

DF-4890 β€” VERDICT

Finding: AcpiOsAcquireLock/AcpiOsReleaseLock (sys/dev/acpica/Osd/OsdSynch.c:374-429) implement one ACPICA lock with two disjoint primitives chosen by caller class β€” spin_lock(&Spin->slock) for the idle thread (OsdSynch.c:383-384, :423-424) and lockmgr(&Spin->lock, LK_EXCLUSIVE)+crit_enter() for every other thread (OsdSynch.c:385-387, :425-428). The primitives are unrelated: an idle-thread holder on CPU A and a normal-thread holder on CPU B are both "inside" the lock at the same time. ACPICA requires AcpiOsAcquireLock to be a single exclusive lock (see the file's own comment, OsdSynch.c:367-373: "OS-dependent locking primitives... called from an interrupt-handler or cpu_idle thread").

Status: not_reproduced (on this guest) / statically certain (code-provable).

Static proof chain (every hop verified in-tree)

  1. Idle-thread entry into ACPICA under the OSL lock: - cpu_idle_hook = acpi_cst_idle β€” sys/dev/acpica/acpi_cpu_cstate.c:786 (runs in gd_idlethread context by definition of the idle hook). - C3-with-bus-master path: AcpiReadBitRegister(ACPI_BITREG_BUS_MASTER_STATUS) (acpi_cpu_cstate.c:933) and AcpiWriteBitRegister(...,1) (:935); ARB preamble: AcpiWriteBitRegister(ACPI_BITREG_ARB_DISABLE,1|0) (:958, :977). - AcpiWriteBitRegister wraps a read-modify-write of PM1_STS/PM2_CNT in AcpiOsAcquireLock(AcpiGbl_HardwareLock) .. AcpiOsReleaseLock β€” sys/contrib/dev/acpica/source/components/hardware/hwxface.c:411-471. - In the idle thread that acquire takes the spin_lock(&Spin->slock) branch only (OsdSynch.c:383-384).
  2. Normal-thread takers of the same lock instance: - AcpiWriteBitRegister(BUS_MASTER_RLD) in CST probe/attach/notify paths β€” acpi_cpu_cstate.c:668, :810, :1082-1086 (normal kernel threads). - AcpiHwClearAcuiStatus() β€” sys/contrib/dev/acpica/source/components/hardware/hwregs.c:574-581, taken on ACPI subsystem enable and sleep-state transitions (S3 enter/exit). - Those acquires take the lockmgr(&Spin->lock) branch only (OsdSynch.c:385-387) β€” slock is never taken by normal threads, and Spin->lock is never taken by the idle thread. No exclusion between the two classes.
  3. Both sides perform masked RMW of the same PM1/PM2 registers inside their "locked" region β†’ interleaving loses updates (lost BM_STS clear, lost ARB_DISABLE set/clear, lost RLD write, mangled PM1_STS clears during resume).

Net effect: the ACPICA hardware-lock invariant is broken across CPUs; real-world symptoms are ACPI power-management malfunction (missed wake events, wrong sleep transitions, C3 entered with bus-master arbitration disabled). No kernel-memory corruption: the lock protects register I/O serialization only (AcpiGbl_GpeLock/AcpiGbl_ReferenceCountLock are never touched from the idle thread β€” verified by rg over sys/dev/acpica: gd_idlethread appears only in OsdSynch.c).

Why the mutex/semaphore side is NOT broken (audit answers for the record)

  • ACPICA mutexes on DFly are 1-unit counting semaphores: #define AcpiOsAcquireMutex(H,T) AcpiOsWaitSemaphore(H,1,T) β€” sys/contrib/dev/acpica/source/include/actypes.h:382-385.
  • Recursive AML-mutex acquire is handled inside ACPICA before the OSL is called (AcquisitionDepth, exmutex.c:302-308/461-469), so the non-recursive semaphore mapping cannot self-deadlock via AML.
  • ACPICA's own global-mutex ordering/deadlock check is compiled only under ACPI_MUTEX_DEBUG (utmutex.c:381-415); a coding slip there would self-block β€” but bounded: ACPI_WAIT_FOREVER is capped at ~66 s by OsdSynch.c:158-161, and PCATCH signals break the ssleep (OsdSynch.c:215, EINTR falls to the absolute-deadline check :236-245).
  • No ACPICA site nests the same OSL spinlock (HardwareLock sections are leaf), so the non-recursive lockmgr LK_EXCLUSIVE cannot self-panic.

Guest run (attempt)

Command (see run.log): dfbsd-qemu/vm.sh run_root 'uname -a; sysctl hw.ncpu; sysctl -a | grep cx_; dmesg | grep cst' β†’ env.txt.

Result: hw.ncpu: 6, cpu_cst0..5 attached, but every CPU reports cx_supported: C1/0 and hw.acpi.cpu.cx_lowest: C1. QEMU's ACPI tables expose no C2/C3, so acpi_cst_idle always selects the C1 HLT path (acpi_cpu_cstate.c:943-947) which performs no ACPICA bit-register access β€” the idle-thread side of the race never executes in this guest. sysctl -w hw.acpi.cpu.cx_lowest=C3 cannot create states the platform does not advertise. Hence: not_reproduced on this guest, preconditions absent (target hardware feature β€” deep ACPI C-states β€” not implemented by QEMU's DSDT). The defect itself is code-provable; on real SMP laptops/servers with CST C3 (the default configuration of the hardware class) both sides are live.

How to demonstrate on real hardware (methodology, for whoever has it)

Instrumented kernel (in-guest /usr/src copy, NOT the audit tree): add counters in both branches of AcpiOsAcquireLock gated on Spin == AcpiGbl_HardwareLock, and a kprintf when both counters are non-zero with overlapping TSC intervals; boot with cx_lowest=C3, then run race_trigger.sh (CST notify + RLD churn) for minutes. Expected: overlap counter > 0. Stock-kernel observable: dmesg ACPI errors / missed wakeups / S3 anomalies under the same load, non-deterministic.

Exploitability assessment

Impact ceiling is availability + platform-state integrity (PM1/PM2 register lost updates): no user-controllable data crosses the race, no kernel memory is corrupted, no privilege boundary is crossed. An unprivileged user can only amplify the coincidence rate (inducing idle+resume/notify overlap); there is no weaponizable primitive beyond DoS/malfunction. Severity Low per the project rubric ("narrow trigger, low impact, specific hardware class"), confidence certain for the lock-exclusion defect itself.

Fix validation

fix.diff authored against the read-only sys/ tree (normal threads also take slock around the section; lockmgr retained as the sleepable contention lock; idle threads keep pure-spin). Not built/validated: the baseline race is not hostable on the QEMU guest (no C2/C3), so there is no baseline to compare a patched kernel against. Fix reasoning is line-accurate: HardwareLock holders never block inside the section (hwxface.c:411-471, hwregs.c:574-581 β€” pure register I/O), so holding slock across the section is safe; the idle thread never takes GpeLock/ReferenceCountLock, so no idle-thread spin-against-sleeper hazard is introduced.

aspect value
status not_reproduced (guest lacks C2/C3 ACPI states; static proof certain)
reproduced 0
impact none observed (potential dos on affected hardware)
confidence certain (defect), likely (real-world manifestation)

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against read-only sys/; not built/validated because the baseline race cannot be hosted on the QEMU guest (no ACPI C2/C3 states advertised β€” see env.txt). The patch is mechanically minimal and its safety argument (OSL-spinlock sections are short non-blocking register RMWs, so the idle thread never spins against a sleeping holder) is documented in VERDICT.md and the diff comment.

findings/poc/DF-4890/fix.diff; VERDICT.md 'Fix validation' section; env.txt (guest C1-only)
↓ fix.diffper-fix-DF-4890

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full static proof with path:line and guest-run explanation); env.txt + run.log (guest probe: hw.ncpu=6, cpu_cst0-5 attached, cx_supported C1/0 only -> C2/C3 precondition ABSENT on this guest); precond_probe.sh (runnable probe); race_trigger.sh (real-hardware amplifier); fix.diff (single-primitive fix)

PoC changes

n/a (finding authored in verify-of-self audit; probe written fresh β€” no seed PoC existed)

Verified recommended fix

Take spin_lock(&Spin->slock) in BOTH branches of AcpiOsAcquireLock/AcpiOsReleaseLock around the critical section, keeping lockmgr only as the sleepable contention lock for normal threads

Verdict

Defect is statically certain: AcpiOsAcquireLock (sys/dev/acpica/Osd/OsdSynch.c:383-388) takes spin_lock(&Spin->slock) when called from the idle thread but lockmgr(&Spin->lock)+crit_enter() for all other threads, and release (OsdSynch.c:423-428) mirrors the split β€” the two classes never exclude each other across CPUs. The idle-thread side is live via cpu_idle_hook=acpi_cst_idle (acpi_cpu_cstate.c:786) whose C3 BM/ARB paths call AcpiWriteBitRegister (acpi_cpu_cstate.c:935,958,977), which holds AcpiGbl_HardwareLock around a PM1/PM2 register RMW (hwxface.c:411-471); normal-thread takers of the same lock (acpi_cpu_cstate.c:668,810; AcpiHwClearAcuiStatus hwregs.c:574 on resume/sleep) run concurrently on other CPUs, interleaving masked RMWs of the same registers (lost BM_STS clears, lost ARB_DISABLE, mangled PM1_STS clears). Not reproduced at runtime: the QEMU guest has 6 CPUs and cpu_cst attached but advertises only C1/0 (env.txt/run.log), so the idle-side taker never executes there; reproduction requires real hardware with CST C2/C3 plus an instrumented kernel (methodology in VERDICT.md). Impact ceiling is availability/platform-state malfunction, not kernel memory corruption; severity Low.