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, + pub board_vendor: Option, + pub board_name: Option, + pub board_version: Option, + pub product_name: Option, + pub product_version: Option, + pub bios_version: Option, +} + +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::(); + + let mut offset = 0; + while offset + header_size <= bytes.len() { + if &bytes[offset..offset + 4] == b"_SM_" { + let entry = + plain::from_bytes::(&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::(); + + let mut offset = 0; + while offset + header_size <= bytes.len() { + if &bytes[offset..offset + 5] == b"_SM3_" { + let entry = + plain::from_bytes::(&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 { + 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 { + 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::() <= bytes.len() { + let header = plain::from_bytes::( + &bytes[offset..offset + mem::size_of::()], + ) + .ok()?; + let formatted_len = header.length as usize; + if formatted_len < mem::size_of::() + || 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 { + let (addr, len) = scan_smbios3().or_else(scan_smbios2)?; + parse_smbios_table(addr, len) +} + +#[derive(Clone, Debug, Default)] +struct AcpiTableMatchRule { + sys_vendor: Option, + board_vendor: Option, + board_name: Option, + board_version: Option, + product_name: Option, + product_version: Option, + bios_version: Option, +} + +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, actual: &Option) -> 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 { + table.get(field).and_then(Value::as_str).map(str::to_string) +} + +fn parse_acpi_table_quirks(document: &Value, path: &str) -> Vec { + 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 { + 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::>(); + 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::() 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, dmi_info: Option<&DmiInfo>) -> Vec { + 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, page_cache: Arc>, - aml_region_handlers: Vec<(RegionSpace, Box)>, } impl AmlSymbols { - pub fn new(aml_region_handlers: Vec<(RegionSpace, Box)>) -> 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 for AmlEvalError { fn from(value: AmlError) -> Self { @@ -375,10 +1002,169 @@ impl From for AmlEvalError { } } +fn panic_payload_to_string(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::<&'static str>() { + (*message).to_string() + } else if let Some(message) = payload.downcast_ref::() { + 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, + pub remaining_capacity: Option, + pub present_voltage: Option, + pub power_unit: Option, + pub design_capacity: Option, + pub last_full_capacity: Option, + pub design_voltage: Option, + pub technology: Option, + pub model: Option, + pub serial: Option, + pub battery_type: Option, + pub oem_info: Option, + pub percentage: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct AcpiPowerSnapshot { + pub adapters: Vec, + pub batteries: Vec, +} + +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 { + 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 { + match value { + AmlSerdeValue::Integer(value) => Some(*value), + _ => None, + } +} + +fn aml_string(value: &AmlSerdeValue) -> Option { + 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 { + 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, dsdt: Option, fadt: Option, + pm1a_cnt_blk: u64, + pm1b_cnt_blk: u64, + slp_s5_values: RwLock>, + reset_reg: Option, + reset_value: u8, + dmi_info: Option, + pci_fd: RwLock>, aml_symbols: RwLock, @@ -397,7 +1183,8 @@ impl AcpiContext { args: Vec, ) -> Result { 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::, 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, - ec: Vec<(RegionSpace, Box)>, - ) -> 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, 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::>(); + + 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 { + self.evaluate_acpi_method(device_path, "_PPC", &[])? + .into_iter() + .next() + .ok_or(AmlEvalError::DeserializationError) + } + + pub fn init(rxsdt_physaddrs: impl Iterator) -> 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::>(); + .collect::>(), + 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 { + let symbols = self.aml_symbols()?; + let symbol_names = symbols + .symbols_cache() + .keys() + .cloned() + .collect::>(); + drop(symbols); + + let mut adapter_paths = symbol_names + .iter() + .filter_map(|symbol| symbol_parent_path(symbol, "._PSR")) + .collect::>(); + adapter_paths.sort(); + adapter_paths.dedup(); + + let mut battery_paths = symbol_names + .iter() + .filter_map(|symbol| symbol_parent_path(symbol, "._BST")) + .collect::>(); + 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 { - 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, AmlError> { + pub fn aml_symbols(&self) -> Result, 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::::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::::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::::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::::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::::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}"); + } } }