From 181a36a4e4a184ece4efaa99d5d3192d44b586fb Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Tue, 30 Jun 2026 07:14:00 +0300 Subject: [PATCH] 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. --- drivers/acpid/src/acpi.rs | 52 +++++++++++++++++++++++++++++- drivers/acpid/src/acpi/dmar/mod.rs | 31 +++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs index 92226c4273..7b4c3ea8fb 100644 --- a/drivers/acpid/src/acpi.rs +++ b/drivers/acpid/src/acpi.rs @@ -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 { + 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). diff --git a/drivers/acpid/src/acpi/dmar/mod.rs b/drivers/acpid/src/acpi/dmar/mod.rs index c42b379a10..17ffde7044 100644 --- a/drivers/acpid/src/acpi/dmar/mod.rs +++ b/drivers/acpid/src/acpi/dmar/mod.rs @@ -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 {