b906ad688a
Closes the v4.0 plan P3 'acpid pci_fd is not registered' item.
Two related defects:
1. scheme.rs::on_sendfd had a one-shot EINVAL when self.pci_fd was
already Some. Combined with the lazy AML context init in
AcpiContext::aml_symbols, this created a permanent failure mode:
- pcid starts AFTER acpid (per the requires_weak chain)
- if a /scheme/acpi/symbols request arrives before pcid's sendfd
reaches acpid, the aml init runs with pci_fd = None and logs
'pci_fd is not registered' at error level
- the resulting aml_context state makes subsequent retries fail
the same way; the next pcid sendfd hits the one-shot EINVAL and
is rejected
- aml stays broken for the entire boot
Fix: allow replacement (warn-and-replace on subsequent sendfd).
Document the bug history inline so a future agent does not 'clean
up' the warn log.
2. aml_physmem.rs::AmlPhysMemHandler::new logged the missing fd at
log::error. Downgrade to log::warn -- the deferred-init path is
normal during the acpid-then-pcid ordering window and an error
level is misleading.
3. scheme.rs::on_sendfd now eagerly calls self.ctx.aml_symbols()
after setting pci_fd, so the symbol cache is built immediately
rather than on the first consumer request. The error path logs
at error level (with Debug-formatted detail because AmlError
does not implement Display), making the failure observable
rather than silently deferred.
Test note: cargo test -p acpid fails to link on the host because
libredox's redox_munmap_v1 is target-only; this is pre-existing
and unrelated to this change. cargo check -p acpid is clean.
1103 lines
42 KiB
Rust
1103 lines
42 KiB
Rust
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, ENODEV, 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, PowerCache, SdtSignature};
|
|
use crate::dmi::DMI_FIELDS;
|
|
|
|
pub struct AcpiScheme<'acpi, 'sock> {
|
|
ctx: &'acpi AcpiContext,
|
|
handles: HandleMap<Handle<'acpi>>,
|
|
pci_fd: Option<Fd>,
|
|
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<Fd>,
|
|
power_cache: PowerCache,
|
|
/// Cache of evaluated static processor methods, keyed by (cpu index,
|
|
/// method name). `_PSS`/`_PSD`/`_CST`/`_CPC` describe fixed P-state,
|
|
/// C-state, dependency, and CPPC capability tables that do not change
|
|
/// after boot, yet cpufreqd polls them repeatedly. Each miss re-runs the
|
|
/// AML interpreter under the global lock on acpid's single serving thread;
|
|
/// caching removes that recurring load (and the head-of-line pressure it
|
|
/// adds). Dynamic methods (`_PPC`, battery `_BST`) are NOT cached — acpid
|
|
/// does not expose `_PPC` as a processor file, and battery state uses the
|
|
/// separate live `power_cache` path.
|
|
processor_cache: std::collections::HashMap<(u32, &'static str), String>,
|
|
}
|
|
|
|
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,
|
|
ThermalZone { zone: String, kind: ThermalFileKind },
|
|
PmTimer,
|
|
/// `/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,
|
|
PowerBatteries,
|
|
PowerBattery { name: String, file: PowerFileKind },
|
|
PowerAdapter { name: String, file: PowerFileKind },
|
|
/// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity
|
|
/// fields (consumed by `redox-driver-sys` quirks loader).
|
|
Dmi,
|
|
/// `/scheme/acpi/dmi/<field>` -- 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.
|
|
/// `/scheme/acpi/lid` -- directory holding `state`.
|
|
Lid,
|
|
/// `/scheme/acpi/lid/state` -- "open" / "closed" / "unknown".
|
|
LidState,
|
|
/// `/scheme/acpi/button` -- directory holding `power` / `sleep`.
|
|
ButtonDir,
|
|
/// `/scheme/acpi/button/{power,sleep}` -- number of presses since the
|
|
/// last read (edge counter).
|
|
Button { power: bool },
|
|
/// `/scheme/acpi/notifications` -- drained AML Notify event log
|
|
/// ("<device> <value>" lines).
|
|
Notifications,
|
|
/// `/scheme/acpi/fan` -- directory of ACPI 4.0 fan devices (indices).
|
|
Fan,
|
|
/// `/scheme/acpi/fan/<idx>/state` -- `_FST` readout (percentage).
|
|
FanState(usize),
|
|
/// `/scheme/acpi/fan/<idx>/speed` -- `_FST` on read, `_FSL` on write.
|
|
FanSpeed(usize),
|
|
Processor,
|
|
/// `/scheme/acpi/processor/<cpu>/<file>` -- 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)]
|
|
enum PowerFileKind {
|
|
State,
|
|
Percentage,
|
|
Online,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum ThermalFileKind {
|
|
Temperature,
|
|
Passive,
|
|
Critical,
|
|
Active,
|
|
}
|
|
|
|
#[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 | Self::Processor | Self::DmiDir | Self::PowerBatteries => true,
|
|
Self::ThermalZone { .. } => false,
|
|
Self::PowerBattery { .. } | Self::PowerAdapter { .. } => false,
|
|
Self::Dmi => true,
|
|
Self::DmiField(_) => false,
|
|
Self::ProcFile { .. } => false,
|
|
Self::LidState | Self::Button { .. } | Self::Notifications => false,
|
|
Self::PmTimer => false,
|
|
Self::Lid | Self::ButtonDir => true,
|
|
Self::Fan => true,
|
|
Self::FanState(_) | Self::FanSpeed(_) => false,
|
|
}
|
|
}
|
|
fn len(&self, acpi_ctx: &AcpiContext) -> Result<usize> {
|
|
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),
|
|
Self::PowerBatteries | Self::PowerBattery { .. } | Self::PowerAdapter { .. } => 2,
|
|
Self::LidState => 8,
|
|
Self::Button { .. } => 2,
|
|
Self::Notifications | Self::Lid | Self::ButtonDir | Self::Fan => 0,
|
|
Self::FanState(_) | Self::FanSpeed(_) => 4,
|
|
Self::ThermalZone { .. } => 16,
|
|
Self::PmTimer => 16,
|
|
// Directories
|
|
Self::TopLevel | Self::Symbols(_) | Self::Tables => 0,
|
|
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir => 0,
|
|
// ProcFile contents (e.g. PSS table) are bounded by the
|
|
// platform's ACPI table sizes; the maximum reasonable size
|
|
// is one page (4096 bytes). Report the file as a fixed
|
|
// size so the kernel-side read can mmap it.
|
|
Self::ProcFile { .. } => 4096,
|
|
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,
|
|
power_cache: PowerCache::default(),
|
|
processor_cache: std::collections::HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn power_cache(&mut self) -> &PowerCache {
|
|
if self.power_cache.batteries.is_empty() && self.power_cache.adapter.is_none() {
|
|
self.power_cache = self.ctx.power_devices();
|
|
}
|
|
&self.power_cache
|
|
}
|
|
|
|
/// 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<u64> {
|
|
let handle = self.kstop_fd.as_ref().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))
|
|
}
|
|
|
|
/// Phase J: ask the kernel to enter s2idle (Modern
|
|
/// Standby / S0ix). This is the typed-AcpiVerb equivalent
|
|
/// of writing "s2idle" to /scheme/sys/kstop — the kstop
|
|
/// string-arg path was Phase I.5's fallback while we
|
|
/// couldn't extend the syscall crate due to the libredox
|
|
/// cross-version issue. Phase J: with the local libredox
|
|
/// fork (which uses the local syscall fork with
|
|
/// EnterS2Idle/ExitS2Idle), this typed path is the
|
|
/// preferred API. The kstop string-arg path remains for
|
|
/// backward compatibility with older acpid builds.
|
|
///
|
|
/// Hardware-agnostic: works for any platform with Modern
|
|
/// Standby firmware (Dell, HP, Lenovo, LG Gram, etc.).
|
|
/// Mirrors Linux 7.1 `acpi_s2idle_begin` in
|
|
/// `kernel/power/suspend.c:91`.
|
|
pub fn kstop_enter_s2idle(&self) -> syscall::Result<()> {
|
|
let handle = self.kstop_fd.as_ref().ok_or(syscall::error::Error::new(syscall::error::EBADF))?;
|
|
let verb = AcpiVerb::EnterS2Idle as u64;
|
|
// AcpiVerb::EnterS2Idle doesn't need a write payload;
|
|
// the verb code itself is the signal. The kernel
|
|
// sets S2IDLE_REQUESTED + signals the kstop handle's
|
|
// EVENT_READ.
|
|
handle.call_wo(&[], CallFlags::empty(), &[verb])?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Phase II.X.W: write the kernel's S3 resume
|
|
/// trampoline address to FACS.xfirmware_waking_vector so
|
|
/// the platform firmware jumps to it on S3 wake.
|
|
///
|
|
/// `trampoline_addr` is the address of the kernel's
|
|
/// `s3_resume::s3_trampoline` function. The kernel
|
|
/// writes this to FACS via the `SetS3WakingVector`
|
|
/// AcPiVerb (verb 5).
|
|
pub fn kstop_enter_s3(&self, trampoline_addr: u64) -> syscall::Result<()> {
|
|
let handle = self.kstop_fd.as_ref().ok_or(syscall::error::Error::new(syscall::error::EBADF))?;
|
|
let verb = AcpiVerb::SetS3WakingVector as u64;
|
|
// Payload: 8-byte little-endian u64 (the trampoline
|
|
// address). The kernel's `SetS3WakingVector` handler
|
|
// requires the payload to be exactly 8 bytes.
|
|
let payload = trampoline_addr.to_ne_bytes();
|
|
handle.call_wo(&payload, CallFlags::empty(), &[verb])?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn parse_hex_digit(hex: u8) -> Option<u8> {
|
|
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<u8> {
|
|
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/<field>` 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<String> {
|
|
crate::dmi::read_field(info, field)
|
|
}
|
|
|
|
fn parse_table(table: &[u8]) -> Option<SdtSignature> {
|
|
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<usize> {
|
|
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<OpenResult> {
|
|
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,
|
|
["thermal", zone_name, file] => {
|
|
let zones = self.ctx.thermal_zones();
|
|
let zone_path = format!("\\_TZ.{}", zone_name);
|
|
if !zones.contains(&zone_path) {
|
|
return Err(Error::new(ENOENT));
|
|
}
|
|
let kind = match *file {
|
|
"temperature" => ThermalFileKind::Temperature,
|
|
"passive" => ThermalFileKind::Passive,
|
|
"critical" => ThermalFileKind::Critical,
|
|
"active" => ThermalFileKind::Active,
|
|
_ => return Err(Error::new(ENOENT)),
|
|
};
|
|
HandleKind::ThermalZone {
|
|
zone: zone_path,
|
|
kind,
|
|
}
|
|
}
|
|
["power"] => HandleKind::Power,
|
|
["dmi"] => HandleKind::Dmi,
|
|
["processor"] => HandleKind::Processor,
|
|
["power", "batteries"] => HandleKind::PowerBatteries,
|
|
["power", "batteries", name, file] => {
|
|
let file = match *file {
|
|
"state" => PowerFileKind::State,
|
|
"percentage" => PowerFileKind::Percentage,
|
|
_ => return Err(Error::new(ENOENT)),
|
|
};
|
|
HandleKind::PowerBattery { name: (*name).to_owned(), file }
|
|
}
|
|
["power", "adapters", name, file] => {
|
|
let file = match *file {
|
|
"online" => PowerFileKind::Online,
|
|
_ => return Err(Error::new(ENOENT)),
|
|
};
|
|
HandleKind::PowerAdapter { name: (*name).to_owned(), file }
|
|
}
|
|
|
|
["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));
|
|
}
|
|
}
|
|
|
|
["lid"] => HandleKind::Lid,
|
|
["pmtimer"] => HandleKind::PmTimer,
|
|
["lid", "state"] => HandleKind::LidState,
|
|
["button"] => HandleKind::ButtonDir,
|
|
["button", "power"] => HandleKind::Button { power: true },
|
|
["button", "sleep"] => HandleKind::Button { power: false },
|
|
["notifications"] => HandleKind::Notifications,
|
|
["fan"] => HandleKind::Fan,
|
|
["fan", index, "state"] => {
|
|
let index = index.parse::<usize>().map_err(|_| Error::new(ENOENT))?;
|
|
if index >= self.ctx.fan_devices.read().len() {
|
|
return Err(Error::new(ENOENT));
|
|
}
|
|
HandleKind::FanState(index)
|
|
}
|
|
["fan", index, "speed"] => {
|
|
let index = index.parse::<usize>().map_err(|_| Error::new(ENOENT))?;
|
|
if index >= self.ctx.fan_devices.read().len() {
|
|
return Err(Error::new(ENOENT));
|
|
}
|
|
HandleKind::FanSpeed(index)
|
|
}
|
|
|
|
["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/<cpu>/{pss,psd,cst,cpc}
|
|
let cpu: u32 = cpu_str
|
|
.strip_prefix("CPU")
|
|
.and_then(|rest| rest.parse().ok())
|
|
.ok_or(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<usize> {
|
|
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::PowerBattery { name, file } => {
|
|
dmi_buf = match file {
|
|
PowerFileKind::State => self.ctx.battery_state_text(name),
|
|
PowerFileKind::Percentage => self.ctx.battery_percentage_text(name),
|
|
PowerFileKind::Online => String::new(),
|
|
};
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::PowerAdapter { name, file } => {
|
|
dmi_buf = match file {
|
|
PowerFileKind::Online => self.ctx.adapter_online_text(name),
|
|
PowerFileKind::State | PowerFileKind::Percentage => String::new(),
|
|
};
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::LidState => {
|
|
dmi_buf = self.ctx.lid_state_text();
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::Button { power } => {
|
|
let count = self.ctx.take_button_events(*power);
|
|
dmi_buf = format!("{count}\n");
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::FanState(index) | HandleKind::FanSpeed(index) => {
|
|
dmi_buf = match self.ctx.fan_speed(*index) {
|
|
Some(percent) => format!("{percent}\n"),
|
|
None => return Err(Error::new(EIO)),
|
|
};
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::Notifications => {
|
|
let lines = self
|
|
.ctx
|
|
.notifications()
|
|
.drain()
|
|
.iter()
|
|
.map(|(device, value)| format!("{device} {value:#x}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
dmi_buf = if lines.is_empty() { lines } else { format!("{lines}\n") };
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::ThermalZone { zone, kind } => {
|
|
match kind {
|
|
ThermalFileKind::Active => {
|
|
// _AC0.._AC9 active cooling trip points (tenths of K);
|
|
// only the defined ones are returned, one per line.
|
|
let mut lines = String::new();
|
|
for idx in 0..10u8 {
|
|
let method = format!("_AC{idx}");
|
|
if let Ok(values) =
|
|
self.ctx.evaluate_acpi_method(zone, &method, &[])
|
|
{
|
|
if let Some(v) = values.first() {
|
|
lines.push_str(&format!("{method}={v}\n"));
|
|
}
|
|
}
|
|
}
|
|
dmi_buf = lines;
|
|
dmi_buf.as_bytes()
|
|
}
|
|
_ => {
|
|
let method = match kind {
|
|
ThermalFileKind::Temperature => "_TMP",
|
|
ThermalFileKind::Passive => "_PSV",
|
|
ThermalFileKind::Critical => "_CRT",
|
|
ThermalFileKind::Active => unreachable!(),
|
|
};
|
|
let values = self
|
|
.ctx
|
|
.evaluate_acpi_method(zone, method, &[])
|
|
.map_err(|_| Error::new(EIO))?;
|
|
let raw = values.first().copied().unwrap_or(0);
|
|
dmi_buf = format!("{}\n", raw);
|
|
dmi_buf.as_bytes()
|
|
}
|
|
}
|
|
}
|
|
HandleKind::PmTimer => {
|
|
let fadt = self.ctx.fadt().ok_or(Error::new(ENODEV))?;
|
|
let value = crate::gpe::pm_timer_read(fadt).ok_or(Error::new(ENODEV))?;
|
|
dmi_buf = format!("{value}\n");
|
|
dmi_buf.as_bytes()
|
|
}
|
|
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::PowerBatteries | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot | HandleKind::Lid | HandleKind::ButtonDir | HandleKind::Fan => {
|
|
return Err(Error::new(EISDIR));
|
|
}
|
|
HandleKind::ProcFile { cpu, kind } => {
|
|
let method = match kind {
|
|
ProcFileKind::Pss => "_PSS",
|
|
ProcFileKind::Psd => "_PSD",
|
|
ProcFileKind::Cst => "_CST",
|
|
ProcFileKind::Cpc => "_CPC",
|
|
};
|
|
let key = (*cpu, method);
|
|
// These are static after boot; evaluate once, then serve from
|
|
// cache to avoid re-running the AML interpreter under the global
|
|
// lock on every cpufreqd poll.
|
|
if !self.processor_cache.contains_key(&key) {
|
|
let cpu_segment = format!("CPU{}", cpu);
|
|
let text = self.ctx.processor_method_text(&cpu_segment, method);
|
|
self.processor_cache.insert(key, text);
|
|
}
|
|
proc_buf = self.processor_cache[&key].clone().into_bytes();
|
|
proc_buf.as_slice()
|
|
}
|
|
HandleKind::Tables => return Err(Error::new(EISDIR)),
|
|
};
|
|
|
|
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 write(
|
|
&mut self,
|
|
id: usize,
|
|
buf: &[u8],
|
|
_offset: u64,
|
|
_fcntl_flags: u32,
|
|
_ctx: &CallerCtx,
|
|
) -> Result<usize> {
|
|
let handle = self.handles.get(id)?;
|
|
let HandleKind::FanSpeed(index) = handle.kind else {
|
|
return Err(Error::new(EBADF));
|
|
};
|
|
|
|
let text = str::from_utf8(buf).map_err(|_| Error::new(EINVAL))?;
|
|
let percent: u64 = text.trim().parse().map_err(|_| Error::new(EINVAL))?;
|
|
if !self.ctx.fan_set_speed(index, percent) {
|
|
return Err(Error::new(EIO));
|
|
}
|
|
Ok(buf.len())
|
|
}
|
|
|
|
fn getdents<'buf>(
|
|
&mut self,
|
|
id: usize,
|
|
mut buf: DirentBuf<&'buf mut [u8]>,
|
|
opaque_offset: u64,
|
|
) -> Result<DirentBuf<&'buf mut [u8]>> {
|
|
let handle = self.handles.get_mut(id)?;
|
|
|
|
match &handle.kind {
|
|
HandleKind::TopLevel => {
|
|
const TOPLEVEL_ENTRIES: &[&str] = &[
|
|
"tables", "symbols", "thermal", "power", "dmi", "processor",
|
|
"lid", "button", "notifications", "fan",
|
|
];
|
|
|
|
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::Fan => {
|
|
let count = self.ctx.fan_devices.read().len();
|
|
for (idx, name) in (0..count)
|
|
.map(|i| i.to_string())
|
|
.enumerate()
|
|
.skip(opaque_offset as usize)
|
|
{
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: idx as u64 + 1,
|
|
name: &name,
|
|
kind: DirentKind::Directory,
|
|
})?;
|
|
}
|
|
}
|
|
HandleKind::Lid => {
|
|
if opaque_offset == 0 {
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: 1,
|
|
name: "state",
|
|
kind: DirentKind::Regular,
|
|
})?;
|
|
}
|
|
}
|
|
HandleKind::ButtonDir => {
|
|
for (idx, name) in ["power", "sleep"]
|
|
.iter()
|
|
.enumerate()
|
|
.skip(opaque_offset as usize)
|
|
{
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: idx as u64 + 1,
|
|
name,
|
|
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.<zone> 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.<cpu> 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. The directory
|
|
// entry names use the short CPU segment (e.g. "CPU0")
|
|
// so that `processor/CPU0/pss` is a valid sub-path.
|
|
let cpus = self.ctx.cpu_names();
|
|
for (idx, cpu_path) in cpus.iter().enumerate().skip(opaque_offset as usize) {
|
|
let short = cpu_path.strip_prefix("\\_PR.").unwrap_or(cpu_path);
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: idx as u64 + 1,
|
|
name: short,
|
|
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 cache = self.power_cache().clone();
|
|
if !cache.batteries.is_empty() {
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: 1,
|
|
name: "batteries",
|
|
kind: DirentKind::Directory,
|
|
})?;
|
|
}
|
|
if cache.adapter.is_some() {
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: 2,
|
|
name: "adapters",
|
|
kind: DirentKind::Directory,
|
|
})?;
|
|
}
|
|
}
|
|
HandleKind::PowerBatteries => {
|
|
let batteries = &self.power_cache().batteries;
|
|
for (idx, battery) in batteries.iter().enumerate().skip(opaque_offset as usize) {
|
|
buf.entry(DirEntry {
|
|
inode: 0,
|
|
next_opaque_id: idx as u64 + 1,
|
|
name: battery.as_str(),
|
|
kind: DirentKind::Directory,
|
|
})?;
|
|
}
|
|
}
|
|
HandleKind::PowerBattery { .. } => {
|
|
for (idx, file) in ["state", "percentage"].iter().enumerate().skip(opaque_offset as usize) {
|
|
buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: file, kind: DirentKind::Regular })?;
|
|
}
|
|
}
|
|
HandleKind::PowerAdapter { .. } => {
|
|
buf.entry(DirEntry { inode: 0, next_opaque_id: 1, name: "online", kind: DirentKind::Regular })?;
|
|
}
|
|
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<usize> {
|
|
let handle = self.handles.get_mut(id)?;
|
|
if !handle.allowed_to_eval {
|
|
return Err(Error::new(EPERM));
|
|
}
|
|
|
|
let Ok(args): Result<Vec<AmlSerdeValue>, 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<usize> {
|
|
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);
|
|
|
|
// Allow replacement: pcid may resend the fd after a restart,
|
|
// and the previous one-shot EINVAL left aml init permanently
|
|
// broken if the first sendfd raced with an early aml request.
|
|
if self.pci_fd.is_some() {
|
|
log::warn!(
|
|
"acpid: replacing previously-registered PCI fd; AML symbol cache will rebuild on next request"
|
|
);
|
|
}
|
|
self.pci_fd = Some(new_fd);
|
|
|
|
// Kick aml symbol init now that pci_fd is registered. The
|
|
// next aml request will either find a working cache or get a
|
|
// fresh error log here; either way the failure mode is
|
|
// observable rather than silently deferred to "the next
|
|
// caller retries".
|
|
match self.ctx.aml_symbols(self.pci_fd.as_ref()) {
|
|
Ok(_) => log::info!("acpid: AML symbols initialized on PCI fd registration"),
|
|
Err(err) => {
|
|
log::error!("acpid: AML symbol init failed after PCI fd registration");
|
|
log::error!("acpid: AML error detail: {:?}", err);
|
|
}
|
|
}
|
|
|
|
Ok(num_fds)
|
|
}
|
|
|
|
fn on_close(&mut self, id: usize) {
|
|
self.handles.remove(id);
|
|
}
|
|
}
|