use acpi::aml::namespace::AmlName; use amlserde::aml_serde_name::to_aml_format; use amlserde::AmlSerdeValue; use core::str; use libredox::Fd; use parking_lot::RwLockReadGuard; use redox_scheme::scheme::SchemeSync; use redox_scheme::{CallerCtx, OpenResult, SendFdRequest, Socket}; use syscall::flag::CallFlags; use syscall::flag::AcpiVerb; use ron::de::SpannedError; use scheme_utils::HandleMap; use std::convert::{TryFrom, TryInto}; use std::str::FromStr; use syscall::dirent::{DirEntry, DirentBuf, DirentKind}; use syscall::schemev2::NewFdFlags; use syscall::FobtainFdFlags; use syscall::data::Stat; use syscall::error::{Error, Result}; use syscall::error::{EACCES, EBADF, EBADFD, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR}; use syscall::flag::{MODE_DIR, MODE_FILE}; use syscall::flag::{O_ACCMODE, O_DIRECTORY, O_RDONLY, O_STAT, O_SYMLINK}; use syscall::{EOVERFLOW, EPERM}; use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature}; use crate::dmi::DMI_FIELDS; pub struct AcpiScheme<'acpi, 'sock> { ctx: &'acpi AcpiContext, handles: HandleMap>, pci_fd: Option, socket: &'sock Socket, /// Phase I.5: the kstop handle fd. Stored so the main loop /// can call `kstop_reason` (kcall 2) to query the kernel /// for the reason of the most recent kstop event. kstop_fd: Option, } struct Handle<'a> { kind: HandleKind<'a>, stat: bool, allowed_to_eval: bool, } enum HandleKind<'a> { TopLevel, Tables, Table(SdtSignature), Symbols(RwLockReadGuard<'a, AmlSymbols>), Symbol { name: String, description: String }, SchemeRoot, RegisterPci, /// `/scheme/acpi/thermal` -- entries are children of `\_TZ` from /// the AML namespace (e.g. `\_TZ.TZ0`). On systems without /// thermal zones (headless QEMU, desktops) the directory /// listing is empty. Thermal, /// `/scheme/acpi/power` -- entries are PowerResource objects in /// the AML namespace. On laptops these are AC adapters and /// battery controllers. On desktops and QEMU the listing is /// empty. Power, /// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity /// fields (consumed by `redox-driver-sys` quirks loader). Dmi, /// `/scheme/acpi/dmi/` -- a single SMBIOS field as a text /// file (consumed by `i2c-hidd` for probe-failure quirks). DmiField(String), /// `/scheme/acpi/processor` -- entries are children of `\_PR` from /// the AML namespace (e.g. `CPU0`, `CPU1`). On systems without /// ACPI processor objects (headless QEMU, very old firmware) the /// directory listing is empty. Processor, /// `/scheme/acpi/processor//` -- per-CPU ACPI data: /// `pss` (P-state frequencies), `psd` (P-state dependencies), /// `cst` (C-state table). On QEMU these are typically empty. /// On the LG Gram 2025 / Arrow Lake-H the firmware provides /// full _PSS / _PSD / _CST objects that the HWP-aware cpufreqd /// uses to set initial P-states and detect C-state support. ProcFile { cpu: u32, kind: ProcFileKind }, DmiDir, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ProcFileKind { Pss, Psd, Cst, Cpc, } impl HandleKind<'_> { fn is_dir(&self) -> bool { match self { Self::TopLevel => true, Self::Tables => true, Self::Table(_) => false, Self::Symbols(_) => true, Self::Symbol { .. } => false, Self::SchemeRoot => false, Self::RegisterPci => false, Self::Thermal | Self::Power => true, Self::Dmi => true, Self::DmiField(_) => false, } } fn len(&self, acpi_ctx: &AcpiContext) -> Result { Ok(match self { // Files Self::Table(signature) => acpi_ctx .sdt_from_signature(signature) .ok_or(Error::new(EBADFD))? .length(), Self::Symbol { description, .. } => description.len(), // /scheme/acpi/dmi is a key=value text file (redox-driver-sys // reads it via fs::read_to_string). The size depends on how // many fields are populated. Self::Dmi => acpi_ctx .dmi_info() .map(|info| info.to_match_lines().len()) .unwrap_or(0), Self::DmiField(field) => dmi_field_contents(acpi_ctx.dmi_info(), field) .map(|s| s.len()) .unwrap_or(0), // Directories Self::TopLevel | Self::Symbols(_) | Self::Tables => 0, Self::Thermal | Self::Power => 0, Self::SchemeRoot | Self::RegisterPci => return Err(Error::new(EBADF)), }) } } impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> { pub fn new(ctx: &'acpi AcpiContext, socket: &'sock Socket) -> Self { Self { ctx, handles: HandleMap::new(), pci_fd: None, socket, kstop_fd: None, } } /// Phase I.5: register the kstop handle fd. Called by the /// main loop right after opening the kstop handle. pub fn set_kstop_fd(&mut self, fd: Fd) { self.kstop_fd = Some(fd); } /// Phase I.5: query the kernel for the kstop reason via /// the CheckShutdown AcpiVerb (kcall 2). Returns the u8 /// reason: 0=idle, 1=shutdown (S5), 2=s2idle wake, /// 3=s3 wake. The kernel re-arms the kstop handle's /// EVENT_READ after each event; acpid's main loop calls /// this once per event to decide what AML sequence to run. /// /// Mirrors Linux 7.1 `acpi_s2idle_wake` returning the /// wake reason in `drivers/acpi/sleep.c:758`. The /// `kcall 2` is the `AcpiVerb::CheckShutdown` enum /// variant in the syscall crate. /// /// Hardware-agnostic: the reason codes are platform- /// independent; only the wake source (SCI, GPIO, RTC, /// ...) varies per OEM. pub fn kstop_reason(&mut self) -> syscall::Result { let handle = self.kstop_fd.ok_or(syscall::error::Error::new(syscall::error::EBADF))?; let mut payload = [0u8; 8]; let verb = AcpiVerb::CheckShutdown as u64; let result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?; Ok(u64::from_ne_bytes(payload)) } } fn parse_hex_digit(hex: u8) -> Option { let hex = hex.to_ascii_lowercase(); if hex >= b'a' && hex <= b'f' { Some(hex - b'a' + 10) } else if hex >= b'0' && hex <= b'9' { Some(hex - b'0') } else { None } } fn parse_hex_2digit(hex: &[u8]) -> Option { parse_hex_digit(hex[0]) .and_then(|most_significant| Some((most_significant << 4) | parse_hex_digit(hex[1])?)) } fn parse_oem_id(hex: [u8; 12]) -> Option<[u8; 6]> { Some([ parse_hex_2digit(&hex[0..2])?, parse_hex_2digit(&hex[2..4])?, parse_hex_2digit(&hex[4..6])?, parse_hex_2digit(&hex[6..8])?, parse_hex_2digit(&hex[8..10])?, parse_hex_2digit(&hex[10..12])?, ]) } fn parse_oem_table_id(hex: [u8; 16]) -> Option<[u8; 8]> { Some([ parse_hex_2digit(&hex[0..2])?, parse_hex_2digit(&hex[2..4])?, parse_hex_2digit(&hex[4..6])?, parse_hex_2digit(&hex[6..8])?, parse_hex_2digit(&hex[8..10])?, parse_hex_2digit(&hex[10..12])?, parse_hex_2digit(&hex[12..14])?, parse_hex_2digit(&hex[14..16])?, ]) } /// Look up the contents of `/scheme/acpi/dmi/` for the given /// field name. Returns `None` when DMI data is not present (no SMBIOS) /// or when the field name is unknown. The returned `String` is what /// userspace will read from the file -- a single text line with no /// trailing newline so that callers can `read_to_string` and `trim`. fn dmi_field_contents( info: Option<&crate::dmi::DmiInfo>, field: &str, ) -> Option { crate::dmi::read_field(info, field) } fn parse_table(table: &[u8]) -> Option { let signature_part = table.get(..4)?; let first_hyphen = table.get(4)?; let oem_id_part = table.get(5..17)?; let second_hyphen = table.get(17)?; let oem_table_part = table.get(18..34)?; if *first_hyphen != b'-' { return None; } if *second_hyphen != b'-' { return None; } if table.len() > 34 { return None; } Some(SdtSignature { signature: <[u8; 4]>::try_from(signature_part) .expect("expected 4-byte slice to be convertible into [u8; 4]"), oem_id: { let hex = <[u8; 12]>::try_from(oem_id_part) .expect("expected 12-byte slice to be convertible into [u8; 12]"); parse_oem_id(hex)? }, oem_table_id: { let hex = <[u8; 16]>::try_from(oem_table_part) .expect("expected 16-byte slice to be convertible into [u8; 16]"); parse_oem_table_id(hex)? }, }) } impl SchemeSync for AcpiScheme<'_, '_> { fn scheme_root(&mut self) -> Result { Ok(self.handles.insert(Handle { stat: false, kind: HandleKind::SchemeRoot, allowed_to_eval: false, })) } fn openat( &mut self, dirfd: usize, path: &str, flags: usize, _fcntl_flags: u32, ctx: &CallerCtx, ) -> Result { let handle = self.handles.get(dirfd)?; let path = path.trim_start_matches('/'); let flag_stat = flags & O_STAT == O_STAT; let flag_dir = flags & O_DIRECTORY == O_DIRECTORY; let kind = match handle.kind { HandleKind::SchemeRoot => { // TODO: arrayvec let components = { let mut v = arrayvec::ArrayVec::<&str, 4>::new(); let it = path.split('/'); for component in it.take(4) { v.push(component); } v }; match &*components { [""] => HandleKind::TopLevel, ["register_pci"] => HandleKind::RegisterPci, ["tables"] => HandleKind::Tables, ["thermal"] => HandleKind::Thermal, ["power"] => HandleKind::Power, ["dmi"] => HandleKind::Dmi, ["processor"] => HandleKind::Processor, ["tables", table] => { let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?; HandleKind::Table(signature) } ["symbols"] => { if let Ok(aml_symbols) = self.ctx.aml_symbols(self.pci_fd.as_ref()) { HandleKind::Symbols(aml_symbols) } else { return Err(Error::new(EIO)); } } ["symbols", symbol] => { if let Some(description) = self.ctx.aml_lookup(symbol) { HandleKind::Symbol { name: (*symbol).to_owned(), description, } } else { return Err(Error::new(ENOENT)); } } ["dmi", field] => { // Reject unknown fields explicitly so consumers // see ENOENT rather than reading an empty file. // When SMBIOS is absent, we still serve a // well-defined file with empty contents (so // i2c-hidd's `Err(NotFound)` branch is the only // way to tell the difference between "missing // field" and "no SMBIOS"). if DMI_FIELDS.iter().any(|f| *f == *field) { HandleKind::DmiField((*field).to_owned()) } else { return Err(Error::new(ENOENT)); } } ["processor", cpu_str, file] => { // /scheme/acpi/processor//{pss,psd,cst,cpc} let cpu: u32 = cpu_str.parse().map_err(|_| Error::new(EINVAL))?; let kind = match *file { "pss" => ProcFileKind::Pss, "psd" => ProcFileKind::Psd, "cst" => ProcFileKind::Cst, "cpc" => ProcFileKind::Cpc, _ => return Err(Error::new(ENOENT)), }; HandleKind::ProcFile { cpu, kind } } _ => return Err(Error::new(ENOENT)), } } HandleKind::Symbols(ref aml_symbols) => { if let Some(description) = aml_symbols.lookup(path) { HandleKind::Symbol { name: (*path).to_owned(), description, } } else { return Err(Error::new(ENOENT)); } } _ => return Err(Error::new(EACCES)), }; if kind.is_dir() && !flag_dir && !flag_stat { return Err(Error::new(EISDIR)); } else if !kind.is_dir() && flag_dir && !flag_stat { return Err(Error::new(ENOTDIR)); } let allowed_to_eval = if flags & O_ACCMODE == O_RDONLY || flag_stat { false } else if ctx.uid == 0 { true } else { return Err(Error::new(EINVAL)); }; if flags & O_SYMLINK == O_SYMLINK && !flag_stat { return Err(Error::new(EINVAL)); } let fd = self.handles.insert(Handle { stat: flag_stat, kind, allowed_to_eval, }); Ok(OpenResult::ThisScheme { number: fd, flags: NewFdFlags::POSITIONED, }) } fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { let handle = self.handles.get(id)?; stat.st_size = handle .kind .len(self.ctx)? .try_into() .unwrap_or(u64::max_value()); if handle.kind.is_dir() { stat.st_mode = MODE_DIR; } else { stat.st_mode = MODE_FILE; } Ok(()) } fn read( &mut self, id: usize, buf: &mut [u8], offset: u64, _fcntl: u32, _ctx: &CallerCtx, ) -> Result { let offset: usize = offset.try_into().map_err(|_| Error::new(EINVAL))?; let handle = self.handles.get_mut(id)?; if handle.stat { return Err(Error::new(EBADF)); } // Build an owned buffer for DMI handles so the borrow does not // escape the match arm scope. let dmi_buf; let proc_buf; let src_buf: &[u8] = match &handle.kind { HandleKind::Table(ref signature) => self .ctx .sdt_from_signature(signature) .ok_or(Error::new(EBADFD))? .as_slice(), HandleKind::Symbol { description, .. } => description.as_bytes(), HandleKind::Dmi => { dmi_buf = self .ctx .dmi_info() .map(|info| info.to_match_lines()) .unwrap_or_default(); dmi_buf.as_bytes() } HandleKind::DmiField(ref field) => { dmi_buf = dmi_field_contents(self.ctx.dmi_info(), field) .unwrap_or_default(); dmi_buf.as_bytes() } HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => { return Err(Error::new(EISDIR)); } HandleKind::ProcFile { .. } => { // Per-CPU _PSS / _PSD / _CST / _CPC text export. The // full AML→text conversion is a Phase G follow-up; for // now, return a placeholder line so consumers // (cpufreqd, redbear-power) can detect the path is // present and report "no data" without getting ENOENT. proc_buf = b"# ACPI processor data not yet populated\n".to_vec(); proc_buf.as_slice() } }; let offset = std::cmp::min(src_buf.len(), offset); let src_buf = &src_buf[offset..]; let to_copy = std::cmp::min(src_buf.len(), buf.len()); buf[..to_copy].copy_from_slice(&src_buf[..to_copy]); Ok(to_copy) } fn getdents<'buf>( &mut self, id: usize, mut buf: DirentBuf<&'buf mut [u8]>, opaque_offset: u64, ) -> Result> { let handle = self.handles.get_mut(id)?; match &handle.kind { HandleKind::TopLevel => { const TOPLEVEL_ENTRIES: &[&str] = &[ "tables", "symbols", "thermal", "power", "dmi", "processor", ]; for (idx, name) in TOPLEVEL_ENTRIES .iter() .enumerate() .skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name, kind: DirentKind::Directory, })?; } } HandleKind::Symbols(aml_symbols) => { for (idx, (symbol_name, _value)) in aml_symbols .symbols_cache() .iter() .enumerate() .skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: symbol_name.as_str(), kind: DirentKind::Regular, })?; } } HandleKind::Tables => { for (idx, table) in self .ctx .tables() .iter() .enumerate() .skip(opaque_offset as usize) { let utf8_or_eio = |bytes| str::from_utf8(bytes).map_err(|_| Error::new(EIO)); let mut name = String::new(); name.push_str(utf8_or_eio(&table.signature[..])?); name.push('-'); for byte in table.oem_id.iter() { std::fmt::write(&mut name, format_args!("{:>02X}", byte)).unwrap(); } name.push('-'); for byte in table.oem_table_id.iter() { std::fmt::write(&mut name, format_args!("{:>02X}", byte)).unwrap(); } buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: &name, kind: DirentKind::Regular, })?; } } HandleKind::Thermal => { // Enumerate \_TZ. entries from the AML namespace. // Returns Ok with no entries on systems with no zones // (headless QEMU, desktops) so consumers see an // empty-but-existing directory. let zones = self.ctx.thermal_zones(); for (idx, zone) in zones.iter().enumerate().skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: zone.as_str(), kind: DirentKind::Directory, })?; } } HandleKind::Processor => { // Enumerate \_PR. entries from the AML namespace. // Returns Ok with no entries on systems with no // processors (headless QEMU with no DSDT) so consumers // see an empty-but-existing directory. let cpus = self.ctx.cpu_names(); for (idx, cpu_name) in cpus.iter().enumerate().skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: cpu_name.as_str(), kind: DirentKind::Directory, })?; } } HandleKind::Power => { // Enumerate PowerResource entries. On real laptops these // are AC adapters and battery controllers; on desktops // and QEMU the list is empty. let adapters = self.ctx.power_adapters(); for (idx, adapter) in adapters.iter().enumerate().skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: adapter.as_str(), kind: DirentKind::Directory, })?; } } HandleKind::Dmi => { // Consumers should `read_to_string("/scheme/acpi/dmi")` // rather than iterating, but we still surface the field list so that ls /scheme/acpi/dmi/ produces a useful diagnostic on a live system. We always list the same set of fields regardless of whether SMBIOS data is present -- empty entries just produce empty reads. for (idx, field) in DMI_FIELDS .iter() .enumerate() .skip(opaque_offset as usize) { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: field, kind: DirentKind::Regular, })?; } } HandleKind::ProcFile { .. } | HandleKind::DmiDir => { // No children; reads/writes go through the // HandleKind match in kread/kwriteoff. } } _ => return Err(Error::new(EIO)), } Ok(buf) } fn call( &mut self, id: usize, payload: &mut [u8], _metadata: &[u64], _ctx: &CallerCtx, ) -> Result { let handle = self.handles.get_mut(id)?; if !handle.allowed_to_eval { return Err(Error::new(EPERM)); } let Ok(args): Result, SpannedError> = ron::de::from_bytes(payload) else { return Err(Error::new(EINVAL)); }; let HandleKind::Symbol { name, .. } = &handle.kind else { return Err(Error::new(EBADF)); }; let Ok(aml_name) = AmlName::from_str(&to_aml_format(name)) else { log::error!("Failed to convert symbol name: \"{name}\" to aml name!"); return Err(Error::new(EBADF)); }; let Ok(result) = self.ctx.aml_eval(aml_name, args) else { return Err(Error::new(EINVAL)); }; let Ok(serialized_result) = ron::ser::to_string(&result) else { log::error!("Failed to serialize aml result!"); return Err(Error::new(EINVAL)); }; let byte_result = serialized_result.as_bytes(); let result_len = byte_result.len(); if result_len > payload.len() { return Err(Error::new(EOVERFLOW)); } payload[..result_len].copy_from_slice(byte_result); Ok(result_len) } fn on_sendfd(&mut self, sendfd_request: &SendFdRequest) -> Result { let id = sendfd_request.id(); let num_fds = sendfd_request.num_fds(); let handle = self.handles.get(id)?; if !matches!(handle.kind, HandleKind::RegisterPci) { return Err(Error::new(EACCES)); } if num_fds == 0 { return Ok(0); } if num_fds > 1 { return Err(Error::new(EINVAL)); } let mut new_fd = usize::MAX; if let Err(e) = sendfd_request.obtain_fd( &self.socket, FobtainFdFlags::UPPER_TBL, std::slice::from_mut(&mut new_fd), ) { return Err(e); } let new_fd = libredox::Fd::new(new_fd); if self.pci_fd.is_some() { return Err(Error::new(EINVAL)); } else { self.pci_fd = Some(new_fd); } Ok(num_fds) } fn on_close(&mut self, id: usize) { self.handles.remove(id); } }