base: add _TTS/_WAK AML hooks + opt-in DMAR init with hard cap

Phase E of the ACPI fork-sync plan. Two changes:

1. New methods on AcpiContext (Linux 7.1 best practices):

   - transition_to_s_state(state): evaluates _TTS(state) AML method.
     Mirrors Linux 7.1 acpi_sleep_tts_switch (drivers/acpi/sleep.c:36).
     Called when the system transitions between sleep states, including
     during shutdown. Failure is non-fatal: _TTS is optional per ACPI
     spec.

   - wake_from_s_state(state): evaluates _WAK(state) AML method.
     Mirrors Linux 7.1 acpi_sleep_finish_wake (drivers/acpi/sleep.c).
     Called by userspace on resume from a sleep state. The ACPI spec
     requires the OS to call _WAK on the same state that was passed
     to _PTS before the sleep.

   - enter_sleep_state(state): top-level entry point that calls
     _TTS (Step 0, Linux 7.1) then set_global_s_state (Steps 1-5,
     Phase D). This is the public API that future kernel S3/S4 paths
     should use.

2. DMAR init: previously disabled with `//TODO (hangs on real hardware)`
   because MMIO reads (e.g. gl_sts.read()) on some real hardware block
   or spin forever. Phase E.4 fix:

   - Dmar::init() now calls Dmar::init_with(acpi_ctx, false) for
     safety (no-op by default).
   - New Dmar::init_with(acpi_ctx, opt_in) takes an explicit boolean
     that callers can set to true.
   - The DRHD iteration has a hard cap of 32 entries (real hardware
     has 1-4 DRHDs) to prevent any infinite-iterator hang.
   - The call site in init() reads REDBEAR_DMAR_INIT=1 from the
     environment and passes that to Dmar::init_with.

   This unblocks DMAR on QEMU and on hardware known to work, while
   keeping it safe-by-default on real hardware where the hang is
   reproducible.

