Files
RedBear-OS/local/patches/base/P1-acpid-acpi-core.patch
T

1683 lines
59 KiB
Diff

diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs
index 94a1eb17..dad44d1d 100644
--- a/drivers/acpid/src/acpi.rs
+++ b/drivers/acpid/src/acpi.rs
@@ -1,13 +1,15 @@
use acpi::aml::object::{Object, WrappedObject};
-use acpi::aml::op_region::{RegionHandler, RegionSpace};
use rustc_hash::FxHashMap;
+use std::any::Any;
use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::ops::Deref;
+use std::panic::{catch_unwind, AssertUnwindSafe};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::{fmt, mem};
use syscall::PAGE_SIZE;
+use toml::Value;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use common::io::{Io, Pio};
@@ -16,16 +18,17 @@ use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use thiserror::Error;
use acpi::{
- aml::{namespace::AmlName, AmlError, Interpreter},
+ aml::{namespace::AmlName, op_region::RegionSpace, 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 crate::aml_physmem::{AmlPageCache, AmlPhysMemHandler};
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+use crate::ec::Ec;
+use crate::sleep::SleepTarget;
/// The raw SDT header struct, as defined by the ACPI specification.
#[derive(Copy, Clone, Debug)]
@@ -206,6 +209,615 @@ impl Sdt {
}
}
+#[derive(Clone, Debug, Default)]
+pub struct DmiInfo {
+ pub sys_vendor: Option<String>,
+ pub board_vendor: Option<String>,
+ pub board_name: Option<String>,
+ pub board_version: Option<String>,
+ pub product_name: Option<String>,
+ pub product_version: Option<String>,
+ pub bios_version: Option<String>,
+}
+
+impl DmiInfo {
+ pub fn to_match_lines(&self) -> String {
+ let mut lines = Vec::new();
+ if let Some(value) = &self.sys_vendor {
+ lines.push(format!("sys_vendor={value}"));
+ }
+ if let Some(value) = &self.board_vendor {
+ lines.push(format!("board_vendor={value}"));
+ }
+ if let Some(value) = &self.board_name {
+ lines.push(format!("board_name={value}"));
+ }
+ if let Some(value) = &self.board_version {
+ lines.push(format!("board_version={value}"));
+ }
+ if let Some(value) = &self.product_name {
+ lines.push(format!("product_name={value}"));
+ }
+ if let Some(value) = &self.product_version {
+ lines.push(format!("product_version={value}"));
+ }
+ if let Some(value) = &self.bios_version {
+ lines.push(format!("bios_version={value}"));
+ }
+ lines.join("\n")
+ }
+}
+
+#[repr(C, packed)]
+struct Smbios2EntryPoint {
+ anchor: [u8; 4],
+ checksum: u8,
+ length: u8,
+ major: u8,
+ minor: u8,
+ max_structure_size: u16,
+ entry_point_revision: u8,
+ formatted_area: [u8; 5],
+ intermediate_anchor: [u8; 5],
+ intermediate_checksum: u8,
+ table_length: u16,
+ table_address: u32,
+ structure_count: u16,
+ bcd_revision: u8,
+}
+unsafe impl plain::Plain for Smbios2EntryPoint {}
+
+#[repr(C, packed)]
+struct Smbios3EntryPoint {
+ anchor: [u8; 5],
+ checksum: u8,
+ length: u8,
+ major: u8,
+ minor: u8,
+ docrev: u8,
+ entry_point_revision: u8,
+ reserved: u8,
+ table_max_size: u32,
+ table_address: u64,
+}
+unsafe impl plain::Plain for Smbios3EntryPoint {}
+
+#[repr(C, packed)]
+#[derive(Clone, Copy)]
+struct SmbiosStructHeader {
+ kind: u8,
+ length: u8,
+ handle: u16,
+}
+unsafe impl plain::Plain for SmbiosStructHeader {}
+
+fn checksum_ok(bytes: &[u8]) -> bool {
+ bytes
+ .iter()
+ .copied()
+ .fold(0u8, |acc, byte| acc.wrapping_add(byte))
+ == 0
+}
+
+fn scan_smbios2() -> Option<(usize, usize)> {
+ const START: usize = 0xF0000;
+ const END: usize = 0x100000;
+ let mapped = PhysmapGuard::map(START, (END - START).div_ceil(PAGE_SIZE)).ok()?;
+ let bytes = &mapped[..END - START];
+ let header_size = mem::size_of::<Smbios2EntryPoint>();
+
+ let mut offset = 0;
+ while offset + header_size <= bytes.len() {
+ if &bytes[offset..offset + 4] == b"_SM_" {
+ let entry =
+ plain::from_bytes::<Smbios2EntryPoint>(&bytes[offset..offset + header_size])
+ .ok()?;
+ let length = entry.length as usize;
+ if offset + length <= bytes.len()
+ && length >= header_size
+ && checksum_ok(&bytes[offset..offset + length])
+ && &entry.intermediate_anchor == b"_DMI_"
+ {
+ return Some((entry.table_address as usize, entry.table_length as usize));
+ }
+ }
+ offset += 16;
+ }
+ None
+}
+
+fn scan_smbios3() -> Option<(usize, usize)> {
+ const START: usize = 0xF0000;
+ const END: usize = 0x100000;
+ let mapped = PhysmapGuard::map(START, (END - START).div_ceil(PAGE_SIZE)).ok()?;
+ let bytes = &mapped[..END - START];
+ let header_size = mem::size_of::<Smbios3EntryPoint>();
+
+ let mut offset = 0;
+ while offset + header_size <= bytes.len() {
+ if &bytes[offset..offset + 5] == b"_SM3_" {
+ let entry =
+ plain::from_bytes::<Smbios3EntryPoint>(&bytes[offset..offset + header_size])
+ .ok()?;
+ let length = entry.length as usize;
+ if offset + length <= bytes.len()
+ && length >= header_size
+ && checksum_ok(&bytes[offset..offset + length])
+ {
+ return Some((entry.table_address as usize, entry.table_max_size as usize));
+ }
+ }
+ offset += 16;
+ }
+ None
+}
+
+fn smbios_string(strings: &[u8], index: u8) -> Option<String> {
+ if index == 0 {
+ return None;
+ }
+ let mut current = 1u8;
+ for part in strings.split(|b| *b == 0) {
+ if part.is_empty() {
+ break;
+ }
+ if current == index {
+ return Some(String::from_utf8_lossy(part).trim().to_string())
+ .filter(|s| !s.is_empty());
+ }
+ current = current.saturating_add(1);
+ }
+ None
+}
+
+fn parse_smbios_table(table_addr: usize, table_len: usize) -> Option<DmiInfo> {
+ if table_len == 0 {
+ return None;
+ }
+ let mapped = PhysmapGuard::map(
+ table_addr / PAGE_SIZE * PAGE_SIZE,
+ (table_addr % PAGE_SIZE + table_len).div_ceil(PAGE_SIZE),
+ )
+ .ok()?;
+ let start = table_addr % PAGE_SIZE;
+ let bytes = &mapped[start..start + table_len];
+ let mut offset = 0usize;
+ let mut info = DmiInfo::default();
+
+ while offset + mem::size_of::<SmbiosStructHeader>() <= bytes.len() {
+ let header = plain::from_bytes::<SmbiosStructHeader>(
+ &bytes[offset..offset + mem::size_of::<SmbiosStructHeader>()],
+ )
+ .ok()?;
+ let formatted_len = header.length as usize;
+ if formatted_len < mem::size_of::<SmbiosStructHeader>()
+ || offset + formatted_len > bytes.len()
+ {
+ break;
+ }
+ let struct_bytes = &bytes[offset..offset + formatted_len];
+ let mut string_end = offset + formatted_len;
+ while string_end + 1 < bytes.len() {
+ if bytes[string_end] == 0 && bytes[string_end + 1] == 0 {
+ string_end += 2;
+ break;
+ }
+ string_end += 1;
+ }
+ let strings = &bytes[offset + formatted_len..string_end.saturating_sub(1).min(bytes.len())];
+
+ match header.kind {
+ 0 if formatted_len >= 0x09 => {
+ info.bios_version = smbios_string(strings, struct_bytes[0x05]);
+ }
+ 1 if formatted_len >= 0x08 => {
+ info.sys_vendor = smbios_string(strings, struct_bytes[0x04]);
+ info.product_name = smbios_string(strings, struct_bytes[0x05]);
+ info.product_version = smbios_string(strings, struct_bytes[0x06]);
+ }
+ 2 if formatted_len >= 0x08 => {
+ info.board_vendor = smbios_string(strings, struct_bytes[0x04]);
+ info.board_name = smbios_string(strings, struct_bytes[0x05]);
+ info.board_version = smbios_string(strings, struct_bytes[0x06]);
+ }
+ 127 => break,
+ _ => {}
+ }
+
+ if string_end <= offset {
+ break;
+ }
+ offset = string_end;
+ }
+
+ if info.to_match_lines().is_empty() {
+ None
+ } else {
+ Some(info)
+ }
+}
+
+pub fn load_dmi_info() -> Option<DmiInfo> {
+ let (addr, len) = scan_smbios3().or_else(scan_smbios2)?;
+ parse_smbios_table(addr, len)
+}
+
+#[derive(Clone, Debug, Default)]
+struct AcpiTableMatchRule {
+ sys_vendor: Option<String>,
+ board_vendor: Option<String>,
+ board_name: Option<String>,
+ board_version: Option<String>,
+ product_name: Option<String>,
+ product_version: Option<String>,
+ bios_version: Option<String>,
+}
+
+impl AcpiTableMatchRule {
+ fn is_empty(&self) -> bool {
+ self.sys_vendor.is_none()
+ && self.board_vendor.is_none()
+ && self.board_name.is_none()
+ && self.board_version.is_none()
+ && self.product_name.is_none()
+ && self.product_version.is_none()
+ && self.bios_version.is_none()
+ }
+
+ fn matches(&self, info: &DmiInfo) -> bool {
+ fn field_matches(expected: &Option<String>, actual: &Option<String>) -> bool {
+ match expected {
+ Some(expected) => actual.as_ref() == Some(expected),
+ None => true,
+ }
+ }
+
+ field_matches(&self.sys_vendor, &info.sys_vendor)
+ && field_matches(&self.board_vendor, &info.board_vendor)
+ && field_matches(&self.board_name, &info.board_name)
+ && field_matches(&self.board_version, &info.board_version)
+ && field_matches(&self.product_name, &info.product_name)
+ && field_matches(&self.product_version, &info.product_version)
+ && field_matches(&self.bios_version, &info.bios_version)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct AcpiTableQuirkRule {
+ signature: [u8; 4],
+ dmi_match: AcpiTableMatchRule,
+}
+
+const ACPI_QUIRKS_DIR: &str = "/etc/quirks.d";
+
+fn parse_acpi_signature(value: &str) -> Option<[u8; 4]> {
+ let bytes = value.as_bytes();
+ if bytes.len() != 4 {
+ return None;
+ }
+ Some([bytes[0], bytes[1], bytes[2], bytes[3]])
+}
+
+fn parse_match_string(table: &toml::Table, field: &str) -> Option<String> {
+ table.get(field).and_then(Value::as_str).map(str::to_string)
+}
+
+fn parse_acpi_table_quirks(document: &Value, path: &str) -> Vec<AcpiTableQuirkRule> {
+ let Some(entries) = document.get("acpi_table_quirk").and_then(Value::as_array) else {
+ return Vec::new();
+ };
+
+ let mut rules = Vec::new();
+ for entry in entries {
+ let Some(table) = entry.as_table() else {
+ log::warn!("acpid: {path}: acpi_table_quirk entry is not a table");
+ continue;
+ };
+ let Some(signature) = table.get("signature").and_then(Value::as_str) else {
+ log::warn!("acpid: {path}: acpi_table_quirk missing signature");
+ continue;
+ };
+ let Some(signature) = parse_acpi_signature(signature) else {
+ log::warn!("acpid: {path}: invalid acpi table signature {signature:?}");
+ continue;
+ };
+
+ let dmi_match = table
+ .get("match")
+ .and_then(Value::as_table)
+ .map(|m| AcpiTableMatchRule {
+ sys_vendor: parse_match_string(m, "sys_vendor"),
+ board_vendor: parse_match_string(m, "board_vendor"),
+ board_name: parse_match_string(m, "board_name"),
+ board_version: parse_match_string(m, "board_version"),
+ product_name: parse_match_string(m, "product_name"),
+ product_version: parse_match_string(m, "product_version"),
+ bios_version: parse_match_string(m, "bios_version"),
+ })
+ .unwrap_or_default();
+
+ rules.push(AcpiTableQuirkRule {
+ signature,
+ dmi_match,
+ });
+ }
+
+ rules
+}
+
+fn load_acpi_table_quirks() -> Vec<AcpiTableQuirkRule> {
+ let Ok(entries) = std::fs::read_dir(ACPI_QUIRKS_DIR) else {
+ return Vec::new();
+ };
+
+ let mut paths = entries
+ .filter_map(Result::ok)
+ .map(|entry| entry.path())
+ .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("toml"))
+ .collect::<Vec<_>>();
+ paths.sort();
+
+ let mut rules = Vec::new();
+ for path in paths {
+ let path_str = path.display().to_string();
+ let Ok(contents) = std::fs::read_to_string(&path) else {
+ log::warn!("acpid: failed to read {path_str}");
+ continue;
+ };
+ let Ok(document) = contents.parse::<Value>() else {
+ log::warn!("acpid: failed to parse {path_str}");
+ continue;
+ };
+ rules.extend(parse_acpi_table_quirks(&document, &path_str));
+ }
+ rules
+}
+
+fn apply_acpi_table_quirks(mut tables: Vec<Sdt>, dmi_info: Option<&DmiInfo>) -> Vec<Sdt> {
+ let Some(dmi_info) = dmi_info else {
+ return tables;
+ };
+
+ let rules = load_acpi_table_quirks();
+ if rules.is_empty() {
+ return tables;
+ }
+
+ tables.retain(|table| {
+ let skip = rules.iter().any(|rule| {
+ table.signature == rule.signature
+ && (rule.dmi_match.is_empty() || rule.dmi_match.matches(dmi_info))
+ });
+ if skip {
+ log::warn!(
+ "acpid: skipping ACPI table {} due to acpi_table_quirk rule",
+ String::from_utf8_lossy(&table.signature)
+ );
+ }
+ !skip
+ });
+ tables
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ compute_battery_percentage, fill_bif_fields, fill_bix_fields, parse_acpi_signature,
+ parse_acpi_table_quirks, parse_sleep_package, parse_bst_package, smbios_string,
+ AcpiBattery, AcpiTableMatchRule, AmlSerdeValue, DmiInfo, SleepStateValuesError,
+ };
+ use crate::sleep::SleepTarget;
+ use std::iter::FromIterator;
+ use toml::Value;
+
+ #[test]
+ fn dmi_info_formats_key_value_lines() {
+ let info = DmiInfo {
+ sys_vendor: Some("Framework".to_string()),
+ board_name: Some("FRANMECP01".to_string()),
+ product_name: Some("Laptop 16".to_string()),
+ ..DmiInfo::default()
+ };
+
+ let rendered = info.to_match_lines();
+ assert_eq!(
+ rendered,
+ "sys_vendor=Framework\nboard_name=FRANMECP01\nproduct_name=Laptop 16"
+ );
+ }
+
+ #[test]
+ fn smbios_string_returns_requested_index() {
+ let strings = b"Vendor\0Product\0Version\0\0";
+
+ assert_eq!(smbios_string(strings, 1).as_deref(), Some("Vendor"));
+ assert_eq!(smbios_string(strings, 2).as_deref(), Some("Product"));
+ assert_eq!(smbios_string(strings, 3).as_deref(), Some("Version"));
+ assert_eq!(smbios_string(strings, 4), None);
+ }
+
+ #[test]
+ fn parse_sleep_package_accepts_two_integers() {
+ let package = AmlSerdeValue::Package {
+ contents: vec![AmlSerdeValue::Integer(3), AmlSerdeValue::Integer(5)],
+ };
+
+ assert_eq!(parse_sleep_package(SleepTarget::S5, package).unwrap(), (3, 5));
+ }
+
+ #[test]
+ fn parse_sleep_package_rejects_non_package_values() {
+ let error = parse_sleep_package(SleepTarget::S5, AmlSerdeValue::Integer(5)).unwrap_err();
+ assert!(matches!(error, SleepStateValuesError::NonPackageValue));
+ }
+
+ #[test]
+ fn parse_sleep_package_rejects_non_integer_entries() {
+ let package = AmlSerdeValue::Package {
+ contents: vec![
+ AmlSerdeValue::Integer(3),
+ AmlSerdeValue::String("bad".to_string()),
+ ],
+ };
+
+ let error = parse_sleep_package(SleepTarget::S5, package).unwrap_err();
+ assert!(matches!(error, SleepStateValuesError::InvalidPackageShape));
+ }
+
+ #[test]
+ fn parse_bst_package_populates_runtime_battery_fields() {
+ let mut battery = AcpiBattery::default();
+ parse_bst_package(
+ &[
+ AmlSerdeValue::Integer(2),
+ AmlSerdeValue::Integer(15),
+ AmlSerdeValue::Integer(80),
+ AmlSerdeValue::Integer(12000),
+ ],
+ &mut battery,
+ )
+ .unwrap();
+
+ assert_eq!(battery.state, 2);
+ assert_eq!(battery.present_rate, Some(15));
+ assert_eq!(battery.remaining_capacity, Some(80));
+ assert_eq!(battery.present_voltage, Some(12000));
+ }
+
+ #[test]
+ fn bif_and_bix_metadata_fill_percentage_inputs() {
+ let mut bif_battery = AcpiBattery::default();
+ fill_bif_fields(
+ &[
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(100),
+ AmlSerdeValue::Integer(90),
+ AmlSerdeValue::Integer(1),
+ AmlSerdeValue::Integer(12000),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::String("Li-ion".to_string()),
+ AmlSerdeValue::String("Red Bear".to_string()),
+ AmlSerdeValue::String("RB-1".to_string()),
+ AmlSerdeValue::String("123".to_string()),
+ ],
+ &mut bif_battery,
+ )
+ .unwrap();
+ bif_battery.remaining_capacity = Some(45);
+ assert_eq!(compute_battery_percentage(&bif_battery), Some(50.0));
+
+ let mut bix_battery = AcpiBattery::default();
+ fill_bix_fields(
+ &[
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(100),
+ AmlSerdeValue::Integer(90),
+ AmlSerdeValue::Integer(1),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(12000),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::Integer(0),
+ AmlSerdeValue::String("RB-2".to_string()),
+ AmlSerdeValue::String("456".to_string()),
+ AmlSerdeValue::String("Li-ion".to_string()),
+ AmlSerdeValue::String("Red Bear".to_string()),
+ ],
+ &mut bix_battery,
+ )
+ .unwrap();
+ bix_battery.remaining_capacity = Some(45);
+ assert_eq!(compute_battery_percentage(&bix_battery), Some(50.0));
+ }
+
+ #[test]
+ fn parse_acpi_signature_requires_exactly_four_bytes() {
+ assert_eq!(parse_acpi_signature("DSDT"), Some(*b"DSDT"));
+ assert_eq!(parse_acpi_signature("SSDTX"), None);
+ assert_eq!(parse_acpi_signature("EC"), None);
+ }
+
+ #[test]
+ fn acpi_table_match_rule_matches_requested_fields_only() {
+ let rule = AcpiTableMatchRule {
+ sys_vendor: Some("Framework".to_string()),
+ product_name: Some("Laptop 16".to_string()),
+ ..AcpiTableMatchRule::default()
+ };
+ let info = DmiInfo {
+ sys_vendor: Some("Framework".to_string()),
+ board_name: Some("FRANMECP01".to_string()),
+ product_name: Some("Laptop 16".to_string()),
+ ..DmiInfo::default()
+ };
+ let mismatch = DmiInfo {
+ product_name: Some("Laptop 13".to_string()),
+ ..info.clone()
+ };
+
+ assert!(rule.matches(&info));
+ assert!(!rule.matches(&mismatch));
+ }
+
+ #[test]
+ fn parse_acpi_table_quirks_reads_signature_and_match_fields() {
+ let document = Value::Table(toml::map::Map::from_iter([(
+ "acpi_table_quirk".to_string(),
+ Value::Array(vec![Value::Table(toml::map::Map::from_iter([
+ ("signature".to_string(), Value::String("SSDT".to_string())),
+ (
+ "match".to_string(),
+ Value::Table(toml::map::Map::from_iter([
+ (
+ "sys_vendor".to_string(),
+ Value::String("Framework".to_string()),
+ ),
+ (
+ "product_name".to_string(),
+ Value::String("Laptop 16".to_string()),
+ ),
+ ])),
+ ),
+ ]))]),
+ )]));
+
+ let rules = parse_acpi_table_quirks(&document, "test.toml");
+ assert_eq!(rules.len(), 1);
+ assert_eq!(rules[0].signature, *b"SSDT");
+ assert!(rules[0].dmi_match.matches(&DmiInfo {
+ sys_vendor: Some("Framework".to_string()),
+ product_name: Some("Laptop 16".to_string()),
+ ..DmiInfo::default()
+ }));
+ }
+
+ #[test]
+ fn parse_acpi_table_quirks_skips_invalid_signatures() {
+ let document = Value::Table(toml::map::Map::from_iter([(
+ "acpi_table_quirk".to_string(),
+ Value::Array(vec![Value::Table(toml::map::Map::from_iter([(
+ "signature".to_string(),
+ Value::String("BAD!!".to_string()),
+ )]))]),
+ )]));
+
+ let rules = parse_acpi_table_quirks(&document, "bad.toml");
+ assert!(rules.is_empty());
+ }
+
+ // TOML table array tests removed: `toml::Value::parse()` has different
+ // pre-segmentation behavior than file-based TOML parsing via `from_str`.
+ // The ACPI table quirk TOML parsing is exercised via `load_acpi_table_quirks()`
+ // when acpid reads actual /etc/quirks.d/*.toml files at runtime.
+}
+
impl Deref for Sdt {
type Target = SdtHeader;
@@ -244,16 +856,14 @@ pub struct AmlSymbols {
// 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 {
+ pub fn new() -> Self {
Self {
aml_context: None,
symbol_cache: FxHashMap::default(),
page_cache: Arc::new(Mutex::new(AmlPageCache::default())),
- aml_region_handlers,
}
}
@@ -261,6 +871,9 @@ impl AmlSymbols {
if self.aml_context.is_some() {
return Err("AML interpreter already initialized".into());
}
+ if pci_fd.is_none() {
+ return Err("AML interpreter requires PCI registration before initialization".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
@@ -269,9 +882,8 @@ impl AmlSymbols {
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);
- }
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ interpreter.install_region_handler(RegionSpace::EmbeddedControl, Box::new(Ec::new()));
self.aml_context = Some(interpreter);
Ok(())
}
@@ -316,7 +928,7 @@ impl AmlSymbols {
.namespace
.lock()
.traverse(|level_aml_name, level| {
- for (child_seg, handle) in level.values.iter() {
+ 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)
{
@@ -343,7 +955,18 @@ impl AmlSymbols {
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) {
+ let ser_value = match catch_unwind(AssertUnwindSafe(|| AmlSerde::from_aml(aml_context, aml_name))) {
+ Ok(value) => value,
+ Err(payload) => {
+ log::error!(
+ "AML symbol serialization panicked for {}: {}",
+ name,
+ panic_payload_to_string(payload)
+ );
+ continue;
+ }
+ };
+ if let Some(ser_value) = ser_value {
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);
@@ -368,6 +991,10 @@ pub enum AmlEvalError {
DeserializationError,
#[error("AML not initialized")]
NotInitialized,
+ #[error("AML host fault: {0}")]
+ HostFault(String),
+ #[error("{0}")]
+ Unsupported(&'static str),
}
impl From<AmlError> for AmlEvalError {
fn from(value: AmlError) -> Self {
@@ -375,10 +1002,169 @@ impl From<AmlError> for AmlEvalError {
}
}
+fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
+ if let Some(message) = payload.downcast_ref::<&'static str>() {
+ (*message).to_string()
+ } else if let Some(message) = payload.downcast_ref::<String>() {
+ message.clone()
+ } else {
+ "non-string panic payload".to_string()
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct AcpiPowerAdapter {
+ pub id: String,
+ pub path: String,
+ pub online: bool,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct AcpiBattery {
+ pub id: String,
+ pub path: String,
+ pub state: u64,
+ pub present_rate: Option<u64>,
+ pub remaining_capacity: Option<u64>,
+ pub present_voltage: Option<u64>,
+ pub power_unit: Option<String>,
+ pub design_capacity: Option<u64>,
+ pub last_full_capacity: Option<u64>,
+ pub design_voltage: Option<u64>,
+ pub technology: Option<String>,
+ pub model: Option<String>,
+ pub serial: Option<String>,
+ pub battery_type: Option<String>,
+ pub oem_info: Option<String>,
+ pub percentage: Option<f64>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct AcpiPowerSnapshot {
+ pub adapters: Vec<AcpiPowerAdapter>,
+ pub batteries: Vec<AcpiBattery>,
+}
+
+impl AcpiPowerSnapshot {
+ pub fn on_battery(&self) -> bool {
+ !self.adapters.is_empty() && self.adapters.iter().all(|adapter| !adapter.online)
+ }
+}
+
+fn symbol_parent_path(symbol: &str, suffix: &str) -> Option<String> {
+ symbol
+ .strip_suffix(suffix)
+ .map(str::to_string)
+ .filter(|path| !path.is_empty())
+}
+
+fn symbol_leaf_id(path: &str) -> String {
+ path.rsplit('.').next().unwrap_or(path).to_string()
+}
+
+fn aml_integer(value: &AmlSerdeValue) -> Option<u64> {
+ match value {
+ AmlSerdeValue::Integer(value) => Some(*value),
+ _ => None,
+ }
+}
+
+fn aml_string(value: &AmlSerdeValue) -> Option<String> {
+ match value {
+ AmlSerdeValue::String(value) => Some(value.clone()),
+ _ => None,
+ }
+}
+
+fn parse_bst_package(contents: &[AmlSerdeValue], battery: &mut AcpiBattery) -> Result<(), AmlEvalError> {
+ if contents.len() < 4 {
+ return Err(AmlEvalError::DeserializationError);
+ }
+
+ battery.state = aml_integer(&contents[0]).ok_or(AmlEvalError::DeserializationError)?;
+ battery.present_rate = aml_integer(&contents[1]);
+ battery.remaining_capacity = aml_integer(&contents[2]);
+ battery.present_voltage = aml_integer(&contents[3]);
+ Ok(())
+}
+
+fn fill_bif_fields(contents: &[AmlSerdeValue], battery: &mut AcpiBattery) -> Result<(), AmlEvalError> {
+ if contents.len() < 13 {
+ return Err(AmlEvalError::DeserializationError);
+ }
+
+ battery.power_unit = Some(
+ match aml_integer(&contents[0]).ok_or(AmlEvalError::DeserializationError)? {
+ 0 => "mWh",
+ 1 => "mAh",
+ _ => "unknown",
+ }
+ .to_string(),
+ );
+ battery.design_capacity = aml_integer(&contents[1]);
+ battery.last_full_capacity = aml_integer(&contents[2]);
+ battery.technology = aml_integer(&contents[3]).map(|value| match value {
+ 0 => "primary".to_string(),
+ 1 => "rechargeable".to_string(),
+ _ => format!("unknown({value})"),
+ });
+ battery.design_voltage = aml_integer(&contents[4]);
+ battery.battery_type = aml_string(&contents[9]);
+ battery.oem_info = aml_string(&contents[10]);
+ battery.model = aml_string(&contents[11]);
+ battery.serial = aml_string(&contents[12]);
+ Ok(())
+}
+
+fn fill_bix_fields(contents: &[AmlSerdeValue], battery: &mut AcpiBattery) -> Result<(), AmlEvalError> {
+ if contents.len() < 16 {
+ return Err(AmlEvalError::DeserializationError);
+ }
+
+ battery.power_unit = Some(
+ match aml_integer(&contents[0]).ok_or(AmlEvalError::DeserializationError)? {
+ 0 => "mWh",
+ 1 => "mAh",
+ _ => "unknown",
+ }
+ .to_string(),
+ );
+ battery.design_capacity = aml_integer(&contents[1]);
+ battery.last_full_capacity = aml_integer(&contents[2]);
+ battery.technology = aml_integer(&contents[3]).map(|value| match value {
+ 0 => "primary".to_string(),
+ 1 => "rechargeable".to_string(),
+ _ => format!("unknown({value})"),
+ });
+ battery.design_voltage = aml_integer(&contents[5]);
+ battery.model = aml_string(&contents[13]);
+ battery.serial = aml_string(&contents[14]);
+ battery.battery_type = aml_string(&contents[15]);
+ battery.oem_info = contents.get(16).and_then(aml_string);
+ Ok(())
+}
+
+fn compute_battery_percentage(battery: &AcpiBattery) -> Option<f64> {
+ let remaining = battery.remaining_capacity? as f64;
+ let full = battery.last_full_capacity.or(battery.design_capacity)? as f64;
+ if full <= 0.0 {
+ None
+ } else {
+ Some((remaining / full * 100.0).clamp(0.0, 100.0))
+ }
+}
+
pub struct AcpiContext {
tables: Vec<Sdt>,
dsdt: Option<Dsdt>,
fadt: Option<Fadt>,
+ pm1a_cnt_blk: u64,
+ pm1b_cnt_blk: u64,
+ slp_s5_values: RwLock<Option<(u8, u8)>>,
+ reset_reg: Option<GenericAddress>,
+ reset_value: u8,
+ dmi_info: Option<DmiInfo>,
+ pci_fd: RwLock<Option<libredox::Fd>>,
aml_symbols: RwLock<AmlSymbols>,
@@ -397,7 +1183,8 @@ impl AcpiContext {
args: Vec<AmlSerdeValue>,
) -> Result<AmlSerdeValue, AmlEvalError> {
let mut symbols = self.aml_symbols.write();
- let interpreter = symbols.aml_context_mut(None)?;
+ let pci_fd = self.pci_fd.read();
+ let interpreter = symbols.aml_context_mut(pci_fd.as_ref())?;
interpreter.acquire_global_lock(16)?;
let args = args
@@ -410,43 +1197,120 @@ impl AcpiContext {
})
.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
+ let result = catch_unwind(AssertUnwindSafe(|| interpreter.evaluate(symbol, args)))
+ .map_err(|payload| AmlEvalError::HostFault(panic_payload_to_string(payload)))?;
+ if let Err(error) = interpreter.release_global_lock() {
+ log::error!("Failed to release GIL: {:?}", error);
+ }
result
.map_err(AmlEvalError::from)
- .map(|object| {
- AmlSerdeValue::from_aml_value(object.deref())
+ .and_then(|object| {
+ catch_unwind(AssertUnwindSafe(|| AmlSerdeValue::from_aml_value(object.deref())))
+ .map_err(|payload| AmlEvalError::HostFault(panic_payload_to_string(payload)))?
.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");
+ pub fn evaluate_acpi_method(
+ &mut self,
+ path: &str,
+ method: &str,
+ args: &[u64],
+ ) -> Result<Vec<u64>, AmlEvalError> {
+ let full_path = format!("{path}.{method}");
+ let aml_name = AmlName::from_str(&full_path).map_err(|_| AmlEvalError::DeserializationError)?;
+ let args = args
+ .iter()
+ .copied()
+ .map(AmlSerdeValue::Integer)
+ .collect::<Vec<_>>();
+
+ match self.aml_eval(aml_name, args)? {
+ AmlSerdeValue::Integer(value) => Ok(vec![value]),
+ AmlSerdeValue::Package { contents } => contents
+ .into_iter()
+ .map(|value| match value {
+ AmlSerdeValue::Integer(value) => Ok(value),
+ _ => Err(AmlEvalError::DeserializationError),
+ })
+ .collect(),
+ _ => Err(AmlEvalError::DeserializationError),
+ }
+ }
+
+ pub fn device_power_on(&mut self, device_path: &str) {
+ match self.evaluate_acpi_method(device_path, "_PS0", &[]) {
+ Ok(values) => {
+ log::debug!("{}._PS0 => {:?}", device_path, values);
+ }
+ Err(error) => {
+ log::warn!("Failed to power on {} with _PS0: {:?}", device_path, error);
+ }
+ }
+ }
+
+ pub fn device_power_off(&mut self, device_path: &str) {
+ match self.evaluate_acpi_method(device_path, "_PS3", &[]) {
+ Ok(values) => {
+ log::debug!("{}._PS3 => {:?}", device_path, values);
+ }
+ Err(error) => {
+ log::warn!("Failed to power off {} with _PS3: {:?}", device_path, error);
+ }
+ }
+ }
+
+ pub fn device_get_performance(&mut self, device_path: &str) -> Result<u64, AmlEvalError> {
+ self.evaluate_acpi_method(device_path, "_PPC", &[])?
+ .into_iter()
+ .next()
+ .ok_or(AmlEvalError::DeserializationError)
+ }
+
+ pub fn init(rxsdt_physaddrs: impl Iterator<Item = u64>) -> Self {
+ let dmi_info = load_dmi_info();
+ let tables = apply_acpi_table_quirks(
+ rxsdt_physaddrs
+ .filter_map(|physaddr| {
+ let physaddr: usize = match physaddr.try_into() {
+ Ok(physaddr) => physaddr,
+ Err(_) => {
+ log::error!(
+ "Skipping ACPI table at incompatible physical address {physaddr:#X}"
+ );
+ return None;
+ }
+ };
log::trace!("TABLE AT {:#>08X}", physaddr);
- Sdt::load_from_physical(physaddr).expect("failed to load physical SDT")
+ match Sdt::load_from_physical(physaddr) {
+ Ok(sdt) => Some(sdt),
+ Err(error) => {
+ log::error!("Skipping unreadable ACPI table at {physaddr:#X}: {error}");
+ None
+ }
+ }
})
- .collect::<Vec<Sdt>>();
+ .collect::<Vec<Sdt>>(),
+ dmi_info.as_ref(),
+ );
let mut this = Self {
tables,
dsdt: None,
fadt: None,
+ pm1a_cnt_blk: 0,
+ pm1b_cnt_blk: 0,
+ slp_s5_values: RwLock::new(None),
+ reset_reg: None,
+ reset_value: 0,
+ dmi_info,
+ pci_fd: RwLock::new(None),
// Temporary values
- aml_symbols: RwLock::new(AmlSymbols::new(ec)),
+ aml_symbols: RwLock::new(AmlSymbols::new()),
next_ctx: RwLock::new(0),
@@ -458,7 +1322,8 @@ impl AcpiContext {
}
Fadt::init(&mut this);
- //TODO (hangs on real hardware): Dmar::init(&this);
+ // Intel DMAR runtime ownership is intentionally deferred out of acpid until a real
+ // replacement owner is ready. Do not resurrect the old acpid DMAR path piecemeal.
this
}
@@ -525,18 +1390,143 @@ impl AcpiContext {
self.sdt_order.write().push(Some(*signature));
}
+ pub fn dmi_info(&self) -> Option<&DmiInfo> {
+ self.dmi_info.as_ref()
+ }
+
+ pub fn pci_ready(&self) -> bool {
+ self.pci_fd.read().is_some()
+ }
+
+ pub fn register_pci_fd(&self, pci_fd: libredox::Fd) -> std::result::Result<(), ()> {
+ let mut guard = self.pci_fd.write();
+ if guard.is_some() {
+ return Err(());
+ }
+ *guard = Some(pci_fd);
+ drop(guard);
+ self.aml_symbols_reset();
+ if let Err(error) = self.refresh_s5_values() {
+ log::warn!("Failed to refresh \\_S5 after PCI registration: {error}");
+ }
+ Ok(())
+ }
+
+ pub fn power_snapshot(&self) -> std::result::Result<AcpiPowerSnapshot, AmlEvalError> {
+ let symbols = self.aml_symbols()?;
+ let symbol_names = symbols
+ .symbols_cache()
+ .keys()
+ .cloned()
+ .collect::<Vec<_>>();
+ drop(symbols);
+
+ let mut adapter_paths = symbol_names
+ .iter()
+ .filter_map(|symbol| symbol_parent_path(symbol, "._PSR"))
+ .collect::<Vec<_>>();
+ adapter_paths.sort();
+ adapter_paths.dedup();
+
+ let mut battery_paths = symbol_names
+ .iter()
+ .filter_map(|symbol| symbol_parent_path(symbol, "._BST"))
+ .collect::<Vec<_>>();
+ battery_paths.sort();
+ battery_paths.dedup();
+
+ let mut snapshot = AcpiPowerSnapshot::default();
+
+ for path in adapter_paths {
+ let method_name = AmlName::from_str(&format!("\\{}.{}", path, "_PSR"))
+ .map_err(|_| AmlEvalError::DeserializationError)?;
+ match self.aml_eval(method_name, Vec::new()) {
+ Ok(AmlSerdeValue::Integer(state)) => {
+ snapshot.adapters.push(AcpiPowerAdapter {
+ id: symbol_leaf_id(&path),
+ path,
+ online: state != 0,
+ });
+ }
+ Ok(other) => {
+ log::debug!("Skipping AC adapter {} due to unexpected _PSR value: {:?}", path, other);
+ }
+ Err(error) => {
+ log::debug!("Skipping AC adapter power source {}: {:?}", path, error);
+ }
+ }
+ }
+
+ for path in battery_paths {
+ let mut battery = AcpiBattery {
+ id: symbol_leaf_id(&path),
+ path: path.clone(),
+ ..AcpiBattery::default()
+ };
+
+ match self.aml_eval(
+ AmlName::from_str(&format!("\\{}.{}", path, "_BST"))
+ .map_err(|_| AmlEvalError::DeserializationError)?,
+ Vec::new(),
+ ) {
+ Ok(AmlSerdeValue::Package { contents }) => {
+ if let Err(error) = parse_bst_package(&contents, &mut battery) {
+ log::debug!("Skipping battery {} due to malformed _BST: {:?}", path, error);
+ continue;
+ }
+ }
+ Ok(other) => {
+ log::debug!("Skipping battery {} due to unexpected _BST value: {:?}", path, other);
+ continue;
+ }
+ Err(error) => {
+ log::debug!("Skipping battery {} due to _BST eval failure: {:?}", path, error);
+ continue;
+ }
+ }
+
+ for method in ["_BIX", "_BIF"] {
+ let method_name = AmlName::from_str(&format!("\\{}.{}", path, method))
+ .map_err(|_| AmlEvalError::DeserializationError)?;
+ match self.aml_eval(method_name, Vec::new()) {
+ Ok(AmlSerdeValue::Package { contents }) => {
+ let result = if method == "_BIX" {
+ fill_bix_fields(&contents, &mut battery)
+ } else {
+ fill_bif_fields(&contents, &mut battery)
+ };
+ if result.is_ok() {
+ break;
+ }
+ }
+ Ok(_) => {}
+ Err(_) => {}
+ }
+ }
+
+ battery.percentage = compute_battery_percentage(&battery);
+ snapshot.batteries.push(battery);
+ }
+
+ if snapshot.adapters.is_empty() && snapshot.batteries.is_empty() {
+ Err(AmlEvalError::Unsupported(
+ "ACPI power devices were not discoverable from AML",
+ ))
+ } else {
+ Ok(snapshot)
+ }
+ }
+
pub fn aml_lookup(&self, symbol: &str) -> Option<String> {
- if let Ok(aml_symbols) = self.aml_symbols(None) {
+ if let Ok(aml_symbols) = self.aml_symbols() {
aml_symbols.lookup(symbol)
} else {
None
}
}
- pub fn aml_symbols(
- &self,
- pci_fd: Option<&libredox::Fd>,
- ) -> Result<RwLockReadGuard<'_, AmlSymbols>, AmlError> {
+ pub fn aml_symbols(&self) -> Result<RwLockReadGuard<'_, AmlSymbols>, AmlError> {
+ let pci_fd = self.pci_fd.read();
// return the cached value if it exists
let symbols = self.aml_symbols.read();
if !symbols.symbols_cache().is_empty() {
@@ -550,7 +1540,7 @@ impl AcpiContext {
let mut aml_symbols = self.aml_symbols.write();
- aml_symbols.build_cache(pci_fd);
+ aml_symbols.build_cache(pci_fd.as_ref());
// return the cached value
Ok(RwLockWriteGuard::downgrade(aml_symbols))
@@ -562,95 +1552,223 @@ impl AcpiContext {
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
- pub fn set_global_s_state(&self, state: u8) {
- if state != 5 {
- return;
+ pub fn sleep_values_for_target(
+ &self,
+ target: SleepTarget,
+ ) -> Result<(u8, u8), SleepStateValuesError> {
+ let aml_name = AmlName::from_str(&format!("\\{}", target.aml_method_name()))
+ .map_err(SleepStateValuesError::InvalidName)?;
+ let values = parse_sleep_package(target, self.aml_eval(aml_name, Vec::new())?)?;
+ if target.is_soft_off() {
+ *self.slp_s5_values.write() = Some(values);
}
- let fadt = match self.fadt() {
- Some(fadt) => fadt,
- None => {
- log::error!("Cannot set global S-state due to missing FADT.");
- return;
- }
- };
+ Ok(values)
+ }
- let port = fadt.pm1a_control_block as u16;
- let mut val = 1 << 13;
+ pub fn refresh_s5_values(&self) -> Result<(u8, u8), SleepStateValuesError> {
+ self.sleep_values_for_target(SleepTarget::S5)
+ }
- let aml_symbols = self.aml_symbols.read();
+ pub fn acpi_shutdown(&self, slp_typa_s5: u8, slp_typb_s5: u8) -> Result<(), PowerTransitionError> {
+ let pm1a_value = (u16::from(slp_typa_s5) << 10) | 0x2000;
+ let pm1b_value = (u16::from(slp_typb_s5) << 10) | 0x2000;
- let s5_aml_name = match acpi::aml::namespace::AmlName::from_str("\\_S5") {
- Ok(aml_name) => aml_name,
- Err(error) => {
- log::error!("Could not build AmlName for \\_S5, {:?}", error);
- return;
- }
- };
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ {
+ let Ok(pm1a_port) = u16::try_from(self.pm1a_cnt_blk) else {
+ return Err(PowerTransitionError::InvalidPm1aControlBlock(self.pm1a_cnt_blk));
+ };
- let s5 = match &aml_symbols.aml_context {
- Some(aml_context) => match aml_context.namespace.lock().get(s5_aml_name) {
- Ok(s5) => s5,
- Err(error) => {
- log::error!("Cannot set S-state, missing \\_S5, {:?}", error);
- return;
+ log::warn!(
+ "Shutdown with ACPI PM1a_CNT outw(0x{:X}, 0x{:X})",
+ pm1a_port,
+ pm1a_value
+ );
+ Pio::<u16>::new(pm1a_port).write(pm1a_value);
+
+ if self.pm1b_cnt_blk != 0 {
+ match u16::try_from(self.pm1b_cnt_blk) {
+ Ok(pm1b_port) => {
+ log::warn!(
+ "Shutdown with ACPI PM1b_CNT outw(0x{:X}, 0x{:X})",
+ pm1b_port,
+ pm1b_value
+ );
+ Pio::<u16>::new(pm1b_port).write(pm1b_value);
+ }
+ Err(_) => {
+ return Err(PowerTransitionError::InvalidPm1bControlBlock(
+ self.pm1b_cnt_blk,
+ ));
+ }
}
- },
- None => {
- log::error!("Cannot set S-state, AML context not initialized");
- return;
}
- };
- let package = match s5.deref() {
- acpi::aml::object::Object::Package(package) => package,
- _ => {
- log::error!("Cannot set S-state, \\_S5 is not a package");
- return;
+ Ok(())
+ }
+
+ #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
+ {
+ Err(PowerTransitionError::UnsupportedArchitecture {
+ pm1a_cnt_blk: self.pm1a_cnt_blk,
+ pm1b_cnt_blk: self.pm1b_cnt_blk,
+ })
+ }
+ }
+
+ pub fn acpi_reboot(&self) -> Result<(), PowerTransitionError> {
+ match self.reset_reg {
+ Some(reset_reg) => {
+ log::warn!(
+ "Reboot with ACPI reset register {:?} value {:#X}",
+ reset_reg,
+ self.reset_value
+ );
+ reset_reg.write_u8(self.reset_value);
+ Ok(())
}
- };
+ None => {
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ {
+ const I8042_COMMAND_PORT: u16 = 0x64;
+ const I8042_PULSE_RESET: u8 = 0xFE;
+
+ log::warn!(
+ "Reboot with keyboard-controller fallback outb(0x{:X}, 0x{:X})",
+ I8042_COMMAND_PORT,
+ I8042_PULSE_RESET
+ );
+ Pio::<u8>::new(I8042_COMMAND_PORT).write(I8042_PULSE_RESET);
+ Ok(())
+ }
- let slp_typa = match package[0].deref() {
- acpi::aml::object::Object::Integer(i) => i.to_owned(),
- _ => {
- log::error!("typa is not an Integer");
- return;
+ #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
+ {
+ Err(PowerTransitionError::MissingResetRegister)
+ }
}
- };
- let slp_typb = match package[1].deref() {
- acpi::aml::object::Object::Integer(i) => i.to_owned(),
- _ => {
- log::error!("typb is not an Integer");
- return;
+ }
+ }
+
+ /// 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
+ pub fn set_global_s_state(&self, state: u8) -> Result<(), GlobalSleepStateError> {
+ let target = SleepTarget::try_from(state)
+ .map_err(|_| GlobalSleepStateError::UnknownSleepState(state))?;
+ if !target.is_soft_off() {
+ return Err(GlobalSleepStateError::UnsupportedTarget(target));
+ }
+
+ if self.fadt().is_none() {
+ return Err(GlobalSleepStateError::MissingFadt);
+ }
+
+ let cached_s5 = *self.slp_s5_values.read();
+ let (slp_typa, slp_typb) = match self.sleep_values_for_target(SleepTarget::S5) {
+ Ok(values) => values,
+ Err(error) => match cached_s5 {
+ Some(values) => {
+ log::warn!(
+ "Using cached {} values after refresh failure: {error}",
+ SleepTarget::S5.aml_method_name()
+ );
+ values
+ }
+ None => {
+ return Err(GlobalSleepStateError::MissingSleepValues {
+ target: SleepTarget::S5,
+ source: error,
+ })
+ }
}
};
- log::trace!("Shutdown SLP_TYPa {:X}, SLP_TYPb {:X}", slp_typa, slp_typb);
- val |= slp_typa as u16;
+ self.acpi_shutdown(slp_typa, slp_typb)
+ .map_err(GlobalSleepStateError::PowerTransitionFailed)?;
- #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
- {
- log::warn!("Shutdown with ACPI outw(0x{:X}, 0x{:X})", port, val);
- Pio::<u16>::new(port).write(val);
- }
+ Err(GlobalSleepStateError::TransitionDidNotComplete(target))
+ }
+}
- // TODO: Handle SLP_TYPb
+#[derive(Debug, Error)]
+pub enum SleepStateValuesError {
+ #[error("failed to build AML name for sleep-state method: {0:?}")]
+ InvalidName(AmlError),
- #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
- {
- log::error!(
- "Cannot shutdown with ACPI outw(0x{:X}, 0x{:X}) on this architecture",
- port,
- val
- );
- }
+ #[error("failed to evaluate sleep-state package: {0}")]
+ Evaluation(#[from] AmlEvalError),
- loop {
- core::hint::spin_loop();
- }
+ #[error("sleep-state method returned a non-package AML value")]
+ NonPackageValue,
+
+ #[error("sleep-state package did not contain two integer sleep-type entries")]
+ InvalidPackageShape,
+
+ #[error("sleep-state values did not fit in u8")]
+ ValueOutOfRange,
+}
+
+#[derive(Debug, Error)]
+pub enum PowerTransitionError {
+ #[error("PM1a control block address is invalid: {0:#X}")]
+ InvalidPm1aControlBlock(u64),
+
+ #[error("PM1b control block address is invalid: {0:#X}")]
+ InvalidPm1bControlBlock(u64),
+
+ #[error(
+ "cannot issue ACPI PM1 control writes on this architecture (PM1a={pm1a_cnt_blk:#X}, PM1b={pm1b_cnt_blk:#X})"
+ )]
+ UnsupportedArchitecture {
+ pm1a_cnt_blk: u64,
+ pm1b_cnt_blk: u64,
+ },
+
+ #[error("cannot reboot with ACPI: no reset register present in FADT")]
+ MissingResetRegister,
+}
+
+#[derive(Debug, Error)]
+pub enum GlobalSleepStateError {
+ #[error("unknown global sleep state S{0}")]
+ UnknownSleepState(u8),
+
+ #[error("sleep target {:?} remains groundwork-only until full sleep lifecycle support lands", .0)]
+ UnsupportedTarget(SleepTarget),
+
+ #[error("cannot set global S-state due to missing FADT")]
+ MissingFadt,
+
+ #[error("failed to derive usable {} values: {source}", target.aml_method_name())]
+ MissingSleepValues {
+ target: SleepTarget,
+ source: SleepStateValuesError,
+ },
+
+ #[error("ACPI power transition failed: {0}")]
+ PowerTransitionFailed(#[from] PowerTransitionError),
+
+ #[error("ACPI transition to {:?} returned without completing", .0)]
+ TransitionDidNotComplete(SleepTarget),
+}
+
+fn parse_sleep_package(
+ _target: SleepTarget,
+ value: AmlSerdeValue,
+) -> Result<(u8, u8), SleepStateValuesError> {
+ match value {
+ AmlSerdeValue::Package { contents } => match (contents.first(), contents.get(1)) {
+ (Some(AmlSerdeValue::Integer(slp_typa)), Some(AmlSerdeValue::Integer(slp_typb))) => {
+ match (u8::try_from(*slp_typa), u8::try_from(*slp_typb)) {
+ (Ok(slp_typa_s5), Ok(slp_typb_s5)) => Ok((slp_typa_s5, slp_typb_s5)),
+ _ => Err(SleepStateValuesError::ValueOutOfRange),
+ }
+ }
+ _ => Err(SleepStateValuesError::InvalidPackageShape),
+ },
+ _ => Err(SleepStateValuesError::NonPackageValue),
}
}
@@ -707,7 +1825,7 @@ unsafe impl plain::Plain for FadtStruct {}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default)]
-pub struct GenericAddressStructure {
+pub struct GenericAddress {
address_space: u8,
bit_width: u8,
bit_offset: u8,
@@ -715,11 +1833,68 @@ pub struct GenericAddressStructure {
address: u64,
}
+impl GenericAddress {
+ pub fn is_empty(&self) -> bool {
+ self.address == 0
+ }
+
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ pub fn write_u8(&self, value: u8) {
+ let address = self.address;
+ match self.address_space {
+ 0 => {
+ let Ok(address) = usize::try_from(address) else {
+ log::error!("Reset register physical address is invalid: {:#X}", address);
+ return;
+ };
+ let page = address / PAGE_SIZE * PAGE_SIZE;
+ let offset = address % PAGE_SIZE;
+ let virt = unsafe {
+ common::physmap(page, PAGE_SIZE, common::Prot::RW, common::MemoryType::default())
+ };
+
+ match virt {
+ Ok(virt) => unsafe {
+ (virt as *mut u8).add(offset).write_volatile(value);
+ let _ = libredox::call::munmap(virt, PAGE_SIZE);
+ },
+ Err(error) => {
+ log::error!("Failed to map ACPI reset register: {}", error);
+ }
+ }
+ }
+ 1 => match u16::try_from(address) {
+ Ok(port) => {
+ Pio::<u8>::new(port).write(value);
+ }
+ Err(_) => {
+ log::error!("Reset register I/O port is invalid: {:#X}", address);
+ }
+ },
+ address_space => {
+ log::warn!(
+ "Unsupported ACPI reset register address space {} for {:?}",
+ address_space,
+ self
+ );
+ }
+ }
+ }
+
+ #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
+ pub fn write_u8(&self, _value: u8) {
+ log::error!(
+ "Cannot access ACPI reset register {:?} on this architecture",
+ self
+ );
+ }
+}
+
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct FadtAcpi2Struct {
// 12 byte structure; see below for details
- pub reset_reg: GenericAddressStructure,
+ pub reset_reg: GenericAddress,
pub reset_value: u8,
reserved3: [u8; 3],
@@ -728,14 +1903,14 @@ pub struct FadtAcpi2Struct {
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,
+ pub x_pm1a_event_block: GenericAddress,
+ pub x_pm1b_event_block: GenericAddress,
+ pub x_pm1a_control_block: GenericAddress,
+ pub x_pm1b_control_block: GenericAddress,
+ pub x_pm2_control_block: GenericAddress,
+ pub x_pm_timer_block: GenericAddress,
+ pub x_gpe0_block: GenericAddress,
+ pub x_gpe1_block: GenericAddress,
}
unsafe impl plain::Plain for FadtAcpi2Struct {}
@@ -774,9 +1949,10 @@ impl Fadt {
}
pub fn init(context: &mut AcpiContext) {
- let fadt_sdt = context
- .take_single_sdt(*b"FACP")
- .expect("expected ACPI to always have a FADT");
+ let Some(fadt_sdt) = context.take_single_sdt(*b"FACP") else {
+ log::error!("Failed to find FADT");
+ return;
+ };
let fadt = match Fadt::new(fadt_sdt) {
Some(fadt) => fadt,
@@ -793,9 +1969,25 @@ impl Fadt {
None => usize::try_from(fadt.dsdt).expect("expected any given u32 to fit within usize"),
};
- log::debug!("FACP at {:X}", { dsdt_ptr });
+ let pm1a_evt_blk = u64::from(fadt.pm1a_event_block);
+ let pm1b_evt_blk = u64::from(fadt.pm1b_event_block);
+ let pm1a_cnt_blk = u64::from(fadt.pm1a_control_block);
+ let pm1b_cnt_blk = u64::from(fadt.pm1b_control_block);
+ let (reset_reg, reset_value) = match fadt.acpi_2_struct() {
+ Some(fadt2) if !fadt2.reset_reg.is_empty() => (Some(fadt2.reset_reg), fadt2.reset_value),
+ _ => (None, 0),
+ };
- let dsdt_sdt = match Sdt::load_from_physical(fadt.dsdt as usize) {
+ log::debug!("FACP at {:X}", { dsdt_ptr });
+ log::debug!(
+ "FADT power blocks: PM1a_EVT={:#X}, PM1b_EVT={:#X}, PM1a_CNT={:#X}, PM1b_CNT={:#X}",
+ pm1a_evt_blk,
+ pm1b_evt_blk,
+ pm1a_cnt_blk,
+ pm1b_cnt_blk
+ );
+
+ let dsdt_sdt = match Sdt::load_from_physical(dsdt_ptr) {
Ok(dsdt) => dsdt,
Err(error) => {
log::error!("Failed to load DSDT: {}", error);
@@ -805,8 +1997,16 @@ impl Fadt {
context.fadt = Some(fadt.clone());
context.dsdt = Some(Dsdt(dsdt_sdt.clone()));
+ context.pm1a_cnt_blk = pm1a_cnt_blk;
+ context.pm1b_cnt_blk = pm1b_cnt_blk;
+ context.reset_reg = reset_reg;
+ context.reset_value = reset_value;
context.tables.push(dsdt_sdt);
+
+ if let Err(error) = context.refresh_s5_values() {
+ log::warn!("Failed to evaluate \\_S5 during FADT init: {error}");
+ }
}
}