Files
RedBear-OS/drivers/acpid/src/acpi.rs
T
Red Bear OS c335553c7e acpid: add /scheme/acpi/processor/ route + cpu_names() (Phase G.6)
On the LG Gram 2025 (Core Ultra 7 255H, Arrow Lake-H) the firmware
exposes ACPI processor objects under \_PR.CPU0..\_PR.CPU15 along
with full _PSS, _PSD, _CST, and _CPC objects. The HWP-aware
cpufreqd (Phase G.2) reads these to discover the P-state range
and the HWP activity window. Before this commit acpid exposed
nothing at /scheme/acpi/processor — cpufreqd was falling back
to its hardcoded 4-state table (2400/2000/1600/1200 kHz) on every
system including Arrow Lake.

This commit adds:

1. AcpiContext::cpu_names() — walks the symbol cache and returns
   direct child names of \_PR whose serialized form is a Processor
   object. Matches on the \_PR.<name> prefix (no further dots) to
   avoid returning sub-objects like \_PR.CPU0._PSS.

2. HandleKind::Processor variant for the /scheme/acpi/processor/
   directory and HandleKind::ProcFile for the per-CPU files. Adds
   the ProcFileKind enum (Pss, Psd, Cst, Cpc) so the scheme can
   route each file to its own data source.

3. kopenat() route for /scheme/acpi/processor/<cpu>/<file>
   where <file> ∈ {pss, psd, cst, cpc}. Path-component match
   extended to 4 elements (was 3); cpu_id parsed as u32.

4. getdents() entry for HandleKind::Processor using
   self.ctx.cpu_names() — matches the same pattern as Thermal
   and Power. getdents() also covers ProcFile and DmiDir (no
   children; reads/writes go through kread/kwriteoff).

5. kread() entry for HandleKind::ProcFile returns a placeholder
   "ACPI processor data not yet populated" line so consumers
   (cpufreqd, redbear-power) can detect the path is present and
   report "no data" instead of getting ENOENT. The full AML-to-
   text conversion for _PSS / _PSD / _CST / _CPC is a follow-up
   that walks the AML namespace and emits the canonical cpufreq
   text format ("freq power latency control").

6. kread() also covers HandleKind::Processor and HandleKind::DmiDir
   with EISDIR — they are directory types, not file types.

The acpid version remains at 0.1.0 — the policy in AGENTS.md
("In-house crate versioning") classifies local/sources/base/ as
an Upstream Redox fork and keeps upstream versioning. Phase G.6
adds infrastructure only, not a version bump.

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 14:40. QEMU mini boot reaches Red Bear
login: as before. The /scheme/acpi/processor/ path is now
present and read returns the placeholder line.
2026-06-30 14:41:16 +03:00

1202 lines
40 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>,
/// Decoded FACS (Firmware ACPI Control Structure). Contains the
/// `firmware_ctrl` block and the 32-bit `firmware_waking_vector`
/// used for S3 resume. `None` if no FACS table is present.
facs: Option<Facs>,
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,
facs: None,
};
for table in &this.tables {
this.new_index(&table.signature());
}
// FACS (Firmware ACPI Control Structure) — points to the
// firmware_waking_vector used for S3 resume and the firmware
// control block. Located via the FADT's x_firmware_control /
// firmware_control fields. Optional: ACPI 1.0 systems without
// S3 support have no FACS, and even systems with S3 may omit
// it. The kernel-side S3 path uses the wakeup vector if
// present.
this.facs = this.take_single_sdt(*b"FACS").and_then(Facs::new);
// 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()
}
/// Access the parsed FACS (Firmware ACPI Control Structure), if
/// present. Returns `None` if no FACS table was found in the RSDT/XSDT.
pub fn facs(&self) -> Option<&Facs> {
self.facs.as_ref()
}
/// Returns the FACS 32-bit firmware_waking_vector, or `None` if no
/// FACS is present. The kernel-side S3 entry path uses this to
/// know where to resume execution after the BIOS wakes the system.
pub fn acpi_waking_vector(&self) -> Option<u32> {
self.facs.as_ref().map(|f| f.waking_vector())
}
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
}
/// Enumerate CPU names under `\_PR` (the AML processor hierarchy).
/// Returns direct child names whose serialized form is a
/// `Processor` object (e.g. "\_PR.CPU0", "\_PR.CPU1"). Returns an
/// empty Vec if no processor objects are present in the AML.
pub fn cpu_names(&self) -> Vec<String> {
let mut cpus = Vec::new();
let Ok(aml_symbols) = self.aml_symbols(None) else {
return cpus;
};
for (name, value) in aml_symbols.symbols_cache().iter() {
// Direct children of \_PR have the form "\_PR.CPU0" with
// no further dots. Match on Processor-serialized form to
// filter out non-CPU objects in the \_PR subtree.
if value.contains("Processor(") {
if let Some(rest) = name.strip_prefix("\\_PR.") {
if !rest.contains('.') {
cpus.push(name.clone());
}
}
}
}
cpus
}
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);
}
}
/// FACS (Firmware ACPI Control Structure)
///
/// The FACS holds the 32-bit `firmware_waking_vector` that the BIOS
/// jumps to after a wake event (S3 resume). The kernel reads this on
/// resume to restore kernel state.
///
/// See ACPI 6.5 spec §5.2.10 "Firmware ACPI Control Structure".
///
/// Note: this struct describes a subset of FACS — we only model fields
/// the kernel needs (the 32-bit waking_vector). The full FACS table
/// is much larger; we read only what we need from the SDT bytes.
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct FacsStruct {
pub header: SdtHeader,
pub hardware_signature: u32,
pub waking_vector: u32,
pub global_lock: u32,
pub flags: u32,
pub x_waking_vector: u64,
pub version: u8,
reserved: [u8; 3],
}
unsafe impl plain::Plain for FacsStruct {}
#[derive(Clone, Debug)]
pub struct Facs(Sdt);
impl Facs {
pub fn new(sdt: Sdt) -> Option<Facs> {
if sdt.signature != *b"FACS" {
return None;
}
if sdt.length() < 32 {
// FACS has at minimum the 36-byte header; the table is
// valid only if it has the SdtHeader and at least the
// waking_vector (4 bytes after the 32-byte header).
log::warn!("FACS table too small ({} B)", sdt.length());
return None;
}
Some(Facs(sdt))
}
/// 32-bit firmware waking vector (used for S3 resume).
/// The kernel sets this to the resume trampoline before S3 entry.
pub fn waking_vector(&self) -> u32 {
let facs: &FacsStruct = plain::from_bytes(&self.0 .0)
.expect("FACS already validated in new()");
facs.waking_vector
}
}
impl Deref for Facs {
type Target = FacsStruct;
fn deref(&self) -> &Self::Target {
plain::from_bytes::<FacsStruct>(&self.0 .0)
.expect("FACS struct to already be validated in Deref impl")
}
}
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
}
}