181a36a4e4
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.
1090 lines
36 KiB
Rust
1090 lines
36 KiB
Rust
use acpi::aml::object::{Object, WrappedObject};
|
|
use acpi::aml::op_region::{RegionHandler, RegionSpace};
|
|
use rustc_hash::FxHashMap;
|
|
use std::convert::{TryFrom, TryInto};
|
|
use std::error::Error;
|
|
use std::ops::Deref;
|
|
use std::str::FromStr;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::{fmt, mem};
|
|
use syscall::PAGE_SIZE;
|
|
|
|
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
|
use common::io::{Io, Pio};
|
|
|
|
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
|
use thiserror::Error;
|
|
|
|
use acpi::{
|
|
aml::{namespace::AmlName, AmlError, Interpreter},
|
|
platform::AcpiPlatform,
|
|
AcpiTables,
|
|
};
|
|
use amlserde::aml_serde_name::aml_to_symbol;
|
|
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};
|
|
|
|
/// The raw SDT header struct, as defined by the ACPI specification.
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(C, packed)]
|
|
pub struct SdtHeader {
|
|
pub signature: [u8; 4],
|
|
pub length: u32,
|
|
pub revision: u8,
|
|
pub checksum: u8,
|
|
pub oem_id: [u8; 6],
|
|
pub oem_table_id: [u8; 8],
|
|
pub oem_revision: u32,
|
|
pub creator_id: u32,
|
|
pub creator_revision: u32,
|
|
}
|
|
unsafe impl plain::Plain for SdtHeader {}
|
|
|
|
impl SdtHeader {
|
|
pub fn signature(&self) -> SdtSignature {
|
|
SdtSignature {
|
|
signature: self.signature,
|
|
oem_id: self.oem_id,
|
|
oem_table_id: self.oem_table_id,
|
|
}
|
|
}
|
|
pub fn length(&self) -> usize {
|
|
self.length
|
|
.try_into()
|
|
.expect("expected usize to be at least 32 bits")
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
pub struct SdtSignature {
|
|
pub signature: [u8; 4],
|
|
pub oem_id: [u8; 6],
|
|
pub oem_table_id: [u8; 8],
|
|
}
|
|
|
|
impl fmt::Display for SdtSignature {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"{}-{}-{}",
|
|
String::from_utf8_lossy(&self.signature),
|
|
String::from_utf8_lossy(&self.oem_id),
|
|
String::from_utf8_lossy(&self.oem_table_id)
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum TablePhysLoadError {
|
|
// TODO: Make syscall::Error implement std::error::Error, when enabling a Cargo feature.
|
|
#[error("i/o error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
|
|
#[error("invalid SDT: {0}")]
|
|
Validity(#[from] InvalidSdtError),
|
|
}
|
|
#[derive(Debug, Error)]
|
|
pub enum InvalidSdtError {
|
|
#[error("invalid size")]
|
|
InvalidSize,
|
|
|
|
#[error("invalid checksum")]
|
|
BadChecksum,
|
|
}
|
|
|
|
struct PhysmapGuard {
|
|
virt: *const u8,
|
|
size: usize,
|
|
}
|
|
impl PhysmapGuard {
|
|
fn map(page: usize, page_count: usize) -> std::io::Result<Self> {
|
|
let size = page_count * PAGE_SIZE;
|
|
let virt = unsafe {
|
|
common::physmap(page, size, common::Prot::RO, common::MemoryType::default())
|
|
.map_err(|error| std::io::Error::from_raw_os_error(error.errno()))?
|
|
};
|
|
|
|
Ok(Self {
|
|
virt: virt as *const u8,
|
|
size,
|
|
})
|
|
}
|
|
}
|
|
impl Deref for PhysmapGuard {
|
|
type Target = [u8];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
unsafe { std::slice::from_raw_parts(self.virt as *const u8, self.size) }
|
|
}
|
|
}
|
|
impl Drop for PhysmapGuard {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
let _ = libredox::call::munmap(self.virt as *mut (), self.size);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Sdt(Arc<[u8]>);
|
|
|
|
impl Sdt {
|
|
pub fn new(slice: Arc<[u8]>) -> Result<Self, InvalidSdtError> {
|
|
let header = match plain::from_bytes::<SdtHeader>(&slice) {
|
|
Ok(header) => header,
|
|
Err(plain::Error::TooShort) => return Err(InvalidSdtError::InvalidSize),
|
|
Err(plain::Error::BadAlignment) => panic!(
|
|
"plain::from_bytes failed due to alignment, but SdtHeader is #[repr(packed)]!"
|
|
),
|
|
};
|
|
|
|
if header.length() != slice.len() {
|
|
return Err(InvalidSdtError::InvalidSize);
|
|
}
|
|
|
|
let checksum = slice
|
|
.iter()
|
|
.copied()
|
|
.fold(0_u8, |current_sum, item| current_sum.wrapping_add(item));
|
|
|
|
if checksum != 0 {
|
|
return Err(InvalidSdtError::BadChecksum);
|
|
}
|
|
|
|
Ok(Self(slice))
|
|
}
|
|
pub fn load_from_physical(physaddr: usize) -> Result<Self, TablePhysLoadError> {
|
|
let physaddr_start_page = physaddr / PAGE_SIZE * PAGE_SIZE;
|
|
let physaddr_page_offset = physaddr % PAGE_SIZE;
|
|
|
|
// Begin by reading and validating the header first. The SDT header is always 36 bytes
|
|
// long, and can thus span either one or two page table frames.
|
|
let needs_extra_page = (PAGE_SIZE - physaddr_page_offset)
|
|
.checked_sub(mem::size_of::<SdtHeader>())
|
|
.is_none();
|
|
let page_table_count = 1 + if needs_extra_page { 1 } else { 0 };
|
|
|
|
let pages = PhysmapGuard::map(physaddr_start_page, page_table_count)?;
|
|
assert!(pages.len() >= mem::size_of::<SdtHeader>());
|
|
let sdt_mem = &pages[physaddr_page_offset..];
|
|
|
|
let sdt = plain::from_bytes::<SdtHeader>(&sdt_mem[..mem::size_of::<SdtHeader>()])
|
|
.expect("either alignment is wrong, or the length is too short, both of which are already checked for");
|
|
|
|
let total_length = sdt.length();
|
|
let base_length = std::cmp::min(total_length, sdt_mem.len());
|
|
let extended_length = total_length - base_length;
|
|
|
|
let mut loaded = sdt_mem[..base_length].to_owned();
|
|
loaded.reserve(extended_length);
|
|
|
|
const SIMULTANEOUS_PAGE_COUNT: usize = 4;
|
|
|
|
let mut left = extended_length;
|
|
let mut offset = physaddr_start_page + page_table_count * PAGE_SIZE;
|
|
|
|
let length_per_iteration = PAGE_SIZE * SIMULTANEOUS_PAGE_COUNT;
|
|
|
|
while left > 0 {
|
|
let to_copy = std::cmp::min(left, length_per_iteration);
|
|
let additional_pages = PhysmapGuard::map(offset, to_copy.div_ceil(PAGE_SIZE))?;
|
|
|
|
loaded.extend(&additional_pages[..to_copy]);
|
|
|
|
left -= to_copy;
|
|
offset += to_copy;
|
|
}
|
|
assert_eq!(left, 0);
|
|
|
|
Self::new(loaded.into()).map_err(Into::into)
|
|
}
|
|
pub fn as_slice(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Deref for Sdt {
|
|
type Target = SdtHeader;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
plain::from_bytes::<SdtHeader>(&self.0)
|
|
.expect("expected already validated Sdt to be able to get its header")
|
|
}
|
|
}
|
|
|
|
impl Sdt {
|
|
pub fn data(&self) -> &[u8] {
|
|
&self.0[mem::size_of::<SdtHeader>()..]
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Sdt {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("Sdt")
|
|
.field("header", &*self as &SdtHeader)
|
|
.field("extra_len", &self.data().len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub struct Dsdt(Sdt);
|
|
pub struct Ssdt(Sdt);
|
|
|
|
// Current AML implementation builds the aml_context.namespace at startup,
|
|
// but the cache for symbols is lazy-loaded when someone
|
|
// reads from the acpi:/symbols scheme.
|
|
// If you dynamically add an SDT, you can add to the namespace, but you
|
|
// must empty the cache so it is rebuilt.
|
|
// If you modify an SDT, you must discard the aml_context and rebuild it.
|
|
pub struct AmlSymbols {
|
|
aml_context: Option<Interpreter<AmlPhysMemHandler>>,
|
|
// k = name, v = description
|
|
symbol_cache: FxHashMap<String, String>,
|
|
page_cache: Arc<Mutex<AmlPageCache>>,
|
|
aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>,
|
|
}
|
|
|
|
impl AmlSymbols {
|
|
pub fn new(aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>) -> Self {
|
|
Self {
|
|
aml_context: None,
|
|
symbol_cache: FxHashMap::default(),
|
|
page_cache: Arc::new(Mutex::new(AmlPageCache::default())),
|
|
aml_region_handlers,
|
|
}
|
|
}
|
|
|
|
pub fn init(&mut self, pci_fd: Option<&libredox::Fd>) -> Result<(), Box<dyn Error>> {
|
|
if self.aml_context.is_some() {
|
|
return Err("AML interpreter already initialized".into());
|
|
}
|
|
let format_err = |err| format!("{:?}", err);
|
|
let handler = AmlPhysMemHandler::new(pci_fd, Arc::clone(&self.page_cache));
|
|
//TODO: use these parsed tables for the rest of acpid
|
|
let rsdp_address = usize::from_str_radix(&std::env::var("RSDP_ADDR")?, 16)?;
|
|
let tables =
|
|
unsafe { AcpiTables::from_rsdp(handler.clone(), rsdp_address).map_err(format_err)? };
|
|
let platform = AcpiPlatform::new(tables, handler).map_err(format_err)?;
|
|
let interpreter = Interpreter::new_from_platform(&platform).map_err(format_err)?;
|
|
for (region, handler) in self.aml_region_handlers.drain(..) {
|
|
interpreter.install_region_handler(region, handler);
|
|
}
|
|
self.aml_context = Some(interpreter);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn aml_context_mut(
|
|
&mut self,
|
|
pci_fd: Option<&libredox::Fd>,
|
|
) -> Result<&mut Interpreter<AmlPhysMemHandler>, AmlEvalError> {
|
|
if self.aml_context.is_none() {
|
|
match self.init(pci_fd) {
|
|
Ok(()) => (),
|
|
Err(err) => {
|
|
log::error!("failed to initialize AML context: {}", err);
|
|
}
|
|
}
|
|
}
|
|
self.aml_context
|
|
.as_mut()
|
|
.ok_or(AmlEvalError::NotInitialized)
|
|
}
|
|
|
|
pub fn symbols_cache(&self) -> &FxHashMap<String, String> {
|
|
&self.symbol_cache
|
|
}
|
|
|
|
pub fn lookup(&self, symbol: &str) -> Option<String> {
|
|
if let Some(description) = self.symbol_cache.get(symbol) {
|
|
log::trace!("Found symbol in cache, {}, {}", symbol, description);
|
|
return Some(description.to_owned());
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn build_cache(&mut self, pci_fd: Option<&libredox::Fd>) {
|
|
let Ok(aml_context) = self.aml_context_mut(pci_fd) else {
|
|
return;
|
|
};
|
|
|
|
let mut symbol_list: Vec<(AmlName, String)> = Vec::with_capacity(5000);
|
|
|
|
if aml_context
|
|
.namespace
|
|
.lock()
|
|
.traverse(|level_aml_name, level| {
|
|
for (child_seg, handle) in level.values.iter() {
|
|
if let Ok(aml_name) =
|
|
AmlName::from_name_seg(child_seg.to_owned()).resolve(level_aml_name)
|
|
{
|
|
let name = aml_to_symbol(&aml_name);
|
|
symbol_list.push((aml_name, name));
|
|
} else {
|
|
log::error!(
|
|
"AmlName resolve failed, {:?}:{:?}",
|
|
level_aml_name,
|
|
child_seg
|
|
);
|
|
}
|
|
}
|
|
Ok(true)
|
|
})
|
|
.is_err()
|
|
{
|
|
log::error!("Namespace traverse failed");
|
|
return;
|
|
}
|
|
|
|
let mut symbol_cache: FxHashMap<String, String> = FxHashMap::default();
|
|
|
|
for (aml_name, name) in &symbol_list {
|
|
// create an empty entry, in case something goes wrong with serialization
|
|
symbol_cache.insert(name.to_owned(), "".to_owned());
|
|
if let Some(ser_value) = AmlSerde::from_aml(aml_context, aml_name) {
|
|
if let Ok(ser_string) = ron::ser::to_string_pretty(&ser_value, Default::default()) {
|
|
// replace the empty entry
|
|
symbol_cache.insert(name.to_owned(), ser_string);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache the new list
|
|
log::trace!("Updating symbols list");
|
|
|
|
self.symbol_cache = symbol_cache;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum AmlEvalError {
|
|
#[error("AML error")]
|
|
AmlError(AmlError),
|
|
#[error("Failed to serialize argument")]
|
|
SerializationError,
|
|
#[error("Failed to deserialize")]
|
|
DeserializationError,
|
|
#[error("AML not initialized")]
|
|
NotInitialized,
|
|
}
|
|
impl From<AmlError> for AmlEvalError {
|
|
fn from(value: AmlError) -> Self {
|
|
AmlEvalError::AmlError(value)
|
|
}
|
|
}
|
|
|
|
pub struct AcpiContext {
|
|
tables: Vec<Sdt>,
|
|
dsdt: Option<Dsdt>,
|
|
fadt: Option<Fadt>,
|
|
|
|
aml_symbols: RwLock<AmlSymbols>,
|
|
|
|
// TODO: The kernel ACPI code seemed to use load_table quite ubiquitously, however ACPI 5.1
|
|
// states that DDBHandles can only be obtained when loading XSDT-pointed tables. So, we'll
|
|
// generate an index only for those.
|
|
sdt_order: RwLock<Vec<Option<SdtSignature>>>,
|
|
|
|
/// Decoded SMBIOS / DMI identity fields. `None` when no SMBIOS entry
|
|
/// point could be located or the firmware table was unusable; this is
|
|
/// not an error condition — many embedded targets ship without
|
|
/// SMBIOS, and downstream quirks systems tolerate the absence.
|
|
dmi: Option<DmiInfo>,
|
|
|
|
pub next_ctx: RwLock<u64>,
|
|
}
|
|
|
|
impl AcpiContext {
|
|
pub fn aml_eval(
|
|
&self,
|
|
symbol: AmlName,
|
|
args: Vec<AmlSerdeValue>,
|
|
) -> Result<AmlSerdeValue, AmlEvalError> {
|
|
let mut symbols = self.aml_symbols.write();
|
|
let interpreter = symbols.aml_context_mut(None)?;
|
|
interpreter.acquire_global_lock(16)?;
|
|
|
|
let args = args
|
|
.into_iter()
|
|
.map(|aml_serde_value| {
|
|
aml_serde_value
|
|
.to_aml_object()
|
|
.map(Object::wrap)
|
|
.ok_or(AmlEvalError::DeserializationError)
|
|
})
|
|
.collect::<Result<Vec<WrappedObject>, AmlEvalError>>()?;
|
|
|
|
let result = interpreter.evaluate(symbol, args);
|
|
interpreter
|
|
.release_global_lock()
|
|
.expect("Failed to release GIL!"); //TODO: check if this should panic
|
|
|
|
result
|
|
.map_err(AmlEvalError::from)
|
|
.map(|object| {
|
|
AmlSerdeValue::from_aml_value(object.deref())
|
|
.ok_or(AmlEvalError::SerializationError)
|
|
})
|
|
.flatten()
|
|
}
|
|
|
|
pub fn init(
|
|
rxsdt_physaddrs: impl Iterator<Item = u64>,
|
|
ec: Vec<(RegionSpace, Box<dyn RegionHandler>)>,
|
|
) -> Self {
|
|
let tables = rxsdt_physaddrs
|
|
.map(|physaddr| {
|
|
let physaddr: usize = physaddr
|
|
.try_into()
|
|
.expect("expected ACPI addresses to be compatible with the current word size");
|
|
|
|
log::trace!("TABLE AT {:#>08X}", physaddr);
|
|
|
|
Sdt::load_from_physical(physaddr).expect("failed to load physical SDT")
|
|
})
|
|
.collect::<Vec<Sdt>>();
|
|
|
|
let mut this = Self {
|
|
tables,
|
|
dsdt: None,
|
|
fadt: None,
|
|
|
|
// Temporary values
|
|
aml_symbols: RwLock::new(AmlSymbols::new(ec)),
|
|
|
|
next_ctx: RwLock::new(0),
|
|
|
|
sdt_order: RwLock::new(Vec::new()),
|
|
dmi: None,
|
|
};
|
|
|
|
for table in &this.tables {
|
|
this.new_index(&table.signature());
|
|
}
|
|
|
|
// DMI / SMBIOS scan. Independent of ACPI table parsing — SMBIOS
|
|
// lives in a separate firmware structure anchored at the legacy
|
|
// BIOS segment, not in the RSDP/XSDT. We scan after the ACPI
|
|
// tables so any error here cannot affect ACPI parsing, and
|
|
// gracefully accept absence (embedded firmware often omits it).
|
|
match dmi::scan() {
|
|
Ok(Some(table)) => {
|
|
log::info!(
|
|
"SMBIOS {}.{}.{}: sys_vendor={:?} product_name={:?} board_name={:?}",
|
|
table.version.major,
|
|
table.version.minor,
|
|
table.version.revision,
|
|
table.info.sys_vendor,
|
|
table.info.product_name,
|
|
table.info.board_name,
|
|
);
|
|
this.dmi = Some(table.info);
|
|
}
|
|
Ok(None) => {
|
|
log::info!("SMBIOS: no entry point found (DMI quirks disabled)");
|
|
}
|
|
Err(error) => {
|
|
log::warn!("SMBIOS scan failed, continuing without DMI: {}", error);
|
|
}
|
|
}
|
|
|
|
Fadt::init(&mut 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
|
|
}
|
|
|
|
/// Decoded DMI identity fields, if SMBIOS was present on this system.
|
|
pub fn dmi_info(&self) -> Option<&DmiInfo> {
|
|
self.dmi.as_ref()
|
|
}
|
|
|
|
pub fn dsdt(&self) -> Option<&Dsdt> {
|
|
self.dsdt.as_ref()
|
|
}
|
|
pub fn ssdts(&self) -> impl Iterator<Item = Ssdt> + '_ {
|
|
self.find_multiple_sdts(*b"SSDT")
|
|
.map(|sdt| Ssdt(sdt.clone()))
|
|
}
|
|
fn find_single_sdt_pos(&self, signature: [u8; 4]) -> Option<usize> {
|
|
let count = self
|
|
.tables
|
|
.iter()
|
|
.filter(|sdt| sdt.signature == signature)
|
|
.count();
|
|
|
|
if count > 1 {
|
|
log::warn!(
|
|
"Expected only a single SDT of signature `{}` ({:?}), but there were {}",
|
|
String::from_utf8_lossy(&signature),
|
|
signature,
|
|
count
|
|
);
|
|
}
|
|
|
|
self.tables
|
|
.iter()
|
|
.position(|sdt| sdt.signature == signature)
|
|
}
|
|
pub fn find_multiple_sdts<'a>(&'a self, signature: [u8; 4]) -> impl Iterator<Item = &'a Sdt> {
|
|
self.tables
|
|
.iter()
|
|
.filter(move |sdt| sdt.signature == signature)
|
|
}
|
|
pub fn take_single_sdt(&self, signature: [u8; 4]) -> Option<Sdt> {
|
|
self.find_single_sdt_pos(signature)
|
|
.map(|pos| self.tables[pos].clone())
|
|
}
|
|
pub fn fadt(&self) -> Option<&Fadt> {
|
|
self.fadt.as_ref()
|
|
}
|
|
pub fn sdt_from_signature(&self, signature: &SdtSignature) -> Option<&Sdt> {
|
|
self.tables.iter().find(|sdt| {
|
|
sdt.signature == signature.signature
|
|
&& sdt.oem_id == signature.oem_id
|
|
&& sdt.oem_table_id == signature.oem_table_id
|
|
})
|
|
}
|
|
pub fn get_signature_from_index(&self, index: usize) -> Option<SdtSignature> {
|
|
self.sdt_order.read().get(index).copied().flatten()
|
|
}
|
|
pub fn get_index_from_signature(&self, signature: &SdtSignature) -> Option<usize> {
|
|
self.sdt_order
|
|
.read()
|
|
.iter()
|
|
.rposition(|sig| sig.map_or(false, |sig| &sig == signature))
|
|
}
|
|
pub fn tables(&self) -> &[Sdt] {
|
|
&self.tables
|
|
}
|
|
pub fn new_index(&self, signature: &SdtSignature) {
|
|
self.sdt_order.write().push(Some(*signature));
|
|
}
|
|
|
|
pub fn aml_lookup(&self, symbol: &str) -> Option<String> {
|
|
if let Ok(aml_symbols) = self.aml_symbols(None) {
|
|
aml_symbols.lookup(symbol)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Enumerate thermal zones under `\_TZ` by walking the namespace.
|
|
/// Returns zone names (the children of `\_TZ` whose serialized form
|
|
/// is a `ThermalZone` object). An empty Vec means the system has no
|
|
/// thermal zones (typical for headless QEMU and desktops).
|
|
pub fn thermal_zones(&self) -> Vec<String> {
|
|
let mut zones = Vec::new();
|
|
let Ok(aml_symbols) = self.aml_symbols(None) else {
|
|
return zones;
|
|
};
|
|
let prefix = "\\_TZ.";
|
|
for name in aml_symbols.symbols_cache().keys() {
|
|
// Match immediate children of \_TZ, not deeper paths.
|
|
// E.g. "\_TZ.TZ0" matches, "\_TZ.TZ0._TMP" doesn't.
|
|
if let Some(rest) = name.strip_prefix(prefix) {
|
|
if !rest.contains('.') {
|
|
zones.push(name.clone());
|
|
}
|
|
}
|
|
}
|
|
zones
|
|
}
|
|
|
|
/// Enumerate AC adapter objects. Returns adapter names whose
|
|
/// serialized form is a `PowerResource`. AC adapter detection on
|
|
/// laptops requires EC queries that are not yet wired up; this
|
|
/// function returns whatever the AML namespace has declared.
|
|
pub fn power_adapters(&self) -> Vec<String> {
|
|
let mut adapters = Vec::new();
|
|
let Ok(aml_symbols) = self.aml_symbols(None) else {
|
|
return adapters;
|
|
};
|
|
// AC adapters typically live under \_SB.AC or \_SB.PCI0.AC;
|
|
// we surface anything whose serialized form is a PowerResource.
|
|
// This is a best-effort enumeration until we have EC-based
|
|
// battery + AC adapter detection.
|
|
for (name, value) in aml_symbols.symbols_cache().iter() {
|
|
if value.contains("PowerResource(") {
|
|
adapters.push(name.clone());
|
|
}
|
|
}
|
|
adapters
|
|
}
|
|
|
|
pub fn aml_symbols(
|
|
&self,
|
|
pci_fd: Option<&libredox::Fd>,
|
|
) -> Result<RwLockReadGuard<'_, AmlSymbols>, AmlError> {
|
|
// return the cached value if it exists
|
|
let symbols = self.aml_symbols.read();
|
|
if !symbols.symbols_cache().is_empty() {
|
|
return Ok(symbols);
|
|
}
|
|
// free the read lock
|
|
drop(symbols);
|
|
|
|
// List has not been initialized, we have to build it
|
|
log::trace!("Creating symbols list");
|
|
|
|
let mut aml_symbols = self.aml_symbols.write();
|
|
|
|
aml_symbols.build_cache(pci_fd);
|
|
|
|
// return the cached value
|
|
Ok(RwLockWriteGuard::downgrade(aml_symbols))
|
|
}
|
|
|
|
/// Discard any cached symbols list. To be called if the AML namespace changes.
|
|
pub fn aml_symbols_reset(&self) {
|
|
let mut aml_symbols = self.aml_symbols.write();
|
|
aml_symbols.symbol_cache = FxHashMap::default();
|
|
}
|
|
|
|
/// Set Power State
|
|
/// See https://uefi.org/sites/default/files/resources/ACPI_6_1.pdf
|
|
/// - search for PM1a
|
|
/// See https://forum.osdev.org/viewtopic.php?t=16990 for practical details
|
|
///
|
|
/// Follows the Linux 7.1 `acpi_enter_sleep_state` pattern
|
|
/// (drivers/acpi/acpica/hwxfsleep.c:283):
|
|
/// 1. Look up `_Sx` package in AML namespace, extract SLP_TYPa/SLP_TYPb
|
|
/// 2. Evaluate `_PTS(state)` (Prepare To Sleep) - optional
|
|
/// 3. Evaluate `_SST(sst_value)` (System Status) - optional
|
|
/// 4. Write SLP_EN|SLP_TYPa to PM1a, SLP_EN|SLP_TYPb to PM1b
|
|
/// 5. Spin (machine should power off before this returns)
|
|
pub fn set_global_s_state(&self, state: u8) {
|
|
let fadt = match self.fadt() {
|
|
Some(fadt) => fadt,
|
|
None => {
|
|
log::error!("Cannot set global S-state due to missing FADT.");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let aml_symbols = self.aml_symbols.read();
|
|
|
|
// Step 1: Look up the `_Sx` package for this state and extract
|
|
// SLP_TYPa/SLP_TYPb. Mirrors acpi_get_sleep_type_data() in Linux.
|
|
let sx_name_str = format!("\\_S{}", state);
|
|
let sx_aml_name = match acpi::aml::namespace::AmlName::from_str(&sx_name_str) {
|
|
Ok(aml_name) => aml_name,
|
|
Err(error) => {
|
|
log::error!("Could not build AmlName for \\_S{}, {:?}", state, error);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let package = match &aml_symbols.aml_context {
|
|
Some(aml_context) => match aml_context.namespace.lock().get(sx_aml_name) {
|
|
Ok(obj) => match obj.deref() {
|
|
acpi::aml::object::Object::Package(package) => package.clone(),
|
|
_ => {
|
|
log::error!("Cannot set S-state, \\_S{} is not a package", state);
|
|
return;
|
|
}
|
|
},
|
|
Err(error) => {
|
|
log::error!("Cannot set S-state, missing \\_S{}, {:?}", state, error);
|
|
return;
|
|
}
|
|
},
|
|
None => {
|
|
log::error!("Cannot set S-state, AML context not initialized");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let slp_typa = match package.get(0).and_then(|e| match e.deref() {
|
|
acpi::aml::object::Object::Integer(i) => Some(i.to_owned()),
|
|
_ => None,
|
|
}) {
|
|
Some(v) => v,
|
|
None => {
|
|
log::error!("\\_S{} SLP_TYPa is not an Integer", state);
|
|
return;
|
|
}
|
|
};
|
|
let slp_typb = match package.get(1).and_then(|e| match e.deref() {
|
|
acpi::aml::object::Object::Integer(i) => Some(i.to_owned()),
|
|
_ => None,
|
|
}) {
|
|
Some(v) => v,
|
|
None => {
|
|
log::error!("\\_S{} SLP_TYPb is not an Integer", state);
|
|
return;
|
|
}
|
|
};
|
|
|
|
log::trace!(
|
|
"Sleep state S{} SLP_TYPa {:X}, SLP_TYPb {:X}",
|
|
state, slp_typa, slp_typb
|
|
);
|
|
|
|
// Step 2: Evaluate `_PTS(state)` (Prepare To Sleep).
|
|
// Mirrors Linux 7.1 drivers/acpi/acpica/hwxfsleep.c:222-233.
|
|
// ACPI spec says `_PTS` is optional; AE_NOT_FOUND is not fatal.
|
|
if let Err(e) = self.aml_evaluate_simple_method("\\_PTS", state as u64) {
|
|
log::debug!("\\_PTS({}) not evaluated ({}), continuing", state, e);
|
|
}
|
|
|
|
// Step 3: Evaluate `_SST(sst_value)` (System Status indicator).
|
|
// Mirrors Linux 7.1 ACPI_SST_* constants:
|
|
// working=0, sleeping=1, sleep-context=2, indicator-off=7.
|
|
let sst_value: u64 = match state {
|
|
0 => 0,
|
|
1..=3 => 1,
|
|
4 => 2,
|
|
_ => 7,
|
|
};
|
|
if let Err(e) = self.aml_evaluate_simple_method("\\_SST", sst_value) {
|
|
log::debug!("\\_SST({}) not evaluated ({}), continuing", sst_value, e);
|
|
}
|
|
|
|
// Step 4: Write SLP_EN | SLP_TYPa to PM1a, SLP_EN | SLP_TYPb to PM1b.
|
|
let slp_en: u16 = 1 << 13;
|
|
let val_a = slp_en | slp_typa as u16;
|
|
let val_b = slp_en | slp_typb as u16;
|
|
|
|
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
|
{
|
|
let port_a = fadt.pm1a_control_block as u16;
|
|
log::warn!(
|
|
"Sleep S{} with ACPI outw(0x{:X}, 0x{:X})",
|
|
state, port_a, val_a
|
|
);
|
|
Pio::<u16>::new(port_a).write(val_a);
|
|
|
|
// Some hardware requires both PM1a and PM1b to be written for
|
|
// the sleep transition. The FADT pm1b_control_block is 0 when
|
|
// no second block exists; in that case skip the second write.
|
|
let port_b = fadt.pm1b_control_block as u16;
|
|
if port_b != 0 {
|
|
log::warn!(
|
|
"Sleep S{} with ACPI outw(0x{:X}, 0x{:X})",
|
|
state, port_b, val_b
|
|
);
|
|
Pio::<u16>::new(port_b).write(val_b);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
|
{
|
|
log::error!(
|
|
"Cannot sleep with ACPI on this architecture: state=S{} val_a={:X} val_b={:X}",
|
|
state, val_a, val_b
|
|
);
|
|
}
|
|
|
|
// Step 5: Spin. On a working ACPI implementation, the machine
|
|
// powers off (or suspends) before this returns.
|
|
loop {
|
|
core::hint::spin_loop();
|
|
}
|
|
}
|
|
|
|
/// 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).
|
|
///
|
|
/// Returns `Ok(0)` if the method is not present (the ACPI spec
|
|
/// allows `_PTS`/`_SST`/etc. to be optional), `Ok(integer)` if
|
|
/// it returned an integer, or `Err` on evaluation failure.
|
|
fn aml_evaluate_simple_method(&self, path: &str, arg: u64) -> Result<u64, AmlEvalError> {
|
|
let mut symbols = self.aml_symbols.write();
|
|
let aml_name = acpi::aml::namespace::AmlName::from_str(path)
|
|
.map_err(|_| AmlEvalError::DeserializationError)?;
|
|
let interpreter = symbols.aml_context_mut(None)?;
|
|
interpreter.acquire_global_lock(16)?;
|
|
|
|
let args = vec![acpi::aml::object::Object::Integer(arg).wrap()];
|
|
|
|
// Use evaluate_if_present so missing methods return Ok(None) cleanly
|
|
// instead of AmlError::ObjectDoesNotExist.
|
|
let result = interpreter.evaluate_if_present(aml_name, args);
|
|
interpreter
|
|
.release_global_lock()
|
|
.expect("acpid: failed to release AML global lock");
|
|
|
|
match result {
|
|
Ok(Some(obj)) => match obj.deref().as_integer() {
|
|
Ok(n) => Ok(n),
|
|
Err(_) => Ok(0),
|
|
},
|
|
Ok(None) => Ok(0),
|
|
Err(e) => Err(AmlEvalError::from(e)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(C, packed)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FadtStruct {
|
|
pub header: SdtHeader,
|
|
pub firmware_ctrl: u32,
|
|
pub dsdt: u32,
|
|
|
|
// field used in ACPI 1.0; no longer in use, for compatibility only
|
|
reserved: u8,
|
|
|
|
pub preferred_power_managament: u8,
|
|
pub sci_interrupt: u16,
|
|
pub smi_command_port: u32,
|
|
pub acpi_enable: u8,
|
|
pub acpi_disable: u8,
|
|
pub s4_bios_req: u8,
|
|
pub pstate_control: u8,
|
|
pub pm1a_event_block: u32,
|
|
pub pm1b_event_block: u32,
|
|
pub pm1a_control_block: u32,
|
|
pub pm1b_control_block: u32,
|
|
pub pm2_control_block: u32,
|
|
pub pm_timer_block: u32,
|
|
pub gpe0_block: u32,
|
|
pub gpe1_block: u32,
|
|
pub pm1_event_length: u8,
|
|
pub pm1_control_length: u8,
|
|
pub pm2_control_length: u8,
|
|
pub pm_timer_length: u8,
|
|
pub gpe0_ength: u8,
|
|
pub gpe1_length: u8,
|
|
pub gpe1_base: u8,
|
|
pub c_state_control: u8,
|
|
pub worst_c2_latency: u16,
|
|
pub worst_c3_latency: u16,
|
|
pub flush_size: u16,
|
|
pub flush_stride: u16,
|
|
pub duty_offset: u8,
|
|
pub duty_width: u8,
|
|
pub day_alarm: u8,
|
|
pub month_alarm: u8,
|
|
pub century: u8,
|
|
|
|
// reserved in ACPI 1.0; used since ACPI 2.0+
|
|
pub boot_architecture_flags: u16,
|
|
|
|
reserved2: u8,
|
|
pub flags: u32,
|
|
}
|
|
unsafe impl plain::Plain for FadtStruct {}
|
|
|
|
#[repr(C, packed)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct GenericAddressStructure {
|
|
address_space: u8,
|
|
bit_width: u8,
|
|
bit_offset: u8,
|
|
access_size: u8,
|
|
address: u64,
|
|
}
|
|
|
|
#[repr(C, packed)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FadtAcpi2Struct {
|
|
// 12 byte structure; see below for details
|
|
pub reset_reg: GenericAddressStructure,
|
|
|
|
pub reset_value: u8,
|
|
reserved3: [u8; 3],
|
|
|
|
// 64bit pointers - Available on ACPI 2.0+
|
|
pub x_firmware_control: u64,
|
|
pub x_dsdt: u64,
|
|
|
|
pub x_pm1a_event_block: GenericAddressStructure,
|
|
pub x_pm1b_event_block: GenericAddressStructure,
|
|
pub x_pm1a_control_block: GenericAddressStructure,
|
|
pub x_pm1b_control_block: GenericAddressStructure,
|
|
pub x_pm2_control_block: GenericAddressStructure,
|
|
pub x_pm_timer_block: GenericAddressStructure,
|
|
pub x_gpe0_block: GenericAddressStructure,
|
|
pub x_gpe1_block: GenericAddressStructure,
|
|
}
|
|
unsafe impl plain::Plain for FadtAcpi2Struct {}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Fadt(Sdt);
|
|
|
|
impl Fadt {
|
|
pub fn acpi_2_struct(&self) -> Option<&FadtAcpi2Struct> {
|
|
let bytes = &self.0 .0[mem::size_of::<FadtStruct>()..];
|
|
|
|
match plain::from_bytes::<FadtAcpi2Struct>(bytes) {
|
|
Ok(fadt2) => Some(fadt2),
|
|
Err(plain::Error::TooShort) => None,
|
|
Err(plain::Error::BadAlignment) => unreachable!(
|
|
"plain::from_bytes reported bad alignment, but FadtAcpi2Struct is #[repr(packed)]"
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Deref for Fadt {
|
|
type Target = FadtStruct;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
plain::from_bytes::<FadtStruct>(&self.0 .0)
|
|
.expect("expected FADT struct to already be validated in Deref impl")
|
|
}
|
|
}
|
|
|
|
impl Fadt {
|
|
pub fn new(sdt: Sdt) -> Option<Fadt> {
|
|
if sdt.signature != *b"FACP" || sdt.length() < mem::size_of::<Fadt>() {
|
|
return None;
|
|
}
|
|
Some(Fadt(sdt))
|
|
}
|
|
|
|
pub fn init(context: &mut AcpiContext) {
|
|
let fadt_sdt = context
|
|
.take_single_sdt(*b"FACP")
|
|
.expect("expected ACPI to always have a FADT");
|
|
|
|
let fadt = match Fadt::new(fadt_sdt) {
|
|
Some(fadt) => fadt,
|
|
None => {
|
|
log::error!("Failed to find FADT");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let dsdt_ptr = match fadt.acpi_2_struct() {
|
|
Some(fadt2) => usize::try_from(fadt2.x_dsdt).unwrap_or_else(|_| {
|
|
usize::try_from(fadt.dsdt).expect("expected any given u32 to fit within usize")
|
|
}),
|
|
None => usize::try_from(fadt.dsdt).expect("expected any given u32 to fit within usize"),
|
|
};
|
|
|
|
log::debug!("FACP at {:X}", { dsdt_ptr });
|
|
|
|
let dsdt_sdt = match Sdt::load_from_physical(fadt.dsdt as usize) {
|
|
Ok(dsdt) => dsdt,
|
|
Err(error) => {
|
|
log::error!("Failed to load DSDT: {}", error);
|
|
return;
|
|
}
|
|
};
|
|
|
|
context.fadt = Some(fadt.clone());
|
|
context.dsdt = Some(Dsdt(dsdt_sdt.clone()));
|
|
|
|
context.tables.push(dsdt_sdt);
|
|
}
|
|
}
|
|
|
|
pub enum PossibleAmlTables {
|
|
Dsdt(Dsdt),
|
|
Ssdt(Ssdt),
|
|
}
|
|
impl PossibleAmlTables {
|
|
pub fn try_new(inner: Sdt) -> Option<Self> {
|
|
match &inner.signature {
|
|
b"DSDT" => Some(Self::Dsdt(Dsdt(inner))),
|
|
b"SSDT" => Some(Self::Ssdt(Ssdt(inner))),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
impl AmlContainingTable for PossibleAmlTables {
|
|
fn aml(&self) -> &[u8] {
|
|
match self {
|
|
Self::Dsdt(dsdt) => dsdt.aml(),
|
|
Self::Ssdt(ssdt) => ssdt.aml(),
|
|
}
|
|
}
|
|
fn header(&self) -> &SdtHeader {
|
|
match self {
|
|
Self::Dsdt(dsdt) => dsdt.header(),
|
|
Self::Ssdt(ssdt) => ssdt.header(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait AmlContainingTable {
|
|
fn aml(&self) -> &[u8];
|
|
fn header(&self) -> &SdtHeader;
|
|
}
|
|
|
|
impl<T> AmlContainingTable for &T
|
|
where
|
|
T: AmlContainingTable,
|
|
{
|
|
fn aml(&self) -> &[u8] {
|
|
T::aml(*self)
|
|
}
|
|
fn header(&self) -> &SdtHeader {
|
|
T::header(*self)
|
|
}
|
|
}
|
|
|
|
impl AmlContainingTable for Dsdt {
|
|
fn aml(&self) -> &[u8] {
|
|
self.0.data()
|
|
}
|
|
fn header(&self) -> &SdtHeader {
|
|
&*self.0
|
|
}
|
|
}
|
|
impl AmlContainingTable for Ssdt {
|
|
fn aml(&self) -> &[u8] {
|
|
self.0.data()
|
|
}
|
|
fn header(&self) -> &SdtHeader {
|
|
&*self.0
|
|
}
|
|
}
|