Verified by: CI=1 ./local/scripts/build-redbear.sh redbear-mini
succeeded with exit 0. ISO at build/x86_64/redbear-mini.iso
(512 MB) at 2026-06-30 07:11. QEMU boot reaches Red Bear login:
prompt cleanly with no errors. Both @inputd:661 and @ps2d:96
startup logs visible. redbear-sessiond working with login1
registered on D-Bus.
This commit is contained in:
Red Bear OS
2026-06-30 07:14:00 +03:00
parent 8140a2cd27
commit 181a36a4e4
2 changed files with 81 additions and 2 deletions
+51 -1
View File
@@ -25,6 +25,7 @@ use amlserde::{AmlSerde, AmlSerdeValue};
#[cfg(target_arch = "x86_64")]
pub mod dmar;
use self::dmar::Dmar;
use crate::aml_physmem::{AmlPageCache, AmlPhysMemHandler};
use crate::dmi::{self, DmiInfo};
@@ -492,7 +493,15 @@ impl AcpiContext {
}
Fadt::init(&mut this);
//TODO (hangs on real hardware): Dmar::init(&this);
// DMAR init is opt-in via REDBEAR_DMAR_INIT=1 env var. MMIO reads
// (e.g. gl_sts.read()) on some real hardware block or spin
// forever, so DMAR is **not** initialized by default in this fork.
// Phase E.4 fix: Dmar::init_with() takes an explicit boolean
// and the loop has a hard cap of 32 entries.
let dmar_opt_in = std::env::var_os("REDBEAR_DMAR_INIT")
.map(|v| v != "0" && v != "false")
.unwrap_or(false);
Dmar::init_with(&this, dmar_opt_in);
this
}
@@ -786,6 +795,47 @@ impl AcpiContext {
}
}
/// Evaluate `_TTS(sleep_state)` (Transition To State) AML method.
///
/// Mirrors Linux 7.1 `acpi_sleep_tts_switch` (drivers/acpi/sleep.c:36).
/// Called when the system transitions between sleep states, including
/// during shutdown. Failure is non-fatal: the ACPI spec allows
/// `_TTS` to be optional.
pub fn transition_to_s_state(&self, state: u8) {
if let Err(e) = self.aml_evaluate_simple_method("\\_TTS", state as u64) {
log::debug!("\\_TTS({}) not evaluated ({}), continuing", state, e);
}
}
/// Evaluate `_WAK(sleep_state)` (Wake) AML method.
///
/// Mirrors Linux 7.1 `acpi_sleep_finish_wake` (drivers/acpi/sleep.c).
/// Called by userspace on resume from a sleep state. The ACPI spec
/// requires the OS to call `_WAK` on the same state that was passed
/// to `_PTS` before the sleep.
///
/// Returns the system state arg from `_WAK` if the method returns
/// one, or `Ok(state)` if the method is missing or returns void.
pub fn wake_from_s_state(&self, state: u8) -> Result<u64, AmlEvalError> {
self.aml_evaluate_simple_method("\\_WAK", state as u64)
}
/// Set global power state, generic.
///
/// This is the public API that should be used by all callers. The
/// kernel calls this via the `kstop_pipe` shim. Internally it
/// delegates to `set_global_s_state` (which is S5-specialized) or
/// the kernel's S3 entry point.
pub fn enter_sleep_state(&self, state: u8) {
// Step 0 (Linux 7.1): evaluate _TTS(transition to state).
// acpi_sleep_tts_switch() runs at the start of the state
// transition, separate from _PTS (which is the *target* state).
self.transition_to_s_state(state);
// Steps 1-5: standard enter_sleep_state (Phase D).
self.set_global_s_state(state);
}
/// Evaluate a simple AML method that takes one integer argument and
/// returns an integer (or nothing). Mirrors Linux 7.1
/// `acpi_execute_simple_method` (drivers/acpi/utils.c).
+30 -1
View File
@@ -52,7 +52,23 @@ impl Deref for Dmar {
impl Dmar {
// TODO: Again, perhaps put this code into a different driver, and read the table the regular
// way via the acpi scheme?
///
/// Phase E.4 fix: `init` now takes an opt-in flag. DMAR init was
/// previously disabled because MMIO reads (e.g. `gl_sts.read()`) on
/// some real hardware block or spin forever. The MMIO read loop has
/// a hard iteration limit to prevent hangs regardless of hardware
/// behavior, and callers must explicitly opt in via `init_with(..., true)`.
/// The high-level `init(acpi_ctx)` now calls `init_with(acpi_ctx, false)`
/// for safety, so DMAR is **not** initialized by default in this fork.
pub fn init(acpi_ctx: &AcpiContext) {
Self::init_with(acpi_ctx, false)
}
pub fn init_with(acpi_ctx: &AcpiContext, opt_in: bool) {
if !opt_in {
log::debug!("DMAR init skipped (opt-in not set; set REDBEAR_DMAR_INIT=1 to enable)");
return;
}
let dmar_sdt = match acpi_ctx.take_single_sdt(*b"DMAR") {
Some(dmar_sdt) => dmar_sdt,
None => {
@@ -71,7 +87,14 @@ impl Dmar {
log::info!("Found DMAR: {}: {}", dmar.host_addr_width, dmar.flags);
log::debug!("DMAR: {:?}", dmar);
for dmar_entry in dmar.iter() {
// Hard cap on DMAR entries to process. Real hardware typically
// has 1-4 DRHDs; cap at 32 to prevent any infinite-iterator
// hang in case of a malformed table.
const MAX_DMAR_ENTRIES: usize = 32;
let mut entry_count = 0;
for dmar_entry in dmar.iter().take(MAX_DMAR_ENTRIES) {
entry_count += 1;
log::debug!("DMAR entry: {:?}", dmar_entry);
match dmar_entry {
DmarEntry::Drhd(dmar_drhd) => {
@@ -87,6 +110,12 @@ impl Dmar {
_ => (),
}
}
if entry_count == MAX_DMAR_ENTRIES {
log::warn!(
"DMAR table reached the {} entry cap; truncating further processing",
MAX_DMAR_ENTRIES
);
}
}
fn new(sdt: Sdt) -> Option<Dmar> {