From 220f053ad8d55dd13afefb48e161a63af48e30d5 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Sat, 25 Apr 2026 19:30:53 +0100 Subject: [PATCH] Complete base patch split and update rust toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base patch extraction (12 new patches, 11,017 lines from the 17k monolith): - P2-acpid-core-refactor: acpi.rs, dmar, aml_physmem, ec, scheme (3,150 lines) - P2-ihdad-device-refactor: CodecTopology, ControllerPolicy, InputStream (1,022 lines) - P2-ac97d-ihdad-main: AC97 + ihdad daemon error handling (287 lines) - P2-inputd: inputd lib + main with named producers (896 lines) - P2-network-driver-mains: e1000/ixgbe/rtl8139/rtl8168d/virtio-net mains (607 lines) - P2-pcid-driver-interface: BAR, cap, config, IRQ helpers, MSI, scheme (1,463 lines) - P2-storage-driver-mains: ahcid/ided/nvmed/virtio-blk main.rs files (625 lines) - P2-xhcid-remaining: xhcid main, device_enumerator, xhci mod+scheme (2,033 lines) - P2-virtio-core-vbox: virtio-core arch/probe/transport + vboxd (413 lines) - P2-init-subsystems: scheduler, service, unit management (292 lines) - P2-logd: logd main + scheme (164 lines) - P2-hwd-misc: hwd Cargo.toml + main.rs (64 lines) Graphics drivers (ihdgd, vesad, virtio-gpud, fbcond scheme/text, graphics-ipc) already fully covered by existing P2-daemon-hardening.patch — no duplicates created. Rust toolchain: nightly-2025-10-03 → nightly-2026-04-01 (1.96.0-nightly). Cookbook builds clean, no feature gates in codebase. --- local/patches/base/P2-ac97d-ihdad-main.patch | 287 ++ .../patches/base/P2-acpid-core-refactor.patch | 3150 +++++++++++++++++ local/patches/base/P2-hwd-misc.patch | 64 + .../base/P2-ihdad-device-refactor.patch | 1022 ++++++ local/patches/base/P2-init-subsystems.patch | 292 ++ local/patches/base/P2-inputd.patch | 896 +++++ local/patches/base/P2-logd.patch | 164 + .../base/P2-network-driver-mains.patch | 607 ++++ .../base/P2-pcid-driver-interface.patch | 1463 ++++++++ .../base/P2-storage-driver-mains.patch | 625 ++++ local/patches/base/P2-virtio-core-vbox.patch | 413 +++ local/patches/base/P2-xhcid-remaining.patch | 2033 +++++++++++ rust-toolchain.toml | 2 +- 13 files changed, 11017 insertions(+), 1 deletion(-) create mode 100644 local/patches/base/P2-ac97d-ihdad-main.patch create mode 100644 local/patches/base/P2-acpid-core-refactor.patch create mode 100644 local/patches/base/P2-hwd-misc.patch create mode 100644 local/patches/base/P2-ihdad-device-refactor.patch create mode 100644 local/patches/base/P2-init-subsystems.patch create mode 100644 local/patches/base/P2-inputd.patch create mode 100644 local/patches/base/P2-logd.patch create mode 100644 local/patches/base/P2-network-driver-mains.patch create mode 100644 local/patches/base/P2-pcid-driver-interface.patch create mode 100644 local/patches/base/P2-storage-driver-mains.patch create mode 100644 local/patches/base/P2-virtio-core-vbox.patch create mode 100644 local/patches/base/P2-xhcid-remaining.patch diff --git a/local/patches/base/P2-ac97d-ihdad-main.patch b/local/patches/base/P2-ac97d-ihdad-main.patch new file mode 100644 index 00000000..04af504f --- /dev/null +++ b/local/patches/base/P2-ac97d-ihdad-main.patch @@ -0,0 +1,287 @@ +# P2-ac97d-ihdad-main.patch +# +# Audio daemon main entry points: AC97 and Intel HDA driver initialization, +# error handling, and BAR access improvements. +# +# Covers: +# - ac97d/src/main.rs: BAR access, error handling, codec initialization +# - ihdad/src/main.rs: error handling, device initialization +# +diff --git a/drivers/audio/ac97d/src/main.rs b/drivers/audio/ac97d/src/main.rs +index ffa8a94b..e4dbf930 100644 +--- a/drivers/audio/ac97d/src/main.rs ++++ b/drivers/audio/ac97d/src/main.rs +@@ -3,6 +3,7 @@ use std::os::unix::io::AsRawFd; + use std::usize; + + use event::{user_data, EventQueue}; ++use log::error; + use pcid_interface::PciFunctionHandle; + use redox_scheme::scheme::register_sync_scheme; + use redox_scheme::Socket; +@@ -22,13 +23,28 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + let mut name = pci_config.func.name(); + name.push_str("_ac97"); + +- let bar0 = pci_config.func.bars[0].expect_port(); +- let bar1 = pci_config.func.bars[1].expect_port(); ++ let bar0 = match pci_config.func.bars[0].try_port() { ++ Ok(port) => port, ++ Err(err) => { ++ error!("ac97d: invalid BAR0: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ let bar1 = match pci_config.func.bars[1].try_port() { ++ Ok(port) => port, ++ Err(err) => { ++ error!("ac97d: invalid BAR1: {err}"); ++ std::process::exit(1); ++ } ++ }; + + let irq = pci_config + .func + .legacy_interrupt_line +- .expect("ac97d: no legacy interrupts supported"); ++ .unwrap_or_else(|| { ++ error!("ac97d: no legacy interrupts supported"); ++ std::process::exit(1); ++ }); + + println!(" + ac97 {}", pci_config.func.display()); + +@@ -40,13 +56,35 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + common::file_level(), + ); + +- common::acquire_port_io_rights().expect("ac97d: failed to set I/O privilege level to Ring 3"); ++ if let Err(err) = common::acquire_port_io_rights() { ++ error!("ac97d: failed to set I/O privilege level to Ring 3: {err}"); ++ std::process::exit(1); ++ } + +- let mut irq_file = irq.irq_handle("ac97d"); ++ let mut irq_file = match irq.try_irq_handle("ac97d") { ++ Ok(file) => file, ++ Err(err) => { ++ error!("ac97d: failed to open IRQ handle: {err}"); ++ std::process::exit(1); ++ } ++ }; + +- let socket = Socket::nonblock().expect("ac97d: failed to create socket"); +- let mut device = +- unsafe { device::Ac97::new(bar0, bar1).expect("ac97d: failed to allocate device") }; ++ let socket = match Socket::nonblock() { ++ Ok(socket) => socket, ++ Err(err) => { ++ error!("ac97d: failed to create socket: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ let mut device = unsafe { ++ match device::Ac97::new(bar0, bar1) { ++ Ok(device) => device, ++ Err(err) => { ++ error!("ac97d: failed to allocate device: {err}"); ++ std::process::exit(1); ++ } ++ } ++ }; + let mut readiness_based = ReadinessBased::new(&socket, 16); + + user_data! { +@@ -56,49 +94,81 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = EventQueue::::new().expect("ac97d: Could not create event queue."); ++ let event_queue = match EventQueue::::new() { ++ Ok(queue) => queue, ++ Err(err) => { ++ error!("ac97d: could not create event queue: {err}"); ++ std::process::exit(1); ++ } ++ }; + event_queue + .subscribe( + irq_file.as_raw_fd() as usize, + Source::Irq, + event::EventFlags::READ, + ) +- .unwrap(); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to subscribe IRQ fd: {err}"); ++ std::process::exit(1); ++ }); + event_queue + .subscribe( + socket.inner().raw(), + Source::Scheme, + event::EventFlags::READ, + ) +- .unwrap(); +- +- register_sync_scheme(&socket, "audiohw", &mut device) +- .expect("ac97d: failed to register audiohw scheme to namespace"); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to subscribe scheme fd: {err}"); ++ std::process::exit(1); ++ }); ++ ++ register_sync_scheme(&socket, "audiohw", &mut device).unwrap_or_else(|err| { ++ error!("ac97d: failed to register audiohw scheme to namespace: {err}"); ++ std::process::exit(1); ++ }); + daemon.ready(); + +- libredox::call::setrens(0, 0).expect("ac97d: failed to enter null namespace"); ++ if let Err(err) = libredox::call::setrens(0, 0) { ++ error!("ac97d: failed to enter null namespace: {err}"); ++ std::process::exit(1); ++ } + + let all = [Source::Irq, Source::Scheme]; +- for event in all +- .into_iter() +- .chain(event_queue.map(|e| e.expect("ac97d: failed to get next event").user_data)) +- { ++ for event in all.into_iter().chain(event_queue.map(|e| match e { ++ Ok(event) => event.user_data, ++ Err(err) => { ++ error!("ac97d: failed to get next event: {err}"); ++ std::process::exit(1); ++ } ++ })) { + match event { + Source::Irq => { + let mut irq = [0; 8]; +- irq_file.read(&mut irq).unwrap(); ++ if let Err(err) = irq_file.read(&mut irq) { ++ error!("ac97d: failed to read IRQ file: {err}"); ++ std::process::exit(1); ++ } + + if !device.irq() { + continue; + } +- irq_file.write(&mut irq).unwrap(); ++ if let Err(err) = irq_file.write(&mut irq) { ++ error!("ac97d: failed to acknowledge IRQ: {err}"); ++ std::process::exit(1); ++ } + + readiness_based + .poll_all_requests(&mut device) +- .expect("ac97d: failed to poll requests"); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to poll requests: {err}"); ++ std::process::exit(1); ++ }); + readiness_based + .write_responses() +- .expect("ac97d: failed to write to socket"); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to write to socket: {err}"); ++ std::process::exit(1); ++ }); + + /* + let next_read = device_irq.next_read(); +@@ -110,10 +180,16 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + Source::Scheme => { + readiness_based + .read_and_process_requests(&mut device) +- .expect("ac97d: failed to read from socket"); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to read from socket: {err}"); ++ std::process::exit(1); ++ }); + readiness_based + .write_responses() +- .expect("ac97d: failed to write to socket"); ++ .unwrap_or_else(|err| { ++ error!("ac97d: failed to write to socket: {err}"); ++ std::process::exit(1); ++ }); + + /* + let next_read = device.borrow().next_read(); +@@ -125,7 +201,7 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + } + } + +- std::process::exit(0); ++ std::process::exit(1); + } + + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + +diff --git a/drivers/audio/ihdad/src/main.rs b/drivers/audio/ihdad/src/main.rs +index 31a2add7..11d80133 100755 +--- a/drivers/audio/ihdad/src/main.rs ++++ b/drivers/audio/ihdad/src/main.rs +@@ -6,7 +6,7 @@ use std::os::unix::io::AsRawFd; + use std::usize; + + use event::{user_data, EventQueue}; +-use pcid_interface::irq_helpers::pci_allocate_interrupt_vector; ++use pcid_interface::irq_helpers::try_pci_allocate_interrupt_vector; + use pcid_interface::PciFunctionHandle; + + pub mod hda; +@@ -38,9 +38,19 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + log::info!("IHDA {}", pci_config.func.display()); + ++ if let Err(err) = pci_config.func.bars[0].try_mem() { ++ log::error!("ihdad: invalid BAR0: {err}"); ++ std::process::exit(1); ++ } + let address = unsafe { pcid_handle.map_bar(0) }.ptr.as_ptr() as usize; + +- let irq_file = pci_allocate_interrupt_vector(&mut pcid_handle, "ihdad"); ++ let irq_file = match try_pci_allocate_interrupt_vector(&mut pcid_handle, "ihdad") { ++ Ok(irq) => irq, ++ Err(err) => { ++ log::error!("ihdad: failed to allocate interrupt vector: {err}"); ++ std::process::exit(1); ++ } ++ }; + + { + let vend_prod: u32 = ((pci_config.func.full_device_id.vendor_id as u32) << 16) +@@ -53,11 +63,28 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = +- EventQueue::::new().expect("ihdad: Could not create event queue."); +- let socket = Socket::nonblock().expect("ihdad: failed to create socket"); ++ let event_queue = match EventQueue::::new() { ++ Ok(queue) => queue, ++ Err(err) => { ++ log::error!("ihdad: could not create event queue: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ let socket = match Socket::nonblock() { ++ Ok(socket) => socket, ++ Err(err) => { ++ log::error!("ihdad: failed to create socket: {err}"); ++ std::process::exit(1); ++ } ++ }; + let mut device = unsafe { +- hda::IntelHDA::new(address, vend_prod).expect("ihdad: failed to allocate device") ++ match hda::IntelHDA::new(address, vend_prod) { ++ Ok(device) => device, ++ Err(err) => { ++ log::error!("ihdad: failed to allocate device: {err}"); ++ std::process::exit(1); ++ } ++ } + }; + let mut readiness_based = ReadinessBased::new(&socket, 16); + diff --git a/local/patches/base/P2-acpid-core-refactor.patch b/local/patches/base/P2-acpid-core-refactor.patch new file mode 100644 index 00000000..8b6f40dd --- /dev/null +++ b/local/patches/base/P2-acpid-core-refactor.patch @@ -0,0 +1,3150 @@ +# P2-acpid-core-refactor.patch +# +# Core acpid refactoring: DMI/SMBIOS discovery, ACPI power snapshot, sleep/S5 +# handling, FADT power blocks, GenericAddress I/O, AML mutex implementation, +# EC multi-byte region handler, DMAR validation, and scheme resources/power/DMI. +# +# Covers: +# - acpid/src/acpi.rs: DmiInfo, AcpiPowerSnapshot, sleep/S5, Fadt, GenericAddress, EC, quirks +# - acpid/src/acpi/dmar/mod.rs: DMAR structure length validation +# - acpid/src/aml_physmem.rs: AmlMutex implementation with Condvar +# - acpid/src/ec.rs: EC error type, multi-byte read/write, checked offsets +# - acpid/src/scheme.rs: resources, power, DMI directory entries (full section) +# +diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs +index 94a1eb17..a7cde5d6 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(()) + } +@@ -284,7 +896,11 @@ impl AmlSymbols { + match self.init(pci_fd) { + Ok(()) => (), + Err(err) => { +- log::error!("failed to initialize AML context: {}", err); ++ if pci_fd.is_none() { ++ log::debug!("AML init deferred until PCI registration: {}", err); ++ } else { ++ log::error!("failed to initialize AML context: {}", err); ++ } + } + } + } +@@ -316,7 +932,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 +959,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 +995,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 +1006,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 +1187,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 +1201,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 +1326,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 +1394,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 +1544,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 +1556,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 +1829,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 +1837,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 +1907,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 +1953,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 +1973,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 +2001,20 @@ 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 context.pci_ready() { ++ if let Err(error) = context.refresh_s5_values() { ++ log::warn!("Failed to evaluate \\_S5 during FADT init: {error}"); ++ } ++ } else { ++ log::debug!("Deferring \\_S5 evaluation until PCI registration"); ++ } + } + } + + +diff --git a/drivers/acpid/src/acpi/dmar/mod.rs b/drivers/acpid/src/acpi/dmar/mod.rs +index c42b379a..f4dff276 100644 +--- a/drivers/acpid/src/acpi/dmar/mod.rs ++++ b/drivers/acpid/src/acpi/dmar/mod.rs +@@ -474,10 +474,13 @@ impl<'sdt> Iterator for DmarRawIter<'sdt> { + let len_bytes = <[u8; 2]>::try_from(type_bytes) + .expect("expected a 2-byte slice to be convertible to [u8; 2]"); + +- let ty = u16::from_ne_bytes(type_bytes); +- let len = u16::from_ne_bytes(len_bytes); ++ let len = u16::from_ne_bytes(len_bytes) as usize; + +- let len = usize::try_from(len).expect("expected u16 to fit within usize"); ++ if len < 4 { ++ return None; ++ } ++ ++ let ty = u16::from_ne_bytes(type_bytes); + + if len > remainder.len() { + log::warn!("DMAR remapping structure length was smaller than the remaining length of the table."); + +diff --git a/drivers/acpid/src/aml_physmem.rs b/drivers/acpid/src/aml_physmem.rs +index 2bdd667b..69b8c48b 100644 +--- a/drivers/acpid/src/aml_physmem.rs ++++ b/drivers/acpid/src/aml_physmem.rs +@@ -6,7 +6,10 @@ use rustc_hash::FxHashMap; + use std::fmt::LowerHex; + use std::mem::size_of; + use std::ptr::NonNull; +-use std::sync::{Arc, Mutex}; ++use std::sync::atomic::{AtomicU32, Ordering}; ++use std::sync::{Arc, Condvar, Mutex}; ++use std::thread::ThreadId; ++use std::time::{Duration, Instant}; + use syscall::PAGE_SIZE; + + const PAGE_MASK: usize = !(PAGE_SIZE - 1); +@@ -141,6 +144,20 @@ impl AmlPageCache { + pub struct AmlPhysMemHandler { + page_cache: Arc>, + pci_fd: Arc>, ++ aml_mutexes: Arc>>>, ++ next_mutex_handle: Arc, ++} ++ ++#[derive(Debug, Default)] ++struct AmlMutexState { ++ owner: Option, ++ depth: u32, ++} ++ ++#[derive(Debug, Default)] ++struct AmlMutex { ++ state: Mutex, ++ condvar: Condvar, + } + + /// Read from a physical address. +@@ -156,6 +173,30 @@ impl AmlPhysMemHandler { + Self { + page_cache, + pci_fd: Arc::new(pci_fd), ++ aml_mutexes: Arc::new(Mutex::new(FxHashMap::default())), ++ next_mutex_handle: Arc::new(AtomicU32::new(1)), ++ } ++ } ++ ++ fn aml_mutex(&self, handle: Handle) -> Option> { ++ self.aml_mutexes ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()) ++ .get(&handle.0) ++ .cloned() ++ } ++ ++ fn read_phys_or_fault(&self, address: usize) -> T ++ where ++ T: PrimInt + LowerHex, ++ { ++ let mut page_cache = self ++ .page_cache ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ match page_cache.read_from_phys::(address) { ++ Ok(value) => value, ++ Err(error) => panic!("AML physmem read failed at {:#x}: {}", address, error), + } + } + +@@ -240,43 +281,19 @@ impl acpi::Handler for AmlPhysMemHandler { + + fn read_u8(&self, address: usize) -> u8 { + log::trace!("read u8 {:X}", address); +- if let Ok(mut page_cache) = self.page_cache.lock() { +- if let Ok(value) = page_cache.read_from_phys::(address) { +- return value; +- } +- } +- log::error!("failed to read u8 {:#x}", address); +- 0 ++ self.read_phys_or_fault::(address) + } + fn read_u16(&self, address: usize) -> u16 { + log::trace!("read u16 {:X}", address); +- if let Ok(mut page_cache) = self.page_cache.lock() { +- if let Ok(value) = page_cache.read_from_phys::(address) { +- return value; +- } +- } +- log::error!("failed to read u16 {:#x}", address); +- 0 ++ self.read_phys_or_fault::(address) + } + fn read_u32(&self, address: usize) -> u32 { + log::trace!("read u32 {:X}", address); +- if let Ok(mut page_cache) = self.page_cache.lock() { +- if let Ok(value) = page_cache.read_from_phys::(address) { +- return value; +- } +- } +- log::error!("failed to read u32 {:#x}", address); +- 0 ++ self.read_phys_or_fault::(address) + } + fn read_u64(&self, address: usize) -> u64 { + log::trace!("read u64 {:X}", address); +- if let Ok(mut page_cache) = self.page_cache.lock() { +- if let Ok(value) = page_cache.read_from_phys::(address) { +- return value; +- } +- } +- log::error!("failed to read u64 {:#x}", address); +- 0 ++ self.read_phys_or_fault::(address) + } + + fn write_u8(&self, address: usize, value: u8) { +@@ -415,16 +432,102 @@ impl acpi::Handler for AmlPhysMemHandler { + } + + fn create_mutex(&self) -> Handle { +- log::debug!("TODO: Handler::create_mutex"); +- Handle(0) ++ let handle = self.next_mutex_handle.fetch_add(1, Ordering::Relaxed); ++ self.aml_mutexes ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()) ++ .insert(handle, Arc::new(AmlMutex::default())); ++ log::trace!("created AML mutex handle {handle}"); ++ Handle(handle) + } + + fn acquire(&self, mutex: Handle, timeout: u16) -> Result<(), AmlError> { +- log::debug!("TODO: Handler::acquire"); +- Ok(()) ++ let Some(aml_mutex) = self.aml_mutex(mutex) else { ++ log::error!("attempted to acquire unknown AML mutex handle {}", mutex.0); ++ return Err(AmlError::MutexAcquireTimeout); ++ }; ++ ++ let current_thread = std::thread::current().id(); ++ let deadline = (timeout != 0xffff).then(|| Instant::now() + Duration::from_millis(timeout.into())); ++ ++ let mut state = aml_mutex ++ .state ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ ++ loop { ++ match state.owner { ++ None => { ++ state.owner = Some(current_thread); ++ state.depth = 1; ++ return Ok(()); ++ } ++ Some(owner) if owner == current_thread => { ++ state.depth = state.depth.saturating_add(1); ++ return Ok(()); ++ } ++ Some(_) if timeout == 0 => return Err(AmlError::MutexAcquireTimeout), ++ Some(_) if timeout == 0xffff => { ++ state = aml_mutex ++ .condvar ++ .wait(state) ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ } ++ Some(_) => { ++ let Some(deadline) = deadline else { ++ return Err(AmlError::MutexAcquireTimeout); ++ }; ++ let now = Instant::now(); ++ if now >= deadline { ++ return Err(AmlError::MutexAcquireTimeout); ++ } ++ ++ let remaining = deadline.saturating_duration_since(now); ++ let (next_state, wait_result) = aml_mutex ++ .condvar ++ .wait_timeout(state, remaining) ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ state = next_state; ++ ++ if wait_result.timed_out() && state.owner != Some(current_thread) { ++ return Err(AmlError::MutexAcquireTimeout); ++ } ++ } ++ } ++ } + } + + fn release(&self, mutex: Handle) { +- log::debug!("TODO: Handler::release"); ++ let Some(aml_mutex) = self.aml_mutex(mutex) else { ++ log::error!("attempted to release unknown AML mutex handle {}", mutex.0); ++ return; ++ }; ++ ++ let current_thread = std::thread::current().id(); ++ let mut state = aml_mutex ++ .state ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ ++ match state.owner { ++ Some(owner) if owner == current_thread => { ++ if state.depth > 1 { ++ state.depth -= 1; ++ } else { ++ state.owner = None; ++ state.depth = 0; ++ aml_mutex.condvar.notify_one(); ++ } ++ } ++ Some(_) => { ++ log::warn!( ++ "ignoring AML mutex release for handle {} from non-owner thread", ++ mutex.0 ++ ); ++ } ++ None => { ++ log::warn!("ignoring AML mutex release for unlocked handle {}", mutex.0); ++ } ++ } + } + } + +diff --git a/drivers/acpid/src/ec.rs b/drivers/acpid/src/ec.rs +index c322790a..99842586 100644 +--- a/drivers/acpid/src/ec.rs ++++ b/drivers/acpid/src/ec.rs +@@ -1,3 +1,4 @@ ++use std::convert::TryFrom; + use std::time::Duration; + + use acpi::aml::{ +@@ -30,6 +31,28 @@ const BURST_ACK: u8 = 0x90; + + pub const DEFAULT_EC_TIMEOUT: Duration = Duration::from_millis(10); + ++#[derive(Debug, Clone, Copy)] ++enum EcError { ++ Timeout, ++ OffsetOutOfRange, ++} ++ ++impl EcError { ++ fn as_aml_error(self) -> AmlError { ++ match self { ++ EcError::Timeout | EcError::OffsetOutOfRange => { ++ AmlError::NoHandlerForRegionAccess(RegionSpace::EmbeddedControl) ++ } ++ } ++ } ++} ++ ++impl From for AmlError { ++ fn from(value: EcError) -> Self { ++ value.as_aml_error() ++ } ++} ++ + #[repr(transparent)] + pub struct ScBits(u8); + #[allow(dead_code)] +@@ -90,28 +113,33 @@ impl Ec { + Pio::::new(self.data).write(value); + } + #[inline] +- fn wait_for_write_ready(&self) -> Option<()> { ++ fn wait_for_write_ready(&self) -> Result<(), EcError> { + let timeout = Timeout::new(self.timeout); + loop { + if !self.read_reg_sc().ibf() { +- return Some(()); ++ return Ok(()); + } +- timeout.run().ok()?; ++ timeout.run().map_err(|_| EcError::Timeout)?; + } + } + #[inline] +- fn wait_for_read_ready(&self) -> Option<()> { ++ fn wait_for_read_ready(&self) -> Result<(), EcError> { + let timeout = Timeout::new(self.timeout); + loop { + if self.read_reg_sc().obf() { +- return Some(()); ++ return Ok(()); + } +- timeout.run().ok()?; ++ timeout.run().map_err(|_| EcError::Timeout)?; + } + } + ++ #[inline] ++ fn checked_address(offset: usize, byte_index: usize) -> Result { ++ u8::try_from(offset + byte_index).map_err(|_| EcError::OffsetOutOfRange) ++ } ++ + //https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/12_ACPI_Embedded_Controller_Interface_Specification/embedded-controller-command-set.html +- pub fn read(&self, address: u8) -> Option { ++ fn read(&self, address: u8) -> Result { + trace!("ec read addr: {:x}", address); + self.wait_for_write_ready()?; + +@@ -125,9 +153,9 @@ impl Ec { + + let val = self.read_reg_data(); + trace!("got: {:x}", val); +- Some(val) ++ Ok(val) + } +- pub fn write(&self, address: u8, value: u8) -> Option<()> { ++ fn write(&self, address: u8, value: u8) -> Result<(), EcError> { + trace!("ec write addr: {:x}, with: {:x}", address, value); + self.wait_for_write_ready()?; + +@@ -141,7 +169,22 @@ impl Ec { + + self.write_reg_data(value); + trace!("done"); +- Some(()) ++ Ok(()) ++ } ++ ++ fn read_bytes(&self, offset: usize) -> Result<[u8; N], EcError> { ++ let mut bytes = [0u8; N]; ++ for (index, byte) in bytes.iter_mut().enumerate() { ++ *byte = self.read(Self::checked_address(offset, index)?)?; ++ } ++ Ok(bytes) ++ } ++ ++ fn write_bytes(&self, offset: usize, bytes: [u8; N]) -> Result<(), EcError> { ++ for (index, byte) in IntoIterator::into_iter(bytes).enumerate() { ++ self.write(Self::checked_address(offset, index)?, byte)?; ++ } ++ Ok(()) + } + // disabled if not met + // First Access - 400 microseconds +@@ -151,11 +194,11 @@ impl Ec { + #[allow(dead_code)] + fn enable_burst(&self) -> bool { + trace!("ec burst enable"); +- self.wait_for_write_ready(); ++ let _ = self.wait_for_write_ready(); + + self.write_reg_sc(BE_EC); + +- self.wait_for_read_ready(); ++ let _ = self.wait_for_read_ready(); + + let res = self.read_reg_data() == BURST_ACK; + trace!("success: {}", res); +@@ -164,7 +207,7 @@ impl Ec { + #[allow(dead_code)] + fn disable_burst(&self) { + trace!("ec burst disable"); +- self.wait_for_write_ready(); ++ let _ = self.wait_for_write_ready(); + self.write_reg_sc(BD_EC); + trace!("done"); + } +@@ -172,11 +215,11 @@ impl Ec { + #[allow(dead_code)] + fn queue_query(&mut self) -> u8 { + trace!("ec query"); +- self.wait_for_write_ready(); ++ let _ = self.wait_for_write_ready(); + + self.write_reg_sc(QR_EC); + +- self.wait_for_read_ready(); ++ let _ = self.wait_for_read_ready(); + + let val = self.read_reg_data(); + trace!("got: {}", val); +@@ -190,7 +233,10 @@ impl RegionHandler for Ec { + offset: usize, + ) -> Result { + assert_eq!(region.space, RegionSpace::EmbeddedControl); +- self.read(offset as u8).ok_or(AmlError::MutexAcquireTimeout) // TODO proper error type ++ self.read(Self::checked_address(offset, 0)?).map_err(|error| { ++ warn!("EC read_u8 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) + } + fn write_u8( + &self, +@@ -199,58 +245,73 @@ impl RegionHandler for Ec { + value: u8, + ) -> Result<(), acpi::aml::AmlError> { + assert_eq!(region.space, RegionSpace::EmbeddedControl); +- self.write(offset as u8, value) +- .ok_or(AmlError::MutexAcquireTimeout) // TODO proper error type +- } +- fn read_u16(&self, _region: &OpRegion, _offset: usize) -> Result { +- warn!("Got u16 EC read from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type +- } +- fn read_u32(&self, _region: &OpRegion, _offset: usize) -> Result { +- warn!("Got u32 EC read from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type +- } +- fn read_u64(&self, _region: &OpRegion, _offset: usize) -> Result { +- warn!("Got u64 EC read from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type ++ self.write(Self::checked_address(offset, 0)?, value) ++ .map_err(|error| { ++ warn!("EC write_u8 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) ++ } ++ fn read_u16(&self, region: &OpRegion, offset: usize) -> Result { ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.read_bytes::<2>(offset) ++ .map(u16::from_le_bytes) ++ .map_err(|error| { ++ warn!("EC read_u16 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) ++ } ++ fn read_u32(&self, region: &OpRegion, offset: usize) -> Result { ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.read_bytes::<4>(offset) ++ .map(u32::from_le_bytes) ++ .map_err(|error| { ++ warn!("EC read_u32 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) ++ } ++ fn read_u64(&self, region: &OpRegion, offset: usize) -> Result { ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.read_bytes::<8>(offset) ++ .map(u64::from_le_bytes) ++ .map_err(|error| { ++ warn!("EC read_u64 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) + } + fn write_u16( + &self, +- _region: &OpRegion, +- _offset: usize, +- _value: u16, ++ region: &OpRegion, ++ offset: usize, ++ value: u16, + ) -> Result<(), acpi::aml::AmlError> { +- warn!("Got u16 EC write from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.write_bytes(offset, value.to_le_bytes()).map_err(|error| { ++ warn!("EC write_u16 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) + } + fn write_u32( + &self, +- _region: &OpRegion, +- _offset: usize, +- _value: u32, ++ region: &OpRegion, ++ offset: usize, ++ value: u32, + ) -> Result<(), acpi::aml::AmlError> { +- warn!("Got u32 EC write from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.write_bytes(offset, value.to_le_bytes()).map_err(|error| { ++ warn!("EC write_u32 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) + } + fn write_u64( + &self, +- _region: &OpRegion, +- _offset: usize, +- _value: u64, ++ region: &OpRegion, ++ offset: usize, ++ value: u64, + ) -> Result<(), acpi::aml::AmlError> { +- warn!("Got u64 EC write from AML!"); +- Err(acpi::aml::AmlError::NoHandlerForRegionAccess( +- RegionSpace::EmbeddedControl, +- )) // TODO proper error type ++ assert_eq!(region.space, RegionSpace::EmbeddedControl); ++ self.write_bytes(offset, value.to_le_bytes()).map_err(|error| { ++ warn!("EC write_u64 failed at offset {offset:#x}: {error:?}"); ++ error.as_aml_error() ++ }) + } + } + +diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs +index 5a5040c3..4fe3b8d8 100644 +--- a/drivers/acpid/src/scheme.rs ++++ b/drivers/acpid/src/scheme.rs +@@ -2,7 +2,6 @@ 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}; +@@ -16,17 +15,22 @@ 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::error::{ ++ EACCES, EAGAIN, EBADF, EBADFD, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR, EOPNOTSUPP, ++}; + 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::acpi::{ ++ AcpiBattery, AcpiContext, AcpiPowerAdapter, AcpiPowerSnapshot, AmlSymbols, DmiInfo, ++ SdtSignature, ++}; ++use crate::resources::{decode_resource_template, ResourceDescriptor}; + + pub struct AcpiScheme<'acpi, 'sock> { + ctx: &'acpi AcpiContext, + handles: HandleMap>, +- pci_fd: Option, + socket: &'sock Socket, + } + +@@ -41,10 +45,204 @@ enum HandleKind<'a> { + Table(SdtSignature), + Symbols(RwLockReadGuard<'a, AmlSymbols>), + Symbol { name: String, description: String }, ++ ResourcesDir, ++ Resources(String), ++ Reboot, ++ DmiDir, ++ Dmi(String), ++ PowerDir, ++ PowerAdaptersDir, ++ PowerAdapterDir(String), ++ PowerBatteriesDir, ++ PowerBatteryDir(String), ++ PowerFile(String), + SchemeRoot, + RegisterPci, + } + ++const DMI_DIRECTORY_ENTRIES: &[&str] = &[ ++ "sys_vendor", ++ "board_vendor", ++ "board_name", ++ "board_version", ++ "product_name", ++ "product_version", ++ "bios_version", ++ "match_all", ++]; ++ ++fn dmi_contents(dmi_info: Option<&DmiInfo>, name: &str) -> Option { ++ Some(match name { ++ "sys_vendor" => dmi_info ++ .and_then(|info| info.sys_vendor.clone()) ++ .unwrap_or_default(), ++ "board_vendor" => dmi_info ++ .and_then(|info| info.board_vendor.clone()) ++ .unwrap_or_default(), ++ "board_name" => dmi_info ++ .and_then(|info| info.board_name.clone()) ++ .unwrap_or_default(), ++ "board_version" => dmi_info ++ .and_then(|info| info.board_version.clone()) ++ .unwrap_or_default(), ++ "product_name" => dmi_info ++ .and_then(|info| info.product_name.clone()) ++ .unwrap_or_default(), ++ "product_version" => dmi_info ++ .and_then(|info| info.product_version.clone()) ++ .unwrap_or_default(), ++ "bios_version" => dmi_info ++ .and_then(|info| info.bios_version.clone()) ++ .unwrap_or_default(), ++ "match_all" => dmi_info.map(DmiInfo::to_match_lines).unwrap_or_default(), ++ _ => return None, ++ }) ++} ++ ++fn power_bool_contents(value: bool) -> String { ++ if value { ++ String::from("1\n") ++ } else { ++ String::from("0\n") ++ } ++} ++ ++fn power_u64_contents(value: u64) -> String { ++ format!("{value}\n") ++} ++ ++fn power_f64_contents(value: f64) -> String { ++ format!("{value}\n") ++} ++ ++fn power_string_contents(value: &str) -> String { ++ format!("{value}\n") ++} ++ ++fn power_adapter_file_contents(adapter: &AcpiPowerAdapter, name: &str) -> Option { ++ Some(match name { ++ "path" => power_string_contents(&adapter.path), ++ "online" => power_bool_contents(adapter.online), ++ _ => return None, ++ }) ++} ++ ++fn power_adapter_entry_names() -> &'static [&'static str] { ++ &["path", "online"] ++} ++ ++fn power_battery_file_contents(battery: &AcpiBattery, name: &str) -> Option { ++ Some(match name { ++ "path" => power_string_contents(&battery.path), ++ "state" => power_u64_contents(battery.state), ++ "present_rate" => power_u64_contents(battery.present_rate?), ++ "remaining_capacity" => power_u64_contents(battery.remaining_capacity?), ++ "present_voltage" => power_u64_contents(battery.present_voltage?), ++ "power_unit" => power_string_contents(battery.power_unit.as_deref()?), ++ "design_capacity" => power_u64_contents(battery.design_capacity?), ++ "last_full_capacity" => power_u64_contents(battery.last_full_capacity?), ++ "design_voltage" => power_u64_contents(battery.design_voltage?), ++ "technology" => power_string_contents(battery.technology.as_deref()?), ++ "model" => power_string_contents(battery.model.as_deref()?), ++ "serial" => power_string_contents(battery.serial.as_deref()?), ++ "battery_type" => power_string_contents(battery.battery_type.as_deref()?), ++ "oem_info" => power_string_contents(battery.oem_info.as_deref()?), ++ "percentage" => power_f64_contents(battery.percentage?), ++ _ => return None, ++ }) ++} ++ ++fn power_battery_entry_names(battery: &AcpiBattery) -> Vec<&'static str> { ++ let mut names = vec!["path", "state"]; ++ ++ if battery.present_rate.is_some() { ++ names.push("present_rate"); ++ } ++ if battery.remaining_capacity.is_some() { ++ names.push("remaining_capacity"); ++ } ++ if battery.present_voltage.is_some() { ++ names.push("present_voltage"); ++ } ++ if battery.power_unit.is_some() { ++ names.push("power_unit"); ++ } ++ if battery.design_capacity.is_some() { ++ names.push("design_capacity"); ++ } ++ if battery.last_full_capacity.is_some() { ++ names.push("last_full_capacity"); ++ } ++ if battery.design_voltage.is_some() { ++ names.push("design_voltage"); ++ } ++ if battery.technology.is_some() { ++ names.push("technology"); ++ } ++ if battery.model.is_some() { ++ names.push("model"); ++ } ++ if battery.serial.is_some() { ++ names.push("serial"); ++ } ++ if battery.battery_type.is_some() { ++ names.push("battery_type"); ++ } ++ if battery.oem_info.is_some() { ++ names.push("oem_info"); ++ } ++ if battery.percentage.is_some() { ++ names.push("percentage"); ++ } ++ ++ names ++} ++ ++fn top_level_entries(power_available: bool) -> Vec<(&'static str, DirentKind)> { ++ let mut entries = vec![ ++ ("tables", DirentKind::Directory), ++ ("symbols", DirentKind::Directory), ++ ("resources", DirentKind::Directory), ++ ("dmi", DirentKind::Directory), ++ ("reboot", DirentKind::Regular), ++ ]; ++ if power_available { ++ entries.push(("power", DirentKind::Directory)); ++ } ++ entries ++} ++ ++fn resource_symbol_path(path: &str) -> Option { ++ let normalized = path.trim_matches('/').trim_start_matches('\\'); ++ if normalized.is_empty() { ++ return None; ++ } ++ ++ let normalized = normalized.replace('/', "."); ++ if normalized.is_empty() { ++ None ++ } else { ++ Some(format!("{normalized}._CRS")) ++ } ++} ++ ++fn resource_entry_name(symbol: &str) -> Option { ++ symbol ++ .strip_suffix("._CRS") ++ .map(str::to_string) ++ .filter(|path| !path.is_empty()) ++} ++ ++fn resource_dir_entries<'a>(symbols: impl IntoIterator) -> Vec { ++ let mut entries = symbols ++ .into_iter() ++ .filter_map(resource_entry_name) ++ .collect::>(); ++ entries.sort_unstable(); ++ entries.dedup(); ++ entries ++} ++ + impl HandleKind<'_> { + fn is_dir(&self) -> bool { + match self { +@@ -53,6 +251,17 @@ impl HandleKind<'_> { + Self::Table(_) => false, + Self::Symbols(_) => true, + Self::Symbol { .. } => false, ++ Self::ResourcesDir => true, ++ Self::Resources(_) => false, ++ Self::Reboot => false, ++ Self::DmiDir => true, ++ Self::Dmi(_) => false, ++ Self::PowerDir => true, ++ Self::PowerAdaptersDir => true, ++ Self::PowerAdapterDir(_) => true, ++ Self::PowerBatteriesDir => true, ++ Self::PowerBatteryDir(_) => true, ++ Self::PowerFile(_) => false, + Self::SchemeRoot => false, + Self::RegisterPci => false, + } +@@ -65,8 +274,21 @@ impl HandleKind<'_> { + .ok_or(Error::new(EBADFD))? + .length(), + Self::Symbol { description, .. } => description.len(), ++ Self::Resources(contents) => contents.len(), ++ Self::Reboot => 0, ++ Self::Dmi(contents) => contents.len(), ++ Self::PowerFile(contents) => contents.len(), + // Directories +- Self::TopLevel | Self::Symbols(_) | Self::Tables => 0, ++ Self::TopLevel ++ | Self::Symbols(_) ++ | Self::ResourcesDir ++ | Self::Tables ++ | Self::DmiDir ++ | Self::PowerDir ++ | Self::PowerAdaptersDir ++ | Self::PowerAdapterDir(_) ++ | Self::PowerBatteriesDir ++ | Self::PowerBatteryDir(_) => 0, + Self::SchemeRoot | Self::RegisterPci => return Err(Error::new(EBADF)), + }) + } +@@ -77,10 +299,163 @@ impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> { + Self { + ctx, + handles: HandleMap::new(), +- pci_fd: None, + socket, + } + } ++ ++ fn power_snapshot(&self) -> Result { ++ self.ctx.power_snapshot().map_err(|error| match error { ++ crate::acpi::AmlEvalError::NotInitialized => Error::new(EAGAIN), ++ crate::acpi::AmlEvalError::Unsupported(message) => { ++ log::warn!("ACPI power surface unavailable: {message}"); ++ Error::new(EOPNOTSUPP) ++ } ++ other => { ++ log::warn!("Failed to build ACPI power snapshot: {:?}", other); ++ Error::new(EIO) ++ } ++ }) ++ } ++ ++ fn power_available(&self) -> bool { ++ matches!(self.ctx.power_snapshot(), Ok(_)) ++ } ++ ++ fn resources_handle(&self, path: &str) -> Result> { ++ if !self.ctx.pci_ready() { ++ let display_path = if path.is_empty() { "resources" } else { path }; ++ log::warn!( ++ "Deferring ACPI resource lookup for {display_path} until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::ResourcesDir); ++ } ++ ++ let symbol_path = resource_symbol_path(normalized).ok_or(Error::new(ENOENT))?; ++ if self.ctx.aml_lookup(&symbol_path).is_none() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let aml_name = ++ AmlName::from_str(&format!("\\{symbol_path}")).map_err(|_| Error::new(ENOENT))?; ++ let buffer = match self.ctx.aml_eval(aml_name, Vec::new()) { ++ Ok(AmlSerdeValue::Buffer(bytes)) => bytes, ++ Ok(other) => { ++ log::debug!( ++ "Skipping ACPI resources for {normalized} due to unexpected _CRS value: {:?}", ++ other ++ ); ++ return Err(Error::new(ENOENT)); ++ } ++ Err(error) => { ++ log::debug!( ++ "Failed to evaluate ACPI resources for {symbol_path}: {:?}", ++ error ++ ); ++ return Err(Error::new(ENOENT)); ++ } ++ }; ++ ++ let descriptors: Vec = ++ decode_resource_template(&buffer).map_err(|error| { ++ log::warn!("Failed to decode ACPI _CRS for {symbol_path}: {error}"); ++ Error::new(EIO) ++ })?; ++ let serialized = ron::ser::to_string(&descriptors).map_err(|error| { ++ log::warn!("Failed to serialize decoded ACPI resources for {symbol_path}: {error}"); ++ Error::new(EIO) ++ })?; ++ ++ Ok(HandleKind::Resources(serialized)) ++ } ++ ++ fn power_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ self.power_snapshot()?; ++ ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerDir); ++ } ++ if normalized == "on_battery" { ++ return Ok(HandleKind::PowerFile(power_bool_contents( ++ self.power_snapshot()?.on_battery(), ++ ))); ++ } ++ if normalized == "adapters" { ++ return Ok(HandleKind::PowerAdaptersDir); ++ } ++ if let Some(rest) = normalized.strip_prefix("adapters/") { ++ return self.power_adapter_handle(rest); ++ } ++ if normalized == "batteries" { ++ return Ok(HandleKind::PowerBatteriesDir); ++ } ++ if let Some(rest) = normalized.strip_prefix("batteries/") { ++ return self.power_battery_handle(rest); ++ } ++ ++ Err(Error::new(ENOENT)) ++ } ++ ++ fn power_adapter_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerAdaptersDir); ++ } ++ ++ let mut parts = normalized.split('/'); ++ let adapter_id = parts.next().ok_or(Error::new(ENOENT))?; ++ let field = parts.next(); ++ if parts.next().is_some() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let snapshot = self.power_snapshot()?; ++ let adapter = snapshot ++ .adapters ++ .iter() ++ .find(|adapter| adapter.id == adapter_id) ++ .ok_or(Error::new(ENOENT))?; ++ ++ match field { ++ None | Some("") => Ok(HandleKind::PowerAdapterDir(adapter.id.clone())), ++ Some(name) => Ok(HandleKind::PowerFile( ++ power_adapter_file_contents(adapter, name).ok_or(Error::new(ENOENT))?, ++ )), ++ } ++ } ++ ++ fn power_battery_handle(&self, path: &str) -> Result> { ++ let normalized = path.trim_matches('/'); ++ if normalized.is_empty() { ++ return Ok(HandleKind::PowerBatteriesDir); ++ } ++ ++ let mut parts = normalized.split('/'); ++ let battery_id = parts.next().ok_or(Error::new(ENOENT))?; ++ let field = parts.next(); ++ if parts.next().is_some() { ++ return Err(Error::new(ENOENT)); ++ } ++ ++ let snapshot = self.power_snapshot()?; ++ let battery = snapshot ++ .batteries ++ .iter() ++ .find(|battery| battery.id == battery_id) ++ .ok_or(Error::new(ENOENT))?; ++ ++ match field { ++ None | Some("") => Ok(HandleKind::PowerBatteryDir(battery.id.clone())), ++ Some(name) => Ok(HandleKind::PowerFile( ++ power_battery_file_contents(battery, name).ok_or(Error::new(ENOENT))?, ++ )), ++ } ++ } + } + + fn parse_hex_digit(hex: u8) -> Option { +@@ -182,49 +557,97 @@ impl SchemeSync for AcpiScheme<'_, '_> { + + let kind = match handle.kind { + HandleKind::SchemeRoot => { +- // TODO: arrayvec +- let components = { +- let mut v = arrayvec::ArrayVec::<&str, 3>::new(); +- let it = path.split('/'); +- for component in it.take(3) { +- v.push(component); +- } +- +- v +- }; +- +- match &*components { +- [""] => HandleKind::TopLevel, +- ["register_pci"] => HandleKind::RegisterPci, +- ["tables"] => HandleKind::Tables, ++ if path == "resources" || path == "resources/" { ++ self.resources_handle("")? ++ } else if let Some(rest) = path.strip_prefix("resources/") { ++ self.resources_handle(rest)? ++ } else { ++ // 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); ++ } + +- ["tables", table] => { +- let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?; +- HandleKind::Table(signature) +- } ++ v ++ }; ++ ++ match &*components { ++ [""] => HandleKind::TopLevel, ++ ["reboot"] => HandleKind::Reboot, ++ ["dmi"] => { ++ if flag_dir || flag_stat || path.ends_with('/') { ++ HandleKind::DmiDir ++ } else { ++ HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), "match_all") ++ .expect("match_all should always resolve"), ++ ) ++ } ++ } ++ ["dmi", ""] => HandleKind::DmiDir, ++ ["dmi", field] => HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), field).ok_or(Error::new(ENOENT))?, ++ ), ++ ["power"] => self.power_handle("")?, ++ ["power", tail] => self.power_handle(tail)?, ++ ["power", a, b] => self.power_handle(&format!("{a}/{b}"))?, ++ ["power", a, b, c] => self.power_handle(&format!("{a}/{b}/{c}"))?, ++ ["register_pci"] => HandleKind::RegisterPci, ++ ["tables"] => HandleKind::Tables, ++ ++ ["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"] => { ++ if !self.ctx.pci_ready() { ++ log::warn!( ++ "Deferring AML symbol scan until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ if let Ok(aml_symbols) = self.ctx.aml_symbols() { ++ 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, ++ ["symbols", symbol] => { ++ if !self.ctx.pci_ready() { ++ log::warn!( ++ "Deferring AML symbol lookup for {symbol} until PCI registration is ready" ++ ); ++ return Err(Error::new(EAGAIN)); ++ } ++ if let Some(description) = self.ctx.aml_lookup(symbol) { ++ HandleKind::Symbol { ++ name: (*symbol).to_owned(), ++ description, ++ } ++ } else { ++ return Err(Error::new(ENOENT)); + } +- } else { +- return Err(Error::new(ENOENT)); + } +- } + +- _ => return Err(Error::new(ENOENT)), ++ _ => return Err(Error::new(ENOENT)), ++ } ++ } ++ } ++ HandleKind::DmiDir => { ++ if path.is_empty() { ++ HandleKind::DmiDir ++ } else { ++ HandleKind::Dmi( ++ dmi_contents(self.ctx.dmi_info(), path).ok_or(Error::new(ENOENT))?, ++ ) + } + } ++ HandleKind::ResourcesDir => self.resources_handle(path)?, + HandleKind::Symbols(ref aml_symbols) => { + if let Some(description) = aml_symbols.lookup(path) { + HandleKind::Symbol { +@@ -235,6 +658,23 @@ impl SchemeSync for AcpiScheme<'_, '_> { + return Err(Error::new(ENOENT)); + } + } ++ HandleKind::PowerDir => self.power_handle(path)?, ++ HandleKind::PowerAdaptersDir => self.power_adapter_handle(path)?, ++ HandleKind::PowerAdapterDir(ref adapter_id) => { ++ if path.is_empty() { ++ HandleKind::PowerAdapterDir(adapter_id.clone()) ++ } else { ++ self.power_adapter_handle(&format!("{adapter_id}/{path}"))? ++ } ++ } ++ HandleKind::PowerBatteriesDir => self.power_battery_handle(path)?, ++ HandleKind::PowerBatteryDir(ref battery_id) => { ++ if path.is_empty() { ++ HandleKind::PowerBatteryDir(battery_id.clone()) ++ } else { ++ self.power_battery_handle(&format!("{battery_id}/{path}"))? ++ } ++ } + _ => return Err(Error::new(EACCES)), + }; + +@@ -296,7 +736,7 @@ impl SchemeSync for AcpiScheme<'_, '_> { + ) -> Result { + let offset: usize = offset.try_into().map_err(|_| Error::new(EINVAL))?; + +- let handle = self.handles.get_mut(id)?; ++ let handle = self.handles.get(id)?; + + if handle.stat { + return Err(Error::new(EBADF)); +@@ -309,6 +749,9 @@ impl SchemeSync for AcpiScheme<'_, '_> { + .ok_or(Error::new(EBADFD))? + .as_slice(), + HandleKind::Symbol { description, .. } => description.as_bytes(), ++ HandleKind::Resources(contents) => contents.as_bytes(), ++ HandleKind::Dmi(contents) => contents.as_bytes(), ++ HandleKind::PowerFile(contents) => contents.as_bytes(), + _ => return Err(Error::new(EINVAL)), + }; + +@@ -328,13 +771,95 @@ impl SchemeSync for AcpiScheme<'_, '_> { + mut buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + ) -> Result> { +- let handle = self.handles.get_mut(id)?; ++ let handle = self.handles.get(id)?; + + match &handle.kind { + HandleKind::TopLevel => { +- const TOPLEVEL_ENTRIES: &[&str] = &["tables", "symbols"]; ++ for (idx, (name, kind)) in top_level_entries(self.power_available()) ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ } ++ HandleKind::DmiDir => { ++ for (idx, name) in DMI_DIRECTORY_ENTRIES ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::ResourcesDir => { ++ let aml_symbols = self.ctx.aml_symbols().map_err(|_| Error::new(EIO))?; ++ let entries = ++ resource_dir_entries(aml_symbols.symbols_cache().keys().map(String::as_str)); ++ for (idx, name) in entries.iter().enumerate().skip(opaque_offset as usize) { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::PowerDir => { ++ const POWER_ROOT_ENTRIES: &[(&str, DirentKind)] = &[ ++ ("on_battery", DirentKind::Regular), ++ ("adapters", DirentKind::Directory), ++ ("batteries", DirentKind::Directory), ++ ]; ++ ++ for (idx, (name, kind)) in POWER_ROOT_ENTRIES ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ } ++ HandleKind::PowerAdaptersDir => { ++ let snapshot = self.power_snapshot()?; ++ for (idx, adapter) in snapshot ++ .adapters ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name: adapter.id.as_str(), ++ kind: DirentKind::Directory, ++ })?; ++ } ++ } ++ HandleKind::PowerAdapterDir(adapter_id) => { ++ let snapshot = self.power_snapshot()?; ++ let _adapter = snapshot ++ .adapters ++ .iter() ++ .find(|adapter| adapter.id == *adapter_id) ++ .ok_or(Error::new(EIO))?; + +- for (idx, name) in TOPLEVEL_ENTRIES ++ for (idx, name) in power_adapter_entry_names() + .iter() + .enumerate() + .skip(opaque_offset as usize) +@@ -343,10 +868,44 @@ impl SchemeSync for AcpiScheme<'_, '_> { + inode: 0, + next_opaque_id: idx as u64 + 1, + name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } ++ HandleKind::PowerBatteriesDir => { ++ let snapshot = self.power_snapshot()?; ++ for (idx, battery) in snapshot ++ .batteries ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name: battery.id.as_str(), + kind: DirentKind::Directory, + })?; + } + } ++ HandleKind::PowerBatteryDir(battery_id) => { ++ let snapshot = self.power_snapshot()?; ++ let battery = snapshot ++ .batteries ++ .iter() ++ .find(|battery| battery.id == *battery_id) ++ .ok_or(Error::new(EIO))?; ++ let entry_names = power_battery_entry_names(battery); ++ ++ for (idx, name) in entry_names.iter().enumerate().skip(opaque_offset as usize) { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: DirentKind::Regular, ++ })?; ++ } ++ } + HandleKind::Symbols(aml_symbols) => { + for (idx, (symbol_name, _value)) in aml_symbols + .symbols_cache() +@@ -444,6 +1003,38 @@ impl SchemeSync for AcpiScheme<'_, '_> { + Ok(result_len) + } + ++ fn write( ++ &mut self, ++ id: usize, ++ buf: &[u8], ++ _offset: u64, ++ _flags: u32, ++ _ctx: &CallerCtx, ++ ) -> Result { ++ let handle = self.handles.get_mut(id)?; ++ ++ if handle.stat { ++ return Err(Error::new(EBADF)); ++ } ++ if !handle.allowed_to_eval { ++ return Err(Error::new(EPERM)); ++ } ++ ++ match handle.kind { ++ HandleKind::Reboot => { ++ if buf.is_empty() { ++ return Err(Error::new(EINVAL)); ++ } ++ self.ctx.acpi_reboot().map_err(|error| { ++ log::error!("ACPI reboot failed: {error}"); ++ Error::new(EIO) ++ })?; ++ Ok(buf.len()) ++ } ++ _ => Err(Error::new(EBADF)), ++ } ++ } ++ + fn on_sendfd(&mut self, sendfd_request: &SendFdRequest) -> Result { + let id = sendfd_request.id(); + let num_fds = sendfd_request.num_fds(); +@@ -470,10 +1061,8 @@ impl SchemeSync for AcpiScheme<'_, '_> { + } + let new_fd = libredox::Fd::new(new_fd); + +- if self.pci_fd.is_some() { ++ if self.ctx.register_pci_fd(new_fd).is_err() { + return Err(Error::new(EINVAL)); +- } else { +- self.pci_fd = Some(new_fd); + } + + Ok(num_fds) +@@ -483,3 +1072,90 @@ impl SchemeSync for AcpiScheme<'_, '_> { + self.handles.remove(id); + } + } ++ ++#[cfg(test)] ++mod tests { ++ use super::{dmi_contents, resource_dir_entries, resource_symbol_path, top_level_entries}; ++ use crate::acpi::DmiInfo; ++ use syscall::dirent::DirentKind; ++ ++ #[test] ++ fn dmi_contents_exposes_individual_fields_and_match_all() { ++ let dmi_info = DmiInfo { ++ sys_vendor: Some("Framework".to_string()), ++ board_name: Some("FRANMECP01".to_string()), ++ product_name: Some("Laptop 16".to_string()), ++ ..DmiInfo::default() ++ }; ++ ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "sys_vendor").as_deref(), ++ Some("Framework") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "board_name").as_deref(), ++ Some("FRANMECP01") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "product_name").as_deref(), ++ Some("Laptop 16") ++ ); ++ assert_eq!( ++ dmi_contents(Some(&dmi_info), "match_all").as_deref(), ++ Some("sys_vendor=Framework\nboard_name=FRANMECP01\nproduct_name=Laptop 16") ++ ); ++ assert_eq!(dmi_contents(None, "bios_version").as_deref(), Some("")); ++ assert_eq!(dmi_contents(Some(&dmi_info), "unknown"), None); ++ } ++ ++ #[test] ++ fn top_level_entries_always_include_reboot() { ++ let entries = top_level_entries(false); ++ assert!(entries ++ .iter() ++ .any(|(name, kind)| { *name == "reboot" && matches!(kind, DirentKind::Regular) })); ++ } ++ ++ #[test] ++ fn top_level_entries_only_include_power_when_available() { ++ let hidden = top_level_entries(false); ++ let visible = top_level_entries(true); ++ ++ assert!(!hidden.iter().any(|(name, _)| *name == "power")); ++ assert!(visible ++ .iter() ++ .any(|(name, kind)| { *name == "power" && matches!(kind, DirentKind::Directory) })); ++ } ++ ++ #[test] ++ fn top_level_entries_always_include_resources() { ++ let entries = top_level_entries(false); ++ assert!(entries ++ .iter() ++ .any(|(name, kind)| { *name == "resources" && matches!(kind, DirentKind::Directory) })); ++ } ++ ++ #[test] ++ fn resource_symbol_path_accepts_dotted_and_slash_paths() { ++ assert_eq!( ++ resource_symbol_path("_SB.PCI0.I2C0.TPD0").as_deref(), ++ Some("_SB.PCI0.I2C0.TPD0._CRS") ++ ); ++ assert_eq!( ++ resource_symbol_path("\\_SB/PCI0/I2C0/TPD0").as_deref(), ++ Some("_SB.PCI0.I2C0.TPD0._CRS") ++ ); ++ } ++ ++ #[test] ++ fn resource_dir_entries_list_devices_with_crs_suffix() { ++ assert_eq!( ++ resource_dir_entries([ ++ "_SB.PCI0.I2C0.TPD0._CRS", ++ "_SB.PCI0.I2C1.TPD1._HID", ++ "_SB.PCI0.I2C0.TPD0._CRS", ++ ]), ++ vec!["_SB.PCI0.I2C0.TPD0".to_string()] ++ ); ++ } ++} diff --git a/local/patches/base/P2-hwd-misc.patch b/local/patches/base/P2-hwd-misc.patch new file mode 100644 index 00000000..70ce39e5 --- /dev/null +++ b/local/patches/base/P2-hwd-misc.patch @@ -0,0 +1,64 @@ +# P2-hwd-misc.patch +# Extract hwd (hardware daemon) Cargo.toml and main.rs improvements. +# +# Files: drivers/hwd/Cargo.toml, drivers/hwd/src/main.rs + +diff --git a/drivers/hwd/Cargo.toml b/drivers/hwd/Cargo.toml +index 3d37cfb3..40b51a1b 100644 +--- a/drivers/hwd/Cargo.toml ++++ b/drivers/hwd/Cargo.toml +@@ -6,6 +6,7 @@ edition = "2018" + + [dependencies] + fdt.workspace = true ++libc.workspace = true + log.workspace = true + ron.workspace = true + libredox = { workspace = true, default-features = false, features = ["std", "call"] } +diff --git a/drivers/hwd/src/main.rs b/drivers/hwd/src/main.rs +index 79360e34..a0462f51 100644 +--- a/drivers/hwd/src/main.rs ++++ b/drivers/hwd/src/main.rs +@@ -1,3 +1,5 @@ ++use std::os::fd::AsRawFd; ++use std::os::unix::process::CommandExt; + use std::process; + + mod backend; +@@ -37,8 +39,34 @@ fn daemon(daemon: daemon::Daemon) -> ! { + + //TODO: launch pcid based on backend information? + // Must launch after acpid but before probe calls /scheme/acpi/symbols +- #[allow(deprecated, reason = "we can't yet move this to init")] +- daemon::Daemon::spawn(process::Command::new("pcid")); ++ // Fire-and-forget: daemon::Daemon::spawn blocks until pcid signals readiness, ++ // but pcid only signals after full PCI enumeration. If enumeration hangs on ++ // real hardware (unresponsive device, complex AML), hwd deadlocks initfs. ++ { ++ match std::io::pipe() { ++ Ok((_read_end, write_end)) => { ++ let write_fd: std::os::fd::OwnedFd = write_end.into(); ++ let raw_fd = write_fd.as_raw_fd(); ++ let mut cmd = std::process::Command::new("pcid"); ++ cmd.env("INIT_NOTIFY", raw_fd.to_string()); ++ unsafe { ++ cmd.pre_exec(move || { ++ if libc::fcntl(raw_fd, libc::F_SETFD, 0) == -1 { ++ return Err(std::io::Error::last_os_error()); ++ } ++ Ok(()) ++ }); ++ } ++ match cmd.spawn() { ++ Ok(_) => {} ++ Err(err) => log::error!("hwd: failed to spawn pcid: {}", err), ++ } ++ } ++ Err(err) => { ++ log::error!("hwd: failed to create pcid notification pipe: {}", err); ++ } ++ } ++ } + + daemon.ready(); + diff --git a/local/patches/base/P2-ihdad-device-refactor.patch b/local/patches/base/P2-ihdad-device-refactor.patch new file mode 100644 index 00000000..006517e5 --- /dev/null +++ b/local/patches/base/P2-ihdad-device-refactor.patch @@ -0,0 +1,1022 @@ +# P2-ihdad-device-refactor.patch +# +# Intel HDA device refactoring: CodecTopology, ControllerPolicy, InputStream, +# configure_input, interrupt handling, and associated device-level improvements. +# +# Covers: +# - ihdad/src/hda/device.rs: CodecTopology, ControllerPolicy, InputStream, +# configure_input, interrupt handling +# +diff --git a/drivers/audio/ihdad/src/hda/device.rs b/drivers/audio/ihdad/src/hda/device.rs +index 78e8f0a2..e5742f80 100755 +--- a/drivers/audio/ihdad/src/hda/device.rs ++++ b/drivers/audio/ihdad/src/hda/device.rs +@@ -1,6 +1,6 @@ + #![allow(dead_code)] + +-use std::collections::HashMap; ++use std::collections::{HashMap, HashSet}; + use std::fmt::Write; + use std::str; + use std::task::Poll; +@@ -14,20 +14,55 @@ use redox_scheme::scheme::SchemeSync; + use redox_scheme::CallerCtx; + use redox_scheme::OpenResult; + use scheme_utils::{FpathWriter, HandleMap}; +-use syscall::error::{Error, Result, EACCES, EBADF, EIO, ENODEV, EWOULDBLOCK}; ++use syscall::error::{Error, Result, EACCES, EBADF, EIO, ENODEV, ENOENT, EWOULDBLOCK}; + + use spin::Mutex; + use syscall::schemev2::NewFdFlags; + + use super::common::*; ++use super::parser::AutoPinConfig; + use super::BitsPerSample; + use super::BufferDescriptorListEntry; + use super::CommandBuffer; ++use super::digital::DigitalCodecInfo; ++use super::dispatch::RouteDecision; ++use super::FixupEngine; + use super::HDANode; ++use super::InputStream; + use super::OutputStream; + use super::StreamBuffer; + use super::StreamDescriptorRegs; + ++#[derive(Debug, Clone)] ++pub struct ControllerPolicy { ++ pub prefer_msi: bool, ++ pub single_cmd_fallback: bool, ++ pub probe_mask: u16, ++ pub position_fix: PositionFixPolicy, ++ pub poll_jack: bool, ++} ++ ++#[derive(Debug, Clone, Copy, PartialEq, Eq)] ++pub enum PositionFixPolicy { ++ Auto, ++ Lpib, ++ Posbuf, ++ Dpic, ++ None, ++} ++ ++impl Default for ControllerPolicy { ++ fn default() -> Self { ++ ControllerPolicy { ++ prefer_msi: true, ++ single_cmd_fallback: false, ++ probe_mask: 0xFFFF, ++ position_fix: PositionFixPolicy::Auto, ++ poll_jack: false, ++ } ++ } ++} ++ + // GCTL - Global Control + const CRST: u32 = 1 << 0; // 1 bit + const FNCTRL: u32 = 1 << 1; // 1 bit +@@ -55,6 +90,20 @@ const RIRBDMAEN: u8 = 1 << 1; // 1 bit + const ICB: u16 = 1 << 0; + const IRV: u16 = 1 << 1; + ++// INTCTL bits ++const INTCTL_GIE: u32 = 1 << 31; // Global Interrupt Enable ++const INTCTL_CIE: u32 = 1 << 30; // Controller Interrupt Enable ++ ++// RIRBSTS bits (write-1-to-clear) ++const RIRBSTS_RINTFL: u8 = 1 << 0; // Response Interrupt Flag ++const RIRBSTS_RIRBOIS: u8 = 1 << 2; // RIRB Overrun Interrupt Status ++ ++// CORBSTS bits (write-1-to-clear) ++const CORBSTS_CMEI: u8 = 1 << 0; // CORB Memory Error Interrupt ++ ++// STATESTS mask — one bit per codec slot (bits 0-14) ++const STATESTS_MASK: u16 = 0x7FFF; ++ + // CORB and RIRB offset + + const COMMAND_BUFFER_OFFSET: usize = 0x40; +@@ -63,9 +112,8 @@ const NUM_SUB_BUFFS: usize = 32; + const SUB_BUFF_SIZE: usize = 2048; + + enum Handle { +- Todo, +- Pcmout(usize, usize, usize), // Card, index, block_ptr +- Pcmin(usize, usize, usize), // Card, index, block_ptr ++ Pcmout { stream_index: usize }, ++ Pcmin { stream_index: usize }, + StrBuf(Vec), + SchemeRoot, + } +@@ -121,6 +169,34 @@ struct Regs { + dpubase: Mmio, // 0x74 + } + ++struct CodecTopology { ++ codec_addr: CodecAddr, ++ afgs: Vec, ++ widget_map: HashMap, ++ outputs: Vec, ++ inputs: Vec, ++ output_pins: Vec, ++ input_pins: Vec, ++ beep_addr: Option, ++ pin_config: AutoPinConfig, ++} ++ ++impl CodecTopology { ++ fn new(codec_addr: CodecAddr) -> Self { ++ CodecTopology { ++ codec_addr, ++ afgs: Vec::new(), ++ widget_map: HashMap::new(), ++ outputs: Vec::new(), ++ inputs: Vec::new(), ++ output_pins: Vec::new(), ++ input_pins: Vec::new(), ++ beep_addr: None, ++ pin_config: AutoPinConfig::default(), ++ } ++ } ++} ++ + pub struct IntelHDA { + vend_prod: u32, + +@@ -131,6 +207,7 @@ pub struct IntelHDA { + cmd: CommandBuffer, + + codecs: Vec, ++ codecs_topology: HashMap, + + outputs: Vec, + inputs: Vec, +@@ -140,15 +217,20 @@ pub struct IntelHDA { + output_pins: Vec, + input_pins: Vec, + +- beep_addr: WidgetAddr, ++ beep_addr: Option, + + buff_desc: Dma<[BufferDescriptorListEntry; 256]>, ++ input_buff_desc: Dma<[BufferDescriptorListEntry; 256]>, + + output_streams: Vec, ++ input_streams: Vec, + + buffs: Vec>, + + int_counter: usize, ++ policy: ControllerPolicy, ++ fixup_engine: FixupEngine, ++ digital_codecs: Vec, + handles: Mutex>, + } + +@@ -160,6 +242,10 @@ impl IntelHDA { + .expect("Could not allocate physical memory for buffer descriptor list.") + .assume_init(); + ++ let input_buff_desc = Dma::<[BufferDescriptorListEntry; 256]>::zeroed() ++ .expect("Could not allocate physical memory for input buffer descriptor list.") ++ .assume_init(); ++ + log::debug!( + "Virt: {:016X}, Phys: {:016X}", + buff_desc.as_ptr() as usize, +@@ -182,11 +268,12 @@ impl IntelHDA { + + cmd: CommandBuffer::new(base + COMMAND_BUFFER_OFFSET, cmd_buff), + +- beep_addr: (0, 0), ++ beep_addr: None, + + widget_map: HashMap::::new(), + + codecs: Vec::::new(), ++ codecs_topology: HashMap::::new(), + + outputs: Vec::::new(), + inputs: Vec::::new(), +@@ -195,21 +282,34 @@ impl IntelHDA { + input_pins: Vec::::new(), + + buff_desc, ++ input_buff_desc, + + output_streams: Vec::::new(), ++ input_streams: Vec::::new(), + + buffs: Vec::>::new(), + + int_counter: 0, ++ policy: ControllerPolicy::default(), ++ fixup_engine: FixupEngine::new(), ++ digital_codecs: Vec::new(), + handles: Mutex::new(HandleMap::new()), + }; + + module.init()?; + ++ let vendor_id = ((module.vend_prod >> 16) & 0xFFFF) as u16; ++ let device_id = (module.vend_prod & 0xFFFF) as u16; ++ let route = RouteDecision::decide(vendor_id, device_id, 0x04, 0x03); ++ log::info!("IHDA: audio route decision: {:?} ({})", route.route, route.reason); ++ + module.info(); + module.enumerate()?; + + module.configure()?; ++ if let Err(err) = module.configure_input() { ++ log::debug!("IHDA: input configuration skipped: {:?}", err); ++ } + log::debug!("IHDA: Initialization finished."); + Ok(module) + } +@@ -219,23 +319,28 @@ impl IntelHDA { + + let use_immediate_command_interface = match self.vend_prod { + 0x8086_2668 => false, +- _ => true, ++ _ => !self.policy.single_cmd_fallback, + }; + + self.cmd.init(use_immediate_command_interface)?; ++ ++ self.regs.gctl.writef(UNSOL, true); ++ + self.init_interrupts(); + + Ok(()) + } + + pub fn init_interrupts(&mut self) { +- // TODO: provide a function to enable certain interrupts +- // This just enables the first output stream interupt and the global interrupt +- + let iss = self.num_input_streams(); +- self.regs +- .intctl +- .write((1 << 31) | /* (1 << 30) |*/ (1 << iss)); ++ ++ let mut wakeen: u16 = 0; ++ for &codec in &self.codecs { ++ wakeen |= 1 << codec; ++ } ++ self.regs.wakeen.write(wakeen); ++ ++ self.regs.intctl.write(INTCTL_GIE | INTCTL_CIE | (1 << iss) | (1 << 0)); + } + + pub fn irq(&mut self) -> bool { +@@ -248,6 +353,19 @@ impl IntelHDA { + self.int_counter + } + ++ pub fn policy(&self) -> &ControllerPolicy { ++ &self.policy ++ } ++ ++ pub fn set_policy(&mut self, policy: ControllerPolicy) { ++ log::info!( ++ "IHDA: policy updated msi={} single_cmd={} probe_mask={:04X} pos_fix={:?} poll_jack={}", ++ policy.prefer_msi, policy.single_cmd_fallback, policy.probe_mask, ++ policy.position_fix, policy.poll_jack ++ ); ++ self.policy = policy; ++ } ++ + pub fn read_node(&mut self, addr: WidgetAddr) -> Result { + let mut node = HDANode::new(); + let mut temp: u64; +@@ -341,70 +459,156 @@ impl IntelHDA { + } + + pub fn enumerate(&mut self) -> Result<()> { ++ // Clear old global state (kept for migration safety) + self.output_pins.clear(); + self.input_pins.clear(); ++ self.outputs.clear(); ++ self.inputs.clear(); ++ self.widget_map.clear(); ++ self.codecs_topology.clear(); ++ ++ let codec_addrs = self.codecs.clone(); ++ for codec in codec_addrs { ++ let mut topo = CodecTopology::new(codec); ++ ++ let root = self.read_node((codec, 0))?; ++ log::debug!("{}", root); ++ ++ let root_count = root.subnode_count; ++ let root_start = root.subnode_start; ++ ++ for i in 0..root_count { ++ let afg = self.read_node((codec, root_start + i))?; ++ log::debug!("{}", afg); ++ ++ // Only process audio function groups (type 0x01) ++ if afg.function_group_type != 0x01 { ++ log::debug!( ++ "Codec {}: function group {} is type {}, not audio \u{2014} skipping", ++ codec, ++ afg.addr.1, ++ afg.function_group_type ++ ); ++ continue; ++ } ++ ++ topo.afgs.push(afg.addr.1); + +- let codec: u8 = 0; +- +- let root = self.read_node((codec, 0))?; +- +- log::debug!("{}", root); +- +- let root_count = root.subnode_count; +- let root_start = root.subnode_start; +- +- //FIXME: So basically the way this is set up is to only support one codec and hopes the first one is an audio +- for i in 0..root_count { +- let afg = self.read_node((codec, root_start + i))?; +- log::debug!("{}", afg); +- let afg_count = afg.subnode_count; +- let afg_start = afg.subnode_start; +- +- for j in 0..afg_count { +- let mut widget = self.read_node((codec, afg_start + j))?; +- widget.is_widget = true; +- match widget.widget_type() { +- HDAWidgetType::AudioOutput => self.outputs.push(widget.addr), +- HDAWidgetType::AudioInput => self.inputs.push(widget.addr), +- HDAWidgetType::BeepGenerator => self.beep_addr = widget.addr, +- HDAWidgetType::PinComplex => { +- let config = widget.configuration_default(); +- if config.is_output() { +- self.output_pins.push(widget.addr); +- } else if config.is_input() { +- self.input_pins.push(widget.addr); ++ let afg_count = afg.subnode_count; ++ let afg_start = afg.subnode_start; ++ ++ for j in 0..afg_count { ++ let mut widget = self.read_node((codec, afg_start + j))?; ++ widget.is_widget = true; ++ match widget.widget_type() { ++ HDAWidgetType::AudioOutput => { ++ self.outputs.push(widget.addr); ++ topo.outputs.push(widget.addr); ++ } ++ HDAWidgetType::AudioInput => { ++ self.inputs.push(widget.addr); ++ topo.inputs.push(widget.addr); + } ++ HDAWidgetType::BeepGenerator => { ++ self.beep_addr = Some(widget.addr); ++ topo.beep_addr = Some(widget.addr); ++ } ++ HDAWidgetType::PinComplex => { ++ let config = widget.configuration_default(); ++ if config.is_output() { ++ self.output_pins.push(widget.addr); ++ topo.output_pins.push(widget.addr); ++ } else if config.is_input() { ++ self.input_pins.push(widget.addr); ++ topo.input_pins.push(widget.addr); ++ } ++ } ++ _ => {} + } +- _ => {} ++ ++ log::debug!("{}", widget); ++ self.widget_map.insert(widget.addr(), widget.clone()); ++ topo.widget_map.insert(widget.addr(), widget); + } ++ } + +- log::debug!("{}", widget); +- self.widget_map.insert(widget.addr(), widget); ++ log::debug!( ++ "Codec {}: {} AFGs, {} outputs, {} inputs, {} output pins, {} input pins", ++ codec, ++ topo.afgs.len(), ++ topo.outputs.len(), ++ topo.inputs.len(), ++ topo.output_pins.len(), ++ topo.input_pins.len(), ++ ); ++ ++ let widget_list: Vec<(WidgetAddr, HDANode)> = topo.widget_map.iter().map(|(a, n)| (*a, n.clone())).collect(); ++ topo.pin_config = AutoPinConfig::parse(&widget_list); ++ ++ self.codecs_topology.insert(codec, topo); ++ } ++ ++ for (codec, topo) in &self.codecs_topology { ++ if let Some(digi) = DigitalCodecInfo::detect_from_topology(*codec, &topo.widget_map) { ++ log::info!( ++ "IHDA: digital codec detected at {} (hdmi={}, {} pins, {} converters)", ++ codec, digi.is_hdmi, digi.pin_widgets.len(), digi.converter_widgets.len() ++ ); ++ self.digital_codecs.push(digi); + } + } + + Ok(()) + } + +- pub fn find_best_output_pin(&mut self) -> Result { +- let outs = &self.output_pins; ++ fn pick_primary_codec_for_output(&self) -> Option { ++ let mut candidates: Vec = self ++ .codecs_topology ++ .values() ++ .filter(|topo| !topo.output_pins.is_empty() && !topo.outputs.is_empty()) ++ .map(|topo| topo.codec_addr) ++ .collect(); ++ candidates.sort(); ++ candidates.into_iter().next() ++ } ++ ++ pub fn find_best_output_pin(&mut self, codec: CodecAddr) -> Result { ++ let outs: Vec = self ++ .codecs_topology ++ .get(&codec) ++ .ok_or_else(|| { ++ log::error!("No topology for codec {}", codec); ++ Error::new(ENODEV) ++ })? ++ .output_pins ++ .clone(); ++ + if outs.len() == 1 { + return Ok(outs[0]); + } else if outs.len() > 1 { +- //TODO: change output based on "unsolicited response" interrupts +- // Check for devices in this order: Headphone, Speaker, Line Out + for supported_device in &[DefaultDevice::HPOut, DefaultDevice::Speaker] { +- for &out in outs { +- let widget = self.widget_map.get(&out).unwrap(); +- let cd = widget.configuration_default(); ++ for &out in &outs { ++ let (addr, config_default) = { ++ let widget = self ++ .codecs_topology ++ .get(&codec) ++ .and_then(|t| t.widget_map.get(&out)) ++ .ok_or_else(|| { ++ log::error!( ++ "Widget {:?} not found in codec {} topology", ++ out, ++ codec ++ ); ++ Error::new(ENODEV) ++ })?; ++ (widget.addr, widget.config_default) ++ }; ++ let cd = ConfigurationDefault::from_u32(config_default); + if cd.sequence() == 0 && &cd.default_device() == supported_device { +- // Check for jack detect bit +- let pin_caps = self.cmd.cmd12(widget.addr, 0xF00, 0x0C)?; ++ let pin_caps = self.cmd.cmd12(addr, 0xF00, 0x0C)?; + if pin_caps & (1 << 2) != 0 { +- // Check for presence +- let pin_sense = self.cmd.cmd12(widget.addr, 0xF09, 0)?; ++ let pin_sense = self.cmd.cmd12(addr, 0xF09, 0)?; + if pin_sense & (1 << 31) == 0 { +- // Skip if nothing is plugged in + continue; + } + } +@@ -416,13 +620,26 @@ impl IntelHDA { + Err(Error::new(ENODEV)) + } + +- pub fn find_path_to_dac(&self, addr: WidgetAddr) -> Option> { +- let widget = self.widget_map.get(&addr).unwrap(); ++ pub fn find_path_to_dac( ++ &self, ++ addr: WidgetAddr, ++ codec: CodecAddr, ++ visited: &mut HashSet, ++ ) -> Option> { ++ if visited.contains(&addr) { ++ log::warn!("Cycle detected in widget graph at {:?}", addr); ++ return None; ++ } ++ visited.insert(addr); ++ ++ let topo = self.codecs_topology.get(&codec)?; ++ let widget = topo.widget_map.get(&addr)?; ++ + if widget.widget_type() == HDAWidgetType::AudioOutput { + Some(vec![addr]) + } else { + let connection = widget.connections.get(widget.connection_default as usize)?; +- let mut path = self.find_path_to_dac(*connection)?; ++ let mut path = self.find_path_to_dac(*connection, codec, visited)?; + path.insert(0, addr); + Some(path) + } +@@ -466,72 +683,92 @@ impl IntelHDA { + } + + pub fn configure(&mut self) -> Result<()> { +- let outpin = self.find_best_output_pin()?; ++ let codec = self.pick_primary_codec_for_output().ok_or_else(|| { ++ log::error!("No suitable codec found for audio output"); ++ Error::new(ENODEV) ++ })?; ++ ++ log::debug!("Selected codec {} for output", codec); ++ ++ let topo = self.codecs_topology.get(&codec).ok_or_else(|| { ++ log::error!("No topology for codec {}", codec); ++ Error::new(ENODEV) ++ })?; ++ ++ let vendor_id = ((self.vend_prod >> 16) & 0xFFFF) as u16; ++ let device_id = (self.vend_prod & 0xFFFF) as u16; ++ self.fixup_engine.match_fixups(vendor_id, device_id, None, &topo.pin_config); ++ ++ let primary_pins = topo.pin_config.primary_output_pins(); ++ let outpin = primary_pins.first().map(|p| p.addr).ok_or_else(|| { ++ log::error!("No primary output pins found by parser on codec {}", codec); ++ Error::new(ENODEV) ++ })?; + + log::debug!("Best pin: {:01X}:{:02X}", outpin.0, outpin.1); + +- let path = self.find_path_to_dac(outpin).unwrap(); ++ let path = { ++ let mut visited = HashSet::new(); ++ self.find_path_to_dac(outpin, codec, &mut visited) ++ .ok_or_else(|| { ++ log::error!( ++ "No path to DAC from pin {:01X}:{:02X} on codec {}", ++ outpin.0, ++ outpin.1, ++ codec ++ ); ++ Error::new(ENODEV) ++ })? ++ }; + +- let dac = *path.last().unwrap(); +- let pin = *path.first().unwrap(); ++ let dac = *path.last().ok_or_else(|| { ++ log::error!("Empty DAC path for pin {:01X}:{:02X}", outpin.0, outpin.1); ++ Error::new(ENODEV) ++ })?; ++ let pin = *path.first().ok_or_else(|| { ++ log::error!("Empty path (no pin) for pin {:01X}:{:02X}", outpin.0, outpin.1); ++ Error::new(ENODEV) ++ })?; + + log::debug!("Path to DAC: {:X?}", path); + +- // Set power state 0 (on) for all widgets in path + for &addr in &path { + self.set_power_state(addr, 0)?; + } + +- // Pin enable (0x80 = headphone amp enable, 0x40 = output enable) + self.cmd.cmd12(pin, 0x707, 0xC0)?; +- +- // EAPD enable + self.cmd.cmd12(pin, 0x70C, 2)?; +- +- // Set DAC stream and channel ++ self.cmd.cmd4(pin, 0x708, (1 << 8) | 1)?; + self.set_stream_channel(dac, 1, 0)?; + + self.update_sound_buffers(); + +- log::debug!( +- "Supported Formats: {:08X}", +- self.get_supported_formats((0, 0x1))? +- ); +- log::debug!("Capabilities: {:08X}", self.get_capabilities(path[0])?); ++ let (rate, bps, channels) = self.negotiate_stream_format(dac)?; ++ log::debug!("IHDA: negotiated stream format bps={:?} ch={}", bps, channels); + +- // Create output stream + let output = self.get_output_stream_descriptor(0).unwrap(); + output.set_address(self.buff_desc.physical()); +- output.set_pcm_format(&super::SR_44_1, BitsPerSample::Bits16, 2); +- output.set_cyclic_buffer_length((NUM_SUB_BUFFS * SUB_BUFF_SIZE) as u32); // number of bytes ++ output.set_pcm_format(rate, bps, channels); ++ output.set_cyclic_buffer_length((NUM_SUB_BUFFS * SUB_BUFF_SIZE) as u32); + output.set_stream_number(1); + output.set_last_valid_index((NUM_SUB_BUFFS - 1) as u16); + output.set_interrupt_on_completion(true); + +- // Set DAC converter format +- self.set_converter_format(dac, &super::SR_44_1, BitsPerSample::Bits16, 2)?; ++ self.set_converter_format(dac, rate, bps, channels)?; + +- // Get DAC converter format +- //TODO: should validate? + self.cmd.cmd12(dac, 0xA00, 0)?; + +- // Unmute and set gain to 0db for input and output amplifiers on all widgets in path + for &addr in &path { +- // Read widget capabilities + let caps = self.cmd.cmd12(addr, 0xF00, 0x09)?; + +- //TODO: do we need to set any other indexes? + let left = true; + let right = true; + let index = 0; + let mute = false; + +- // Check for input amp + if (caps & (1 << 1)) != 0 { +- // Read input capabilities + let in_caps = self.cmd.cmd12(addr, 0xF00, 0x0D)?; + let in_gain = (in_caps & 0x7f) as u8; +- // Set input gain + let output = false; + let input = true; + self.set_amplifier_gain_mute( +@@ -540,12 +777,9 @@ impl IntelHDA { + log::debug!("Set {:X?} input gain to 0x{:X}", addr, in_gain); + } + +- // Check for output amp + if (caps & (1 << 2)) != 0 { +- // Read output capabilities + let out_caps = self.cmd.cmd12(addr, 0xF00, 0x12)?; + let out_gain = (out_caps & 0x7f) as u8; +- // Set output gain + let output = true; + let input = false; + self.set_amplifier_gain_mute( +@@ -555,8 +789,6 @@ impl IntelHDA { + } + } + +- //TODO: implement hda-verb? +- + output.run(); + { + log::debug!("Waiting for output 0 to start running..."); +@@ -632,20 +864,21 @@ impl IntelHDA { + + */ + +- pub fn dump_codec(&self, codec: u8) -> String { ++ pub fn dump_all_codecs(&self) -> String { + let mut string = String::new(); + +- for (_, widget) in self.widget_map.iter() { +- let _ = writeln!(string, "{}", widget); ++ for (&codec, topo) in &self.codecs_topology { ++ let _ = writeln!(string, "Codec {}:", codec); ++ for (_, widget) in topo.widget_map.iter() { ++ let _ = writeln!(string, " {}", widget); ++ } + } + + string + } + +- // BEEP!! + pub fn beep(&mut self, div: u8) { +- let addr = self.beep_addr; +- if addr != (0, 0) { ++ if let Some(addr) = self.beep_addr { + let _ = self.cmd.cmd12(addr, 0xF0A, div); + } + } +@@ -700,7 +933,7 @@ impl IntelHDA { + log::debug!("Statests: {:04X}", statests); + + for i in 0..15 { +- if (statests >> i) & 0x1 == 1 { ++ if (statests >> i) & 0x1 == 1 && (self.policy.probe_mask >> i) & 0x1 == 1 { + self.codecs.push(i as CodecAddr); + } + } +@@ -812,6 +1045,54 @@ impl IntelHDA { + Ok(self.cmd.cmd12(addr, 0xF00, 0x0A)? as u32) + } + ++ fn negotiate_stream_format( ++ &mut self, ++ dac: WidgetAddr, ++ ) -> Result<(&'static super::SampleRate, BitsPerSample, u8)> { ++ let fmt = self.get_supported_formats(dac)?; ++ log::debug!("IHDA: DAC {:01X}:{:02X} supported formats: {:08X}", dac.0, dac.1, fmt); ++ ++ let rate = if fmt & (1 << 14) != 0 { ++ &super::SR_48 ++ } else if fmt & (1 << 13) != 0 { ++ &super::SR_44_1 ++ } else if fmt & (1 << 12) != 0 { ++ &super::SR_32 ++ } else if fmt & (1 << 11) != 0 { ++ &super::SR_22_05 ++ } else if fmt & (1 << 10) != 0 { ++ &super::SR_16 ++ } else if fmt & (1 << 9) != 0 { ++ &super::SR_11_025 ++ } else if fmt & (1 << 8) != 0 { ++ &super::SR_8 ++ } else { ++ log::error!("IHDA: no supported sample rate found in format {:08X}", fmt); ++ return Err(Error::new(ENODEV)); ++ }; ++ ++ let bps = if fmt & (1 << 21) != 0 { ++ BitsPerSample::Bits16 ++ } else if fmt & (1 << 23) != 0 { ++ BitsPerSample::Bits24 ++ } else if fmt & (1 << 24) != 0 { ++ BitsPerSample::Bits32 ++ } else if fmt & (1 << 22) != 0 { ++ BitsPerSample::Bits20 ++ } else if fmt & (1 << 20) != 0 { ++ BitsPerSample::Bits8 ++ } else { ++ log::error!("IHDA: no supported bit depth found in format {:08X}", fmt); ++ return Err(Error::new(ENODEV)); ++ }; ++ ++ let caps = self.get_capabilities(dac)?; ++ let max_channels = ((caps >> 0) & 0xFF) as u8 + 1; ++ let channels = if max_channels >= 2 { 2 } else { max_channels }; ++ ++ Ok((rate, bps, channels)) ++ } ++ + fn get_capabilities(&mut self, addr: WidgetAddr) -> Result { + Ok(self.cmd.cmd12(addr, 0xF00, 0x09)? as u32) + } +@@ -873,13 +1154,98 @@ impl IntelHDA { + //log::trace!("Status: {:02X} Pos: {:08X} Output CTL: {:06X}", output.status(), output.link_position(), output.control()); + + if os.current_block() == (open_block + 3) % NUM_SUB_BUFFS { +- // Block if we already are 3 buffers ahead + Poll::Pending + } else { + Poll::Ready(os.write_block(buf)) + } + } + ++ pub fn configure_input(&mut self) -> Result<()> { ++ let primary_codec = match self.pick_primary_codec_for_output() { ++ Some(c) => c, ++ None => return Err(Error::new(ENODEV)), ++ }; ++ ++ let topo = self.codecs_topology.get(&primary_codec).ok_or_else(|| { ++ log::error!("IHDA: no topology for codec {}", primary_codec); ++ Error::new(ENODEV) ++ })?; ++ ++ let input_pin = topo.input_pins.first().cloned().ok_or_else(|| { ++ log::debug!("IHDA: no input pins found on codec {}", primary_codec); ++ Error::new(ENODEV) ++ })?; ++ ++ let adc = topo.inputs.first().cloned().ok_or_else(|| { ++ log::debug!("IHDA: no ADC widgets found on codec {}", primary_codec); ++ Error::new(ENODEV) ++ })?; ++ ++ log::debug!( ++ "IHDA: configuring input: pin={:01X}:{:02X} adc={:01X}:{:02X}", ++ input_pin.0, input_pin.1, adc.0, adc.1 ++ ); ++ ++ self.cmd.cmd12(input_pin, 0x707, 0xC0)?; ++ self.set_power_state(input_pin, 0)?; ++ self.set_power_state(adc, 0)?; ++ self.set_stream_channel(adc, 2, 0)?; ++ ++ let iss = self.num_input_streams(); ++ if iss == 0 { ++ log::warn!("IHDA: no input streams available"); ++ return Err(Error::new(ENODEV)); ++ } ++ ++ let input_regs = self.get_input_stream_descriptor(0).ok_or_else(|| { ++ log::error!("IHDA: failed to get input stream descriptor 0"); ++ Error::new(ENODEV) ++ })?; ++ ++ let mut input_stream = InputStream::new(NUM_SUB_BUFFS, SUB_BUFF_SIZE, input_regs); ++ ++ for i in 0..NUM_SUB_BUFFS { ++ self.input_buff_desc[i].set_address(input_stream.phys() as u64 + (i * SUB_BUFF_SIZE) as u64); ++ self.input_buff_desc[i].set_length(SUB_BUFF_SIZE as u32); ++ self.input_buff_desc[i].set_interrupt_on_complete(true); ++ } ++ ++ let (rate, bps, channels) = self.negotiate_stream_format(adc)?; ++ ++ input_stream.regs().set_address(self.input_buff_desc.physical()); ++ input_stream.regs().set_pcm_format(rate, bps, channels); ++ input_stream.regs().set_cyclic_buffer_length((NUM_SUB_BUFFS * SUB_BUFF_SIZE) as u32); ++ input_stream.regs().set_stream_number(2); ++ input_stream.regs().set_last_valid_index((NUM_SUB_BUFFS - 1) as u16); ++ input_stream.regs().set_interrupt_on_completion(true); ++ ++ self.set_converter_format(adc, rate, bps, channels)?; ++ ++ self.input_streams.push(input_stream); ++ ++ let input_ref = self.input_streams.last_mut().unwrap(); ++ input_ref.regs().run(); ++ ++ log::debug!("IHDA: input stream 0 configured and running"); ++ Ok(()) ++ } ++ ++ pub fn read_from_input(&mut self, index: usize, buf: &mut [u8]) -> Poll> { ++ let input_stream = match self.input_streams.get_mut(index) { ++ Some(s) => s, ++ None => return Poll::Ready(Err(Error::new(EBADF))), ++ }; ++ ++ let pos = input_stream.regs().link_position() as usize; ++ let hw_block = pos / input_stream.block_size(); ++ ++ if input_stream.current_block() == hw_block { ++ Poll::Pending ++ } else { ++ Poll::Ready(input_stream.read_block(buf)) ++ } ++ } ++ + pub fn handle_interrupts(&mut self) -> bool { + let intsts = self.regs.intsts.read(); + if ((intsts >> 31) & 1) == 1 { +@@ -897,7 +1263,56 @@ impl IntelHDA { + intsts != 0 + } + +- pub fn handle_controller_interrupt(&mut self) {} ++ pub fn handle_controller_interrupt(&mut self) { ++ let statests = self.regs.statests.read(); ++ if statests & STATESTS_MASK != 0 { ++ for i in 0..15 { ++ if (statests >> i) & 1 != 0 { ++ log::info!("IHDA: state change on codec {}", i); ++ } ++ } ++ self.regs.statests.write(statests); ++ } ++ ++ let rirbsts = self.regs.rirbsts.read(); ++ if rirbsts & (RIRBSTS_RINTFL | RIRBSTS_RIRBOIS) != 0 { ++ let rirbwp = self.regs.rirbwp.read(); ++ let wp = rirbwp & 0xFF; ++ if wp != 0 { ++ log::debug!("IHDA: RIRB response available, wp={}", wp); ++ } ++ if rirbsts & RIRBSTS_RIRBOIS != 0 { ++ log::warn!("IHDA: RIRB overrun, clearing"); ++ } ++ self.regs.rirbsts.write(rirbsts & (RIRBSTS_RINTFL | RIRBSTS_RIRBOIS)); ++ } ++ ++ let corbsts = self.regs.corbsts.read(); ++ if corbsts & CORBSTS_CMEI != 0 { ++ log::error!("IHDA: CORB memory error, clearing"); ++ self.regs.corbsts.write(CORBSTS_CMEI); ++ } ++ } ++ ++ fn handle_unsolicited_response(&mut self, codec_addr: CodecAddr, response: u32) { ++ let tag = (response >> 26) & 0xF; ++ let payload = response & 0x03FFFFFF; ++ ++ log::info!( ++ "IHDA: unsolicited response codec {} tag={} payload={:06X}", ++ codec_addr, tag, payload ++ ); ++ ++ if tag == 1 { ++ let pin_widget = payload & 0x7F; ++ let plugged = (payload >> 31) & 1; ++ log::info!( ++ "IHDA: jack sense codec {} pin {} {}", ++ codec_addr, pin_widget, ++ if plugged != 0 { "plugged" } else { "unplugged" } ++ ); ++ } ++ } + + pub fn handle_stream_interrupts(&mut self, sis: u32) { + let iss = self.num_input_streams(); +@@ -1017,9 +1432,10 @@ impl SchemeSync for IntelHDA { + return Err(Error::new(EACCES)); + } + let handle = match path.trim_matches('/') { +- //TODO: allow multiple codecs +- "codec" => Handle::StrBuf(self.dump_codec(0).into_bytes()), +- _ => Handle::Todo, ++ "codec" => Handle::StrBuf(self.dump_all_codecs().into_bytes()), ++ "" | "pcmout" | "pcmout0" => Handle::Pcmout { stream_index: 0 }, ++ "pcmin" | "pcmin0" => Handle::Pcmin { stream_index: 0 }, ++ _ => return Err(Error::new(ENOENT)), + }; + let id = self.handles.lock().insert(handle); + +@@ -1038,18 +1454,44 @@ impl SchemeSync for IntelHDA { + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { +- let handles = self.handles.lock(); +- let Handle::StrBuf(strbuf) = handles.get(id)? else { +- return Err(Error::new(EBADF)); ++ let (is_strbuf, is_pcmin) = { ++ let handles = self.handles.lock(); ++ match handles.get(id)? { ++ Handle::StrBuf(_) => (true, false), ++ Handle::Pcmin { .. } => (false, true), ++ _ => return Err(Error::new(EBADF)), ++ } + }; + +- let src = usize::try_from(offset) +- .ok() +- .and_then(|o| strbuf.get(o..)) +- .unwrap_or(&[]); +- let len = src.len().min(buf.len()); +- buf[..len].copy_from_slice(&src[..len]); +- Ok(len) ++ if is_strbuf { ++ let handles = self.handles.lock(); ++ let Handle::StrBuf(strbuf) = handles.get(id)? else { ++ return Err(Error::new(EBADF)); ++ }; ++ let src = usize::try_from(offset) ++ .ok() ++ .and_then(|o| strbuf.get(o..)) ++ .unwrap_or(&[]); ++ let len = src.len().min(buf.len()); ++ buf[..len].copy_from_slice(&src[..len]); ++ return Ok(len); ++ } ++ ++ if is_pcmin { ++ let index = { ++ let handles = self.handles.lock(); ++ match handles.get(id)? { ++ Handle::Pcmin { stream_index, .. } => *stream_index, ++ _ => return Err(Error::new(EBADF)), ++ } ++ }; ++ return match self.read_from_input(index, buf) { ++ Poll::Ready(r) => r, ++ Poll::Pending => Err(Error::new(EWOULDBLOCK)), ++ }; ++ } ++ ++ Err(Error::new(EBADF)) + } + + fn write( +@@ -1061,23 +1503,29 @@ impl SchemeSync for IntelHDA { + _ctx: &CallerCtx, + ) -> Result { + let index = { +- let mut handles = self.handles.lock(); +- match handles.get_mut(id)? { +- Handle::Todo => 0, ++ let handles = self.handles.lock(); ++ match handles.get(id)? { ++ Handle::Pcmout { stream_index, .. } => *stream_index as u8, + _ => return Err(Error::new(EBADF)), + } + }; + +- //log::debug!("Int count: {}", self.int_counter); +- + match self.write_to_output(index, buf) { + Poll::Ready(r) => r, + Poll::Pending => Err(Error::new(EWOULDBLOCK)), + } + } + +- fn fpath(&mut self, _id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { +- FpathWriter::with(buf, "audiohw", |_| Ok(())) ++ fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { ++ let handles = self.handles.lock(); ++ let handle = handles.get(id)?; ++ let path = match handle { ++ Handle::Pcmout { .. } => "audiohw:pcmout0", ++ Handle::Pcmin { .. } => "audiohw:pcmin0", ++ Handle::StrBuf(_) => "audiohw:codec", ++ Handle::SchemeRoot => "audiohw:", ++ }; ++ FpathWriter::with(buf, path, |_| Ok(())) + } + + fn on_close(&mut self, id: usize) { diff --git a/local/patches/base/P2-init-subsystems.patch b/local/patches/base/P2-init-subsystems.patch new file mode 100644 index 00000000..f0d84446 --- /dev/null +++ b/local/patches/base/P2-init-subsystems.patch @@ -0,0 +1,292 @@ +# P2-init-subsystems.patch +# Extract init subsystem hardening: service respawn, unit store Option returns, +# scheduler PID tracking, and service spawn error handling. +# +# Files: init/src/scheduler.rs, init/src/service.rs, init/src/unit.rs + +diff --git a/init/src/scheduler.rs b/init/src/scheduler.rs +index d42a4e57..64e64e1e 100644 +--- a/init/src/scheduler.rs ++++ b/init/src/scheduler.rs +@@ -5,6 +5,7 @@ use crate::unit::{Unit, UnitId, UnitKind, UnitStore}; + + pub struct Scheduler { + pending: VecDeque, ++ pub respawn_pids: Vec<(UnitId, u32)>, + } + + struct Job { +@@ -20,6 +21,7 @@ impl Scheduler { + pub fn new() -> Scheduler { + Scheduler { + pending: VecDeque::new(), ++ respawn_pids: Vec::new(), + } + } + +@@ -43,7 +45,10 @@ impl Scheduler { + ) { + let loaded_units = unit_store.load_units(unit_id.clone(), errors); + for unit_id in loaded_units { +- if !unit_store.unit(&unit_id).conditions_met() { ++ let Some(unit) = unit_store.unit(&unit_id) else { ++ continue; ++ }; ++ if !unit.conditions_met() { + continue; + } + +@@ -62,7 +67,10 @@ impl Scheduler { + + match job.kind { + JobKind::Start => { +- let unit = unit_store.unit_mut(&job.unit); ++ let Some(unit) = unit_store.unit_mut(&job.unit) else { ++ eprintln!("init: unit {} not found in store, skipping", job.unit.0); ++ continue 'a; ++ }; + + for dep in &unit.info.requires_weak { + for pending_job in &self.pending { +@@ -73,14 +81,17 @@ impl Scheduler { + } + } + +- run(unit, init_config); ++ let pid = run(unit, init_config); ++ if let Some(pid) = pid { ++ self.respawn_pids.push((job.unit.clone(), pid)); ++ } + } + } + } + } + } + +-fn run(unit: &mut Unit, config: &mut InitConfig) { ++fn run(unit: &mut Unit, config: &mut InitConfig) -> Option { + match &unit.kind { + UnitKind::LegacyScript { script } => { + for cmd in script.clone() { +@@ -92,25 +103,30 @@ fn run(unit: &mut Unit, config: &mut InitConfig) { + } + UnitKind::Service { service } => { + if config.skip_cmd.contains(&service.cmd) { +- eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" ")); +- return; ++ eprintln!("init: skipping {} {}", service.cmd, service.args.join(" ")); ++ return None; + } +- if config.log_debug { ++ eprintln!( ++ "init: starting {} ({})", ++ unit.info.description.as_ref().unwrap_or(&unit.id.0), ++ service.cmd, ++ ); ++ let pid = service.spawn(&config.envs); ++ if pid.is_some() { + eprintln!( +- "Starting {} ({})", ++ "init: started {} (pid {})", + unit.info.description.as_ref().unwrap_or(&unit.id.0), +- service.cmd, ++ pid.unwrap_or(0), + ); + } +- service.spawn(&config.envs); ++ return pid; + } + UnitKind::Target {} => { +- if config.log_debug { +- eprintln!( +- "Reached target {}", +- unit.info.description.as_ref().unwrap_or(&unit.id.0), +- ); +- } ++ eprintln!( ++ "init: reached target {}", ++ unit.info.description.as_ref().unwrap_or(&unit.id.0), ++ ); + } + } ++ None + } +diff --git a/init/src/service.rs b/init/src/service.rs +index ed0023e9..cc95d02b 100644 +--- a/init/src/service.rs ++++ b/init/src/service.rs +@@ -22,6 +22,8 @@ pub struct Service { + pub inherit_envs: BTreeSet, + #[serde(rename = "type")] + pub type_: ServiceType, ++ #[serde(default)] ++ pub respawn: bool, + } + + #[derive(Clone, Debug, Default, Deserialize)] +@@ -35,7 +37,7 @@ pub enum ServiceType { + } + + impl Service { +- pub fn spawn(&self, base_envs: &BTreeMap) { ++ pub fn spawn(&self, base_envs: &BTreeMap) -> Option { + let mut command = Command::new(&self.cmd); + command.args(self.args.iter().map(|arg| subst_env(arg))); + command.env_clear(); +@@ -46,20 +48,28 @@ impl Service { + } + command.envs(base_envs).envs(&self.envs); + +- let (mut read_pipe, write_pipe) = io::pipe().unwrap(); ++ let (mut read_pipe, write_pipe) = match io::pipe() { ++ Ok(pair) => pair, ++ Err(err) => { ++ eprintln!("init: failed to create readiness pipe for {:?}: {err}", command); ++ return None; ++ } ++ }; + unsafe { pass_fd(&mut command, "INIT_NOTIFY", write_pipe.into()) }; + + let mut child = match command.spawn() { + Ok(child) => child, + Err(err) => { + eprintln!("init: failed to execute {:?}: {}", command, err); +- return; ++ return None; + } + }; + + match &self.type_ { + ServiceType::Notify => match read_pipe.read_exact(&mut [0]) { +- Ok(()) => {} ++ Ok(()) => { ++ eprintln!("init: {} ready (notify)", self.cmd); ++ } + Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => { + eprintln!("init: {command:?} exited without notifying readiness"); + } +@@ -81,23 +91,34 @@ impl Service { + }) => continue, + Ok(0) => { + eprintln!("init: {command:?} exited without notifying readiness"); +- return; ++ return None; + } + Ok(1) => break, + Ok(n) => { + eprintln!("init: incorrect amount of fds {n} returned"); +- return; ++ return None; + } + Err(err) => { + eprintln!("init: failed to wait for {command:?}: {err}"); +- return; ++ return None; + } + } + } + +- let current_namespace_fd = libredox::call::getns().expect("TODO"); +- libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd) +- .expect("TODO"); ++ let current_namespace_fd = match libredox::call::getns() { ++ Ok(fd) => fd, ++ Err(err) => { ++ eprintln!("init: failed to get current namespace for {command:?}: {err}"); ++ return None; ++ } ++ }; ++ if let Err(err) = ++ libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd) ++ { ++ eprintln!("init: failed to register scheme {scheme:?} for {command:?}: {err}"); ++ } else { ++ eprintln!("init: {} ready (scheme {})", self.cmd, scheme); ++ } + } + ServiceType::Oneshot => { + drop(read_pipe); +@@ -105,6 +126,8 @@ impl Service { + Ok(exit_status) => { + if !exit_status.success() { + eprintln!("init: {command:?} failed with {exit_status}"); ++ } else { ++ eprintln!("init: {} done (oneshot)", self.cmd); + } + } + Err(err) => { +@@ -112,8 +135,13 @@ impl Service { + } + } + } +- ServiceType::OneshotAsync => {} ++ ServiceType::OneshotAsync => { ++ if self.respawn { ++ return Some(child.id()); ++ } ++ } + } ++ None + } + } + +diff --git a/init/src/unit.rs b/init/src/unit.rs +index 98053cb2..a58bfb96 100644 +--- a/init/src/unit.rs ++++ b/init/src/unit.rs +@@ -23,8 +23,14 @@ impl UnitStore { + } + + pub fn set_runtime_target(&mut self, unit_id: UnitId) { +- assert!(self.runtime_target.is_none()); +- assert!(self.units.contains_key(&unit_id)); ++ if self.runtime_target.is_some() { ++ eprintln!("init: runtime target already set, ignoring {}", unit_id.0); ++ return; ++ } ++ if !self.units.contains_key(&unit_id) { ++ eprintln!("init: runtime target {} not found in unit store", unit_id.0); ++ return; ++ } + self.runtime_target = Some(unit_id); + } + +@@ -85,8 +91,10 @@ impl UnitStore { + let unit = self.load_single_unit(unit_id, errors); + if let Some(unit) = unit { + loaded_units.push(unit.clone()); +- for dep in &self.unit(&unit).info.requires_weak { +- pending_units.push(dep.clone()); ++ if let Some(u) = self.unit(&unit) { ++ for dep in &u.info.requires_weak { ++ pending_units.push(dep.clone()); ++ } + } + } + } +@@ -94,12 +102,12 @@ impl UnitStore { + loaded_units + } + +- pub fn unit(&self, unit: &UnitId) -> &Unit { +- self.units.get(unit).unwrap() ++ pub fn unit(&self, unit: &UnitId) -> Option<&Unit> { ++ self.units.get(unit) + } + +- pub fn unit_mut(&mut self, unit: &UnitId) -> &mut Unit { +- self.units.get_mut(unit).unwrap() ++ pub fn unit_mut(&mut self, unit: &UnitId) -> Option<&mut Unit> { ++ self.units.get_mut(unit) + } + } + +@@ -180,7 +188,7 @@ impl Unit { + ) -> io::Result { + let config = fs::read_to_string(config_path)?; + +- let Some(ext) = config_path.extension().map(|ext| ext.to_str().unwrap()) else { ++ let Some(ext) = config_path.extension().and_then(|ext| ext.to_str()) else { + let script = Script::from_str(&config, errors)?; + return Ok(Unit { + id, diff --git a/local/patches/base/P2-inputd.patch b/local/patches/base/P2-inputd.patch new file mode 100644 index 00000000..545465ce --- /dev/null +++ b/local/patches/base/P2-inputd.patch @@ -0,0 +1,896 @@ +# P2-inputd.patch +# Extract inputd improvements: named producers, device consumers, hotplug events, +# error handling, and input scheme extensions. +# +# Files: drivers/inputd/src/lib.rs, drivers/inputd/src/main.rs + +diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs +index b68e8211..f07a411d 100644 +--- a/drivers/inputd/src/lib.rs ++++ b/drivers/inputd/src/lib.rs +@@ -64,25 +64,53 @@ impl ConsumerHandle { + let fd = self.0.as_raw_fd(); + let written = libredox::call::fpath(fd as usize, &mut buffer)?; + +- assert!(written <= buffer.len()); +- +- let mut display_path = PathBuf::from( +- std::str::from_utf8(&buffer[..written]) +- .expect("init: display path UTF-8 check failed") +- .to_owned(), +- ); +- display_path.set_file_name(format!( +- "v2/{}", +- display_path.file_name().unwrap().to_str().unwrap() +- )); +- let display_path = display_path.to_str().unwrap(); ++ if written > buffer.len() { ++ return Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ "inputd: display path exceeded buffer size", ++ )); ++ } ++ ++ let path_str = std::str::from_utf8(&buffer[..written]).map_err(|e| { ++ io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("inputd: display path is not valid UTF-8: {e}"), ++ ) ++ })?; ++ let mut display_path = PathBuf::from(path_str.to_owned()); ++ ++ let file_name = display_path ++ .file_name() ++ .and_then(|n| n.to_str()) ++ .ok_or_else(|| { ++ io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!( ++ "inputd: display path has no valid file name: {}", ++ display_path.display() ++ ), ++ ) ++ })?; ++ display_path.set_file_name(format!("v2/{file_name}")); ++ let display_path_str = display_path.to_str().ok_or_else(|| { ++ io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!( ++ "inputd: constructed display path is not valid UTF-8: {}", ++ display_path.display() ++ ), ++ ) ++ })?; + + let display_file = +- libredox::call::open(display_path, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) ++ libredox::call::open(display_path_str, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) + .map(|socket| unsafe { File::from_raw_fd(socket as RawFd) }) +- .unwrap_or_else(|err| { +- panic!("failed to open display {}: {}", display_path, err); +- }); ++ .map_err(|err| { ++ io::Error::new( ++ io::ErrorKind::Other, ++ format!("inputd: failed to open display {display_path_str}: {err}"), ++ ) ++ })?; + + Ok(display_file) + } +@@ -152,8 +180,12 @@ impl DisplayHandle { + + if nread == 0 { + Ok(None) ++ } else if nread != size_of::() { ++ Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("inputd: partial vt event read: got {nread}, expected {}", size_of::()), ++ )) + } else { +- assert_eq!(nread, size_of::()); + Ok(Some(event)) + } + } +@@ -171,13 +203,11 @@ impl ControlHandle { + Ok(Self(File::open(path)?)) + } + +- /// Sent to Handle::Display + pub fn activate_vt(&mut self, vt: usize) -> io::Result { + let cmd = ControlEvent::from(VtActivate { vt }); + self.0.write(unsafe { any_as_u8_slice(&cmd) }) + } + +- /// Sent to Handle::Producer + pub fn activate_keymap(&mut self, keymap: usize) -> io::Result { + let cmd = ControlEvent::from(KeymapActivate { keymap }); + self.0.write(unsafe { any_as_u8_slice(&cmd) }) +@@ -209,3 +239,195 @@ impl ProducerHandle { + Ok(()) + } + } ++ ++pub struct NamedProducerHandle(File); ++ ++impl NamedProducerHandle { ++ pub fn new(name: &str) -> io::Result { ++ let path = format!("/scheme/input/producer/{name}"); ++ Ok(Self(File::open(path)?)) ++ } ++ ++ pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> { ++ self.0.write(&event)?; ++ Ok(()) ++ } ++} ++ ++/// Convenience wrapper that tries a named producer first, ++/// falling back to the legacy anonymous producer on failure. ++pub enum InputProducer { ++ Named(NamedProducerHandle), ++ Legacy(ProducerHandle), ++} ++ ++impl InputProducer { ++ /// Open a named producer (`/scheme/input/producer/{name}`). ++ /// If the named path is unavailable, fall back to the legacy ++ /// `/scheme/input/producer` path so the driver keeps working on ++ /// older inputd builds or degraded schemes. ++ pub fn new_named_or_fallback(name: &str) -> io::Result { ++ match NamedProducerHandle::new(name) { ++ Ok(named) => Ok(InputProducer::Named(named)), ++ Err(named_err) => { ++ log::debug!( ++ "inputd: named producer '{}' unavailable ({}), falling back to legacy", ++ name, ++ named_err ++ ); ++ ProducerHandle::new().map(InputProducer::Legacy) ++ } ++ } ++ } ++ ++ /// Open the legacy anonymous producer directly. ++ pub fn new_legacy() -> io::Result { ++ ProducerHandle::new().map(InputProducer::Legacy) ++ } ++ ++ pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> { ++ match self { ++ InputProducer::Named(h) => h.write_event(event), ++ InputProducer::Legacy(h) => h.write_event(event), ++ } ++ } ++} ++ ++pub struct DeviceConsumerHandle(File); ++ ++pub enum DeviceConsumerHandleEvent<'a> { ++ Events(&'a [Event]), ++} ++ ++impl DeviceConsumerHandle { ++ pub fn new(device_name: &str) -> io::Result { ++ let path = format!("/scheme/input/{device_name}"); ++ Ok(Self(File::open(path)?)) ++ } ++ ++ pub fn event_handle(&self) -> BorrowedFd<'_> { ++ self.0.as_fd() ++ } ++ ++ pub fn read_events<'a>( ++ &self, ++ events: &'a mut [Event], ++ ) -> io::Result> { ++ match read_to_slice(self.0.as_fd(), events) { ++ Ok(count) => Ok(DeviceConsumerHandleEvent::Events(&events[..count])), ++ Err(err) => Err(err.into()), ++ } ++ } ++} ++ ++#[derive(Debug, Clone)] ++#[repr(C)] ++pub struct HotplugEventHeader { ++ pub kind: u32, ++ pub device_id: u32, ++ pub name_len: u32, ++ pub reserved: u32, ++} ++ ++#[derive(Debug, Clone)] ++pub struct HotplugEvent { ++ pub kind: u32, ++ pub device_id: u32, ++ pub name: String, ++} ++ ++pub struct HotplugHandle { ++ file: File, ++ partial: Vec, ++} ++ ++impl HotplugHandle { ++ pub fn new() -> io::Result { ++ let file = File::open("/scheme/input/events")?; ++ Ok(Self { ++ file, ++ partial: Vec::new(), ++ }) ++ } ++ ++ pub fn event_handle(&self) -> BorrowedFd<'_> { ++ self.file.as_fd() ++ } ++ ++ pub fn read_event(&mut self) -> io::Result> { ++ let mut tmp = [0u8; 256]; ++ match self.file.read(&mut tmp) { ++ Ok(0) => {} ++ Ok(n) => self.partial.extend_from_slice(&tmp[..n]), ++ Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} ++ Err(e) => return Err(e), ++ } ++ ++ if self.partial.len() < 16 { ++ return Ok(None); ++ } ++ ++ let header = HotplugEventHeader { ++ kind: u32::from_ne_bytes(self.partial[0..4].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ device_id: u32::from_ne_bytes(self.partial[4..8].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ name_len: u32::from_ne_bytes(self.partial[8..12].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ reserved: 0, ++ }; ++ ++ let total_len = 16 + header.name_len as usize; ++ if self.partial.len() < total_len { ++ return Ok(None); ++ } ++ ++ let name = String::from_utf8(self.partial[16..total_len].to_vec()).map_err(|e| { ++ io::Error::new(io::ErrorKind::InvalidData, format!("invalid UTF-8: {e}")) ++ })?; ++ ++ self.partial.drain(..total_len); ++ ++ Ok(Some(HotplugEvent { ++ kind: header.kind, ++ device_id: header.device_id, ++ name, ++ })) ++ } ++} ++ ++pub const RESERVED_DEVICE_NAMES: &[&str] = &[ ++ "producer", ++ "consumer", ++ "consumer_bootlog", ++ "events", ++ "handle", ++ "handle_early", ++ "control", ++]; ++ ++pub struct InputDeviceLister; ++ ++impl InputDeviceLister { ++ pub fn list() -> io::Result> { ++ let mut dir = std::fs::read_dir("/scheme/input/")?; ++ let mut devices = Vec::new(); ++ loop { ++ match dir.next() { ++ Some(Ok(entry)) => { ++ if let Some(name) = entry.file_name().to_str() { ++ if !RESERVED_DEVICE_NAMES.contains(&name) { ++ devices.push(name.to_owned()); ++ } ++ } ++ } ++ Some(Err(e)) => return Err(e), ++ None => break, ++ } ++ } ++ Ok(devices) ++ } ++} +diff --git a/drivers/inputd/src/main.rs b/drivers/inputd/src/main.rs +index 07aa943e..89018568 100644 +--- a/drivers/inputd/src/main.rs ++++ b/drivers/inputd/src/main.rs +@@ -13,7 +13,7 @@ + + use core::mem::size_of; + use std::borrow::Cow; +-use std::collections::BTreeSet; ++use std::collections::{BTreeMap, BTreeSet}; + use std::mem::transmute; + use std::ops::ControlFlow; + use std::sync::atomic::{AtomicUsize, Ordering}; +@@ -26,8 +26,9 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket}; + + use orbclient::{Event, EventOption}; + use scheme_utils::{Blocking, FpathWriter, HandleMap}; ++use syscall::dirent::{DirEntry, DirentBuf, DirentKind}; + use syscall::schemev2::NewFdFlags; +-use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL}; ++use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL, ENOENT, ENOTDIR}; + + pub mod keymap; + +@@ -35,8 +36,57 @@ use keymap::KeymapKind; + + use crate::keymap::KeymapData; + ++const DEVICE_ADD: u32 = 1; ++const DEVICE_REMOVE: u32 = 2; ++ ++fn validate_producer_name(name: &str) -> Result<(), SysError> { ++ if name.is_empty() || name.contains('/') { ++ return Err(SysError::new(EINVAL)); ++ } ++ if inputd::RESERVED_DEVICE_NAMES.contains(&name) { ++ return Err(SysError::new(EINVAL)); ++ } ++ Ok(()) ++} ++ ++fn serialize_hotplug(kind: u32, device_id: u32, name: &str) -> Vec { ++ let name_bytes = name.as_bytes(); ++ let header = HotplugHeader { ++ kind, ++ device_id, ++ name_len: name_bytes.len() as u32, ++ _reserved: 0, ++ }; ++ let mut out = Vec::with_capacity(16 + name_bytes.len()); ++ out.extend_from_slice(&header.to_bytes()); ++ out.extend_from_slice(name_bytes); ++ out ++} ++ ++#[repr(C)] ++struct HotplugHeader { ++ kind: u32, ++ device_id: u32, ++ name_len: u32, ++ _reserved: u32, ++} ++ ++impl HotplugHeader { ++ fn to_bytes(&self) -> [u8; 16] { ++ let mut buf = [0u8; 16]; ++ buf[0..4].copy_from_slice(&self.kind.to_ne_bytes()); ++ buf[4..8].copy_from_slice(&self.device_id.to_ne_bytes()); ++ buf[8..12].copy_from_slice(&self.name_len.to_ne_bytes()); ++ buf[12..16].copy_from_slice(&self._reserved.to_ne_bytes()); ++ buf ++ } ++} ++ + enum Handle { + Producer, ++ NamedProducer { ++ name: String, ++ }, + Consumer { + events: EventFlags, + pending: Vec, +@@ -46,6 +96,17 @@ enum Handle { + notified: bool, + vt: usize, + }, ++ DeviceConsumer { ++ device_name: String, ++ events: EventFlags, ++ pending: Vec, ++ notified: bool, ++ }, ++ HotplugEvents { ++ events: EventFlags, ++ pending: Vec, ++ notified: bool, ++ }, + Display { + events: EventFlags, + pending: Vec, +@@ -72,6 +133,9 @@ struct InputScheme { + rshift: bool, + + has_new_events: bool, ++ ++ devices: BTreeMap, ++ next_device_id: AtomicUsize, + } + + impl InputScheme { +@@ -90,9 +154,28 @@ impl InputScheme { + lshift: false, + rshift: false, + has_new_events: false, ++ ++ devices: BTreeMap::new(), ++ next_device_id: AtomicUsize::new(1), + } + } + ++ fn emit_hotplug(&mut self, kind: u32, device_id: u32, name: &str) { ++ let record = serialize_hotplug(kind, device_id, name); ++ for handle in self.handles.values_mut() { ++ if let Handle::HotplugEvents { ++ pending, ++ notified, ++ .. ++ } = handle ++ { ++ pending.extend_from_slice(&record); ++ *notified = false; ++ } ++ } ++ self.has_new_events = true; ++ } ++ + fn switch_vt(&mut self, new_active: usize) { + if let Some(active_vt) = self.active_vt { + if new_active == active_vt { +@@ -146,6 +229,43 @@ impl InputScheme { + + self.active_keymap = KeymapData::new(new_active.into()); + } ++ ++ fn deliver_to_legacy_consumers(&mut self, buf: &[u8]) { ++ if let Some(active_vt) = self.active_vt { ++ for handle in self.handles.values_mut() { ++ if let Handle::Consumer { ++ pending, ++ notified, ++ vt, ++ .. ++ } = handle ++ { ++ if *vt != active_vt { ++ continue; ++ } ++ pending.extend_from_slice(buf); ++ *notified = false; ++ } ++ } ++ } ++ } ++ ++ fn deliver_to_device_consumers(&mut self, name: &str, buf: &[u8]) { ++ for handle in self.handles.values_mut() { ++ if let Handle::DeviceConsumer { ++ device_name, ++ pending, ++ notified, ++ .. ++ } = handle ++ { ++ if device_name == name { ++ pending.extend_from_slice(buf); ++ *notified = false; ++ } ++ } ++ } ++ } + } + + impl SchemeSync for InputScheme { +@@ -170,7 +290,23 @@ impl SchemeSync for InputScheme { + let command = path_parts.next().ok_or(SysError::new(EINVAL))?; + + let handle_ty = match command { +- "producer" => Handle::Producer, ++ "producer" => { ++ if let Some(name) = path_parts.next() { ++ validate_producer_name(name)?; ++ if self.devices.contains_key(name) { ++ return Err(SysError::new(EEXIST)); ++ } ++ let device_id = self.next_device_id.fetch_add(1, Ordering::SeqCst) as u32; ++ self.devices.insert(name.to_owned(), device_id); ++ let handle = Handle::NamedProducer { ++ name: name.to_owned(), ++ }; ++ self.emit_hotplug(DEVICE_ADD, device_id, name); ++ handle ++ } else { ++ Handle::Producer ++ } ++ } + "consumer" => { + let vt = self.next_vt_id.fetch_add(1, Ordering::Relaxed); + self.vts.insert(vt); +@@ -253,9 +389,23 @@ impl SchemeSync for InputScheme { + } + "control" => Handle::Control, + +- _ => { +- log::error!("invalid path '{path}'"); +- return Err(SysError::new(EINVAL)); ++ "events" => Handle::HotplugEvents { ++ events: EventFlags::empty(), ++ pending: Vec::new(), ++ notified: false, ++ }, ++ ++ // dynamic device consumer: must be a currently registered device ++ name => { ++ if !self.devices.contains_key(name) { ++ return Err(SysError::new(ENOENT)); ++ } ++ Handle::DeviceConsumer { ++ device_name: name.to_owned(), ++ events: EventFlags::empty(), ++ pending: Vec::new(), ++ notified: false, ++ } + } + }; + +@@ -274,7 +424,7 @@ impl SchemeSync for InputScheme { + let handle = self.handles.get(id)?; + + if let Handle::Consumer { vt, .. } = handle { +- write!(w, "{vt}").unwrap(); ++ write!(w, "{vt}").map_err(|_| SysError::new(EINVAL))?; + Ok(()) + } else { + Err(SysError::new(EINVAL)) +@@ -282,6 +432,50 @@ impl SchemeSync for InputScheme { + }) + } + ++ fn getdents<'buf>( ++ &mut self, ++ id: usize, ++ mut buf: DirentBuf<&'buf mut [u8]>, ++ opaque_offset: u64, ++ ) -> syscall::Result> { ++ let handle = self.handles.get(id)?; ++ if !matches!(handle, Handle::SchemeRoot) { ++ return Err(SysError::new(ENOTDIR)); ++ } ++ ++ let static_entries: &[&str] = &[ ++ "producer", ++ "consumer", ++ "consumer_bootlog", ++ "events", ++ "handle", ++ "handle_early", ++ "control", ++ ]; ++ ++ let device_names: Vec<&str> = self.devices.keys().map(|s| s.as_str()).collect(); ++ ++ let all_entries: Vec<(&str, DirentKind)> = static_entries ++ .iter() ++ .map(|&name| (name, DirentKind::Directory)) ++ .chain(device_names.iter().map(|&name| (name, DirentKind::Unspecified))) ++ .collect(); ++ ++ for (idx, (name, kind)) in all_entries ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ Ok(buf) ++ } ++ + fn read( + &mut self, + id: usize, +@@ -313,6 +507,22 @@ impl SchemeSync for InputScheme { + Ok(copy) + } + ++ Handle::DeviceConsumer { pending, .. } => { ++ let copy = core::cmp::min(pending.len(), buf.len()); ++ for (i, byte) in pending.drain(..copy).enumerate() { ++ buf[i] = byte; ++ } ++ Ok(copy) ++ } ++ ++ Handle::HotplugEvents { pending, .. } => { ++ let copy = core::cmp::min(pending.len(), buf.len()); ++ for (i, byte) in pending.drain(..copy).enumerate() { ++ buf[i] = byte; ++ } ++ Ok(copy) ++ } ++ + Handle::Display { pending, .. } => { + if buf.len() % size_of::() == 0 { + let copy = core::cmp::min(pending.len(), buf.len() / size_of::()); +@@ -334,6 +544,10 @@ impl SchemeSync for InputScheme { + log::error!("producer tried to read"); + return Err(SysError::new(EINVAL)); + } ++ Handle::NamedProducer { .. } => { ++ log::error!("named producer tried to read"); ++ return Err(SysError::new(EINVAL)); ++ } + Handle::Control => { + log::error!("control tried to read"); + return Err(SysError::new(EINVAL)); +@@ -379,11 +593,20 @@ impl SchemeSync for InputScheme { + log::error!("consumer tried to write"); + return Err(SysError::new(EINVAL)); + } ++ Handle::DeviceConsumer { .. } => { ++ log::error!("device consumer tried to write"); ++ return Err(SysError::new(EINVAL)); ++ } ++ Handle::HotplugEvents { .. } => { ++ log::error!("hotplug events tried to write"); ++ return Err(SysError::new(EINVAL)); ++ } + Handle::Display { .. } => { + log::error!("display tried to write"); + return Err(SysError::new(EINVAL)); + } + Handle::Producer => {} ++ Handle::NamedProducer { .. } => {} + Handle::SchemeRoot => return Err(SysError::new(EBADF)), + } + +@@ -397,6 +620,11 @@ impl SchemeSync for InputScheme { + buf.len() / size_of::(), + ) + }); ++ let producer_name = match self.handles.get(id)? { ++ Handle::NamedProducer { ref name } => Some(name.clone()), ++ Handle::Producer => None, ++ _ => return Err(SysError::new(EBADF)), ++ }; + + for i in 0..events.len() { + let mut new_active_opt = None; +@@ -437,38 +665,21 @@ impl SchemeSync for InputScheme { + } + } + +- let handle = self.handles.get_mut(id)?; +- assert!(matches!(handle, Handle::Producer)); +- +- let buf = unsafe { ++ let serialized = unsafe { + core::slice::from_raw_parts( + (events.as_ptr()) as *const u8, + events.len() * size_of::(), + ) + }; + +- if let Some(active_vt) = self.active_vt { +- for handle in self.handles.values_mut() { +- match handle { +- Handle::Consumer { +- pending, +- notified, +- vt, +- .. +- } => { +- if *vt != active_vt { +- continue; +- } +- +- pending.extend_from_slice(buf); +- *notified = false; +- } +- _ => continue, +- } +- } ++ if let Some(ref name) = producer_name { ++ self.deliver_to_device_consumers(name, serialized); + } + +- Ok(buf.len()) ++ // named producers also feed the legacy path; legacy producers only feed legacy ++ self.deliver_to_legacy_consumers(serialized); ++ ++ Ok(serialized.len()) + } + + fn fevent( +@@ -487,6 +698,24 @@ impl SchemeSync for InputScheme { + *notified = false; + Ok(EventFlags::empty()) + } ++ Handle::DeviceConsumer { ++ ref mut events, ++ ref mut notified, ++ .. ++ } => { ++ *events = flags; ++ *notified = false; ++ Ok(EventFlags::empty()) ++ } ++ Handle::HotplugEvents { ++ ref mut events, ++ ref mut notified, ++ .. ++ } => { ++ *events = flags; ++ *notified = false; ++ Ok(EventFlags::empty()) ++ } + Handle::Display { + ref mut events, + ref mut notified, +@@ -496,7 +725,7 @@ impl SchemeSync for InputScheme { + *notified = false; + Ok(EventFlags::empty()) + } +- Handle::Producer | Handle::Control => { ++ Handle::Producer | Handle::NamedProducer { .. } | Handle::Control => { + log::error!("producer or control tried to use an event queue"); + Err(SysError::new(EINVAL)) + } +@@ -505,8 +734,8 @@ impl SchemeSync for InputScheme { + } + + fn on_close(&mut self, id: usize) { +- match self.handles.remove(id).unwrap() { +- Handle::Consumer { vt, .. } => { ++ match self.handles.remove(id) { ++ Some(Handle::Consumer { vt, .. }) => { + self.vts.remove(&vt); + if self.active_vt == Some(vt) { + if let Some(&new_vt) = self.vts.last() { +@@ -516,7 +745,15 @@ impl SchemeSync for InputScheme { + } + } + } +- _ => {} ++ Some(Handle::NamedProducer { name, .. }) => { ++ if let Some(device_id) = self.devices.remove(&name) { ++ self.emit_hotplug(DEVICE_REMOVE, device_id, &name); ++ } ++ } ++ Some(_) => {} ++ None => { ++ log::warn!("inputd: on_close called with unknown handle id {id}"); ++ } + } + } + } +@@ -564,6 +801,39 @@ fn deamon(daemon: daemon::SchemeDaemon) -> anyhow::Result<()> { + + *notified = true; + } ++ Handle::DeviceConsumer { ++ events, ++ pending, ++ ref mut notified, ++ .. ++ } => { ++ if pending.is_empty() || *notified || !events.contains(EventFlags::EVENT_READ) { ++ continue; ++ } ++ ++ socket_file.write_response( ++ Response::post_fevent(*id, EventFlags::EVENT_READ.bits()), ++ SignalBehavior::Restart, ++ )?; ++ ++ *notified = true; ++ } ++ Handle::HotplugEvents { ++ events, ++ pending, ++ ref mut notified, ++ } => { ++ if pending.is_empty() || *notified || !events.contains(EventFlags::EVENT_READ) { ++ continue; ++ } ++ ++ socket_file.write_response( ++ Response::post_fevent(*id, EventFlags::EVENT_READ.bits()), ++ SignalBehavior::Restart, ++ )?; ++ ++ *notified = true; ++ } + Handle::Display { + events, + pending, +@@ -589,8 +859,11 @@ fn deamon(daemon: daemon::SchemeDaemon) -> anyhow::Result<()> { + } + + fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! { +- deamon(daemon).unwrap(); +- unreachable!(); ++ if let Err(err) = deamon(daemon) { ++ log::error!("inputd: scheme daemon failed: {err}"); ++ std::process::exit(1); ++ } ++ unreachable!() + } + + const HELP: &str = r#" +@@ -608,13 +881,26 @@ fn main() { + match val.as_ref() { + // Activates a VT. + "-A" => { +- let vt = args.next().unwrap().parse::().unwrap(); ++ let vt_str = args.next().unwrap_or_else(|| { ++ eprintln!("inputd: -A requires a VT number argument"); ++ std::process::exit(1); ++ }); ++ let vt = vt_str.parse::().unwrap_or_else(|_| { ++ eprintln!("inputd: invalid VT number: {vt_str}"); ++ std::process::exit(1); ++ }); + +- let mut handle = +- inputd::ControlHandle::new().expect("inputd: failed to open control handle"); +- handle +- .activate_vt(vt) +- .expect("inputd: failed to activate VT"); ++ let mut handle = match inputd::ControlHandle::new() { ++ Ok(h) => h, ++ Err(e) => { ++ eprintln!("inputd: failed to open control handle: {e}"); ++ std::process::exit(1); ++ } ++ }; ++ if let Err(e) = handle.activate_vt(vt) { ++ eprintln!("inputd: failed to activate VT {vt}: {e}"); ++ std::process::exit(1); ++ } + } + // Activates a keymap. + "-K" => { +@@ -630,11 +916,17 @@ fn main() { + std::process::exit(1); + }); + +- let mut handle = +- inputd::ControlHandle::new().expect("inputd: failed to open control handle"); +- handle +- .activate_keymap(vt as usize) +- .expect("inputd: failed to activate keymap"); ++ let mut handle = match inputd::ControlHandle::new() { ++ Ok(h) => h, ++ Err(e) => { ++ eprintln!("inputd: failed to open control handle: {e}"); ++ std::process::exit(1); ++ } ++ }; ++ if let Err(e) = handle.activate_keymap(vt as usize) { ++ eprintln!("inputd: failed to activate keymap: {e}"); ++ std::process::exit(1); ++ } + } + // List available keymaps + "--keymaps" => { +@@ -647,7 +939,10 @@ fn main() { + println!("{}", HELP); + } + +- _ => panic!("inputd: invalid argument: {}", val), ++ _ => { ++ eprintln!("inputd: invalid argument: {val}"); ++ std::process::exit(1); ++ } + } + } else { + common::setup_logging( diff --git a/local/patches/base/P2-logd.patch b/local/patches/base/P2-logd.patch new file mode 100644 index 00000000..4d4b8902 --- /dev/null +++ b/local/patches/base/P2-logd.patch @@ -0,0 +1,164 @@ +# P2-logd.patch +# Extract logd hardening: optional kernel debug/syslog handles, graceful error +# handling, and resilient request processing loop. +# +# Files: logd/src/main.rs, logd/src/scheme.rs + +diff --git a/logd/src/main.rs b/logd/src/main.rs +index 3636f1fa..559d8993 100644 +--- a/logd/src/main.rs ++++ b/logd/src/main.rs +@@ -6,18 +6,30 @@ use crate::scheme::LogScheme; + mod scheme; + + fn daemon(daemon: daemon::SchemeDaemon) -> ! { +- let socket = Socket::create().expect("logd: failed to create log scheme"); ++ let socket = match Socket::create() { ++ Ok(s) => s, ++ Err(e) => { ++ eprintln!("logd: failed to create log scheme: {e}"); ++ std::process::exit(1); ++ } ++ }; + + let mut scheme = LogScheme::new(&socket); + let handler = Blocking::new(&socket, 16); + + let _ = daemon.ready_sync_scheme(&socket, &mut scheme); + +- libredox::call::setrens(0, 0).expect("logd: failed to enter null namespace"); +- +- handler +- .process_requests_blocking(scheme) +- .expect("logd: failed to process requests"); ++ if let Err(e) = libredox::call::setrens(0, 0) { ++ eprintln!("logd: failed to enter null namespace: {e}"); ++ } ++ ++ match handler.process_requests_blocking(scheme) { ++ Ok(never) => match never {}, ++ Err(e) => { ++ eprintln!("logd: failed to process requests: {e}"); ++ std::process::exit(1); ++ } ++ } + } + + fn main() { +diff --git a/logd/src/scheme.rs b/logd/src/scheme.rs +index 070de3d6..ef3e175c 100644 +--- a/logd/src/scheme.rs ++++ b/logd/src/scheme.rs +@@ -22,7 +22,7 @@ pub enum LogHandle { + + pub struct LogScheme<'sock> { + socket: &'sock Socket, +- kernel_debug: File, ++ kernel_debug: Option, + output_tx: Sender, + handles: HandleMap, + } +@@ -34,12 +34,24 @@ enum OutputCmd { + + impl<'sock> LogScheme<'sock> { + pub fn new(socket: &'sock Socket) -> Self { +- let kernel_debug = OpenOptions::new() ++ let kernel_debug = match OpenOptions::new() + .write(true) + .open("/scheme/debug") +- .unwrap(); ++ { ++ Ok(f) => Some(f), ++ Err(e) => { ++ eprintln!("logd: failed to open /scheme/debug: {e}"); ++ None ++ } ++ }; + +- let mut kernel_sys_log = std::fs::File::open("/scheme/sys/log").unwrap(); ++ let kernel_sys_log = match std::fs::File::open("/scheme/sys/log") { ++ Ok(f) => Some(f), ++ Err(e) => { ++ eprintln!("logd: failed to open /scheme/sys/log: {e}"); ++ None ++ } ++ }; + + let (output_tx, output_rx) = mpsc::channel::(); + +@@ -72,20 +84,28 @@ impl<'sock> LogScheme<'sock> { + } + }); + +- let output_tx2 = output_tx.clone(); +- std::thread::spawn(move || { +- let mut handle_buf = vec![]; +- let mut buf = [0; 4096]; +- buf[.."kernel: ".len()].copy_from_slice(b"kernel: "); +- loop { +- let n = kernel_sys_log.read(&mut buf["kernel: ".len()..]).unwrap(); +- if n == 0 { +- // FIXME currently possible as /scheme/log/kernel presents a snapshot of the log queue +- break; ++ if let Some(mut kernel_sys_log) = kernel_sys_log { ++ let output_tx2 = output_tx.clone(); ++ std::thread::spawn(move || { ++ let mut handle_buf = vec![]; ++ let mut buf = [0; 4096]; ++ buf[.."kernel: ".len()].copy_from_slice(b"kernel: "); ++ loop { ++ let n = match kernel_sys_log.read(&mut buf["kernel: ".len()..]) { ++ Ok(n) => n, ++ Err(e) => { ++ eprintln!("logd: error reading kernel log: {e}"); ++ break; ++ } ++ }; ++ if n == 0 { ++ // FIXME currently possible as /scheme/log/kernel presents a snapshot of the log queue ++ break; ++ } ++ Self::write_logs(&output_tx2, &mut handle_buf, "kernel", &buf, None); + } +- Self::write_logs(&output_tx2, &mut handle_buf, "kernel", &buf, None); +- } +- }); ++ }); ++ } + + LogScheme { + socket, +@@ -120,9 +140,9 @@ impl<'sock> LogScheme<'sock> { + let _ = kernel_debug.flush(); + } + +- output_tx +- .send(OutputCmd::Log(mem::take(handle_buf))) +- .unwrap(); ++ if let Err(e) = output_tx.send(OutputCmd::Log(mem::take(handle_buf))) { ++ eprintln!("logd: failed to send log output: {e}"); ++ } + } + + i += 1; +@@ -196,7 +216,7 @@ impl<'sock> SchemeSync for LogScheme<'sock> { + handle_buf, + context, + buf, +- Some(&mut self.kernel_debug), ++ self.kernel_debug.as_mut(), + ); + + Ok(buf.len()) +@@ -217,7 +237,10 @@ impl<'sock> SchemeSync for LogScheme<'sock> { + ) { + return Err(e); + } +- self.output_tx.send(OutputCmd::AddSink(new_fd)).unwrap(); ++ if let Err(e) = self.output_tx.send(OutputCmd::AddSink(new_fd)) { ++ eprintln!("logd: failed to add log sink: {e}"); ++ return Err(Error::new(EIO)); ++ } + + Ok(1) + } diff --git a/local/patches/base/P2-network-driver-mains.patch b/local/patches/base/P2-network-driver-mains.patch new file mode 100644 index 00000000..6c60f9dd --- /dev/null +++ b/local/patches/base/P2-network-driver-mains.patch @@ -0,0 +1,607 @@ +# P2-network-driver-mains.patch +# Extract network driver main.rs hardening: replace panic/unwrap/expect with +# proper error handling and graceful exits. +# +# Files: drivers/net/e1000d/src/main.rs, drivers/net/ixgbed/src/main.rs, +# drivers/net/rtl8139d/src/main.rs, drivers/net/rtl8168d/src/main.rs, +# drivers/net/virtio-netd/src/main.rs + +diff --git a/drivers/net/e1000d/src/main.rs b/drivers/net/e1000d/src/main.rs +index 373ea9b3..8ff57b33 100644 +--- a/drivers/net/e1000d/src/main.rs ++++ b/drivers/net/e1000d/src/main.rs +@@ -1,5 +1,6 @@ + use std::io::{Read, Write}; + use std::os::unix::io::AsRawFd; ++use std::process; + + use driver_network::NetworkScheme; + use event::{user_data, EventQueue}; +@@ -25,10 +26,13 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + common::file_level(), + ); + +- let irq = pci_config +- .func +- .legacy_interrupt_line +- .expect("e1000d: no legacy interrupts supported"); ++ let irq = match pci_config.func.legacy_interrupt_line { ++ Some(irq) => irq, ++ None => { ++ log::error!("e1000d: no legacy interrupts supported"); ++ process::exit(1); ++ } ++ }; + + log::info!("E1000 {}", pci_config.func.display()); + +@@ -38,7 +42,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + let mut scheme = NetworkScheme::new( + move || unsafe { +- device::Intel8254x::new(address).expect("e1000d: failed to allocate device") ++ device::Intel8254x::new(address).unwrap_or_else(|err| { ++ log::error!("e1000d: failed to allocate device: {err}"); ++ process::exit(1); ++ }) + }, + daemon, + format!("network.{name}"), +@@ -51,7 +58,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = EventQueue::::new().expect("e1000d: failed to create event queue"); ++ let mut event_queue = EventQueue::::new().unwrap_or_else(|err| { ++ log::error!("e1000d: failed to create event queue: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe( +@@ -59,32 +69,65 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + Source::Irq, + event::EventFlags::READ, + ) +- .expect("e1000d: failed to subscribe to IRQ fd"); ++ .unwrap_or_else(|err| { ++ log::error!("e1000d: failed to subscribe to IRQ fd: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + scheme.event_handle().raw(), + Source::Scheme, + event::EventFlags::READ, + ) +- .expect("e1000d: failed to subscribe to scheme fd"); +- +- libredox::call::setrens(0, 0).expect("e1000d: failed to enter null namespace"); +- +- scheme.tick().unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("e1000d: failed to subscribe to scheme fd: {err}"); ++ process::exit(1); ++ }); ++ ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("e1000d: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("e1000d: failed initial scheme tick: {err}"); ++ process::exit(1); ++ } + +- for event in event_queue.map(|e| e.expect("e1000d: failed to get event")) { ++ loop { ++ let event = match event_queue.next() { ++ Some(Ok(event)) => event, ++ Some(Err(err)) => { ++ log::error!("e1000d: failed to get event: {err}"); ++ continue; ++ } ++ None => break, ++ }; + match event.user_data { + Source::Irq => { + let mut irq = [0; 8]; +- irq_file.read(&mut irq).unwrap(); ++ if let Err(err) = irq_file.read(&mut irq) { ++ log::error!("e1000d: failed to read IRQ: {err}"); ++ continue; ++ } + if unsafe { scheme.adapter().irq() } { +- irq_file.write(&mut irq).unwrap(); +- +- scheme.tick().expect("e1000d: failed to handle IRQ") ++ if let Err(err) = irq_file.write(&mut irq) { ++ log::error!("e1000d: failed to write IRQ: {err}"); ++ continue; ++ } ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("e1000d: failed to handle IRQ: {err}"); ++ } ++ } ++ } ++ Source::Scheme => { ++ if let Err(err) = scheme.tick() { ++ log::error!("e1000d: failed to handle scheme op: {err}"); + } + } +- Source::Scheme => scheme.tick().expect("e1000d: failed to handle scheme op"), + } + } +- unreachable!() ++ ++ process::exit(0); + } +diff --git a/drivers/net/ixgbed/src/main.rs b/drivers/net/ixgbed/src/main.rs +index 4a6ce74d..855d339d 100644 +--- a/drivers/net/ixgbed/src/main.rs ++++ b/drivers/net/ixgbed/src/main.rs +@@ -1,5 +1,6 @@ + use std::io::{Read, Write}; + use std::os::unix::io::AsRawFd; ++use std::process; + + use driver_network::NetworkScheme; + use event::{user_data, EventQueue}; +@@ -19,12 +20,23 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + let mut name = pci_config.func.name(); + name.push_str("_ixgbe"); + +- let irq = pci_config +- .func +- .legacy_interrupt_line +- .expect("ixgbed: no legacy interrupts supported"); ++ common::setup_logging( ++ "net", ++ "pci", ++ &name, ++ common::output_level(), ++ common::file_level(), ++ ); ++ ++ let irq = match pci_config.func.legacy_interrupt_line { ++ Some(irq) => irq, ++ None => { ++ log::error!("ixgbed: no legacy interrupts supported"); ++ process::exit(1); ++ } ++ }; + +- println!(" + IXGBE {}", pci_config.func.display()); ++ log::info!("IXGBE {}", pci_config.func.display()); + + let mut irq_file = irq.irq_handle("ixgbed"); + +@@ -34,8 +46,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + let mut scheme = NetworkScheme::new( + move || { +- device::Intel8259x::new(address as usize, size) +- .expect("ixgbed: failed to allocate device") ++ device::Intel8259x::new(address as usize, size).unwrap_or_else(|err| { ++ log::error!("ixgbed: failed to allocate device: {err}"); ++ process::exit(1); ++ }) + }, + daemon, + format!("network.{name}"), +@@ -48,41 +62,77 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = EventQueue::::new().expect("ixgbed: Could not create event queue."); ++ let mut event_queue = EventQueue::::new().unwrap_or_else(|err| { ++ log::error!("ixgbed: failed to create event queue: {err}"); ++ process::exit(1); ++ }); ++ + event_queue + .subscribe( + irq_file.as_raw_fd() as usize, + Source::Irq, + event::EventFlags::READ, + ) +- .unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("ixgbed: failed to subscribe to IRQ fd: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + scheme.event_handle().raw(), + Source::Scheme, + event::EventFlags::READ, + ) +- .unwrap(); +- +- libredox::call::setrens(0, 0).expect("ixgbed: failed to enter null namespace"); ++ .unwrap_or_else(|err| { ++ log::error!("ixgbed: failed to subscribe to scheme fd: {err}"); ++ process::exit(1); ++ }); ++ ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("ixgbed: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("ixgbed: failed initial scheme tick: {err}"); ++ process::exit(1); ++ } + +- scheme.tick().unwrap(); ++ loop { ++ let event = match event_queue.next() { ++ Some(Ok(event)) => event, ++ Some(Err(err)) => { ++ log::error!("ixgbed: failed to get event: {err}"); ++ continue; ++ } ++ None => break, ++ }; + +- for event in event_queue.map(|e| e.expect("ixgbed: failed to get next event")) { + match event.user_data { + Source::Irq => { + let mut irq = [0; 8]; +- irq_file.read(&mut irq).unwrap(); ++ if let Err(err) = irq_file.read(&mut irq) { ++ log::error!("ixgbed: failed to read IRQ: {err}"); ++ continue; ++ } + if scheme.adapter().irq() { +- irq_file.write(&mut irq).unwrap(); +- +- scheme.tick().unwrap(); ++ if let Err(err) = irq_file.write(&mut irq) { ++ log::error!("ixgbed: failed to write IRQ: {err}"); ++ continue; ++ } ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("ixgbed: failed to handle IRQ: {err}"); ++ } + } + } + Source::Scheme => { +- scheme.tick().unwrap(); ++ if let Err(err) = scheme.tick() { ++ log::error!("ixgbed: failed to handle scheme op: {err}"); ++ } + } + } + } +- unreachable!() ++ ++ process::exit(0); + } +diff --git a/drivers/net/rtl8139d/src/main.rs b/drivers/net/rtl8139d/src/main.rs +index d470e814..64335a23 100644 +--- a/drivers/net/rtl8139d/src/main.rs ++++ b/drivers/net/rtl8139d/src/main.rs +@@ -1,5 +1,6 @@ + use std::io::{Read, Write}; + use std::os::unix::io::AsRawFd; ++use std::process; + + use driver_network::NetworkScheme; + use event::{user_data, EventQueue}; +@@ -32,7 +33,8 @@ fn map_bar(pcid_handle: &mut PciFunctionHandle) -> *mut u8 { + other => log::warn!("BAR {} is {:?} instead of memory BAR", barnum, other), + } + } +- panic!("rtl8139d: failed to find BAR"); ++ log::error!("rtl8139d: failed to find BAR"); ++ process::exit(1); + } + + fn main() { +@@ -61,7 +63,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + let mut scheme = NetworkScheme::new( + move || unsafe { +- device::Rtl8139::new(bar as usize).expect("rtl8139d: failed to allocate device") ++ device::Rtl8139::new(bar as usize).unwrap_or_else(|err| { ++ log::error!("rtl8139d: failed to allocate device: {err}"); ++ process::exit(1); ++ }) + }, + daemon, + format!("network.{name}"), +@@ -74,42 +79,76 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = EventQueue::::new().expect("rtl8139d: Could not create event queue."); ++ let mut event_queue = EventQueue::::new().unwrap_or_else(|err| { ++ log::error!("rtl8139d: failed to create event queue: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + irq_file.irq_handle().as_raw_fd() as usize, + Source::Irq, + event::EventFlags::READ, + ) +- .unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("rtl8139d: failed to subscribe to IRQ fd: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + scheme.event_handle().raw(), + Source::Scheme, + event::EventFlags::READ, + ) +- .unwrap(); +- +- libredox::call::setrens(0, 0).expect("rtl8139d: failed to enter null namespace"); +- +- scheme.tick().unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("rtl8139d: failed to subscribe to scheme fd: {err}"); ++ process::exit(1); ++ }); ++ ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("rtl8139d: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8139d: failed initial scheme tick: {err}"); ++ process::exit(1); ++ } + +- for event in event_queue.map(|e| e.expect("rtl8139d: failed to get next event")) { ++ loop { ++ let event = match event_queue.next() { ++ Some(Ok(event)) => event, ++ Some(Err(err)) => { ++ log::error!("rtl8139d: failed to get next event: {err}"); ++ continue; ++ } ++ None => break, ++ }; + match event.user_data { + Source::Irq => { + let mut irq = [0; 8]; +- irq_file.irq_handle().read(&mut irq).unwrap(); ++ if let Err(err) = irq_file.irq_handle().read(&mut irq) { ++ log::error!("rtl8139d: failed to read IRQ: {err}"); ++ continue; ++ } + //TODO: This may be causing spurious interrupts + if unsafe { scheme.adapter_mut().irq() } { +- irq_file.irq_handle().write(&mut irq).unwrap(); +- +- scheme.tick().unwrap(); ++ if let Err(err) = irq_file.irq_handle().write(&mut irq) { ++ log::error!("rtl8139d: failed to write IRQ: {err}"); ++ continue; ++ } ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8139d: failed to handle IRQ tick: {err}"); ++ } + } + } + Source::Scheme => { +- scheme.tick().unwrap(); ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8139d: failed to handle scheme op: {err}"); ++ } + } + } + } +- unreachable!() ++ ++ process::exit(0); + } +diff --git a/drivers/net/rtl8168d/src/main.rs b/drivers/net/rtl8168d/src/main.rs +index 1d9963a3..bd2fcb1a 100644 +--- a/drivers/net/rtl8168d/src/main.rs ++++ b/drivers/net/rtl8168d/src/main.rs +@@ -1,5 +1,6 @@ + use std::io::{Read, Write}; + use std::os::unix::io::AsRawFd; ++use std::process; + + use driver_network::NetworkScheme; + use event::{user_data, EventQueue}; +@@ -32,7 +33,8 @@ fn map_bar(pcid_handle: &mut PciFunctionHandle) -> *mut u8 { + other => log::warn!("BAR {} is {:?} instead of memory BAR", barnum, other), + } + } +- panic!("rtl8168d: failed to find BAR"); ++ log::error!("rtl8168d: failed to find BAR"); ++ process::exit(1); + } + + fn main() { +@@ -61,7 +63,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + let mut scheme = NetworkScheme::new( + move || unsafe { +- device::Rtl8168::new(bar as usize).expect("rtl8168d: failed to allocate device") ++ device::Rtl8168::new(bar as usize).unwrap_or_else(|err| { ++ log::error!("rtl8168d: failed to allocate device: {err}"); ++ process::exit(1); ++ }) + }, + daemon, + format!("network.{name}"), +@@ -74,42 +79,76 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = EventQueue::::new().expect("rtl8168d: Could not create event queue."); ++ let mut event_queue = EventQueue::::new().unwrap_or_else(|err| { ++ log::error!("rtl8168d: failed to create event queue: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + irq_file.irq_handle().as_raw_fd() as usize, + Source::Irq, + event::EventFlags::READ, + ) +- .unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("rtl8168d: failed to subscribe to IRQ fd: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe( + scheme.event_handle().raw(), + Source::Scheme, + event::EventFlags::READ, + ) +- .unwrap(); +- +- libredox::call::setrens(0, 0).expect("rtl8168d: failed to enter null namespace"); +- +- scheme.tick().unwrap(); ++ .unwrap_or_else(|err| { ++ log::error!("rtl8168d: failed to subscribe to scheme fd: {err}"); ++ process::exit(1); ++ }); ++ ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("rtl8168d: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8168d: failed initial scheme tick: {err}"); ++ process::exit(1); ++ } + +- for event in event_queue.map(|e| e.expect("rtl8168d: failed to get next event")) { ++ loop { ++ let event = match event_queue.next() { ++ Some(Ok(event)) => event, ++ Some(Err(err)) => { ++ log::error!("rtl8168d: failed to get next event: {err}"); ++ continue; ++ } ++ None => break, ++ }; + match event.user_data { + Source::Irq => { + let mut irq = [0; 8]; +- irq_file.irq_handle().read(&mut irq).unwrap(); ++ if let Err(err) = irq_file.irq_handle().read(&mut irq) { ++ log::error!("rtl8168d: failed to read IRQ: {err}"); ++ continue; ++ } + //TODO: This may be causing spurious interrupts + if unsafe { scheme.adapter_mut().irq() } { +- irq_file.irq_handle().write(&mut irq).unwrap(); +- +- scheme.tick().unwrap(); ++ if let Err(err) = irq_file.irq_handle().write(&mut irq) { ++ log::error!("rtl8168d: failed to write IRQ: {err}"); ++ continue; ++ } ++ ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8168d: failed to handle IRQ tick: {err}"); ++ } + } + } + Source::Scheme => { +- scheme.tick().unwrap(); ++ if let Err(err) = scheme.tick() { ++ log::error!("rtl8168d: failed to handle scheme op: {err}"); ++ } + } + } + } +- unreachable!() ++ ++ process::exit(0); + } +diff --git a/drivers/net/virtio-netd/src/main.rs b/drivers/net/virtio-netd/src/main.rs +index 17d168ef..adbd1086 100644 +--- a/drivers/net/virtio-netd/src/main.rs ++++ b/drivers/net/virtio-netd/src/main.rs +@@ -3,6 +3,7 @@ mod scheme; + use std::fs::File; + use std::io::{Read, Write}; + use std::mem; ++use std::process; + + use driver_network::NetworkScheme; + use pcid_interface::PciFunctionHandle; +@@ -31,8 +32,11 @@ fn main() { + } + + fn daemon_runner(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { +- deamon(daemon, pcid_handle).unwrap(); +- unreachable!(); ++ deamon(daemon, pcid_handle).unwrap_or_else(|err| { ++ log::error!("virtio-netd: daemon failed: {err}"); ++ process::exit(1); ++ }); ++ process::exit(0); + } + + fn deamon( +@@ -52,7 +56,10 @@ fn deamon( + // 0x1000 - virtio-net + let pci_config = pcid_handle.config(); + +- assert_eq!(pci_config.func.full_device_id.device_id, 0x1000); ++ if pci_config.func.full_device_id.device_id != 0x1000 { ++ log::error!("virtio-netd: unexpected device ID {:#06x}, expected 0x1000", pci_config.func.full_device_id.device_id); ++ process::exit(1); ++ } + log::info!("virtio-net: initiating startup sequence :^)"); + + let device = virtio_core::probe_device(&mut pcid_handle)?; +@@ -84,7 +91,8 @@ fn deamon( + device.transport.ack_driver_feature(VIRTIO_NET_F_MAC); + mac + } else { +- unimplemented!() ++ log::error!("virtio-netd: device does not support MAC feature"); ++ return Err("virtio-netd: VIRTIO_NET_F_MAC not supported".into()); + }; + + device.transport.finalize_features(); +@@ -126,12 +134,23 @@ fn deamon( + data: 0, + })?; + +- libredox::call::setrens(0, 0).expect("virtio-netd: failed to enter null namespace"); ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("virtio-netd: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); + +- scheme.tick()?; ++ if let Err(err) = scheme.tick() { ++ log::error!("virtio-netd: failed initial scheme tick: {err}"); ++ process::exit(1); ++ } + + loop { +- event_queue.read(&mut [0; mem::size_of::()])?; // Wait for event +- scheme.tick()?; ++ if let Err(err) = event_queue.read(&mut [0; mem::size_of::()]) { ++ log::error!("virtio-netd: failed to read event: {err}"); ++ continue; ++ } ++ if let Err(err) = scheme.tick() { ++ log::error!("virtio-netd: failed to handle scheme event: {err}"); ++ } + } diff --git a/local/patches/base/P2-pcid-driver-interface.patch b/local/patches/base/P2-pcid-driver-interface.patch new file mode 100644 index 00000000..3d935598 --- /dev/null +++ b/local/patches/base/P2-pcid-driver-interface.patch @@ -0,0 +1,1463 @@ +# P2-pcid-driver-interface.patch +# Extract pcid driver interface hardening: vendor capability try_parse, BAR +# try_port/try_mem, MSI/MSI-X error types, IRQ helper try_ variants, scheme +# config endpoint, UnrecognizedRequest error variant, send/recv try_ variants. +# +# Files: drivers/pcid/src/driver_handler.rs, drivers/pcid/src/driver_interface/bar.rs, +# drivers/pcid/src/driver_interface/cap.rs, drivers/pcid/src/driver_interface/config.rs, +# drivers/pcid/src/driver_interface/irq_helpers.rs, drivers/pcid/src/driver_interface/mod.rs, +# drivers/pcid/src/driver_interface/msi.rs, drivers/pcid/src/scheme.rs + +diff --git a/drivers/pcid/src/driver_handler.rs b/drivers/pcid/src/driver_handler.rs +index f70a7f6d..64701f6c 100644 +--- a/drivers/pcid/src/driver_handler.rs ++++ b/drivers/pcid/src/driver_handler.rs +@@ -48,8 +48,18 @@ impl<'a> DriverHandler<'a> { + self.capabilities + .iter() + .filter_map(|capability| match capability { +- PciCapability::Vendor(addr) => unsafe { +- Some(VendorSpecificCapability::parse(*addr, self.pcie)) ++ PciCapability::Vendor(addr) => match unsafe { ++ VendorSpecificCapability::try_parse(*addr, self.pcie) ++ } { ++ Ok(capability) => Some(capability), ++ Err(err) => { ++ log::warn!( ++ "pcid: skipping malformed vendor capability at {:#x}: {}", ++ addr.offset, ++ err ++ ); ++ None ++ } + }, + _ => None, + }) +@@ -230,10 +240,14 @@ impl<'a> DriverHandler<'a> { + } + info.set_message_info( + message_addr, +- message_addr_and_data +- .data +- .try_into() +- .expect("pcid: MSI message data too big"), ++ match message_addr_and_data.data.try_into() { ++ Ok(d) => d, ++ Err(_) => { ++ return PcidClientResponse::Error( ++ PcidServerResponseError::InvalidBitPattern, ++ ) ++ } ++ }, + self.pcie, + ); + } +@@ -266,7 +280,7 @@ impl<'a> DriverHandler<'a> { + ); + } + } +- _ => unreachable!(), ++ _ => PcidClientResponse::Error(PcidServerResponseError::UnrecognizedRequest), + }, + PcidClientRequest::ReadConfig(offset) => { + let value = unsafe { self.pcie.read(self.func.addr, offset) }; +@@ -278,7 +292,7 @@ impl<'a> DriverHandler<'a> { + } + return PcidClientResponse::WriteConfig; + } +- _ => unreachable!(), ++ _ => PcidClientResponse::Error(PcidServerResponseError::UnrecognizedRequest), + } + } + } +diff --git a/drivers/pcid/src/driver_interface/bar.rs b/drivers/pcid/src/driver_interface/bar.rs +index b2c1d35b..3a83bb4d 100644 +--- a/drivers/pcid/src/driver_interface/bar.rs ++++ b/drivers/pcid/src/driver_interface/bar.rs +@@ -1,7 +1,38 @@ + use std::convert::TryInto; ++use std::fmt; ++use std::process; + + use serde::{Deserialize, Serialize}; + ++#[derive(Clone, Copy, Debug, Eq, PartialEq)] ++pub enum PciBarError { ++ Missing, ++ ExpectedPortFoundMemory, ++ ExpectedMemoryFoundPort, ++ AddressTooLarge, ++ SizeTooLarge, ++} ++ ++impl fmt::Display for PciBarError { ++ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ++ match self { ++ PciBarError::Missing => write!(f, "expected BAR to exist"), ++ PciBarError::ExpectedPortFoundMemory => { ++ write!(f, "expected port BAR, found memory BAR") ++ } ++ PciBarError::ExpectedMemoryFoundPort => { ++ write!(f, "expected memory BAR, found port BAR") ++ } ++ PciBarError::AddressTooLarge => { ++ write!(f, "conversion from 64-bit BAR address to usize failed") ++ } ++ PciBarError::SizeTooLarge => { ++ write!(f, "conversion from 64-bit BAR size to usize failed") ++ } ++ } ++ } ++} ++ + // This type is used instead of [pci_types::Bar] in the driver interface as the + // latter can't be serialized and is missing the convenience functions of [PciBar]. + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +@@ -30,26 +61,88 @@ impl PciBar { + } + + pub fn expect_port(&self) -> u16 { ++ match self.try_port() { ++ Ok(port) => port, ++ Err(err) => { ++ log::error!("{err}"); ++ process::exit(1); ++ } ++ } ++ } ++ ++ pub fn try_port(&self) -> Result { + match *self { +- PciBar::Port(port) => port, ++ PciBar::Port(port) => Ok(port), + PciBar::Memory32 { .. } | PciBar::Memory64 { .. } => { +- panic!("expected port BAR, found memory BAR"); ++ Err(PciBarError::ExpectedPortFoundMemory) + } +- PciBar::None => panic!("expected BAR to exist"), ++ PciBar::None => Err(PciBarError::Missing), + } + } + + pub fn expect_mem(&self) -> (usize, usize) { ++ match self.try_mem() { ++ Ok(result) => result, ++ Err(err) => { ++ log::error!("{err}"); ++ process::exit(1); ++ } ++ } ++ } ++ ++ pub fn try_mem(&self) -> Result<(usize, usize), PciBarError> { + match *self { +- PciBar::Memory32 { addr, size } => (addr as usize, size as usize), +- PciBar::Memory64 { addr, size } => ( +- addr.try_into() +- .expect("conversion from 64bit BAR to usize failed"), +- size.try_into() +- .expect("conversion from 64bit BAR size to usize failed"), +- ), +- PciBar::Port(_) => panic!("expected memory BAR, found port BAR"), +- PciBar::None => panic!("expected BAR to exist"), ++ PciBar::Memory32 { addr, size } => Ok((addr as usize, size as usize)), ++ PciBar::Memory64 { addr, size } => Ok(( ++ addr.try_into().map_err(|_| PciBarError::AddressTooLarge)?, ++ size.try_into().map_err(|_| PciBarError::SizeTooLarge)?, ++ )), ++ PciBar::Port(_) => Err(PciBarError::ExpectedMemoryFoundPort), ++ PciBar::None => Err(PciBarError::Missing), + } + } + } ++ ++#[cfg(test)] ++mod tests { ++ use super::{PciBar, PciBarError}; ++ ++ #[test] ++ fn try_port_accepts_port_bar() { ++ assert_eq!(PciBar::Port(0x1234).try_port(), Ok(0x1234)); ++ } ++ ++ #[test] ++ fn try_port_rejects_non_port_bars() { ++ assert_eq!( ++ PciBar::Memory32 { ++ addr: 0x1000, ++ size: 0x100, ++ } ++ .try_port(), ++ Err(PciBarError::ExpectedPortFoundMemory) ++ ); ++ assert_eq!(PciBar::None.try_port(), Err(PciBarError::Missing)); ++ } ++ ++ #[test] ++ fn try_mem_accepts_memory_bars() { ++ assert_eq!( ++ PciBar::Memory32 { ++ addr: 0x1000, ++ size: 0x200, ++ } ++ .try_mem(), ++ Ok((0x1000, 0x200)) ++ ); ++ } ++ ++ #[test] ++ fn try_mem_rejects_non_memory_bars() { ++ assert_eq!( ++ PciBar::Port(0x1234).try_mem(), ++ Err(PciBarError::ExpectedMemoryFoundPort) ++ ); ++ assert_eq!(PciBar::None.try_mem(), Err(PciBarError::Missing)); ++ } ++} +diff --git a/drivers/pcid/src/driver_interface/cap.rs b/drivers/pcid/src/driver_interface/cap.rs +index 19521608..17c26c0c 100644 +--- a/drivers/pcid/src/driver_interface/cap.rs ++++ b/drivers/pcid/src/driver_interface/cap.rs +@@ -1,14 +1,44 @@ + use pci_types::capability::PciCapabilityAddress; + use pci_types::ConfigRegionAccess; + use serde::{Deserialize, Serialize}; ++use std::fmt; ++use std::process; + + #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] + pub struct VendorSpecificCapability { + pub data: Vec, + } + ++#[derive(Clone, Copy, Debug, Eq, PartialEq)] ++pub enum VendorSpecificCapabilityError { ++ InvalidLength(u16), ++} ++ ++impl fmt::Display for VendorSpecificCapabilityError { ++ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ++ match self { ++ VendorSpecificCapabilityError::InvalidLength(length) => { ++ write!(f, "invalid vendor capability length: {length}") ++ } ++ } ++ } ++} ++ + impl VendorSpecificCapability { + pub unsafe fn parse(addr: PciCapabilityAddress, access: &dyn ConfigRegionAccess) -> Self { ++ match Self::try_parse(addr, access) { ++ Ok(cap) => cap, ++ Err(err) => { ++ log::error!("{err}"); ++ process::exit(1); ++ } ++ } ++ } ++ ++ pub unsafe fn try_parse( ++ addr: PciCapabilityAddress, ++ access: &dyn ConfigRegionAccess, ++ ) -> Result { + let dword = access.read(addr.address, addr.offset); + let length = ((dword >> 16) & 0xFF) as u16; + // let next = (dword >> 8) & 0xFF; +@@ -17,11 +47,9 @@ impl VendorSpecificCapability { + // addr.offset + // ); + let data = if length > 0 { +- assert!( +- length > 3 && length % 4 == 0, +- "invalid range length: {}", +- length +- ); ++ if !(length > 3 && length % 4 == 0) { ++ return Err(VendorSpecificCapabilityError::InvalidLength(length)); ++ } + let mut raw_data = { + (addr.offset..addr.offset + length) + .step_by(4) +@@ -33,6 +61,75 @@ impl VendorSpecificCapability { + log::warn!("Vendor specific capability is invalid"); + Vec::new() + }; +- VendorSpecificCapability { data } ++ Ok(VendorSpecificCapability { data }) ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use super::{VendorSpecificCapability, VendorSpecificCapabilityError}; ++ use pci_types::capability::PciCapabilityAddress; ++ use pci_types::{ConfigRegionAccess, PciAddress}; ++ use std::collections::BTreeMap; ++ use std::sync::Mutex; ++ ++ #[derive(Default)] ++ struct MockConfigRegionAccess { ++ values: Mutex>, ++ } ++ ++ impl MockConfigRegionAccess { ++ fn with_read(address: PciAddress, offset: u16, value: u32) -> Self { ++ let mut map = BTreeMap::new(); ++ map.insert((address, offset), value); ++ Self { ++ values: Mutex::new(map), ++ } ++ } ++ } ++ ++ impl ConfigRegionAccess for MockConfigRegionAccess { ++ unsafe fn read(&self, address: PciAddress, offset: u16) -> u32 { ++ self.values ++ .lock() ++ .expect("mock config lock poisoned") ++ .get(&(address, offset)) ++ .copied() ++ .unwrap_or_default() ++ } ++ ++ unsafe fn write(&self, _address: PciAddress, _offset: u16, _value: u32) {} ++ } ++ ++ #[test] ++ fn try_parse_accepts_valid_vendor_capability() { ++ let address = PciAddress::new(0, 0, 1, 0); ++ let capability = PciCapabilityAddress { ++ address, ++ offset: 0x40, ++ }; ++ let access = MockConfigRegionAccess::with_read(address, 0x40, 0x0010_0000); ++ ++ let capability = unsafe { VendorSpecificCapability::try_parse(capability, &access) }; ++ assert_eq!( ++ capability ++ .expect("valid vendor capability should parse") ++ .data ++ .len(), ++ 13 ++ ); ++ } ++ ++ #[test] ++ fn try_parse_rejects_invalid_length() { ++ let address = PciAddress::new(0, 0, 1, 0); ++ let capability = PciCapabilityAddress { ++ address, ++ offset: 0x40, ++ }; ++ let access = MockConfigRegionAccess::with_read(address, 0x40, 0x0005_0000); ++ ++ let error = unsafe { VendorSpecificCapability::try_parse(capability, &access) }.unwrap_err(); ++ assert_eq!(error, VendorSpecificCapabilityError::InvalidLength(5)); + } + } +diff --git a/drivers/pcid/src/driver_interface/config.rs b/drivers/pcid/src/driver_interface/config.rs +index e148b26c..041f0ced 100644 +--- a/drivers/pcid/src/driver_interface/config.rs ++++ b/drivers/pcid/src/driver_interface/config.rs +@@ -47,7 +47,13 @@ impl DriverConfig { + let mut device_found = false; + for (vendor, devices) in ids { + let vendor_without_prefix = vendor.trim_start_matches("0x"); +- let vendor = i64::from_str_radix(vendor_without_prefix, 16).unwrap() as u16; ++ let Ok(vendor_val) = i64::from_str_radix(vendor_without_prefix, 16) else { ++ log::warn!( ++ "invalid hex vendor ID '{vendor_without_prefix}' in driver config, skipping" ++ ); ++ continue; ++ }; ++ let vendor = vendor_val as u16; + + if vendor != id.vendor_id { + continue; +diff --git a/drivers/pcid/src/driver_interface/irq_helpers.rs b/drivers/pcid/src/driver_interface/irq_helpers.rs +index 28ca077a..bff35650 100644 +--- a/drivers/pcid/src/driver_interface/irq_helpers.rs ++++ b/drivers/pcid/src/driver_interface/irq_helpers.rs +@@ -7,6 +7,7 @@ use std::convert::TryFrom; + use std::fs::{self, File}; + use std::io::{self, prelude::*}; + use std::num::NonZeroU8; ++use std::process; + + use crate::driver_interface::msi::{MsiAddrAndData, MsixTableEntry}; + +@@ -24,11 +25,13 @@ pub fn read_bsp_apic_id() -> io::Result { + buffer[0], buffer[1], buffer[2], buffer[3], + ])) + } else { +- panic!( +- "`/scheme/irq` scheme responded with {} bytes, expected {}", +- bytes_read, +- std::mem::size_of::() +- ); ++ return Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!( ++ "`/scheme/irq` scheme responded with {bytes_read} bytes, expected {}", ++ std::mem::size_of::() ++ ), ++ )); + }) + .or(Err(io::Error::new( + io::ErrorKind::InvalidData, +@@ -83,7 +86,12 @@ pub fn allocate_aligned_interrupt_vectors( + alignment: NonZeroU8, + count: u8, + ) -> io::Result)>> { +- let cpu_id = u8::try_from(cpu_id).expect("usize cpu ids not implemented yet"); ++ let cpu_id = u8::try_from(cpu_id).map_err(|_| { ++ io::Error::new( ++ io::ErrorKind::InvalidInput, ++ format!("CPU id {cpu_id} too large for u8 (usize cpu ids not supported)"), ++ ) ++ })?; + if count == 0 { + return Ok(None); + } +@@ -163,7 +171,7 @@ pub fn allocate_aligned_interrupt_vectors( + /// Allocate at most `count` interrupt vectors, which can start at any offset. Unless MSI is used + /// and an entire aligned range of vectors is needed, this function should be used. + pub fn allocate_interrupt_vectors(cpu_id: usize, count: u8) -> io::Result)>> { +- allocate_aligned_interrupt_vectors(cpu_id, NonZeroU8::new(1).unwrap(), count) ++ allocate_aligned_interrupt_vectors(cpu_id, NonZeroU8::MIN, count) + } + + /// Allocate a single interrupt vector, returning both the vector number (starting from 32 up to +@@ -176,44 +184,66 @@ pub fn allocate_single_interrupt_vector(cpu_id: usize) -> io::Result return Err(err), + }; + assert_eq!(files.len(), 1); +- Ok(Some((base, files.pop().unwrap()))) ++ let handle = files.pop().ok_or_else(|| { ++ io::Error::new( ++ io::ErrorKind::Other, ++ "allocate_interrupt_vectors returned empty file list despite count=1", ++ ) ++ })?; ++ Ok(Some((base, handle))) + } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +-pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndData, File) { ++pub fn try_allocate_single_interrupt_vector_for_msi( ++ cpu_id: usize, ++) -> io::Result<(MsiAddrAndData, File)> { + use crate::driver_interface::msi::x86 as x86_msix; + +- // FIXME for cpu_id >255 we need to use the IOMMU to use IRQ remapping +- let lapic_id = u8::try_from(cpu_id).expect("CPU id couldn't fit inside u8"); ++ let lapic_id = u8::try_from(cpu_id).map_err(|_| { ++ io::Error::new( ++ io::ErrorKind::InvalidInput, ++ format!("CPU id {cpu_id} could not fit inside u8"), ++ ) ++ })?; + let rh = false; + let dm = false; + let addr = x86_msix::message_address(lapic_id, rh, dm); + +- let (vector, interrupt_handle) = allocate_single_interrupt_vector(cpu_id) +- .expect("failed to allocate interrupt vector") +- .expect("no interrupt vectors left"); ++ let (vector, interrupt_handle) = allocate_single_interrupt_vector(cpu_id)? ++ .ok_or_else(|| io::Error::other("no interrupt vectors left"))?; + let msg_data = x86_msix::message_data_edge_triggered(x86_msix::DeliveryMode::Fixed, vector); + +- ( ++ Ok(( + MsiAddrAndData { + addr, + data: msg_data, + }, + interrupt_handle, +- ) ++ )) + } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +-pub fn allocate_first_msi_interrupt_on_bsp( ++pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndData, File) { ++ match try_allocate_single_interrupt_vector_for_msi(cpu_id) { ++ Ok(result) => result, ++ Err(err) => { ++ log::error!("failed to allocate MSI interrupt vector: {err}"); ++ process::exit(1); ++ } ++ } ++} ++ ++#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] ++pub fn try_allocate_first_msi_interrupt_on_bsp( + pcid_handle: &mut crate::driver_interface::PciFunctionHandle, +-) -> File { ++) -> Result { + use crate::driver_interface::{MsiSetFeatureInfo, PciFeature, SetFeatureInfo}; + +- // TODO: Allow allocation of up to 32 vectors. +- +- let destination_id = read_bsp_apic_id().expect("failed to read BSP apic id"); +- let (msg_addr_and_data, interrupt_handle) = +- allocate_single_interrupt_vector_for_msi(destination_id); ++ let destination_id = read_bsp_apic_id().map_err(InterruptVectorError::ApicId)?; ++ let (msg_addr_and_data, interrupt_handle) = try_allocate_single_interrupt_vector_for_msi( ++ destination_id, ++ ) ++ .map_err(InterruptVectorError::Allocate)?; + + let set_feature_info = MsiSetFeatureInfo { + multi_message_enable: Some(0), +@@ -222,10 +252,25 @@ pub fn allocate_first_msi_interrupt_on_bsp( + }; + pcid_handle.set_feature_info(SetFeatureInfo::Msi(set_feature_info)); + +- pcid_handle.enable_feature(PciFeature::Msi); ++ pcid_handle ++ .try_enable_feature(PciFeature::Msi) ++ .map_err(InterruptVectorError::IrqHandle)?; + log::debug!("Enabled MSI"); + +- interrupt_handle ++ Ok(interrupt_handle) ++} ++ ++#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] ++pub fn allocate_first_msi_interrupt_on_bsp( ++ pcid_handle: &mut crate::driver_interface::PciFunctionHandle, ++) -> File { ++ match try_allocate_first_msi_interrupt_on_bsp(pcid_handle) { ++ Ok(handle) => handle, ++ Err(err) => { ++ log::error!("failed to allocate first MSI interrupt on BSP: {err}"); ++ process::exit(1); ++ } ++ } + } + + pub struct InterruptVector { +@@ -234,6 +279,39 @@ pub struct InterruptVector { + kind: InterruptVectorKind, + } + ++#[derive(Debug)] ++pub enum InterruptVectorError { ++ MissingMsixFeature, ++ MissingLegacyInterrupt, ++ ApicId(io::Error), ++ Allocate(io::Error), ++ IrqHandle(io::Error), ++ MsixMap(super::msi::MsixMapError), ++} ++ ++impl std::fmt::Display for InterruptVectorError { ++ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ++ match self { ++ InterruptVectorError::MissingMsixFeature => { ++ write!(f, "missing MSI-X feature information") ++ } ++ InterruptVectorError::MissingLegacyInterrupt => { ++ write!(f, "no interrupts supported at all") ++ } ++ InterruptVectorError::ApicId(err) => write!(f, "failed to read BSP APIC ID: {err}"), ++ InterruptVectorError::Allocate(err) => { ++ write!(f, "failed to allocate interrupt vector: {err}") ++ } ++ InterruptVectorError::IrqHandle(err) => { ++ write!(f, "failed to open IRQ handle: {err}") ++ } ++ InterruptVectorError::MsixMap(err) => { ++ write!(f, "failed to map MSI-X registers: {err}") ++ } ++ } ++ } ++} ++ + enum InterruptVectorKind { + Legacy, + Msi, +@@ -266,10 +344,10 @@ impl InterruptVector { + // FIXME allow allocating multiple interrupt vectors + // FIXME move MSI-X IRQ allocation to pcid + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +-pub fn pci_allocate_interrupt_vector( ++pub fn try_pci_allocate_interrupt_vector( + pcid_handle: &mut crate::driver_interface::PciFunctionHandle, + driver: &str, +-) -> InterruptVector { ++) -> Result { + let features = pcid_handle.fetch_all_features(); + + let has_msi = features.iter().any(|feature| feature.is_msi()); +@@ -278,57 +356,89 @@ pub fn pci_allocate_interrupt_vector( + if has_msix { + let msix_info = match pcid_handle.feature_info(super::PciFeature::MsiX) { + super::PciFeatureInfo::MsiX(msix) => msix, +- _ => unreachable!(), ++ _ => return Err(InterruptVectorError::MissingMsixFeature), + }; +- let mut info = unsafe { msix_info.map_and_mask_all(pcid_handle) }; ++ let mut info = unsafe { msix_info.try_map_and_mask_all(pcid_handle) } ++ .map_err(InterruptVectorError::MsixMap)?; + + pcid_handle.enable_feature(crate::driver_interface::PciFeature::MsiX); + + let entry = info.table_entry_pointer(0); + +- let bsp_cpu_id = read_bsp_apic_id() +- .unwrap_or_else(|err| panic!("{driver}: failed to read BSP APIC ID: {err}")); +- let (msg_addr_and_data, irq_handle) = allocate_single_interrupt_vector_for_msi(bsp_cpu_id); ++ let bsp_cpu_id = read_bsp_apic_id().map_err(InterruptVectorError::ApicId)?; ++ let (msg_addr_and_data, irq_handle) = ++ try_allocate_single_interrupt_vector_for_msi(bsp_cpu_id) ++ .map_err(InterruptVectorError::Allocate)?; + entry.write_addr_and_data(msg_addr_and_data); + entry.unmask(); + +- InterruptVector { ++ Ok(InterruptVector { + irq_handle, + vector: 0, + kind: InterruptVectorKind::MsiX { table_entry: entry }, +- } ++ }) + } else if has_msi { +- InterruptVector { ++ Ok(InterruptVector { + irq_handle: allocate_first_msi_interrupt_on_bsp(pcid_handle), + vector: 0, + kind: InterruptVectorKind::Msi, +- } ++ }) + } else if let Some(irq) = pcid_handle.config().func.legacy_interrupt_line { +- // INTx# pin based interrupts. +- InterruptVector { +- irq_handle: irq.irq_handle(driver), ++ Ok(InterruptVector { ++ irq_handle: irq ++ .try_irq_handle(driver) ++ .map_err(InterruptVectorError::IrqHandle)?, + vector: 0, + kind: InterruptVectorKind::Legacy, +- } ++ }) + } else { +- panic!("{driver}: no interrupts supported at all") ++ Err(InterruptVectorError::MissingLegacyInterrupt) + } + } + +-// FIXME support MSI on non-x86 systems +-#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] ++#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + pub fn pci_allocate_interrupt_vector( + pcid_handle: &mut crate::driver_interface::PciFunctionHandle, + driver: &str, + ) -> InterruptVector { ++ match try_pci_allocate_interrupt_vector(pcid_handle, driver) { ++ Ok(vec) => vec, ++ Err(err) => { ++ log::error!("{driver}: {err}"); ++ process::exit(1); ++ } ++ } ++} ++ ++// FIXME support MSI on non-x86 systems ++#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] ++pub fn try_pci_allocate_interrupt_vector( ++ pcid_handle: &mut crate::driver_interface::PciFunctionHandle, ++ driver: &str, ++) -> Result { + if let Some(irq) = pcid_handle.config().func.legacy_interrupt_line { +- // INTx# pin based interrupts. +- InterruptVector { +- irq_handle: irq.irq_handle(driver), ++ Ok(InterruptVector { ++ irq_handle: irq ++ .try_irq_handle(driver) ++ .map_err(InterruptVectorError::IrqHandle)?, + vector: 0, + kind: InterruptVectorKind::Legacy, +- } ++ }) + } else { +- panic!("{driver}: no interrupts supported at all") ++ Err(InterruptVectorError::MissingLegacyInterrupt) ++ } ++} ++ ++#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] ++pub fn pci_allocate_interrupt_vector( ++ pcid_handle: &mut crate::driver_interface::PciFunctionHandle, ++ driver: &str, ++) -> InterruptVector { ++ match try_pci_allocate_interrupt_vector(pcid_handle, driver) { ++ Ok(vec) => vec, ++ Err(err) => { ++ log::error!("{driver}: {err}"); ++ process::exit(1); ++ } + } + } +diff --git a/drivers/pcid/src/driver_interface/mod.rs b/drivers/pcid/src/driver_interface/mod.rs +index bbc7304e..9d7172b9 100644 +--- a/drivers/pcid/src/driver_interface/mod.rs ++++ b/drivers/pcid/src/driver_interface/mod.rs +@@ -30,7 +30,7 @@ pub struct LegacyInterruptLine { + + impl LegacyInterruptLine { + /// Get an IRQ handle for this interrupt line. +- pub fn irq_handle(self, driver: &str) -> File { ++ pub fn try_irq_handle(self, _driver: &str) -> io::Result { + if let Some((phandle, addr, cells)) = self.phandled { + let path = match cells { + 1 => format!("/scheme/irq/phandle-{}/{}", phandle, addr[0]), +@@ -39,15 +39,28 @@ impl LegacyInterruptLine { + "/scheme/irq/phandle-{}/{},{},{}", + phandle, addr[0], addr[1], addr[2] + ), +- _ => panic!( +- "unexpected number of IRQ description cells for phandle {phandle}: {cells}" +- ), ++ _ => { ++ return Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!( ++ "unexpected number of IRQ description cells for phandle {phandle}: {cells}" ++ ), ++ )) ++ } + }; + File::create(path) +- .unwrap_or_else(|err| panic!("{driver}: failed to open IRQ file: {err}")) + } else { + File::open(format!("/scheme/irq/{}", self.irq)) +- .unwrap_or_else(|err| panic!("{driver}: failed to open IRQ file: {err}")) ++ } ++ } ++ ++ pub fn irq_handle(self, driver: &str) -> File { ++ match self.try_irq_handle(driver) { ++ Ok(handle) => handle, ++ Err(err) => { ++ log::error!("{driver}: failed to open IRQ file: {err}"); ++ process::exit(1); ++ } + } + } + } +@@ -59,8 +72,10 @@ impl fmt::Display for LegacyInterruptLine { + 1 => write!(f, "(phandle {}, {:?})", phandle, addr[0]), + 2 => write!(f, "(phandle {}, {:?},{:?})", phandle, addr[0], addr[1]), + 3 => write!(f, "(phandle {}, {:?})", phandle, addr), +- _ => panic!( +- "unexpected number of IRQ description cells for phandle {phandle}: {cells}" ++ _ => write!( ++ f, ++ "(phandle {}, invalid IRQ description cells: {cells})", ++ phandle, + ), + } + } else { +@@ -247,6 +262,7 @@ pub enum PcidClientRequest { + pub enum PcidServerResponseError { + NonexistentFeature(PciFeature), + InvalidBitPattern, ++ UnrecognizedRequest, + } + + #[derive(Debug, Serialize, Deserialize)] +@@ -278,33 +294,51 @@ pub struct PciFunctionHandle { + } + + fn send(w: &mut File, message: &T) { +- let mut data = Vec::new(); +- bincode::serialize_into(&mut data, message).expect("couldn't serialize pcid message"); +- match w.write(&data) { +- Ok(len) => assert_eq!(len, data.len()), ++ if let Err(err) = send_result(w, message) { ++ log::error!("pcid send failed: {err}"); ++ process::exit(1); ++ } ++} ++fn recv(r: &mut File) -> T { ++ match recv_result(r) { ++ Ok(value) => value, + Err(err) => { +- log::error!("writing pcid request failed: {err}"); ++ log::error!("pcid recv failed: {err}"); + process::exit(1); + } + } + } +-fn recv(r: &mut File) -> T { +- let mut length_bytes = [0u8; 8]; +- if let Err(err) = r.read_exact(&mut length_bytes) { +- log::error!("reading pcid response length failed: {err}"); +- process::exit(1); ++ ++fn send_result(w: &mut File, message: &T) -> io::Result<()> { ++ let mut data = Vec::new(); ++ bincode::serialize_into(&mut data, message) ++ .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; ++ ++ let len = w.write(&data)?; ++ if len == data.len() { ++ Ok(()) ++ } else { ++ Err(io::Error::new( ++ io::ErrorKind::WriteZero, ++ format!("short pcid request write: wrote {len} of {} bytes", data.len()), ++ )) + } ++} ++ ++fn recv_result(r: &mut File) -> io::Result { ++ let mut length_bytes = [0u8; 8]; ++ r.read_exact(&mut length_bytes)?; + let length = u64::from_le_bytes(length_bytes); + if length > 0x100_000 { +- panic!("pcid_interface: buffer too large"); ++ return Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("pcid_interface: buffer too large ({length} bytes)"), ++ )); + } + let mut data = vec![0u8; length as usize]; +- if let Err(err) = r.read_exact(&mut data) { +- log::error!("reading pcid response failed: {err}"); +- process::exit(1); +- } +- +- bincode::deserialize_from(&data[..]).expect("couldn't deserialize pcid message") ++ r.read_exact(&mut data)?; ++ bincode::deserialize_from(&data[..]) ++ .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + } + + impl PciFunctionHandle { +@@ -327,11 +361,14 @@ impl PciFunctionHandle { + } + + pub fn connect_by_path(device_path: &Path) -> io::Result { +- let channel_fd = libredox::call::open( +- device_path.join("channel").to_str().unwrap(), +- libredox::flag::O_RDWR, +- 0, +- )?; ++ let channel_path = device_path.join("channel"); ++ let channel_str = channel_path.to_str().ok_or_else(|| { ++ io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("device path contains invalid UTF-8: {}", device_path.display()), ++ ) ++ })?; ++ let channel_fd = libredox::call::open(channel_str, libredox::flag::O_RDWR, 0)?; + Ok(Self::connect_common(channel_fd as RawFd)) + } + +@@ -369,55 +406,99 @@ impl PciFunctionHandle { + self.config.clone() + } + ++ pub fn try_enable_device(&mut self) -> io::Result<()> { ++ send_result(&mut self.channel, &PcidClientRequest::EnableDevice)?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::EnabledDevice => Ok(()), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while enabling device: {other:?}"), ++ )), ++ } ++ } ++ + pub fn enable_device(&mut self) { +- self.send(&PcidClientRequest::EnableDevice); +- match self.recv() { +- PcidClientResponse::EnabledDevice => {} +- other => { +- log::error!("received wrong pcid response: {other:?}"); +- process::exit(1); +- } ++ if let Err(err) = self.try_enable_device() { ++ log::error!("failed to enable PCI device: {err}"); ++ process::exit(1); + } + } + + pub fn get_vendor_capabilities(&mut self) -> Vec { +- self.send(&PcidClientRequest::RequestVendorCapabilities); +- match self.recv() { +- PcidClientResponse::VendorCapabilities(a) => a, +- other => { +- log::error!("received wrong pcid response: {other:?}"); ++ match self.try_get_vendor_capabilities() { ++ Ok(capabilities) => capabilities, ++ Err(err) => { ++ log::error!("failed to fetch vendor capabilities: {err}"); + process::exit(1); + } + } + } + ++ pub fn try_get_vendor_capabilities(&mut self) -> io::Result> { ++ send_result(&mut self.channel, &PcidClientRequest::RequestVendorCapabilities)?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::VendorCapabilities(capabilities) => Ok(capabilities), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!( ++ "received wrong pcid response while requesting vendor capabilities: {other:?}" ++ ), ++ )), ++ } ++ } ++ + // FIXME turn into struct with bool fields ++ pub fn try_fetch_all_features(&mut self) -> io::Result> { ++ send_result(&mut self.channel, &PcidClientRequest::RequestFeatures)?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::AllFeatures(features) => Ok(features), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while fetching features: {other:?}"), ++ )), ++ } ++ } ++ + pub fn fetch_all_features(&mut self) -> Vec { +- self.send(&PcidClientRequest::RequestFeatures); +- match self.recv() { +- PcidClientResponse::AllFeatures(a) => a, +- other => { +- log::error!("received wrong pcid response: {other:?}"); ++ match self.try_fetch_all_features() { ++ Ok(features) => features, ++ Err(err) => { ++ log::error!("failed to fetch PCI features: {err}"); + process::exit(1); + } + } + } ++ pub fn try_enable_feature(&mut self, feature: PciFeature) -> io::Result<()> { ++ send_result(&mut self.channel, &PcidClientRequest::EnableFeature(feature))?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::FeatureEnabled(feat) if feat == feature => Ok(()), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while enabling feature: {other:?}"), ++ )), ++ } ++ } + pub fn enable_feature(&mut self, feature: PciFeature) { +- self.send(&PcidClientRequest::EnableFeature(feature)); +- match self.recv() { +- PcidClientResponse::FeatureEnabled(feat) if feat == feature => {} +- other => { +- log::error!("received wrong pcid response: {other:?}"); +- process::exit(1); +- } ++ if let Err(err) = self.try_enable_feature(feature) { ++ log::error!("failed to enable PCI feature {feature:?}: {err}"); ++ process::exit(1); ++ } ++ } ++ pub fn try_feature_info(&mut self, feature: PciFeature) -> io::Result { ++ send_result(&mut self.channel, &PcidClientRequest::FeatureInfo(feature))?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::FeatureInfo(feat, info) if feat == feature => Ok(info), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while reading feature info: {other:?}"), ++ )), + } + } + pub fn feature_info(&mut self, feature: PciFeature) -> PciFeatureInfo { +- self.send(&PcidClientRequest::FeatureInfo(feature)); +- match self.recv() { +- PcidClientResponse::FeatureInfo(feat, info) if feat == feature => info, +- other => { +- log::error!("received wrong pcid response: {other:?}"); ++ match self.try_feature_info(feature) { ++ Ok(info) => info, ++ Err(err) => { ++ log::error!("failed to fetch PCI feature info for {feature:?}: {err}"); + process::exit(1); + } + } +@@ -433,33 +514,50 @@ impl PciFunctionHandle { + } + } + pub unsafe fn read_config(&mut self, offset: u16) -> u32 { +- self.send(&PcidClientRequest::ReadConfig(offset)); +- match self.recv() { +- PcidClientResponse::ReadConfig(value) => value, +- other => { +- log::error!("received wrong pcid response: {other:?}"); ++ match self.try_read_config(offset) { ++ Ok(value) => value, ++ Err(err) => { ++ log::error!("failed to read PCI config dword at {offset:#x}: {err}"); + process::exit(1); + } + } + } ++ pub unsafe fn try_read_config(&mut self, offset: u16) -> io::Result { ++ send_result(&mut self.channel, &PcidClientRequest::ReadConfig(offset))?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::ReadConfig(value) => Ok(value), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while reading config: {other:?}"), ++ )), ++ } ++ } + pub unsafe fn write_config(&mut self, offset: u16, value: u32) { +- self.send(&PcidClientRequest::WriteConfig(offset, value)); +- match self.recv() { +- PcidClientResponse::WriteConfig => {} +- other => { +- log::error!("received wrong pcid response: {other:?}"); +- process::exit(1); +- } ++ if let Err(err) = self.try_write_config(offset, value) { ++ log::error!("failed to write PCI config dword at {offset:#x}: {err}"); ++ process::exit(1); + } + } +- pub unsafe fn map_bar(&mut self, bir: u8) -> &MappedBar { ++ pub unsafe fn try_write_config(&mut self, offset: u16, value: u32) -> io::Result<()> { ++ send_result(&mut self.channel, &PcidClientRequest::WriteConfig(offset, value))?; ++ match recv_result(&mut self.channel)? { ++ PcidClientResponse::WriteConfig => Ok(()), ++ other => Err(io::Error::new( ++ io::ErrorKind::InvalidData, ++ format!("received wrong pcid response while writing config: {other:?}"), ++ )), ++ } ++ } ++ pub unsafe fn try_map_bar(&mut self, bir: u8) -> io::Result<&MappedBar> { + let mapped_bar = &mut self.mapped_bars[bir as usize]; + if let Some(mapped_bar) = mapped_bar { +- mapped_bar ++ Ok(mapped_bar) + } else { +- let (bar, bar_size) = self.config.func.bars[bir as usize].expect_mem(); ++ let (bar, bar_size) = self.config.func.bars[bir as usize] ++ .try_mem() ++ .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err.to_string()))?; + +- let ptr = match unsafe { ++ let ptr = unsafe { + common::physmap( + bar, + bar_size, +@@ -467,18 +565,25 @@ impl PciFunctionHandle { + // FIXME once the kernel supports this use write-through for prefetchable BAR + common::MemoryType::Uncacheable, + ) +- } { +- Ok(ptr) => ptr, +- Err(err) => { +- log::error!("failed to map BAR at {bar:016X}: {err}"); +- process::exit(1); +- } +- }; ++ } ++ .map_err(|err| io::Error::other(format!("failed to map BAR at {bar:016X}: {err}")))?; + +- mapped_bar.insert(MappedBar { +- ptr: NonNull::new(ptr.cast::()).expect("Mapping a BAR resulted in a nullptr"), ++ Ok(mapped_bar.insert(MappedBar { ++ ptr: NonNull::new(ptr.cast::()).ok_or_else(|| { ++ io::Error::new(io::ErrorKind::Other, "mapping a BAR resulted in a null pointer") ++ })?, + bar_size, +- }) ++ })) ++ } ++ } ++ ++ pub unsafe fn map_bar(&mut self, bir: u8) -> &MappedBar { ++ match self.try_map_bar(bir) { ++ Ok(bar) => bar, ++ Err(err) => { ++ log::error!("failed to map BAR {bir}: {err}"); ++ process::exit(1); ++ } + } + } + } +diff --git a/drivers/pcid/src/driver_interface/msi.rs b/drivers/pcid/src/driver_interface/msi.rs +index 0ca68ec5..cd2fd701 100644 +--- a/drivers/pcid/src/driver_interface/msi.rs ++++ b/drivers/pcid/src/driver_interface/msi.rs +@@ -1,6 +1,8 @@ + use std::fmt; + use std::ptr::NonNull; ++use std::process; + ++use crate::driver_interface::bar::PciBarError; + use crate::driver_interface::PciBar; + use crate::PciFunctionHandle; + +@@ -33,9 +35,74 @@ pub struct MsixInfo { + pub pba_offset: u32, + } + ++#[derive(Debug)] ++pub enum MsixMapError { ++ ReservedBir(u8), ++ InvalidBar { ++ which: &'static str, ++ source: PciBarError, ++ }, ++ TableOutsideBar { ++ offset: usize, ++ end: usize, ++ bar_size: usize, ++ }, ++ PbaOutsideBar { ++ offset: usize, ++ end: usize, ++ bar_size: usize, ++ }, ++ NullPointer, ++} ++ ++impl fmt::Display for MsixMapError { ++ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ++ match self { ++ MsixMapError::ReservedBir(bir) => { ++ write!(f, "MSI-X BIR contained a reserved value: {bir}") ++ } ++ MsixMapError::InvalidBar { which, source } => { ++ write!(f, "MSI-X {which} BAR is invalid: {source}") ++ } ++ MsixMapError::TableOutsideBar { ++ offset, ++ end, ++ bar_size, ++ } => write!( ++ f, ++ "MSI-X table {offset:#x}:{end:#x} outside BAR with length {bar_size:#x}" ++ ), ++ MsixMapError::PbaOutsideBar { ++ offset, ++ end, ++ bar_size, ++ } => write!( ++ f, ++ "MSI-X PBA {offset:#x}:{end:#x} outside BAR with length {bar_size:#x}" ++ ), ++ MsixMapError::NullPointer => { ++ write!(f, "MSI-X BAR mapping resulted in null pointer") ++ }, ++ } ++ } ++} ++ + impl MsixInfo { + pub unsafe fn map_and_mask_all(self, pcid_handle: &mut PciFunctionHandle) -> MappedMsixRegs { +- self.validate(pcid_handle.config().func.bars); ++ match self.try_map_and_mask_all(pcid_handle) { ++ Ok(regs) => regs, ++ Err(err) => { ++ log::error!("{err}"); ++ process::exit(1); ++ } ++ } ++ } ++ ++ pub unsafe fn try_map_and_mask_all( ++ self, ++ pcid_handle: &mut PciFunctionHandle, ++ ) -> Result { ++ self.try_validate(pcid_handle.config().func.bars)?; + + let virt_table_base = unsafe { + pcid_handle +@@ -46,7 +113,8 @@ impl MsixInfo { + }; + + let mut info = MappedMsixRegs { +- virt_table_base: NonNull::new(virt_table_base.cast::()).unwrap(), ++ virt_table_base: NonNull::new(virt_table_base.cast::()) ++ .ok_or(MsixMapError::NullPointer)?, + info: self, + }; + +@@ -56,21 +124,15 @@ impl MsixInfo { + info.table_entry_pointer(i.into()).mask(); + } + +- info ++ Ok(info) + } + +- fn validate(&self, bars: [PciBar; 6]) { ++ pub fn try_validate(&self, bars: [PciBar; 6]) -> Result<(), MsixMapError> { + if self.table_bar > 5 { +- panic!( +- "MSI-X Table BIR contained a reserved enum value: {}", +- self.table_bar +- ); ++ return Err(MsixMapError::ReservedBir(self.table_bar)); + } + if self.pba_bar > 5 { +- panic!( +- "MSI-X PBA BIR contained a reserved enum value: {}", +- self.pba_bar +- ); ++ return Err(MsixMapError::ReservedBir(self.pba_bar)); + } + + let table_size = self.table_size; +@@ -80,28 +142,38 @@ impl MsixInfo { + let pba_offset = self.pba_offset as usize; + let pba_min_length = table_size.div_ceil(8); + +- let (_, table_bar_size) = bars[self.table_bar as usize].expect_mem(); +- let (_, pba_bar_size) = bars[self.pba_bar as usize].expect_mem(); ++ let (_, table_bar_size) = bars[self.table_bar as usize] ++ .try_mem() ++ .map_err(|source| MsixMapError::InvalidBar { ++ which: "table", ++ source, ++ })?; ++ let (_, pba_bar_size) = bars[self.pba_bar as usize] ++ .try_mem() ++ .map_err(|source| MsixMapError::InvalidBar { ++ which: "PBA", ++ source, ++ })?; + + // Ensure that the table and PBA are within the BAR. + + if !(0..table_bar_size as u64).contains(&(table_offset as u64 + table_min_length as u64)) { +- panic!( +- "Table {:#x}:{:#x} outside of BAR with length {:#x}", +- table_offset, +- table_offset + table_min_length as usize, +- table_bar_size +- ); ++ return Err(MsixMapError::TableOutsideBar { ++ offset: table_offset, ++ end: table_offset + table_min_length as usize, ++ bar_size: table_bar_size, ++ }); + } + + if !(0..pba_bar_size as u64).contains(&(pba_offset as u64 + pba_min_length as u64)) { +- panic!( +- "PBA {:#x}:{:#x} outside of BAR with length {:#x}", +- pba_offset, +- pba_offset + pba_min_length as usize, +- pba_bar_size +- ); ++ return Err(MsixMapError::PbaOutsideBar { ++ offset: pba_offset, ++ end: pba_offset + pba_min_length as usize, ++ bar_size: pba_bar_size, ++ }); + } ++ ++ Ok(()) + } + } + +@@ -120,6 +192,68 @@ impl MappedMsixRegs { + } + } + ++#[cfg(test)] ++mod tests { ++ use super::{MsixInfo, MsixMapError}; ++ use crate::driver_interface::PciBar; ++ ++ #[test] ++ fn try_validate_accepts_in_range_table_and_pba() { ++ let info = MsixInfo { ++ table_bar: 0, ++ table_offset: 0x100, ++ table_size: 4, ++ pba_bar: 1, ++ pba_offset: 0x80, ++ }; ++ let mut bars = [PciBar::None; 6]; ++ bars[0] = PciBar::Memory32 { ++ addr: 0x1000, ++ size: 0x400, ++ }; ++ bars[1] = PciBar::Memory32 { ++ addr: 0x2000, ++ size: 0x200, ++ }; ++ ++ assert!(info.try_validate(bars).is_ok()); ++ } ++ ++ #[test] ++ fn try_validate_rejects_reserved_bir() { ++ let info = MsixInfo { ++ table_bar: 6, ++ table_offset: 0, ++ table_size: 1, ++ pba_bar: 0, ++ pba_offset: 0, ++ }; ++ ++ assert!(matches!(info.try_validate([PciBar::None; 6]), Err(MsixMapError::ReservedBir(6)))); ++ } ++ ++ #[test] ++ fn try_validate_rejects_out_of_range_table() { ++ let info = MsixInfo { ++ table_bar: 0, ++ table_offset: 0x100, ++ table_size: 16, ++ pba_bar: 0, ++ pba_offset: 0, ++ }; ++ let mut bars = [PciBar::None; 6]; ++ bars[0] = PciBar::Memory32 { ++ addr: 0x1000, ++ size: 0x80, ++ }; ++ ++ assert!(matches!( ++ info.try_validate(bars), ++ Err(MsixMapError::TableOutsideBar { .. }) ++ )); ++ } ++} ++ + #[repr(C, packed)] + pub struct MsixTableEntry { + pub addr_lo: Mmio, +diff --git a/drivers/pcid/src/scheme.rs b/drivers/pcid/src/scheme.rs +index bb9f39a3..df026ab4 100644 +--- a/drivers/pcid/src/scheme.rs ++++ b/drivers/pcid/src/scheme.rs +@@ -21,6 +21,7 @@ enum Handle { + TopLevel { entries: Vec }, + Access, + Device, ++ Config { addr: PciAddress }, + Channel { addr: PciAddress, st: ChannelState }, + SchemeRoot, + } +@@ -30,14 +31,20 @@ struct HandleWrapper { + } + impl Handle { + fn is_file(&self) -> bool { +- matches!(self, Self::Access | Self::Channel { .. }) ++ matches!( ++ self, ++ Self::Access | Self::Config { .. } | Self::Channel { .. } ++ ) + } + fn is_dir(&self) -> bool { + !self.is_file() + } + // TODO: capability rather than root + fn requires_root(&self) -> bool { +- matches!(self, Self::Access | Self::Channel { .. }) ++ matches!( ++ self, ++ Self::Access | Self::Config { .. } | Self::Channel { .. } ++ ) + } + fn is_scheme_root(&self) -> bool { + matches!(self, Self::SchemeRoot) +@@ -132,6 +139,7 @@ impl SchemeSync for PciScheme { + let (len, mode) = match handle.inner { + Handle::TopLevel { ref entries } => (entries.len(), MODE_DIR | 0o755), + Handle::Device => (DEVICE_CONTENTS.len(), MODE_DIR | 0o755), ++ Handle::Config { .. } => (256, MODE_CHR | 0o600), + Handle::Access | Handle::Channel { .. } => (0, MODE_CHR | 0o600), + Handle::SchemeRoot => return Err(Error::new(EBADF)), + }; +@@ -156,6 +164,18 @@ impl SchemeSync for PciScheme { + match handle.inner { + Handle::TopLevel { .. } => Err(Error::new(EISDIR)), + Handle::Device => Err(Error::new(EISDIR)), ++ Handle::Config { addr } => { ++ let offset = _offset as u16; ++ let dword_offset = offset & !0x3; ++ let byte_offset = (offset & 0x3) as usize; ++ let bytes_to_read = buf.len().min(4 - byte_offset); ++ ++ let dword = unsafe { self.pcie.read(addr, dword_offset) }; ++ let bytes = dword.to_le_bytes(); ++ buf[..bytes_to_read] ++ .copy_from_slice(&bytes[byte_offset..byte_offset + bytes_to_read]); ++ Ok(bytes_to_read) ++ } + Handle::Channel { + addr: _, + ref mut st, +@@ -193,7 +213,9 @@ impl SchemeSync for PciScheme { + return Ok(buf); + } + Handle::Device => DEVICE_CONTENTS, +- Handle::Access | Handle::Channel { .. } => return Err(Error::new(ENOTDIR)), ++ Handle::Access | Handle::Config { .. } | Handle::Channel { .. } => { ++ return Err(Error::new(ENOTDIR)); ++ } + Handle::SchemeRoot => return Err(Error::new(EBADF)), + }; + +@@ -223,6 +245,20 @@ impl SchemeSync for PciScheme { + } + + match handle.inner { ++ Handle::Config { addr } => { ++ let offset = _offset as u16; ++ let dword_offset = offset & !0x3; ++ let byte_offset = (offset & 0x3) as usize; ++ let bytes_to_write = buf.len().min(4 - byte_offset); ++ ++ let mut dword = unsafe { self.pcie.read(addr, dword_offset) }; ++ let mut bytes = dword.to_le_bytes(); ++ bytes[byte_offset..byte_offset + bytes_to_write] ++ .copy_from_slice(&buf[..bytes_to_write]); ++ dword = u32::from_le_bytes(bytes); ++ unsafe { self.pcie.write(addr, dword_offset, dword) }; ++ Ok(buf.len()) ++ } + Handle::Channel { addr, ref mut st } => { + Self::write_channel(&self.pcie, &mut self.tree, addr, st, buf) + } +@@ -316,6 +352,10 @@ impl SchemeSync for PciScheme { + func.enabled = false; + } + } ++ Some(HandleWrapper { ++ inner: Handle::Config { .. }, ++ .. ++ }) => {} + _ => {} + } + } +@@ -341,6 +381,7 @@ impl PciScheme { + let path = &after[1..]; + + match path { ++ "config" => Handle::Config { addr }, + "channel" => { + if func.enabled { + return Err(Error::new(ENOLCK)); +@@ -387,7 +428,7 @@ impl PciScheme { + match *state { + ChannelState::AwaitingResponseRead(_) => return Err(Error::new(EINVAL)), + ChannelState::AwaitingData => { +- let func = tree.get_mut(&addr).unwrap(); ++ let func = tree.get_mut(&addr).ok_or(Error::new(ENOENT))?; + + let request = bincode::deserialize_from(buf).map_err(|_| Error::new(EINVAL))?; + let response = crate::driver_handler::DriverHandler::new( diff --git a/local/patches/base/P2-storage-driver-mains.patch b/local/patches/base/P2-storage-driver-mains.patch new file mode 100644 index 00000000..fb2b8583 --- /dev/null +++ b/local/patches/base/P2-storage-driver-mains.patch @@ -0,0 +1,625 @@ +# P2-storage-driver-mains.patch +# Extract storage driver main.rs hardening: replace panic/unwrap/expect with +# proper error handling, debug_assert for invariant checks, and graceful exits. +# +# Files: drivers/storage/ahcid/src/main.rs, drivers/storage/ided/src/main.rs, +# drivers/storage/nvmed/src/main.rs, drivers/storage/virtio-blkd/src/main.rs + +diff --git a/drivers/storage/ahcid/src/main.rs b/drivers/storage/ahcid/src/main.rs +index 1f130a29..cccd2980 100644 +--- a/drivers/storage/ahcid/src/main.rs ++++ b/drivers/storage/ahcid/src/main.rs +@@ -2,6 +2,7 @@ + + use std::io::{Read, Write}; + use std::os::fd::AsRawFd; ++use std::process; + use std::usize; + + use common::io::Io; +@@ -23,10 +24,13 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + let mut name = pci_config.func.name(); + name.push_str("_ahci"); + +- let irq = pci_config +- .func +- .legacy_interrupt_line +- .expect("ahcid: no legacy interrupts supported"); ++ let irq = match pci_config.func.legacy_interrupt_line { ++ Some(irq) => irq, ++ None => { ++ error!("ahcid: no legacy interrupts supported"); ++ process::exit(1); ++ } ++ }; + + common::setup_logging( + "disk", +@@ -57,46 +61,71 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + let mut irq_file = irq.irq_handle("ahcid"); + let irq_fd = irq_file.as_raw_fd() as usize; + +- let event_queue = RawEventQueue::new().expect("ahcid: failed to create event queue"); ++ let event_queue = RawEventQueue::new().unwrap_or_else(|err| { ++ error!("ahcid: failed to create event queue: {err}"); ++ process::exit(1); ++ }); + +- libredox::call::setrens(0, 0).expect("ahcid: failed to enter null namespace"); ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ error!("ahcid: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe(scheme.event_handle().raw(), 1, EventFlags::READ) +- .expect("ahcid: failed to event scheme socket"); ++ .unwrap_or_else(|err| { ++ error!("ahcid: failed to event scheme socket: {err}"); ++ process::exit(1); ++ }); + event_queue + .subscribe(irq_fd, 1, EventFlags::READ) +- .expect("ahcid: failed to event irq scheme"); ++ .unwrap_or_else(|err| { ++ error!("ahcid: failed to event irq scheme: {err}"); ++ process::exit(1); ++ }); + + for event in event_queue { +- let event = event.unwrap(); ++ let event = match event { ++ Ok(event) => event, ++ Err(err) => { ++ error!("ahcid: failed to get event: {err}"); ++ continue; ++ } ++ }; + if event.fd == scheme.event_handle().raw() { +- FuturesExecutor.block_on(scheme.tick()).unwrap(); ++ if let Err(err) = FuturesExecutor.block_on(scheme.tick()) { ++ error!("ahcid: failed to handle scheme event: {err}"); ++ } + } else if event.fd == irq_fd { + let mut irq = [0; 8]; +- if irq_file +- .read(&mut irq) +- .expect("ahcid: failed to read irq file") +- >= irq.len() +- { +- let is = hba_mem.is.read(); +- if is > 0 { +- let pi = hba_mem.pi.read(); +- let pi_is = pi & is; +- for i in 0..hba_mem.ports.len() { +- if pi_is & 1 << i > 0 { +- let port = &mut hba_mem.ports[i]; +- let is = port.is.read(); +- port.is.write(is); +- } ++ match irq_file.read(&mut irq) { ++ Ok(count) if count >= irq.len() => {} ++ Ok(_) => continue, ++ Err(err) => { ++ error!("ahcid: failed to read irq file: {err}"); ++ continue; ++ } ++ } ++ let is = hba_mem.is.read(); ++ if is > 0 { ++ let pi = hba_mem.pi.read(); ++ let pi_is = pi & is; ++ for i in 0..hba_mem.ports.len() { ++ if pi_is & 1 << i > 0 { ++ let port = &mut hba_mem.ports[i]; ++ let is = port.is.read(); ++ port.is.write(is); + } +- hba_mem.is.write(is); ++ } ++ hba_mem.is.write(is); + +- irq_file +- .write(&irq) +- .expect("ahcid: failed to write irq file"); ++ if let Err(err) = irq_file.write(&irq) { ++ error!("ahcid: failed to write irq file: {err}"); ++ continue; ++ } + +- FuturesExecutor.block_on(scheme.tick()).unwrap(); ++ if let Err(err) = FuturesExecutor.block_on(scheme.tick()) { ++ error!("ahcid: failed to handle IRQ tick: {err}"); + } + } + } else { +@@ -105,5 +134,5 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- std::process::exit(0); ++ process::exit(0); + } +diff --git a/drivers/storage/ided/src/main.rs b/drivers/storage/ided/src/main.rs +index 4197217d..03174554 100644 +--- a/drivers/storage/ided/src/main.rs ++++ b/drivers/storage/ided/src/main.rs +@@ -8,6 +8,7 @@ use std::{ + fs::File, + io::{Read, Write}, + os::unix::io::{FromRawFd, RawFd}, ++ process, + sync::{Arc, Mutex}, + thread::{self, sleep}, + time::Duration, +@@ -45,17 +46,34 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + + let busmaster_base = pci_config.func.bars[4].expect_port(); + let (primary, primary_irq) = if pci_config.func.full_device_id.interface & 1 != 0 { +- panic!("TODO: IDE primary channel is PCI native"); ++ error!("ided: IDE primary channel PCI native mode not supported"); ++ process::exit(1); + } else { +- (Channel::primary_compat(busmaster_base).unwrap(), 14) ++ ( ++ Channel::primary_compat(busmaster_base).unwrap_or_else(|err| { ++ error!("ided: failed to init primary channel: {err}"); ++ process::exit(1); ++ }), ++ 14, ++ ) + }; + let (secondary, secondary_irq) = if pci_config.func.full_device_id.interface & 1 != 0 { +- panic!("TODO: IDE secondary channel is PCI native"); ++ error!("ided: IDE secondary channel PCI native mode not supported"); ++ process::exit(1); + } else { +- (Channel::secondary_compat(busmaster_base + 8).unwrap(), 15) ++ ( ++ Channel::secondary_compat(busmaster_base + 8).unwrap_or_else(|err| { ++ error!("ided: failed to init secondary channel: {err}"); ++ process::exit(1); ++ }), ++ 15, ++ ) + }; + +- common::acquire_port_io_rights().expect("ided: failed to get I/O privilege"); ++ common::acquire_port_io_rights().unwrap_or_else(|err| { ++ error!("ided: failed to get I/O privilege: {err}"); ++ process::exit(1); ++ }); + + //TODO: move this to ide.rs? + let chans = vec![ +@@ -87,13 +105,13 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + for (chan_i, chan_lock) in chans.iter().enumerate() { + let mut chan = chan_lock.lock().unwrap(); + +- println!(" - channel {}", chan_i); ++ log::info!(" - channel {}", chan_i); + + // Disable IRQs + chan.control.write(2); + + for dev in 0..=1 { +- println!(" - device {}", dev); ++ log::info!(" - device {}", dev); + + // Select device + chan.device_select.write(0xA0 | (dev << 4)); +@@ -105,7 +123,7 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + + // Check if device exists + if chan.status.read() == 0 { +- println!(" not found"); ++ log::info!(" not found"); + continue; + } + +@@ -125,7 +143,7 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + + //TODO: probe ATAPI + if error { +- println!(" error"); ++ log::info!(" error"); + continue; + } + +@@ -189,12 +207,12 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + 48 + }; + +- println!(" Serial: {}", serial.trim()); +- println!(" Firmware: {}", firmware.trim()); +- println!(" Model: {}", model.trim()); +- println!(" Size: {} MB", sectors / 2048); +- println!(" DMA: {}", dma); +- println!(" {}-bit LBA", lba_bits); ++ log::info!(" Serial: {}", serial.trim()); ++ log::info!(" Firmware: {}", firmware.trim()); ++ log::info!(" Model: {}", model.trim()); ++ log::info!(" Size: {} MB", sectors / 2048); ++ log::info!(" DMA: {}", dma); ++ log::info!(" {}-bit LBA", lba_bits); + + disks.push(AnyDisk::Ata(AtaDisk { + chan: chan_lock.clone(), +@@ -227,7 +245,10 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + flag::O_RDWR | flag::O_NONBLOCK, + 0, + ) +- .expect("ided: failed to open irq file"); ++ .unwrap_or_else(|err| { ++ error!("ided: failed to open primary irq file: {err}"); ++ process::exit(1); ++ }); + let mut primary_irq_file = unsafe { File::from_raw_fd(primary_irq_fd as RawFd) }; + + let secondary_irq_fd = libredox::call::open( +@@ -235,70 +256,107 @@ fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { + flag::O_RDWR | flag::O_NONBLOCK, + 0, + ) +- .expect("ided: failed to open irq file"); ++ .unwrap_or_else(|err| { ++ error!("ided: failed to open secondary irq file: {err}"); ++ process::exit(1); ++ }); + let mut secondary_irq_file = unsafe { File::from_raw_fd(secondary_irq_fd as RawFd) }; + +- let event_queue = RawEventQueue::new().expect("ided: failed to open event file"); ++ let event_queue = RawEventQueue::new().unwrap_or_else(|err| { ++ error!("ided: failed to open event file: {err}"); ++ process::exit(1); ++ }); + +- libredox::call::setrens(0, 0).expect("ided: failed to enter null namespace"); ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ error!("ided: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe(scheme.event_handle().raw(), 0, EventFlags::READ) +- .expect("ided: failed to event disk scheme"); ++ .unwrap_or_else(|err| { ++ error!("ided: failed to event disk scheme: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe(primary_irq_fd, 0, EventFlags::READ) +- .expect("ided: failed to event irq scheme"); ++ .unwrap_or_else(|err| { ++ error!("ided: failed to event primary irq: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe(secondary_irq_fd, 0, EventFlags::READ) +- .expect("ided: failed to event irq scheme"); ++ .unwrap_or_else(|err| { ++ error!("ided: failed to event secondary irq: {err}"); ++ process::exit(1); ++ }); + + for event in event_queue { +- let event = event.unwrap(); ++ let event = match event { ++ Ok(event) => event, ++ Err(err) => { ++ error!("ided: failed to get event: {err}"); ++ continue; ++ } ++ }; + if event.fd == scheme.event_handle().raw() { +- FuturesExecutor.block_on(scheme.tick()).unwrap(); ++ if let Err(err) = FuturesExecutor.block_on(scheme.tick()) { ++ error!("ided: failed to handle scheme event: {err}"); ++ } + } else if event.fd == primary_irq_fd { + let mut irq = [0; 8]; +- if primary_irq_file +- .read(&mut irq) +- .expect("ided: failed to read irq file") +- >= irq.len() +- { +- let _chan = chans[0].lock().unwrap(); +- //TODO: check chan for irq ++ match primary_irq_file.read(&mut irq) { ++ Ok(count) if count >= irq.len() => {} ++ Ok(_) => continue, ++ Err(err) => { ++ error!("ided: failed to read primary irq file: {err}"); ++ continue; ++ } ++ } ++ let _chan = chans[0].lock().unwrap(); ++ //TODO: check chan for irq + +- primary_irq_file +- .write(&irq) +- .expect("ided: failed to write irq file"); ++ if let Err(err) = primary_irq_file.write(&irq) { ++ error!("ided: failed to write primary irq file: {err}"); ++ continue; ++ } + +- FuturesExecutor.block_on(scheme.tick()).unwrap(); ++ if let Err(err) = FuturesExecutor.block_on(scheme.tick()) { ++ error!("ided: failed to handle primary IRQ tick: {err}"); + } + } else if event.fd == secondary_irq_fd { + let mut irq = [0; 8]; +- if secondary_irq_file +- .read(&mut irq) +- .expect("ided: failed to read irq file") +- >= irq.len() +- { +- let _chan = chans[1].lock().unwrap(); +- //TODO: check chan for irq ++ match secondary_irq_file.read(&mut irq) { ++ Ok(count) if count >= irq.len() => {} ++ Ok(_) => continue, ++ Err(err) => { ++ error!("ided: failed to read secondary irq file: {err}"); ++ continue; ++ } ++ } ++ let _chan = chans[1].lock().unwrap(); ++ //TODO: check chan for irq + +- secondary_irq_file +- .write(&irq) +- .expect("ided: failed to write irq file"); ++ if let Err(err) = secondary_irq_file.write(&irq) { ++ error!("ided: failed to write secondary irq file: {err}"); ++ continue; ++ } + +- FuturesExecutor.block_on(scheme.tick()).unwrap(); ++ if let Err(err) = FuturesExecutor.block_on(scheme.tick()) { ++ error!("ided: failed to handle secondary IRQ tick: {err}"); + } + } else { + error!("Unknown event {}", event.fd); + } + } + +- std::process::exit(0); ++ process::exit(0); + } + + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + fn daemon(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { +- unimplemented!() ++ log::error!("ided: unsupported architecture"); ++ process::exit(1); + } +diff --git a/drivers/storage/nvmed/src/main.rs b/drivers/storage/nvmed/src/main.rs +index beb1b689..3772f4e5 100644 +--- a/drivers/storage/nvmed/src/main.rs ++++ b/drivers/storage/nvmed/src/main.rs +@@ -2,6 +2,7 @@ use std::cell::RefCell; + use std::fs::File; + use std::io::{self, Read, Write}; + use std::os::fd::AsRawFd; ++use std::process; + use std::rc::Rc; + use std::sync::Arc; + use std::usize; +@@ -22,7 +23,10 @@ struct NvmeDisk { + + impl Disk for NvmeDisk { + fn block_size(&self) -> u32 { +- self.ns.block_size.try_into().unwrap() ++ self.ns.block_size.try_into().unwrap_or_else(|_| { ++ log::error!("nvmed: block size {} does not fit in u32", self.ns.block_size); ++ process::exit(1); ++ }) + } + + fn size(&self) -> u64 { +@@ -79,26 +83,43 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + let interrupt_vector = irq_helpers::pci_allocate_interrupt_vector(&mut pcid_handle, "nvmed"); + let iv = interrupt_vector.vector(); +- let irq_handle = interrupt_vector.irq_handle().try_clone().unwrap(); ++ let irq_handle = interrupt_vector.irq_handle().try_clone().unwrap_or_else(|err| { ++ log::error!("nvmed: failed to clone IRQ handle: {err}"); ++ process::exit(1); ++ }); + + let mut nvme = Nvme::new(address.as_ptr() as usize, interrupt_vector, pcid_handle) +- .expect("nvmed: failed to allocate driver data"); +- +- unsafe { nvme.init().expect("nvmed: failed to init") } ++ .unwrap_or_else(|err| { ++ log::error!("nvmed: failed to allocate driver data: {err}"); ++ process::exit(1); ++ }); ++ ++ unsafe { ++ nvme.init().unwrap_or_else(|err| { ++ log::error!("nvmed: failed to init: {err}"); ++ process::exit(1); ++ }); ++ } + log::debug!("Finished base initialization"); + let nvme = Arc::new(nvme); + + let executor = nvme::executor::init(Arc::clone(&nvme), iv, false /* FIXME */, irq_handle); + + let mut time_handle = File::open(&format!("/scheme/time/{}", libredox::flag::CLOCK_MONOTONIC)) +- .expect("failed to open time handle"); ++ .unwrap_or_else(|err| { ++ log::error!("nvmed: failed to open time handle: {err}"); ++ process::exit(1); ++ }); + + let mut time_events = Box::pin( + executor.register_external_event(time_handle.as_raw_fd() as usize, event::EventFlags::READ), + ); + + // Try to init namespaces for 5 seconds +- time_arm(&mut time_handle, 5).expect("failed to arm timer"); ++ time_arm(&mut time_handle, 5).unwrap_or_else(|err| { ++ log::error!("nvmed: failed to arm timer: {err}"); ++ process::exit(1); ++ }); + let namespaces = executor.block_on(async { + let namespaces_future = nvme.init_with_queues(); + let time_future = time_events.as_mut().next(); +@@ -106,7 +127,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + futures::pin_mut!(time_future); + match futures::future::select(namespaces_future, time_future).await { + futures::future::Either::Left((namespaces, _)) => namespaces, +- futures::future::Either::Right(_) => panic!("timeout on init"), ++ futures::future::Either::Right(_) => { ++ log::error!("nvmed: timeout on init"); ++ process::exit(1); ++ } + } + }); + log::debug!("Initialized!"); +@@ -134,7 +158,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + event::EventFlags::READ, + )); + +- libredox::call::setrens(0, 0).expect("nvmed: failed to enter null namespace"); ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("nvmed: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); + + log::debug!("Starting to listen for scheme events"); + +@@ -150,5 +177,5 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + + //TODO: destroy NVMe stuff + +- std::process::exit(0); ++ process::exit(0); + } +diff --git a/drivers/storage/virtio-blkd/src/main.rs b/drivers/storage/virtio-blkd/src/main.rs +index d21236b3..2b777937 100644 +--- a/drivers/storage/virtio-blkd/src/main.rs ++++ b/drivers/storage/virtio-blkd/src/main.rs +@@ -1,6 +1,7 @@ + #![deny(trivial_numeric_casts, unused_allocation)] + + use std::collections::BTreeMap; ++use std::process; + use std::sync::{Arc, Weak}; + + use driver_block::DiskScheme; +@@ -59,14 +60,23 @@ impl BlockDeviceConfig { + T: Sized + TryFrom, + >::Error: std::fmt::Debug, + { +- let transport = self.0.upgrade().unwrap(); ++ let transport = self.0.upgrade().unwrap_or_else(|| { ++ log::error!("virtio-blkd: transport handle dropped"); ++ process::exit(1); ++ }); + + let size = core::mem::size_of::() + .try_into() +- .expect("load_config: invalid size"); ++ .unwrap_or_else(|_| { ++ log::error!("virtio-blkd: load_config: invalid size"); ++ process::exit(1); ++ }); + + let value = transport.load_config(ty as u8, size); +- T::try_from(value).unwrap() ++ T::try_from(value).unwrap_or_else(|_| { ++ log::error!("virtio-blkd: load_config: invalid config value"); ++ process::exit(1); ++ }) + } + + /// Returns the capacity of the block device in bytes. +@@ -103,8 +113,11 @@ fn main() { + } + + fn daemon_runner(redox_daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! { +- daemon(redox_daemon, pcid_handle).unwrap(); +- unreachable!(); ++ daemon(redox_daemon, pcid_handle).unwrap_or_else(|err| { ++ log::error!("virtio-blkd: daemon failed: {err}"); ++ process::exit(1); ++ }); ++ process::exit(0); + } + + fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow::Result<()> { +@@ -121,7 +134,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow: + // 0x1001 - virtio-blk + let pci_config = pcid_handle.config(); + +- assert_eq!(pci_config.func.full_device_id.device_id, 0x1001); ++ if pci_config.func.full_device_id.device_id != 0x1001 { ++ log::error!("virtio-blkd: unexpected device ID {:#06x}, expected 0x1001", pci_config.func.full_device_id.device_id); ++ process::exit(1); ++ } + log::info!("virtio-blk: initiating startup sequence :^)"); + + let device = virtio_core::probe_device(&mut pcid_handle)?; +@@ -147,7 +163,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow: + + let scheme_name = format!("disk.{}", name); + +- let event_queue = event::EventQueue::new().unwrap(); ++ let mut event_queue = event::EventQueue::new().unwrap_or_else(|err| { ++ log::error!("virtio-blkd: failed to create event queue: {err}"); ++ process::exit(1); ++ }); + + event::user_data! { + enum Event { +@@ -162,7 +181,10 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow: + &driver_block::FuturesExecutor, + ); + +- libredox::call::setrens(0, 0).expect("nvmed: failed to enter null namespace"); ++ libredox::call::setrens(0, 0).unwrap_or_else(|err| { ++ log::error!("virtio-blkd: failed to enter null namespace: {err}"); ++ process::exit(1); ++ }); + + event_queue + .subscribe( +@@ -170,11 +192,26 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow: + Event::Scheme, + event::EventFlags::READ, + ) +- .unwrap(); +- +- for event in event_queue { +- match event.unwrap().user_data { +- Event::Scheme => futures::executor::block_on(scheme.tick()).unwrap(), ++ .unwrap_or_else(|err| { ++ log::error!("virtio-blkd: failed to subscribe to scheme events: {err}"); ++ process::exit(1); ++ }); ++ ++ loop { ++ let event = match event_queue.next() { ++ Some(Ok(event)) => event, ++ Some(Err(err)) => { ++ log::error!("virtio-blkd: failed to get event: {err}"); ++ continue; ++ } ++ None => break, ++ }; ++ match event.user_data { ++ Event::Scheme => { ++ if let Err(err) = futures::executor::block_on(scheme.tick()) { ++ log::error!("virtio-blkd: failed to handle scheme event: {err}"); ++ } ++ } + } + } + diff --git a/local/patches/base/P2-virtio-core-vbox.patch b/local/patches/base/P2-virtio-core-vbox.patch new file mode 100644 index 00000000..0d526d17 --- /dev/null +++ b/local/patches/base/P2-virtio-core-vbox.patch @@ -0,0 +1,413 @@ +# P2-virtio-core-vbox.patch +# Extract virtio-core probe/transport hardening and VirtualBox guest driver +# error handling improvements. +# +# Files: drivers/vboxd/src/main.rs, drivers/virtio-core/src/arch/x86.rs, +# drivers/virtio-core/src/probe.rs, drivers/virtio-core/src/spec/split_virtqueue.rs, +# drivers/virtio-core/src/transport.rs + +diff --git a/drivers/vboxd/src/main.rs b/drivers/vboxd/src/main.rs +index bcb9bb15..b9e42d4a 100644 +--- a/drivers/vboxd/src/main.rs ++++ b/drivers/vboxd/src/main.rs +@@ -199,16 +199,28 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + let mut name = pci_config.func.name(); + name.push_str("_vbox"); + +- let bar0 = pci_config.func.bars[0].expect_port(); ++ let bar0 = match pci_config.func.bars[0].try_port() { ++ Ok(port) => port, ++ Err(err) => { ++ eprintln!("vboxd: invalid BAR0: {err}"); ++ std::process::exit(1); ++ } ++ }; + + let irq = pci_config + .func + .legacy_interrupt_line +- .expect("vboxd: no legacy interrupts supported"); ++ .unwrap_or_else(|| { ++ eprintln!("vboxd: no legacy interrupts supported"); ++ std::process::exit(1); ++ }); + + println!(" + VirtualBox {}", pci_config.func.display()); + +- common::acquire_port_io_rights().expect("vboxd: failed to get I/O permission"); ++ if let Err(err) = common::acquire_port_io_rights() { ++ eprintln!("vboxd: failed to get I/O permission: {err}"); ++ std::process::exit(1); ++ } + + let mut width = 0; + let mut height = 0; +@@ -233,25 +245,55 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let mut irq_file = irq.irq_handle("vboxd"); ++ let mut irq_file = match irq.try_irq_handle("vboxd") { ++ Ok(file) => file, ++ Err(err) => { ++ eprintln!("vboxd: failed to open IRQ handle: {err}"); ++ std::process::exit(1); ++ } ++ }; + +- let address = unsafe { pcid_handle.map_bar(1) }.ptr.as_ptr(); ++ let address = match unsafe { pcid_handle.try_map_bar(1) } { ++ Ok(bar) => bar.ptr.as_ptr(), ++ Err(err) => { ++ eprintln!("vboxd: failed to map BAR1: {err}"); ++ std::process::exit(1); ++ } ++ }; + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + let mut port = common::io::Pio::::new(bar0 as u16); + + let vmmdev = unsafe { &mut *(address as *mut VboxVmmDev) }; + +- let mut guest_info = VboxGuestInfo::new().expect("vboxd: failed to map GuestInfo"); ++ let mut guest_info = match VboxGuestInfo::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map GuestInfo: {err}"); ++ std::process::exit(1); ++ } ++ }; + guest_info.version.write(VBOX_VMMDEV_VERSION); + guest_info.ostype.write(0x100); + port.write(guest_info.physical() as u32); + +- let mut guest_caps = VboxGuestCaps::new().expect("vboxd: failed to map GuestCaps"); ++ let mut guest_caps = match VboxGuestCaps::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map GuestCaps: {err}"); ++ std::process::exit(1); ++ } ++ }; + guest_caps.caps.write(1 << 2); + port.write(guest_caps.physical() as u32); + +- let mut set_mouse = VboxSetMouse::new().expect("vboxd: failed to map SetMouse"); ++ let mut set_mouse = match VboxSetMouse::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map SetMouse: {err}"); ++ std::process::exit(1); ++ } ++ }; + set_mouse.features.write(1 << 4 | 1); + port.write(set_mouse.physical() as u32); + +@@ -265,34 +307,71 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + +- let event_queue = +- EventQueue::::new().expect("vboxd: Could not create event queue."); ++ let event_queue = match EventQueue::::new() { ++ Ok(queue) => queue, ++ Err(err) => { ++ eprintln!("vboxd: could not create event queue: {err}"); ++ std::process::exit(1); ++ } ++ }; + event_queue + .subscribe( + irq_file.as_raw_fd() as usize, + Source::Irq, + event::EventFlags::READ, + ) +- .unwrap(); ++ .unwrap_or_else(|err| { ++ eprintln!("vboxd: failed to subscribe IRQ fd: {err}"); ++ std::process::exit(1); ++ }); + + daemon.ready(); + +- libredox::call::setrens(0, 0).expect("vboxd: failed to enter null namespace"); ++ if let Err(err) = libredox::call::setrens(0, 0) { ++ eprintln!("vboxd: failed to enter null namespace: {err}"); ++ std::process::exit(1); ++ } + + let mut bga = crate::bga::Bga::new(); +- let get_mouse = VboxGetMouse::new().expect("vboxd: failed to map GetMouse"); +- let display_change = VboxDisplayChange::new().expect("vboxd: failed to map DisplayChange"); +- let ack_events = VboxAckEvents::new().expect("vboxd: failed to map AckEvents"); ++ let get_mouse = match VboxGetMouse::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map GetMouse: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ let display_change = match VboxDisplayChange::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map DisplayChange: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ let ack_events = match VboxAckEvents::new() { ++ Ok(value) => value, ++ Err(err) => { ++ eprintln!("vboxd: failed to map AckEvents: {err}"); ++ std::process::exit(1); ++ } ++ }; + +- for Source::Irq in iter::once(Source::Irq) +- .chain(event_queue.map(|e| e.expect("vboxd: failed to get next event").user_data)) +- { ++ for Source::Irq in iter::once(Source::Irq).chain(event_queue.map(|e| match e { ++ Ok(event) => event.user_data, ++ Err(err) => { ++ eprintln!("vboxd: failed to get next event: {err}"); ++ std::process::exit(1); ++ } ++ })) { + let mut irq = [0; 8]; +- if irq_file.read(&mut irq).unwrap() >= irq.len() { ++ match irq_file.read(&mut irq) { ++ Ok(read) if read >= irq.len() => { + let host_events = vmmdev.host_events.read(); + if host_events != 0 { + port.write(ack_events.physical() as u32); +- irq_file.write(&irq).unwrap(); ++ if let Err(err) = irq_file.write(&irq) { ++ eprintln!("vboxd: failed to acknowledge IRQ: {err}"); ++ std::process::exit(1); ++ } + + if host_events & VBOX_EVENT_DISPLAY == VBOX_EVENT_DISPLAY { + port.write(display_change.physical() as u32); +@@ -326,8 +405,14 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + } + } + } ++ Ok(_) => {} ++ Err(err) => { ++ eprintln!("vboxd: failed to read IRQ file: {err}"); ++ std::process::exit(1); ++ } ++ } + } + } + +- std::process::exit(0); ++ std::process::exit(1); + } +diff --git a/drivers/virtio-core/src/arch/x86.rs b/drivers/virtio-core/src/arch/x86.rs +index aea86c4a..c5b2767f 100644 +--- a/drivers/virtio-core/src/arch/x86.rs ++++ b/drivers/virtio-core/src/arch/x86.rs +@@ -11,7 +11,10 @@ pub fn enable_msix(pcid_handle: &mut PciFunctionHandle) -> Result { + // Extended message signaled interrupts. + let msix_info = match pcid_handle.feature_info(PciFeature::MsiX) { + PciFeatureInfo::MsiX(capability) => capability, +- _ => unreachable!(), ++ _ => { ++ log::warn!("virtio_core::enable_msix: expected MSI-X feature info"); ++ return Err(Error::Probe("unexpected PCI feature info for MSI-X")); ++ } + }; + let mut info = unsafe { msix_info.map_and_mask_all(pcid_handle) }; + +@@ -21,7 +24,10 @@ pub fn enable_msix(pcid_handle: &mut PciFunctionHandle) -> Result { + let interrupt_handle = { + let table_entry_pointer = info.table_entry_pointer(MSIX_PRIMARY_VECTOR as usize); + +- let destination_id = read_bsp_apic_id().expect("virtio_core: `read_bsp_apic_id()` failed"); ++ let destination_id = read_bsp_apic_id().map_err(|e| { ++ log::warn!("virtio_core::enable_msix: read_bsp_apic_id failed: {e}"); ++ Error::Probe("read_bsp_apic_id failed") ++ })?; + let (msg_addr_and_data, interrupt_handle) = + allocate_single_interrupt_vector_for_msi(destination_id); + table_entry_pointer.write_addr_and_data(msg_addr_and_data); +diff --git a/drivers/virtio-core/src/probe.rs b/drivers/virtio-core/src/probe.rs +index 5631ef67..eaef1b96 100644 +--- a/drivers/virtio-core/src/probe.rs ++++ b/drivers/virtio-core/src/probe.rs +@@ -31,16 +31,16 @@ pub const MSIX_PRIMARY_VECTOR: u16 = 0; + /// before starting the device. + /// * Finally start the device (via [`StandardTransport::run_device`]). At this point, the device + /// is alive. +-/// +-/// ## Panics +-/// This function panics if the device is not a virtio device. + pub fn probe_device(pcid_handle: &mut PciFunctionHandle) -> Result { + let pci_config = pcid_handle.config(); + +- assert_eq!( +- pci_config.func.full_device_id.vendor_id, 6900, +- "virtio_core::probe_device: not a virtio device" +- ); ++ if pci_config.func.full_device_id.vendor_id != 6900 { ++ log::warn!( ++ "virtio_core::probe_device: skipping non-virtio device (vendor ID {:#06x})", ++ pci_config.func.full_device_id.vendor_id ++ ); ++ return Err(Error::Probe("not a virtio device")); ++ } + + let mut common_addr = None; + let mut notify_addr = None; +@@ -55,7 +55,9 @@ pub fn probe_device(pcid_handle: &mut PciFunctionHandle) -> Result continue, + } + +- let (addr, _) = pci_config.func.bars[capability.bar as usize].expect_mem(); ++ let (addr, _) = pci_config.func.bars[capability.bar as usize] ++ .try_mem() ++ .map_err(|_| Error::Probe("BAR is not memory-mapped"))?; + + let address = unsafe { + let addr = addr + capability.offset as usize; +@@ -100,19 +102,23 @@ pub fn probe_device(pcid_handle: &mut PciFunctionHandle) -> Result unreachable!(), ++ _ => continue, + } + } + +- let common_addr = common_addr.expect("virtio common capability missing"); +- let device_addr = device_addr.expect("virtio device capability missing"); +- let (notify_addr, notify_multiplier) = notify_addr.expect("virtio notify capability missing"); +- +- // FIXME this is explicitly allowed by the virtio specification to happen +- assert!( +- notify_multiplier != 0, +- "virtio-core::device_probe: device uses the same Queue Notify addresses for all queues" +- ); ++ let common_addr = common_addr.ok_or(Error::InCapable(CfgType::Common))?; ++ let device_addr = device_addr.ok_or(Error::InCapable(CfgType::Device))?; ++ let (notify_addr, notify_multiplier) = ++ notify_addr.ok_or(Error::InCapable(CfgType::Notify))?; ++ ++ // The virtio specification explicitly allows a zero notify_off_multiplier, ++ // meaning all queues share the same notification address. Handle gracefully. ++ if notify_multiplier == 0 { ++ log::warn!( ++ "virtio_core::probe_device: device uses the same Queue Notify address for all queues" ++ ); ++ return Err(Error::Probe("zero notify_off_multiplier")); ++ } + + let common = unsafe { &mut *(common_addr as *mut CommonCfg) }; + let device_space = unsafe { &mut *(device_addr as *mut u8) }; +@@ -128,8 +134,10 @@ pub fn probe_device(pcid_handle: &mut PciFunctionHandle) -> Result Vec { +- let last_buffer = self.buffers.last_mut().expect("virtio-core: empty chain"); +- last_buffer.flags.remove(DescriptorFlags::NEXT); +- ++ if let Some(last_buffer) = self.buffers.last_mut() { ++ last_buffer.flags.remove(DescriptorFlags::NEXT); ++ } + self.buffers + } + } +diff --git a/drivers/virtio-core/src/transport.rs b/drivers/virtio-core/src/transport.rs +index d3445d2d..99972c95 100644 +--- a/drivers/virtio-core/src/transport.rs ++++ b/drivers/virtio-core/src/transport.rs +@@ -19,6 +19,8 @@ pub enum Error { + SyscallError(#[from] libredox::error::Error), + #[error("the device is incapable of {0:?}")] + InCapable(CfgType), ++ #[error("virtio probe: {0}")] ++ Probe(&'static str), + } + + /// Returns the queue part sizes in bytes. +@@ -59,14 +61,23 @@ pub fn spawn_irq_thread(irq_handle: &File, queue: &Arc>) { + let queue_copy = queue.clone(); + + std::thread::spawn(move || { +- let event_queue = RawEventQueue::new().unwrap(); ++ let event_queue = match RawEventQueue::new() { ++ Ok(eq) => eq, ++ Err(err) => { ++ log::error!("virtio-core: failed to create event queue for IRQ thread: {err}"); ++ return; ++ } ++ }; + +- event_queue +- .subscribe(irq_fd as usize, 0, event::EventFlags::READ) +- .unwrap(); ++ if let Err(err) = event_queue.subscribe(irq_fd as usize, 0, event::EventFlags::READ) { ++ log::error!("virtio-core: failed to subscribe to IRQ fd: {err}"); ++ return; ++ } + +- for _ in event_queue.map(Result::unwrap) { +- // Wake up the tasks waiting on the queue. ++ for event_result in event_queue.map(|res| res) { ++ if event_result.is_err() { ++ break; ++ } + for (_, task) in queue_copy.waker.lock().unwrap().iter() { + task.wake_by_ref(); + } +@@ -604,7 +615,9 @@ impl Transport for StandardTransport<'_> { + // Re-read device status to ensure the `FEATURES_OK` bit is still set: otherwise, + // the device does not support our subset of features and the device is unusable. + let confirm = common.device_status.get(); +- assert!((confirm & DeviceStatusFlags::FEATURES_OK) == DeviceStatusFlags::FEATURES_OK); ++ if (confirm & DeviceStatusFlags::FEATURES_OK) != DeviceStatusFlags::FEATURES_OK { ++ log::error!("virtio-core: device rejected feature set (FEATURES_OK cleared after negotiation)"); ++ } + } + + fn setup_config_notify(&self, vector: u16) { +@@ -640,7 +653,10 @@ impl Transport for StandardTransport<'_> { + + // Set the MSI-X vector. + common.queue_msix_vector.set(vector); +- assert!(common.queue_msix_vector.get() == vector); ++ if common.queue_msix_vector.get() != vector { ++ log::error!("virtio-core: MSI-X vector {vector:#x} was not accepted by device for queue {queue_index}"); ++ return Err(Error::SyscallError(libredox::error::Error::new(libredox::errno::EIO))); ++ } + + // Enable the queue. + common.queue_enable.set(1); +@@ -685,7 +701,9 @@ impl Transport for StandardTransport<'_> { + + // Set the MSI-X vector. + common.queue_msix_vector.set(queue.vector); +- assert!(common.queue_msix_vector.get() == queue.vector); ++ if common.queue_msix_vector.get() != queue.vector { ++ log::error!("virtio-core: MSI-X vector {:#x} was not accepted during reinit for queue {}", queue.vector, queue.queue_index); ++ } + + // Enable the queue. + common.queue_enable.set(1); diff --git a/local/patches/base/P2-xhcid-remaining.patch b/local/patches/base/P2-xhcid-remaining.patch new file mode 100644 index 00000000..7d5baf34 --- /dev/null +++ b/local/patches/base/P2-xhcid-remaining.patch @@ -0,0 +1,2033 @@ +# P2-xhcid-remaining.patch +# Extract xhcid remaining hardening: MSI-X/MSI/legacy IRQ fallback, test hooks, +# port lifecycle management, staged port states, suspend/resume, endpoint +# configuration rollback, and power management. +# +# Files: drivers/usb/xhcid/src/main.rs, drivers/usb/xhcid/src/xhci/device_enumerator.rs, +# drivers/usb/xhcid/src/xhci/mod.rs, drivers/usb/xhcid/src/xhci/scheme.rs + +diff --git a/drivers/usb/xhcid/src/main.rs b/drivers/usb/xhcid/src/main.rs +index d345a52f..562c580a 100644 +--- a/drivers/usb/xhcid/src/main.rs ++++ b/drivers/usb/xhcid/src/main.rs +@@ -33,7 +33,7 @@ use std::sync::Arc; + use pcid_interface::irq_helpers::read_bsp_apic_id; + #[cfg(target_arch = "x86_64")] + use pcid_interface::irq_helpers::{ +- allocate_first_msi_interrupt_on_bsp, allocate_single_interrupt_vector_for_msi, ++ try_allocate_first_msi_interrupt_on_bsp, try_allocate_single_interrupt_vector_for_msi, + }; + use pcid_interface::{PciFeature, PciFeatureInfo, PciFunctionHandle}; + +@@ -61,11 +61,24 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option, Interru + let has_msix = all_pci_features.iter().any(|feature| feature.is_msix()); + + if has_msix { +- let msix_info = match pcid_handle.feature_info(PciFeature::MsiX) { +- PciFeatureInfo::Msi(_) => panic!(), +- PciFeatureInfo::MsiX(s) => s, ++ let msix_info = match pcid_handle.try_feature_info(PciFeature::MsiX) { ++ Ok(PciFeatureInfo::MsiX(s)) => s, ++ Ok(PciFeatureInfo::Msi(_)) => { ++ log::error!("xhcid: invalid MSI-X feature response payload"); ++ return (None, InterruptMethod::Polling); ++ } ++ Err(err) => { ++ log::error!("xhcid: failed to fetch MSI-X feature info: {err}"); ++ return (None, InterruptMethod::Polling); ++ } ++ }; ++ let mut info = match unsafe { msix_info.try_map_and_mask_all(pcid_handle) } { ++ Ok(info) => info, ++ Err(err) => { ++ log::error!("xhcid: failed to map MSI-X registers: {err}"); ++ return (None, InterruptMethod::Polling); ++ } + }; +- let mut info = unsafe { msix_info.map_and_mask_all(pcid_handle) }; + + // Allocate one msi vector. + +@@ -75,27 +88,53 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option, Interru + + let table_entry_pointer = info.table_entry_pointer(k); + +- let destination_id = read_bsp_apic_id().expect("xhcid: failed to read BSP apic id"); ++ let destination_id = match read_bsp_apic_id() { ++ Ok(id) => id, ++ Err(err) => { ++ log::error!("xhcid: failed to read BSP APIC ID: {err}"); ++ return (None, InterruptMethod::Polling); ++ } ++ }; + let (msg_addr_and_data, interrupt_handle) = +- allocate_single_interrupt_vector_for_msi(destination_id); ++ match try_allocate_single_interrupt_vector_for_msi(destination_id) { ++ Ok(result) => result, ++ Err(err) => { ++ log::error!("xhcid: failed to allocate MSI-X vector: {err}"); ++ return (None, InterruptMethod::Polling); ++ } ++ }; + table_entry_pointer.write_addr_and_data(msg_addr_and_data); + table_entry_pointer.unmask(); + + (Some(interrupt_handle), InterruptMethod::Msi) + }; + +- pcid_handle.enable_feature(PciFeature::MsiX); ++ if let Err(err) = pcid_handle.try_enable_feature(PciFeature::MsiX) { ++ log::error!("xhcid: failed to enable MSI-X: {err}"); ++ return (None, InterruptMethod::Polling); ++ } + log::debug!("Enabled MSI-X"); + + method + } else if has_msi { +- let interrupt_handle = allocate_first_msi_interrupt_on_bsp(pcid_handle); +- (Some(interrupt_handle), InterruptMethod::Msi) ++ match try_allocate_first_msi_interrupt_on_bsp(pcid_handle) { ++ Ok(interrupt_handle) => (Some(interrupt_handle), InterruptMethod::Msi), ++ Err(err) => { ++ log::error!("xhcid: failed to allocate MSI interrupt: {err}"); ++ (None, InterruptMethod::Polling) ++ } ++ } + } else if let Some(irq) = pci_config.func.legacy_interrupt_line { + log::debug!("Legacy IRQ {}", irq); + + // legacy INTx# interrupt pins. +- (Some(irq.irq_handle("xhcid")), InterruptMethod::Intx) ++ match irq.try_irq_handle("xhcid") { ++ Ok(file) => (Some(file), InterruptMethod::Intx), ++ Err(err) => { ++ log::error!("xhcid: failed to open legacy IRQ handle: {err}"); ++ (None, InterruptMethod::Polling) ++ } ++ } + } else { + // no interrupts at all + (None, InterruptMethod::Polling) +@@ -109,7 +148,13 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option, Interru + + if let Some(irq) = pci_config.func.legacy_interrupt_line { + // legacy INTx# interrupt pins. +- (Some(irq.irq_handle("xhcid")), InterruptMethod::Intx) ++ match irq.try_irq_handle("xhcid") { ++ Ok(file) => (Some(file), InterruptMethod::Intx), ++ Err(err) => { ++ log::error!("xhcid: failed to open legacy IRQ handle: {err}"); ++ (None, InterruptMethod::Polling) ++ } ++ } + } else { + // no interrupts at all + (None, InterruptMethod::Polling) +@@ -136,23 +181,48 @@ fn daemon_with_context_size( + + log::debug!("XHCI PCI CONFIG: {:?}", pci_config); + +- let address = unsafe { pcid_handle.map_bar(0) }.ptr.as_ptr() as usize; ++ let address = match unsafe { pcid_handle.try_map_bar(0) } { ++ Ok(bar) => bar.ptr.as_ptr() as usize, ++ Err(err) => { ++ log::error!("xhcid: failed to map BAR0: {err}"); ++ std::process::exit(1); ++ } ++ }; ++ ++ let (irq_file, interrupt_method) = get_int_method(&mut pcid_handle); + +- let (irq_file, interrupt_method) = (None, InterruptMethod::Polling); //get_int_method(&mut pcid_handle); +- //TODO: Fix interrupts. ++ match interrupt_method { ++ InterruptMethod::Msi => log::info!("xhcid: using MSI/MSI-X interrupt delivery"), ++ InterruptMethod::Intx => log::info!("xhcid: using legacy INTx interrupt delivery"), ++ InterruptMethod::Polling => log::warn!("xhcid: falling back to polling mode"), ++ } + + log::info!("XHCI {}", pci_config.func.display()); + + let scheme_name = format!("usb.{}", name); +- let socket = Socket::create().expect("xhcid: failed to create usb scheme"); ++ let socket = match Socket::create() { ++ Ok(socket) => socket, ++ Err(err) => { ++ log::error!("xhcid: failed to create usb scheme: {err}"); ++ std::process::exit(1); ++ } ++ }; + let handler = Blocking::new(&socket, 16); + + let hci = Arc::new( +- Xhci::::new(scheme_name.clone(), address, interrupt_method, pcid_handle) +- .expect("xhcid: failed to allocate device"), ++ match Xhci::::new(scheme_name.clone(), address, interrupt_method, pcid_handle) { ++ Ok(hci) => hci, ++ Err(err) => { ++ log::error!("xhcid: failed to allocate device: {err}"); ++ std::process::exit(1); ++ } ++ }, + ); + register_sync_scheme(&socket, &scheme_name, &mut &*hci) +- .expect("xhcid: failed to regsiter scheme to namespace"); ++ .unwrap_or_else(|err| { ++ log::error!("xhcid: failed to register scheme to namespace: {err}"); ++ std::process::exit(1); ++ }); + + daemon.ready(); + +@@ -163,7 +233,10 @@ fn daemon_with_context_size( + + handler + .process_requests_blocking(&*hci) +- .expect("xhcid: failed to process requests"); ++ .unwrap_or_else(|err| { ++ log::error!("xhcid: failed to process requests: {err}"); ++ std::process::exit(1); ++ }); + } + + fn main() { +@@ -171,7 +244,13 @@ fn main() { + } + + fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { +- let address = unsafe { pcid_handle.map_bar(0) }.ptr.as_ptr() as usize; ++ let address = match unsafe { pcid_handle.try_map_bar(0) } { ++ Ok(bar) => bar.ptr.as_ptr() as usize, ++ Err(err) => { ++ log::error!("xhcid: failed to map BAR0: {err}"); ++ std::process::exit(1); ++ } ++ }; + let cap = unsafe { &mut *(address as *mut xhci::CapabilityRegs) }; + if cap.csz() { + daemon_with_context_size::<{ xhci::CONTEXT_64 }>(daemon, pcid_handle) +diff --git a/drivers/usb/xhcid/src/xhci/device_enumerator.rs b/drivers/usb/xhcid/src/xhci/device_enumerator.rs +index 74b9f732..493e79df 100644 +--- a/drivers/usb/xhcid/src/xhci/device_enumerator.rs ++++ b/drivers/usb/xhcid/src/xhci/device_enumerator.rs +@@ -4,9 +4,11 @@ use common::io::Io; + use crossbeam_channel; + use log::{debug, info, warn}; + use std::sync::Arc; +-use std::time::Duration; ++use std::time::{Duration, Instant}; + use syscall::EAGAIN; + ++const DEFAULT_PORT_RESET_SETTLE_MS: u64 = 16; ++ + pub struct DeviceEnumerationRequest { + pub port_id: PortId, + } +@@ -28,7 +30,11 @@ impl DeviceEnumerator { + let request = match self.request_queue.recv() { + Ok(req) => req, + Err(err) => { +- panic!("Failed to received an enumeration request! error: {}", err) ++ warn!( ++ "device enumerator stopping after request queue closed: {}", ++ err ++ ); ++ break; + } + }; + +@@ -38,7 +44,11 @@ impl DeviceEnumerator { + debug!("Device Enumerator request for port {}", port_id); + + let (len, flags) = { +- let ports = self.hci.ports.lock().unwrap(); ++ let ports = self ++ .hci ++ .ports ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); + + let len = ports.len(); + +@@ -62,43 +72,52 @@ impl DeviceEnumerator { + //A USB3 port won't generate a Connect Status Change until it's already enabled, so this check + //will always be skipped for USB3 ports + if !flags.contains(PortFlags::PED) { +- let disabled_state = flags.contains(PortFlags::PP) +- && flags.contains(PortFlags::CCS) +- && !flags.contains(PortFlags::PED) +- && !flags.contains(PortFlags::PR); ++ let disabled_state = Self::port_is_disabled(&flags); + + if !disabled_state { +- panic!( +- "Port {} isn't in the disabled state! Current flags: {:?}", ++ warn!( ++ "Port {} never reached the disabled state before reset-driven enumeration; current flags: {:?}", + port_id, flags + ); ++ continue; + } else { + debug!("Port {} has entered the disabled state.", port_id); + } + + //THIS LOCKS THE PORTS. DO NOT LOCK PORTS BEFORE THIS POINT + debug!("Received a device connect on port {}, but it's not enabled. Resetting the port.", port_id); +- let _ = self.hci.reset_port(port_id); ++ if let Err(err) = self.hci.reset_port(port_id) { ++ warn!( ++ "failed to reset port {} before enumeration; skipping attach: {}", ++ port_id, err ++ ); ++ continue; ++ } + +- let mut ports = self.hci.ports.lock().unwrap(); ++ let mut ports = self ++ .hci ++ .ports ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); + let port = &mut ports[port_array_index]; + + port.clear_prc(); + +- std::thread::sleep(Duration::from_millis(16)); //Some controllers need some extra time to make the transition. ++ drop(ports); + +- let flags = port.flags(); ++ let flags = self.wait_for_port_enabled_state( ++ port_array_index, ++ Duration::from_millis(DEFAULT_PORT_RESET_SETTLE_MS), ++ ); + +- let enabled_state = flags.contains(PortFlags::PP) +- && flags.contains(PortFlags::CCS) +- && flags.contains(PortFlags::PED) +- && !flags.contains(PortFlags::PR); ++ let enabled_state = Self::port_is_enabled(&flags); + + if !enabled_state { + warn!( +- "Port {} isn't in the enabled state! Current flags: {:?}", ++ "Port {} isn't in the enabled state after bounded reset settle; current flags: {:?}", + port_id, flags + ); ++ continue; + } else { + debug!( + "Port {} is in the enabled state. Proceeding with enumeration", +@@ -131,13 +150,60 @@ impl DeviceEnumerator { + Ok(was_connected) => { + if was_connected { + info!("Device on port {} was detached", port_id); ++ } else { ++ debug!( ++ "Ignoring duplicate or out-of-order detach event for unattached port {}", ++ port_id ++ ); + } + } + Err(err) => { +- warn!("processing of device attach request failed! Error: {}", err); ++ warn!("processing of device detach request failed! Error: {}", err); + } + } + } + } + } ++ ++ fn port_is_disabled(flags: &PortFlags) -> bool { ++ flags.contains(PortFlags::PP) ++ && flags.contains(PortFlags::CCS) ++ && !flags.contains(PortFlags::PED) ++ && !flags.contains(PortFlags::PR) ++ } ++ ++ fn port_is_enabled(flags: &PortFlags) -> bool { ++ flags.contains(PortFlags::PP) ++ && flags.contains(PortFlags::CCS) ++ && flags.contains(PortFlags::PED) ++ && !flags.contains(PortFlags::PR) ++ } ++ ++ fn wait_for_port_enabled_state( ++ &self, ++ port_array_index: usize, ++ settle_timeout: Duration, ++ ) -> PortFlags { ++ let start = Instant::now(); ++ ++ loop { ++ let flags = { ++ let ports = self ++ .hci ++ .ports ++ .lock() ++ .unwrap_or_else(|poisoned| poisoned.into_inner()); ++ ports[port_array_index].flags() ++ }; ++ ++ if Self::port_is_enabled(&flags) ++ || !flags.contains(PortFlags::PR) ++ || start.elapsed() >= settle_timeout ++ { ++ return flags; ++ } ++ ++ std::thread::sleep(Duration::from_millis(1)); ++ } ++ } + } +diff --git a/drivers/usb/xhcid/src/xhci/irq_reactor.rs b/drivers/usb/xhcid/src/xhci/irq_reactor.rs +index ac492d5b..310fe51f 100644 +--- a/drivers/usb/xhcid/src/xhci/irq_reactor.rs ++++ b/drivers/usb/xhcid/src/xhci/irq_reactor.rs +@@ -633,7 +633,10 @@ impl Xhci { + pub fn with_ring T>(&self, id: RingId, function: F) -> Option { + use super::RingOrStreams; + +- let slot_state = self.port_states.get(&id.port)?; ++ let slot_state = self ++ .port_states ++ .get(&id.port) ++ .or_else(|| self.staged_port_states.get(&id.port))?; + let endpoint_state = slot_state.endpoint_states.get(&id.endpoint_num)?; + + let ring_ref = match endpoint_state.transfer { +@@ -650,7 +653,10 @@ impl Xhci { + ) -> Option { + use super::RingOrStreams; + +- let mut slot_state = self.port_states.get_mut(&id.port)?; ++ let mut slot_state = self ++ .port_states ++ .get_mut(&id.port) ++ .or_else(|| self.staged_port_states.get_mut(&id.port))?; + let mut endpoint_state = slot_state.endpoint_states.get_mut(&id.endpoint_num)?; + + let ring_ref = match endpoint_state.transfer { +diff --git a/drivers/usb/xhcid/src/xhci/mod.rs b/drivers/usb/xhcid/src/xhci/mod.rs +index f2143676..0d2ec432 100644 +--- a/drivers/usb/xhcid/src/xhci/mod.rs ++++ b/drivers/usb/xhcid/src/xhci/mod.rs +@@ -11,12 +11,13 @@ + //! documents are specified in the crate-level documentation. + use std::collections::BTreeMap; + use std::convert::TryFrom; +-use std::fs::File; ++use std::fs::{self, File}; + use std::sync::atomic::AtomicUsize; +-use std::sync::{Arc, Mutex}; ++use std::sync::{Arc, Condvar, Mutex}; ++use std::time::Duration; + + use std::{mem, process, slice, thread}; +-use syscall::error::{Error, Result, EBADF, EBADMSG, EIO, ENOENT}; ++use syscall::error::{Error, Result, EBADF, EBADMSG, EBUSY, EIO, ENOENT}; + use syscall::{EAGAIN, PAGE_SIZE}; + + use chashmap::CHashMap; +@@ -77,7 +78,52 @@ pub enum InterruptMethod { + Msi, + } + ++const XHCID_TEST_HOOK_PATH: &str = "/tmp/xhcid-test-hook"; ++const XHCID_TEST_HOOK_MAX_DELAY_MS: u64 = 5_000; ++ + impl Xhci { ++ fn read_test_hook_command_from_path(path: &str) -> Option { ++ let contents = fs::read_to_string(path).ok()?; ++ contents ++ .lines() ++ .map(|line| line.trim()) ++ .find(|line| !line.is_empty() && !line.starts_with('#')) ++ .map(|line| line.to_owned()) ++ } ++ ++ fn clear_test_hook_command_path(path: &str) { ++ if let Err(err) = fs::remove_file(path) { ++ if err.kind() != std::io::ErrorKind::NotFound { ++ warn!("failed to remove xhcid test hook file {}: {}", path, err); ++ } ++ } ++ } ++ ++ fn consume_test_hook_from_path(path: &str, expected: &str) -> bool { ++ match Self::read_test_hook_command_from_path(path) { ++ Some(command) if command == expected => { ++ Self::clear_test_hook_command_path(path); ++ true ++ } ++ _ => false, ++ } ++ } ++ ++ fn consume_test_hook_delay_ms_from_path(path: &str, prefix: &str) -> Option { ++ let command = Self::read_test_hook_command_from_path(path)?; ++ let delay_ms = command.strip_prefix(prefix)?.parse::().ok()?; ++ Self::clear_test_hook_command_path(path); ++ Some(delay_ms.min(XHCID_TEST_HOOK_MAX_DELAY_MS)) ++ } ++ ++ pub(crate) fn consume_test_hook(&self, expected: &str) -> bool { ++ Self::consume_test_hook_from_path(XHCID_TEST_HOOK_PATH, expected) ++ } ++ ++ pub(crate) fn consume_test_hook_delay_ms(&self, prefix: &str) -> Option { ++ Self::consume_test_hook_delay_ms_from_path(XHCID_TEST_HOOK_PATH, prefix) ++ } ++ + /// Gets descriptors, before the port state is initiated. + async fn get_desc_raw( + &self, +@@ -104,7 +150,17 @@ impl Xhci { + ); + + let future = { +- let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(ENOENT))?; ++ let mut published_port_state = self.port_states.get_mut(&port); ++ let mut staged_port_state = if published_port_state.is_none() { ++ self.staged_port_states.get_mut(&port) ++ } else { ++ None ++ }; ++ ++ let port_state = published_port_state ++ .as_deref_mut() ++ .or_else(|| staged_port_state.as_deref_mut()) ++ .ok_or(Error::new(ENOENT))?; + let ring = port_state + .endpoint_states + .get_mut(&0) +@@ -283,6 +339,7 @@ pub struct Xhci { + handles: CHashMap, + next_handle: AtomicUsize, + port_states: CHashMap>, ++ staged_port_states: CHashMap>, + drivers: CHashMap>, + scheme_name: String, + +@@ -308,9 +365,97 @@ struct PortState { + slot: u8, + protocol_speed: &'static ProtocolSpeed, + cfg_idx: Option, ++ active_ifaces: BTreeMap, // iface number → active alternate setting + input_context: Mutex>>, + dev_desc: Option, + endpoint_states: BTreeMap, ++ lifecycle: Arc, ++ pm_state: PortPmState, ++} ++ ++#[derive(Clone, Copy, Debug, Eq, PartialEq)] ++pub(crate) enum PortLifecycleState { ++ Attaching, ++ Attached, ++ Detaching, ++} ++ ++struct PortLifecycleInner { ++ state: PortLifecycleState, ++ active_operations: usize, ++} ++ ++pub(crate) struct PortLifecycle { ++ inner: Mutex, ++ idle: Condvar, ++} ++ ++impl PortLifecycle { ++ pub(crate) fn new_attaching() -> Self { ++ Self { ++ inner: Mutex::new(PortLifecycleInner { ++ state: PortLifecycleState::Attaching, ++ active_operations: 1, ++ }), ++ idle: Condvar::new(), ++ } ++ } ++ ++ fn lock_inner(&self) -> std::sync::MutexGuard<'_, PortLifecycleInner> { ++ self.inner.lock().unwrap_or_else(|err| err.into_inner()) ++ } ++ ++ pub(crate) fn finish_attach_success(&self) -> PortLifecycleState { ++ let mut inner = self.lock_inner(); ++ ++ if inner.state == PortLifecycleState::Attaching { ++ inner.state = PortLifecycleState::Attached; ++ } ++ ++ if inner.active_operations != 0 { ++ inner.active_operations -= 1; ++ } ++ if inner.active_operations == 0 { ++ self.idle.notify_all(); ++ } ++ ++ inner.state ++ } ++ ++ pub(crate) fn finish_attach_failure(&self) { ++ let mut inner = self.lock_inner(); ++ inner.state = PortLifecycleState::Detaching; ++ ++ if inner.active_operations != 0 { ++ inner.active_operations -= 1; ++ } ++ if inner.active_operations == 0 { ++ self.idle.notify_all(); ++ } ++ } ++ ++ pub(crate) fn begin_detaching(&self) { ++ let mut inner = self.lock_inner(); ++ inner.state = PortLifecycleState::Detaching; ++ ++ while inner.active_operations != 0 { ++ inner = self.idle.wait(inner).unwrap_or_else(|err| err.into_inner()); ++ } ++ } ++} ++ ++#[derive(Clone, Copy, Debug, Eq, PartialEq)] ++pub(crate) enum PortPmState { ++ Active, ++ Suspended, ++} ++impl PortPmState { ++ pub fn as_str(&self) -> &'static str { ++ match self { ++ Self::Active => "active", ++ Self::Suspended => "suspended", ++ } ++ } + } + + impl PortState { +@@ -463,6 +608,7 @@ impl Xhci { + handles: CHashMap::new(), + next_handle: AtomicUsize::new(0), + port_states: CHashMap::new(), ++ staged_port_states: CHashMap::new(), + drivers: CHashMap::new(), + scheme_name, + +@@ -793,11 +939,14 @@ impl Xhci { + } + + pub async fn attach_device(&self, port_id: PortId) -> syscall::Result<()> { +- if self.port_states.contains_key(&port_id) { ++ if self.port_states.contains_key(&port_id) || self.staged_port_states.contains_key(&port_id) ++ { + debug!("Already contains port {}", port_id); + return Err(syscall::Error::new(EAGAIN)); + } + ++ info!("xhcid: begin attach for port {}", port_id); ++ + let (data, state, speed, flags) = { + let port = &self.ports.lock().unwrap()[port_id.root_hub_port_index()]; + (port.read(), port.state(), port.speed(), port.flags()) +@@ -808,74 +957,102 @@ impl Xhci { + port_id, data, state, speed, flags + ); + +- if flags.contains(port::PortFlags::CCS) { +- let slot_ty = match self.supported_protocol(port_id) { +- Some(protocol) => protocol.proto_slot_ty(), +- None => { +- warn!("Failed to find supported protocol information for port"); +- 0 +- } +- }; +- +- debug!("Slot type: {}", slot_ty); +- debug!("Enabling slot."); +- let slot = match self.enable_port_slot(slot_ty).await { +- Ok(ok) => ok, +- Err(err) => { +- error!("Failed to enable slot for port {}: {}", port_id, err); +- return Err(err); +- } +- }; ++ if !flags.contains(port::PortFlags::CCS) { ++ warn!("Attempted to attach a device that didnt have CCS=1"); ++ return Ok(()); ++ } + +- debug!("Enabled port {}, which the xHC mapped to {}", port_id, slot); ++ let slot_ty = match self.supported_protocol(port_id) { ++ Some(protocol) => protocol.proto_slot_ty(), ++ None => { ++ warn!("Failed to find supported protocol information for port"); ++ 0 ++ } ++ }; + +- //TODO: get correct speed for child devices +- let protocol_speed = self +- .lookup_psiv(port_id, speed) +- .expect("Failed to retrieve speed ID"); ++ debug!("Slot type: {}", slot_ty); ++ debug!("Enabling slot."); ++ let slot = match self.enable_port_slot(slot_ty).await { ++ Ok(ok) => ok, ++ Err(err) => { ++ error!("Failed to enable slot for port {}: {}", port_id, err); ++ return Err(err); ++ } ++ }; + +- let mut input = unsafe { self.alloc_dma_zeroed::>()? }; ++ debug!("Enabled port {}, which the xHC mapped to {}", port_id, slot); + +- debug!("Attempting to address the device"); +- let mut ring = match self +- .address_device(&mut input, port_id, slot_ty, slot, protocol_speed, speed) +- .await +- { +- Ok(device_ring) => device_ring, +- Err(err) => { +- error!("Failed to address device for port {}: `{}`", port_id, err); +- return Err(err); ++ let protocol_speed = match self.lookup_psiv(port_id, speed) { ++ Some(protocol_speed) => protocol_speed, ++ None => { ++ let err = Error::new(EIO); ++ error!("Failed to retrieve speed ID for port {}", port_id); ++ if let Err(disable_err) = self.disable_port_slot(slot).await { ++ warn!( ++ "Failed to disable slot {} after speed lookup failure on port {}: {}", ++ slot, port_id, disable_err ++ ); + } +- }; ++ return Err(err); ++ } ++ }; + +- debug!("Addressed device"); ++ let mut input = unsafe { self.alloc_dma_zeroed::>()? }; + +- // TODO: Should the descriptors be cached in PortState, or refetched? ++ debug!("Attempting to address the device"); ++ let ring = match self ++ .address_device(&mut input, port_id, slot_ty, slot, protocol_speed, speed) ++ .await ++ { ++ Ok(device_ring) => device_ring, ++ Err(err) => { ++ error!("Failed to address device for port {}: `{}`", port_id, err); ++ if let Err(disable_err) = self.disable_port_slot(slot).await { ++ warn!( ++ "Failed to disable slot {} after address failure on port {}: {}", ++ slot, port_id, disable_err ++ ); ++ } ++ return Err(err); ++ } ++ }; + +- let mut port_state = PortState { +- slot, +- protocol_speed, +- input_context: Mutex::new(input), +- dev_desc: None, +- cfg_idx: None, +- endpoint_states: std::iter::once(( +- 0, +- EndpointState { +- transfer: RingOrStreams::Ring(ring), +- driver_if_state: EndpIfState::Init, +- }, +- )) +- .collect::>(), +- }; +- self.port_states.insert(port_id, port_state); +- debug!("Got port states!"); ++ debug!("Addressed device"); + +- // Ensure correct packet size is used ++ let lifecycle = Arc::new(PortLifecycle::new_attaching()); ++ let port_state = PortState { ++ slot, ++ protocol_speed, ++ input_context: Mutex::new(input), ++ dev_desc: None, ++ cfg_idx: None, ++ active_ifaces: BTreeMap::new(), ++ endpoint_states: std::iter::once(( ++ 0, ++ EndpointState { ++ transfer: RingOrStreams::Ring(ring), ++ driver_if_state: EndpIfState::Init, ++ }, ++ )) ++ .collect::>(), ++ lifecycle: Arc::clone(&lifecycle), ++ pm_state: PortPmState::Active, ++ }; ++ self.staged_port_states.insert(port_id, port_state); ++ debug!("Got staged port state!"); ++ ++ let attach_result = async { + let dev_desc_8_byte = self.fetch_dev_desc_8_byte(port_id, slot).await?; + { +- let mut port_state = self.port_states.get_mut(&port_id).unwrap(); ++ let mut port_state = self ++ .staged_port_states ++ .get_mut(&port_id) ++ .ok_or(Error::new(ENOENT))?; + +- let mut input = port_state.input_context.lock().unwrap(); ++ let mut input = port_state ++ .input_context ++ .lock() ++ .unwrap_or_else(|err| err.into_inner()); + + self.update_max_packet_size(&mut *input, slot, dev_desc_8_byte) + .await?; +@@ -885,97 +1062,175 @@ impl Xhci { + + let dev_desc = self.get_desc(port_id, slot).await?; + debug!("Got the full device descriptor!"); +- self.port_states.get_mut(&port_id).unwrap().dev_desc = Some(dev_desc); ++ self.staged_port_states ++ .get_mut(&port_id) ++ .ok_or(Error::new(ENOENT))? ++ .dev_desc = Some(dev_desc); + + debug!("Got the port states again!"); + { +- let mut port_state = self.port_states.get_mut(&port_id).unwrap(); +- +- let mut input = port_state.input_context.lock().unwrap(); ++ let mut port_state = self ++ .staged_port_states ++ .get_mut(&port_id) ++ .ok_or(Error::new(ENOENT))?; ++ ++ let mut input = port_state ++ .input_context ++ .lock() ++ .unwrap_or_else(|err| err.into_inner()); + debug!("Got the input context!"); +- let dev_desc = port_state.dev_desc.as_ref().unwrap(); ++ let dev_desc = port_state.dev_desc.as_ref().ok_or(Error::new(EIO))?; + + self.update_default_control_pipe(&mut *input, slot, dev_desc) + .await?; + } + + debug!("Updated the default control pipe"); ++ Ok(()) ++ } ++ .await; + +- match self.spawn_drivers(port_id) { +- Ok(()) => (), +- Err(err) => { +- error!("Failed to spawn driver for port {}: `{}`", port_id, err) ++ match attach_result { ++ Ok(()) => { ++ if let Some(delay_ms) = ++ self.consume_test_hook_delay_ms("delay_before_attach_commit_ms=") ++ { ++ info!( ++ "xhcid: test hook delaying attach commit for port {} by {} ms", ++ port_id, delay_ms ++ ); ++ thread::sleep(Duration::from_millis(delay_ms)); + } ++ ++ if lifecycle.finish_attach_success() != PortLifecycleState::Attached { ++ warn!( ++ "attach for port {} completed after detach already started; skipping publication", ++ port_id ++ ); ++ return Err(Error::new(EBUSY)); ++ } ++ ++ let staged_port_state = self ++ .staged_port_states ++ .remove(&port_id) ++ .ok_or(Error::new(ENOENT))?; ++ self.port_states.insert(port_id, staged_port_state); ++ ++ match self.spawn_drivers(port_id) { ++ Ok(()) => (), ++ Err(err) => { ++ error!("Failed to spawn driver for port {}: `{}`", port_id, err) ++ } ++ } ++ ++ info!("xhcid: finished attach for port {}", port_id); ++ Ok(()) ++ } ++ Err(err) => { ++ lifecycle.finish_attach_failure(); ++ if let Err(detach_err) = self.detach_device(port_id).await { ++ warn!( ++ "failed to clean up attach failure on port {}: {}", ++ port_id, detach_err ++ ); ++ } ++ Err(err) + } +- } else { +- warn!("Attempted to attach a device that didnt have CCS=1"); + } +- +- Ok(()) + } + + pub async fn detach_device(&self, port_id: PortId) -> Result { +- if let Some(children) = self.drivers.remove(&port_id) { +- for mut child in children { +- info!("killing driver process {} for port {}", child.id(), port_id); +- match child.kill() { +- Ok(()) => { +- info!("killed driver process {} for port {}", child.id(), port_id); +- match child.try_wait() { +- Ok(status_opt) => match status_opt { +- Some(status) => { +- debug!( +- "driver process {} for port {} exited with status {}", +- child.id(), +- port_id, +- status +- ); +- } +- None => { +- //TODO: kill harder ++ let published_state = self.port_states.get(&port_id); ++ let staged_state = if published_state.is_none() { ++ self.staged_port_states.get(&port_id) ++ } else { ++ None ++ }; ++ ++ let (slot, lifecycle, was_published) = match published_state ++ .as_deref() ++ .or_else(|| staged_state.as_deref()) ++ { ++ Some(state) => (state.slot, Arc::clone(&state.lifecycle), published_state.is_some()), ++ None => { ++ debug!( ++ "Attempted to detach from port {}, which wasn't previously attached.", ++ port_id ++ ); ++ return Ok(false); ++ } ++ }; ++ drop(published_state); ++ drop(staged_state); ++ ++ lifecycle.begin_detaching(); ++ ++ if was_published { ++ if let Some(children) = self.drivers.remove(&port_id) { ++ for mut child in children { ++ info!("killing driver process {} for port {}", child.id(), port_id); ++ match child.kill() { ++ Ok(()) => { ++ info!("killed driver process {} for port {}", child.id(), port_id); ++ match child.try_wait() { ++ Ok(status_opt) => match status_opt { ++ Some(status) => { ++ debug!( ++ "driver process {} for port {} exited with status {}", ++ child.id(), ++ port_id, ++ status ++ ); ++ } ++ None => { ++ warn!( ++ "driver process {} for port {} still running", ++ child.id(), ++ port_id ++ ); ++ } ++ }, ++ Err(err) => { + warn!( +- "driver process {} for port {} still running", ++ "failed to wait for the driver process {} for port {}: {}", + child.id(), +- port_id ++ port_id, ++ err + ); + } +- }, +- Err(err) => { +- warn!( +- "failed to wait for the driver process {} for port {}: {}", +- child.id(), +- port_id, +- err +- ); + } + } +- } +- Err(err) => { +- warn!( +- "failed to kill the driver process {} for port {}: {}", +- child.id(), +- port_id, +- err +- ); ++ Err(err) => { ++ warn!( ++ "failed to kill the driver process {} for port {}: {}", ++ child.id(), ++ port_id, ++ err ++ ); ++ } + } + } + } + } + +- if let Some(state) = self.port_states.remove(&port_id) { +- debug!("disabling port slot {} for port {}", state.slot, port_id); +- let result = self.disable_port_slot(state.slot).await.and(Ok(true)); +- debug!( +- "disabled port slot {} for port {} with result: {:?}", +- state.slot, port_id, result +- ); +- result +- } else { +- debug!( +- "Attempted to detach from port {}, which wasn't previously attached.", +- port_id +- ); +- Ok(false) ++ debug!("disabling port slot {} for port {}", slot, port_id); ++ match self.disable_port_slot(slot).await { ++ Ok(()) => { ++ if was_published { ++ let _ = self.port_states.remove(&port_id); ++ } else { ++ let _ = self.staged_port_states.remove(&port_id); ++ } ++ debug!("disabled port slot {} for port {}", slot, port_id); ++ Ok(true) ++ } ++ Err(err) => { ++ warn!( ++ "failed to disable port slot {} for port {}: {}", ++ slot, port_id, err ++ ); ++ Err(err) ++ } + } + } + +@@ -1246,14 +1501,12 @@ impl Xhci { + let drivers_usercfg: &DriversConfig = &DRIVERS_CONFIG; + + for ifdesc in config_desc.interface_descs.iter() { +- //TODO: support alternate settings +- // This is difficult because the device driver must know which alternate +- // to use, but if alternates can have different classes, then a different +- // device driver may be required for each alternate. For now, we will use +- // only the default alternate setting (0) ++ // Only auto-spawn drivers for the default alternate setting (0). ++ // Non-default alternates are selected later by the device driver ++ // via SET_INTERFACE + configure_endpoints with specific alternate_setting. + if ifdesc.alternate_setting != 0 { +- warn!( +- "ignoring port {} iface {} alternate {} class {}.{} proto {}", ++ debug!( ++ "skipping port {} iface {} alternate {} class {}.{} proto {} (non-default alternate)", + port, + ifdesc.number, + ifdesc.alternate_setting, +@@ -1458,6 +1711,53 @@ pub fn start_device_enumerator(hci: &Arc>) { + })); + } + ++#[cfg(test)] ++mod tests { ++ use std::fs; ++ use std::path::Path; ++ use std::time::{SystemTime, UNIX_EPOCH}; ++ ++ use super::{Xhci, XHCID_TEST_HOOK_MAX_DELAY_MS}; ++ ++ fn unique_test_hook_path() -> String { ++ let unique = SystemTime::now() ++ .duration_since(UNIX_EPOCH) ++ .unwrap() ++ .as_nanos(); ++ format!("/tmp/xhcid-test-hook-{}", unique) ++ } ++ ++ #[test] ++ fn consume_test_hook_only_clears_matching_command() { ++ let path = unique_test_hook_path(); ++ fs::write(&path, "fail_after_set_configuration\n").unwrap(); ++ ++ assert!(!Xhci::<16>::consume_test_hook_from_path( ++ &path, ++ "fail_after_configure_endpoint" ++ )); ++ assert!(Path::new(&path).exists()); ++ ++ assert!(Xhci::<16>::consume_test_hook_from_path( ++ &path, ++ "fail_after_set_configuration" ++ )); ++ assert!(!Path::new(&path).exists()); ++ } ++ ++ #[test] ++ fn consume_test_hook_delay_clamps_and_clears() { ++ let path = unique_test_hook_path(); ++ fs::write(&path, "delay_before_attach_commit_ms=999999\n").unwrap(); ++ ++ assert_eq!( ++ Xhci::<16>::consume_test_hook_delay_ms_from_path(&path, "delay_before_attach_commit_ms="), ++ Some(XHCID_TEST_HOOK_MAX_DELAY_MS) ++ ); ++ assert!(!Path::new(&path).exists()); ++ } ++} ++ + #[derive(Deserialize)] + struct DriverConfig { + name: String, +diff --git a/drivers/usb/xhcid/src/xhci/scheme.rs b/drivers/usb/xhcid/src/xhci/scheme.rs +index ca27b3fe..29437294 100644 +--- a/drivers/usb/xhcid/src/xhci/scheme.rs ++++ b/drivers/usb/xhcid/src/xhci/scheme.rs +@@ -20,6 +20,7 @@ use std::convert::TryFrom; + use std::io::prelude::*; + use std::ops::Deref; + use std::sync::atomic; ++use std::collections::BTreeMap; + use std::{cmp, fmt, io, mem, str}; + + use common::dma::Dma; +@@ -33,9 +34,9 @@ use common::io::Io; + use redox_scheme::{CallerCtx, OpenResult}; + use syscall::schemev2::NewFdFlags; + use syscall::{ +- Error, Result, Stat, EACCES, EBADF, EBADFD, EBADMSG, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, +- ENOTDIR, EOPNOTSUPP, EPROTO, ESPIPE, MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, O_RDWR, +- O_STAT, O_WRONLY, SEEK_CUR, SEEK_END, SEEK_SET, ++ Error, Result, Stat, EACCES, EBADF, EBADFD, EBADMSG, EBUSY, EINVAL, EIO, EISDIR, ENOENT, ++ ENOSYS, ENOTDIR, EOPNOTSUPP, EPROTO, ESPIPE, MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, ++ O_RDWR, O_STAT, O_WRONLY, SEEK_CUR, SEEK_END, SEEK_SET, + }; + + use super::{port, usb}; +@@ -61,10 +62,16 @@ lazy_static! { + .expect("Failed to create the regex for the port/attach scheme."); + static ref REGEX_PORT_DETACH: Regex = Regex::new(r"^port([\d\.]+)/detach$") + .expect("Failed to create the regex for the port/detach scheme."); ++ static ref REGEX_PORT_SUSPEND: Regex = Regex::new(r"^port([\d\.]+)/suspend$") ++ .expect("Failed to create the regex for the port/suspend scheme."); ++ static ref REGEX_PORT_RESUME: Regex = Regex::new(r"^port([\d\.]+)/resume$") ++ .expect("Failed to create the regex for the port/resume scheme."); + static ref REGEX_PORT_DESCRIPTORS: Regex = Regex::new(r"^port([\d\.]+)/descriptors$") + .expect("Failed to create the regex for the port/descriptors"); + static ref REGEX_PORT_STATE: Regex = Regex::new(r"^port([\d\.]+)/state$") + .expect("Failed to create the regex for the port/state scheme"); ++ static ref REGEX_PORT_PM_STATE: Regex = Regex::new(r"^port([\d\.]+)/pm_state$") ++ .expect("Failed to create the regex for the port/pm_state scheme"); + static ref REGEX_PORT_REQUEST: Regex = Regex::new(r"^port([\d\.]+)/request$") + .expect("Failed to create the regex for the port/request scheme"); + static ref REGEX_PORT_ENDPOINTS: Regex = Regex::new(r"^port([\d\.]+)/endpoints$") +@@ -138,12 +145,15 @@ pub enum Handle { + Port(PortId, Vec), // port, contents + PortDesc(PortId, Vec), // port, contents + PortState(PortId), // port ++ PortPmState(PortId), // port + PortReq(PortId, PortReqState), // port, state + Endpoints(PortId, Vec), // port, contents + Endpoint(PortId, u8, EndpointHandleTy), // port, endpoint, state + ConfigureEndpoints(PortId), // port + AttachDevice(PortId), // port + DetachDevice(PortId), // port ++ SuspendDevice(PortId), // port ++ ResumeDevice(PortId), // port + SchemeRoot, + } + +@@ -173,6 +183,8 @@ enum SchemeParameters { + PortDesc(PortId), // port number + /// /port/state + PortState(PortId), // port number ++ /// /port/pm_state ++ PortPmState(PortId), // port number + /// /port/request + PortReq(PortId), // port number + /// /port/endpoints +@@ -188,6 +200,10 @@ enum SchemeParameters { + AttachDevice(PortId), // port number + /// /port/detach + DetachDevice(PortId), // port number ++ /// /port/suspend ++ SuspendDevice(PortId), // port number ++ /// /port/resume ++ ResumeDevice(PortId), // port number + } + + impl Handle { +@@ -210,6 +226,9 @@ impl Handle { + Handle::PortState(port_num) => { + format!("port{}/state", port_num) + } ++ Handle::PortPmState(port_num) => { ++ format!("port{}/pm_state", port_num) ++ } + Handle::PortReq(port_num, _) => { + format!("port{}/request", port_num) + } +@@ -236,6 +255,12 @@ impl Handle { + Handle::DetachDevice(port_num) => { + format!("port{}/detach", port_num) + } ++ Handle::SuspendDevice(port_num) => { ++ format!("port{}/suspend", port_num) ++ } ++ Handle::ResumeDevice(port_num) => { ++ format!("port{}/resume", port_num) ++ } + Handle::SchemeRoot => String::from(""), + } + } +@@ -259,10 +284,13 @@ impl Handle { + &Handle::PortReq(_, PortReqState::Tmp) => unreachable!(), + &Handle::PortReq(_, PortReqState::TmpSetup(_)) => unreachable!(), + &Handle::PortState(_) => HandleType::Character, ++ &Handle::PortPmState(_) => HandleType::Character, + &Handle::PortReq(_, _) => HandleType::Character, + &Handle::ConfigureEndpoints(_) => HandleType::Character, + &Handle::AttachDevice(_) => HandleType::Character, + &Handle::DetachDevice(_) => HandleType::Character, ++ &Handle::SuspendDevice(_) => HandleType::Character, ++ &Handle::ResumeDevice(_) => HandleType::Character, + &Handle::Endpoint(_, _, ref st) => match st { + EndpointHandleTy::Data => HandleType::Character, + EndpointHandleTy::Ctl => HandleType::Character, +@@ -290,10 +318,13 @@ impl Handle { + &Handle::PortReq(_, PortReqState::Tmp) => None, + &Handle::PortReq(_, PortReqState::TmpSetup(_)) => None, + &Handle::PortState(_) => None, ++ &Handle::PortPmState(_) => None, + &Handle::PortReq(_, _) => None, + &Handle::ConfigureEndpoints(_) => None, + &Handle::AttachDevice(_) => None, + &Handle::DetachDevice(_) => None, ++ &Handle::SuspendDevice(_) => None, ++ &Handle::ResumeDevice(_) => None, + &Handle::Endpoint(_, _, ref st) => match st { + EndpointHandleTy::Data => None, + EndpointHandleTy::Ctl => None, +@@ -384,6 +415,14 @@ impl SchemeParameters { + let port_num = get_port_id_from_regex(®EX_PORT_DETACH, scheme, 0)?; + + Ok(Self::DetachDevice(port_num)) ++ } else if REGEX_PORT_SUSPEND.is_match(scheme) { ++ let port_num = get_port_id_from_regex(®EX_PORT_SUSPEND, scheme, 0)?; ++ ++ Ok(Self::SuspendDevice(port_num)) ++ } else if REGEX_PORT_RESUME.is_match(scheme) { ++ let port_num = get_port_id_from_regex(®EX_PORT_RESUME, scheme, 0)?; ++ ++ Ok(Self::ResumeDevice(port_num)) + } else if REGEX_PORT_DESCRIPTORS.is_match(scheme) { + let port_num = get_port_id_from_regex(®EX_PORT_DESCRIPTORS, scheme, 0)?; + +@@ -392,6 +431,10 @@ impl SchemeParameters { + let port_num = get_port_id_from_regex(®EX_PORT_STATE, scheme, 0)?; + + Ok(Self::PortState(port_num)) ++ } else if REGEX_PORT_PM_STATE.is_match(scheme) { ++ let port_num = get_port_id_from_regex(®EX_PORT_PM_STATE, scheme, 0)?; ++ ++ Ok(Self::PortPmState(port_num)) + } else if REGEX_PORT_REQUEST.is_match(scheme) { + let port_num = get_port_id_from_regex(®EX_PORT_REQUEST, scheme, 0)?; + +@@ -524,6 +567,39 @@ pub enum AnyDescriptor { + SuperSpeedPlusCompanion(usb::SuperSpeedPlusIsochCmpDescriptor), + } + ++#[derive(Clone, Copy)] ++struct ConfigureContextSnapshot { ++ add_context: u32, ++ drop_context: u32, ++ control: u32, ++ slot_a: u32, ++ slot_b: u32, ++} ++ ++#[derive(Clone, Copy)] ++struct EndpointContextSnapshot { ++ a: u32, ++ b: u32, ++ trl: u32, ++ trh: u32, ++ c: u32, ++} ++ ++impl EndpointContextSnapshot { ++ fn capture_values(a: u32, b: u32, trl: u32, trh: u32, c: u32) -> Self { ++ Self { a, b, trl, trh, c } ++ } ++} ++ ++struct EndpointProgram { ++ endp_num_xhc: u8, ++ a: u32, ++ b: u32, ++ trl: u32, ++ trh: u32, ++ c: u32, ++} ++ + impl AnyDescriptor { + fn parse(bytes: &[u8]) -> Option<(Self, usize)> { + if bytes.len() < 2 { +@@ -640,6 +716,8 @@ impl Xhci { + where + D: FnMut(&mut Trb, bool) -> ControlFlow, + { ++ self.ensure_port_active(port_num)?; ++ + let future = { + let mut port_state = self.port_state_mut(port_num)?; + let slot = port_state.slot; +@@ -710,6 +788,8 @@ impl Xhci { + where + D: FnMut(&mut Trb, bool) -> ControlFlow, + { ++ self.ensure_port_active(port_num)?; ++ + let endp_idx = endp_num.checked_sub(1).ok_or(Error::new(EIO))?; + let mut port_state = self.port_state_mut(port_num)?; + +@@ -835,7 +915,10 @@ impl Xhci { + port, + usb::Setup::set_interface(interface_num, alternate_setting), + ) +- .await ++ .await?; ++ let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; ++ port_state.active_ifaces.insert(interface_num, alternate_setting); ++ Ok(()) + } + + async fn reset_endpoint(&self, port_num: PortId, endp_num: u8, tsp: bool) -> Result<()> { +@@ -950,35 +1033,114 @@ impl Xhci { + self.port_states.get_mut(&port).ok_or(Error::new(EBADF)) + } + ++ fn restore_configure_input_context( ++ &self, ++ port: PortId, ++ snapshot: ConfigureContextSnapshot, ++ endpoint_snapshots: &[(usize, EndpointContextSnapshot)], ++ ) -> Result { ++ let port_state = self.port_states.get(&port).ok_or(Error::new(EBADFD))?; ++ let mut input_context = port_state.input_context.lock().unwrap(); ++ ++ input_context.add_context.write(snapshot.add_context); ++ input_context.drop_context.write(snapshot.drop_context); ++ input_context.control.write(snapshot.control); ++ input_context.device.slot.a.write(snapshot.slot_a); ++ input_context.device.slot.b.write(snapshot.slot_b); ++ ++ for (endp_i, endp_snapshot) in endpoint_snapshots { ++ input_context.device.endpoints[*endp_i].a.write(endp_snapshot.a); ++ input_context.device.endpoints[*endp_i].b.write(endp_snapshot.b); ++ input_context.device.endpoints[*endp_i].trl.write(endp_snapshot.trl); ++ input_context.device.endpoints[*endp_i].trh.write(endp_snapshot.trh); ++ input_context.device.endpoints[*endp_i].c.write(endp_snapshot.c); ++ } ++ ++ Ok(input_context.physical()) ++ } ++ ++ async fn rollback_configure_attempt( ++ &self, ++ port: PortId, ++ slot: u8, ++ configure_snapshot: ConfigureContextSnapshot, ++ endpoint_snapshots: &[(usize, EndpointContextSnapshot)], ++ stage: &str, ++ ) { ++ let rollback_input_context_physical = match self.restore_configure_input_context( ++ port, ++ configure_snapshot, ++ endpoint_snapshots, ++ ) { ++ Ok(physical) => physical, ++ Err(restore_err) => { ++ warn!( ++ "failed to restore configure input context after {}: {:?}", ++ stage, restore_err ++ ); ++ return; ++ } ++ }; ++ ++ let (rollback_event_trb, rollback_command_trb) = self ++ .execute_command(|trb, cycle| { ++ trb.configure_endpoint(slot, rollback_input_context_physical, cycle) ++ }) ++ .await; ++ ++ if let Err(rollback_err) = handle_event_trb( ++ "CONFIGURE_ENDPOINT_ROLLBACK", ++ &rollback_event_trb, ++ &rollback_command_trb, ++ ) { ++ warn!( ++ "failed to roll back CONFIGURE_ENDPOINT after {}: {:?}", ++ stage, rollback_err ++ ); ++ } ++ } ++ + async fn configure_endpoints_once( + &self, + port: PortId, + req: &ConfigureEndpointsReq, + ) -> Result<()> { +- let (endp_desc_count, new_context_entries, configuration_value) = { +- let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; +- +- port_state.cfg_idx = Some(req.config_desc); ++ let (dev_desc, endpoint_descs, new_context_entries, configuration_value, speed_id) = { ++ let port_state = self.port_states.get(&port).ok_or(Error::new(EBADFD))?; ++ let dev_desc = port_state.dev_desc.as_ref().ok_or(Error::new(EBADFD))?.clone(); ++ let speed_id = port_state.protocol_speed; + +- let config_desc = port_state +- .dev_desc +- .as_ref() +- .unwrap() ++ let config_desc = dev_desc + .config_descs + .iter() + .find(|desc| desc.configuration_value == req.config_desc) + .ok_or(Error::new(EBADFD))?; + +- //TODO: USE ENDPOINTS FROM ALL INTERFACES +- let mut endp_desc_count = 0; +- let mut new_context_entries = 1; +- for if_desc in config_desc.interface_descs.iter() { +- for endpoint in if_desc.endpoints.iter() { +- endp_desc_count += 1; +- let entry = Self::endp_num_to_dci(endp_desc_count, endpoint); +- if entry > new_context_entries { +- new_context_entries = entry; +- } ++ let configuration_value = config_desc.configuration_value; ++ ++ let endpoint_descs = if let Some(iface_num) = req.interface_desc { ++ let alt = req.alternate_setting.unwrap_or(0); ++ config_desc ++ .interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.number == iface_num && if_desc.alternate_setting == alt) ++ .flat_map(|if_desc| if_desc.endpoints.iter().copied()) ++ .collect::>() ++ } else { ++ config_desc ++ .interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.alternate_setting == 0) ++ .flat_map(|if_desc| if_desc.endpoints.iter().copied()) ++ .collect::>() ++ }; ++ ++ let endp_desc_count = endpoint_descs.len(); ++ let mut new_context_entries = 1u8; ++ for (endp_idx, endpoint) in endpoint_descs.iter().enumerate() { ++ let entry = Self::endp_num_to_dci(endp_idx as u8 + 1, endpoint); ++ if entry > new_context_entries { ++ new_context_entries = entry; + } + } + new_context_entries += 1; +@@ -989,74 +1151,22 @@ impl Xhci { + } + + ( +- endp_desc_count, ++ dev_desc, ++ endpoint_descs, + new_context_entries, +- config_desc.configuration_value, ++ configuration_value, ++ speed_id, + ) + }; + let lec = self.cap.lec(); + let log_max_psa_size = self.cap.max_psa_size(); + +- let port_speed_id = self.ports.lock().unwrap()[port.root_hub_port_index()].speed(); +- let speed_id: &ProtocolSpeed = self.lookup_psiv(port, port_speed_id).ok_or_else(|| { +- warn!("no speed_id"); +- Error::new(EIO) +- })?; +- +- { +- let port_state = self.port_states.get(&port).ok_or(Error::new(EBADFD))?; +- let mut input_context = port_state.input_context.lock().unwrap(); +- +- // Configure the slot context as well, which holds the last index of the endp descs. +- input_context.add_context.write(1); +- input_context.drop_context.write(0); +- +- const CONTEXT_ENTRIES_MASK: u32 = 0xF800_0000; +- const CONTEXT_ENTRIES_SHIFT: u8 = 27; +- +- const HUB_PORTS_MASK: u32 = 0xFF00_0000; +- const HUB_PORTS_SHIFT: u8 = 24; +- +- let mut current_slot_a = input_context.device.slot.a.read(); +- let mut current_slot_b = input_context.device.slot.b.read(); +- +- // Set context entries +- current_slot_a &= !CONTEXT_ENTRIES_MASK; +- current_slot_a |= +- (u32::from(new_context_entries) << CONTEXT_ENTRIES_SHIFT) & CONTEXT_ENTRIES_MASK; +- +- // Set hub data +- current_slot_a &= !(1 << 26); +- current_slot_b &= !HUB_PORTS_MASK; +- if let Some(hub_ports) = req.hub_ports { +- current_slot_a |= 1 << 26; +- current_slot_b |= (u32::from(hub_ports) << HUB_PORTS_SHIFT) & HUB_PORTS_MASK; +- } +- +- input_context.device.slot.a.write(current_slot_a); +- input_context.device.slot.b.write(current_slot_b); +- +- let control = if self.op.lock().unwrap().cie() { +- (u32::from(req.alternate_setting.unwrap_or(0)) << 16) +- | (u32::from(req.interface_desc.unwrap_or(0)) << 8) +- | u32::from(configuration_value) +- } else { +- 0 +- }; +- input_context.control.write(control); +- } ++ let mut staged_endpoint_states = BTreeMap::new(); ++ let mut endpoint_programs = Vec::new(); + +- for endp_idx in 0..endp_desc_count as u8 { +- let endp_num = endp_idx + 1; +- +- let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; +- let dev_desc = port_state.dev_desc.as_ref().unwrap(); +- let endp_desc = port_state.get_endp_desc(endp_idx).ok_or_else(|| { +- warn!("failed to find endpoint {}", endp_idx); +- Error::new(EIO) +- })?; +- +- let endp_num_xhc = Self::endp_num_to_dci(endp_num, endp_desc); ++ for (endp_idx, endp_desc) in endpoint_descs.iter().copied().enumerate() { ++ let endp_num = endp_idx as u8 + 1; ++ let endp_num_xhc = Self::endp_num_to_dci(endp_num, &endp_desc); + + let usb_log_max_streams = endp_desc.log_max_streams(); + +@@ -1078,20 +1188,20 @@ impl Xhci { + + let mult = endp_desc.isoch_mult(lec); + +- let max_packet_size = Self::endp_ctx_max_packet_size(endp_desc); +- let max_burst_size = Self::endp_ctx_max_burst(speed_id, dev_desc, endp_desc); ++ let max_packet_size = Self::endp_ctx_max_packet_size(&endp_desc); ++ let max_burst_size = Self::endp_ctx_max_burst(speed_id, &dev_desc, &endp_desc); + + let max_esit_payload = Self::endp_ctx_max_esit_payload( + speed_id, +- dev_desc, +- endp_desc, ++ &dev_desc, ++ &endp_desc, + max_packet_size, + max_burst_size, + ); + let max_esit_payload_lo = max_esit_payload as u16; + let max_esit_payload_hi = ((max_esit_payload & 0x00FF_0000) >> 16) as u8; + +- let interval = Self::endp_ctx_interval(speed_id, endp_desc); ++ let interval = Self::endp_ctx_interval(speed_id, &endp_desc); + + let max_error_count = 3; + let ep_ty = endp_desc.xhci_ep_type()?; +@@ -1114,7 +1224,7 @@ impl Xhci { + assert_eq!(max_error_count & 0x3, max_error_count); + assert_ne!(ep_ty, 0); // 0 means invalid. + +- let ring_ptr = if usb_log_max_streams.is_some() { ++ let (endpoint_state, ring_ptr) = if usb_log_max_streams.is_some() { + let mut array = + StreamContextArray::new::(self.cap.ac64(), 1 << (primary_streams + 1))?; + +@@ -1127,15 +1237,13 @@ impl Xhci { + array_ptr, + "stream ctx ptr not aligned to 16 bytes" + ); +- port_state.endpoint_states.insert( +- endp_num, ++ ( + EndpointState { + transfer: super::RingOrStreams::Streams(array), + driver_if_state: EndpIfState::Init, + }, +- ); +- +- array_ptr ++ array_ptr, ++ ) + } else { + let ring = Ring::new::(self.cap.ac64(), 16, true)?; + let ring_ptr = ring.register(); +@@ -1145,68 +1253,205 @@ impl Xhci { + ring_ptr, + "ring pointer not aligned to 16 bytes" + ); +- port_state.endpoint_states.insert( +- endp_num, ++ ( + EndpointState { + transfer: super::RingOrStreams::Ring(ring), + driver_if_state: EndpIfState::Init, + }, +- ); +- ring_ptr ++ ring_ptr, ++ ) + }; + assert_eq!(primary_streams & 0x1F, primary_streams); + +- let mut input_context = port_state.input_context.lock().unwrap(); +- input_context.add_context.writef(1 << endp_num_xhc, true); +- +- let endp_i = endp_num_xhc as usize - 1; +- input_context.device.endpoints[endp_i].a.write( +- u32::from(mult) << 8 ++ staged_endpoint_states.insert(endp_num, endpoint_state); ++ endpoint_programs.push(EndpointProgram { ++ endp_num_xhc, ++ a: u32::from(mult) << 8 + | u32::from(primary_streams) << 10 + | u32::from(linear_stream_array) << 15 + | u32::from(interval) << 16 + | u32::from(max_esit_payload_hi) << 24, +- ); +- input_context.device.endpoints[endp_i].b.write( +- max_error_count << 1 ++ b: max_error_count << 1 + | u32::from(ep_ty) << 3 + | u32::from(host_initiate_disable) << 7 + | u32::from(max_burst_size) << 8 + | u32::from(max_packet_size) << 16, +- ); +- +- input_context.device.endpoints[endp_i] +- .trl +- .write(ring_ptr as u32); +- input_context.device.endpoints[endp_i] +- .trh +- .write((ring_ptr >> 32) as u32); +- +- input_context.device.endpoints[endp_i] +- .c +- .write(u32::from(avg_trb_len) | (u32::from(max_esit_payload_lo) << 16)); ++ trl: ring_ptr as u32, ++ trh: (ring_ptr >> 32) as u32, ++ c: u32::from(avg_trb_len) | (u32::from(max_esit_payload_lo) << 16), ++ }); + +- log::debug!("initialized endpoint {}", endp_num); ++ log::debug!("staged endpoint {}", endp_num); + } + +- { ++ let (configure_snapshot, endpoint_snapshots, input_context_physical) = { + let port_state = self.port_states.get(&port).ok_or(Error::new(EBADFD))?; +- let slot = port_state.slot; +- let input_context_physical = port_state.input_context.lock().unwrap().physical(); ++ let mut input_context = port_state.input_context.lock().unwrap(); ++ ++ let configure_snapshot = ConfigureContextSnapshot { ++ add_context: input_context.add_context.read(), ++ drop_context: input_context.drop_context.read(), ++ control: input_context.control.read(), ++ slot_a: input_context.device.slot.a.read(), ++ slot_b: input_context.device.slot.b.read(), ++ }; + +- let (event_trb, command_trb) = self +- .execute_command(|trb, cycle| { +- trb.configure_endpoint(slot, input_context_physical, cycle) ++ let endpoint_snapshots = endpoint_programs ++ .iter() ++ .map(|program| { ++ let endp_i = program.endp_num_xhc as usize - 1; ++ ( ++ endp_i, ++ EndpointContextSnapshot::capture_values( ++ input_context.device.endpoints[endp_i].a.read(), ++ input_context.device.endpoints[endp_i].b.read(), ++ input_context.device.endpoints[endp_i].trl.read(), ++ input_context.device.endpoints[endp_i].trh.read(), ++ input_context.device.endpoints[endp_i].c.read(), ++ ), ++ ) + }) +- .await; ++ .collect::>(); ++ ++ // Configure the slot context as well, which holds the last index of the endp descs. ++ input_context.add_context.write(1); ++ input_context.drop_context.write(0); + +- //self.event_handler_finished(); ++ const CONTEXT_ENTRIES_MASK: u32 = 0xF800_0000; ++ const CONTEXT_ENTRIES_SHIFT: u8 = 27; ++ ++ const HUB_PORTS_MASK: u32 = 0xFF00_0000; ++ const HUB_PORTS_SHIFT: u8 = 24; ++ ++ let mut current_slot_a = input_context.device.slot.a.read(); ++ let mut current_slot_b = input_context.device.slot.b.read(); ++ ++ current_slot_a &= !CONTEXT_ENTRIES_MASK; ++ current_slot_a |= ++ (u32::from(new_context_entries) << CONTEXT_ENTRIES_SHIFT) & CONTEXT_ENTRIES_MASK; ++ ++ current_slot_a &= !(1 << 26); ++ current_slot_b &= !HUB_PORTS_MASK; ++ if let Some(hub_ports) = req.hub_ports { ++ current_slot_a |= 1 << 26; ++ current_slot_b |= (u32::from(hub_ports) << HUB_PORTS_SHIFT) & HUB_PORTS_MASK; ++ } ++ ++ input_context.device.slot.a.write(current_slot_a); ++ input_context.device.slot.b.write(current_slot_b); ++ ++ let control = if self.op.lock().unwrap().cie() { ++ (u32::from(req.alternate_setting.unwrap_or(0)) << 16) ++ | (u32::from(req.interface_desc.unwrap_or(0)) << 8) ++ | u32::from(configuration_value) ++ } else { ++ 0 ++ }; ++ input_context.control.write(control); ++ ++ for program in &endpoint_programs { ++ let endp_i = program.endp_num_xhc as usize - 1; ++ input_context.add_context.writef(1 << program.endp_num_xhc, true); ++ input_context.device.endpoints[endp_i].a.write(program.a); ++ input_context.device.endpoints[endp_i].b.write(program.b); ++ input_context.device.endpoints[endp_i].trl.write(program.trl); ++ input_context.device.endpoints[endp_i].trh.write(program.trh); ++ input_context.device.endpoints[endp_i].c.write(program.c); ++ } ++ ++ (configure_snapshot, endpoint_snapshots, input_context.physical()) ++ }; ++ ++ let slot = self.port_states.get(&port).ok_or(Error::new(EBADFD))?.slot; ++ ++ let (event_trb, command_trb) = self ++ .execute_command(|trb, cycle| trb.configure_endpoint(slot, input_context_physical, cycle)) ++ .await; ++ ++ if let Err(err) = handle_event_trb("CONFIGURE_ENDPOINT", &event_trb, &command_trb) { ++ self.rollback_configure_attempt( ++ port, ++ slot, ++ configure_snapshot, ++ &endpoint_snapshots, ++ "CONFIGURE_ENDPOINT failure", ++ ) ++ .await; ++ return Err(err); ++ } ++ ++ if self.consume_test_hook("fail_after_configure_endpoint") { ++ info!( ++ "xhcid: test hook injecting failure after CONFIGURE_ENDPOINT for port {}", ++ port ++ ); ++ self.rollback_configure_attempt( ++ port, ++ slot, ++ configure_snapshot, ++ &endpoint_snapshots, ++ "test hook fail_after_configure_endpoint", ++ ) ++ .await; ++ return Err(Error::new(EIO)); ++ } ++ ++ if let Err(err) = self.set_configuration(port, configuration_value).await { ++ self.rollback_configure_attempt( ++ port, ++ slot, ++ configure_snapshot, ++ &endpoint_snapshots, ++ "set_configuration failure", ++ ) ++ .await; ++ return Err(err); ++ } + +- handle_event_trb("CONFIGURE_ENDPOINT", &event_trb, &command_trb)?; ++ if self.consume_test_hook("fail_after_set_configuration") { ++ info!( ++ "xhcid: test hook injecting failure after SET_CONFIGURATION for port {}", ++ port ++ ); ++ self.rollback_configure_attempt( ++ port, ++ slot, ++ configure_snapshot, ++ &endpoint_snapshots, ++ "test hook fail_after_set_configuration", ++ ) ++ .await; ++ return Err(Error::new(EIO)); + } + +- // Tell the device about this configuration. +- self.set_configuration(port, configuration_value).await?; ++ { ++ let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; ++ port_state.cfg_idx = Some(configuration_value); ++ port_state.endpoint_states.retain(|endp_num, _| *endp_num == 0); ++ for (endp_num, endpoint_state) in staged_endpoint_states { ++ port_state.endpoint_states.insert(endp_num, endpoint_state); ++ } ++ if let Some(iface_num) = req.interface_desc { ++ let alt = req.alternate_setting.unwrap_or(0); ++ port_state.active_ifaces.insert(iface_num, alt); ++ } else if port_state.active_ifaces.is_empty() { ++ let default_iface_entries: Vec<(u8, u8)> = port_state ++ .dev_desc ++ .as_ref() ++ .and_then(|dd| dd.config_descs.iter().find(|cd| cd.configuration_value == configuration_value)) ++ .map(|cd| { ++ cd.interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.alternate_setting == 0) ++ .map(|if_desc| (if_desc.number, 0u8)) ++ .collect() ++ }) ++ .unwrap_or_default(); ++ for (iface_num, alt) in default_iface_entries { ++ port_state.active_ifaces.insert(iface_num, alt); ++ } ++ } ++ } + + Ok(()) + } +@@ -1857,7 +2102,7 @@ impl Xhci { + if (flags & O_DIRECTORY != 0) || (flags & O_STAT != 0) { + let mut contents = Vec::new(); + +- write!(contents, "descriptors\nendpoints\n").unwrap(); ++ write!(contents, "descriptors\nendpoints\npm_state\nsuspend\nresume\n").unwrap(); + + if self.slot_state( + self.port_states +@@ -1894,6 +2139,14 @@ impl Xhci { + Ok(Handle::PortState(port_num)) + } + ++ fn open_handle_port_pm_state(&self, port_num: PortId, flags: usize) -> Result { ++ if flags & O_DIRECTORY != 0 && flags & O_STAT == 0 { ++ return Err(Error::new(ENOTDIR)); ++ } ++ ++ Ok(Handle::PortPmState(port_num)) ++ } ++ + /// implements open() for /port/endpoints + /// + /// # Arguments +@@ -2088,6 +2341,30 @@ impl Xhci { + Ok(Handle::DetachDevice(port_num)) + } + ++ fn open_handle_suspend_device(&self, port_num: PortId, flags: usize) -> Result { ++ if flags & O_DIRECTORY != 0 && flags & O_STAT == 0 { ++ return Err(Error::new(ENOTDIR)); ++ } ++ ++ if flags & O_RDWR != O_WRONLY && flags & O_STAT == 0 { ++ return Err(Error::new(EACCES)); ++ } ++ ++ Ok(Handle::SuspendDevice(port_num)) ++ } ++ ++ fn open_handle_resume_device(&self, port_num: PortId, flags: usize) -> Result { ++ if flags & O_DIRECTORY != 0 && flags & O_STAT == 0 { ++ return Err(Error::new(ENOTDIR)); ++ } ++ ++ if flags & O_RDWR != O_WRONLY && flags & O_STAT == 0 { ++ return Err(Error::new(EACCES)); ++ } ++ ++ Ok(Handle::ResumeDevice(port_num)) ++ } ++ + /// implements open() for /port/request + /// + /// # Arguments +@@ -2156,6 +2433,9 @@ impl SchemeSync for &Xhci { + SchemeParameters::PortState(port_number) => { + self.open_handle_port_state(port_number, flags)? + } ++ SchemeParameters::PortPmState(port_number) => { ++ self.open_handle_port_pm_state(port_number, flags)? ++ } + SchemeParameters::PortReq(port_number) => { + self.open_handle_port_request(port_number, flags)? + } +@@ -2174,6 +2454,12 @@ impl SchemeSync for &Xhci { + SchemeParameters::DetachDevice(port_number) => { + self.open_handle_detach_device(port_number, flags)? + } ++ SchemeParameters::SuspendDevice(port_number) => { ++ self.open_handle_suspend_device(port_number, flags)? ++ } ++ SchemeParameters::ResumeDevice(port_number) => { ++ self.open_handle_resume_device(port_number, flags)? ++ } + }; + + let fd = self.next_handle.fetch_add(1, atomic::Ordering::Relaxed); +@@ -2204,7 +2490,11 @@ impl SchemeSync for &Xhci { + + //If we have a handle to the configure scheme, we need to mark it as write only. + match &*guard { +- Handle::ConfigureEndpoints(_) | Handle::AttachDevice(_) | Handle::DetachDevice(_) => { ++ Handle::ConfigureEndpoints(_) ++ | Handle::AttachDevice(_) ++ | Handle::DetachDevice(_) ++ | Handle::SuspendDevice(_) ++ | Handle::ResumeDevice(_) => { + stat.st_mode = stat.st_mode | 0o200; + } + _ => {} +@@ -2254,6 +2544,8 @@ impl SchemeSync for &Xhci { + Handle::ConfigureEndpoints(_) => Err(Error::new(EBADF)), + Handle::AttachDevice(_) => Err(Error::new(EBADF)), + Handle::DetachDevice(_) => Err(Error::new(EBADF)), ++ Handle::SuspendDevice(_) => Err(Error::new(EBADF)), ++ Handle::ResumeDevice(_) => Err(Error::new(EBADF)), + Handle::SchemeRoot => Err(Error::new(EBADF)), + + &mut Handle::Endpoint(port_num, endp_num, ref mut st) => match st { +@@ -2285,6 +2577,10 @@ impl SchemeSync for &Xhci { + + Ok(Xhci::::write_dyn_string(string, buf, offset)) + } ++ &mut Handle::PortPmState(port_num) => { ++ let ps = self.port_states.get(&port_num).ok_or(Error::new(EBADF))?; ++ Ok(Xhci::::write_dyn_string(ps.pm_state.as_str().as_bytes(), buf, offset)) ++ } + &mut Handle::PortReq(port_num, ref mut st) => { + let state = std::mem::replace(st, PortReqState::Tmp); + drop(guard); // release the lock +@@ -2324,6 +2620,14 @@ impl SchemeSync for &Xhci { + block_on(self.detach_device(port_num))?; + Ok(buf.len()) + } ++ &mut Handle::SuspendDevice(port_num) => { ++ block_on(self.suspend_device(port_num))?; ++ Ok(buf.len()) ++ } ++ &mut Handle::ResumeDevice(port_num) => { ++ block_on(self.resume_device(port_num))?; ++ Ok(buf.len()) ++ } + &mut Handle::Endpoint(port_num, endp_num, ref ep_file_ty) => match ep_file_ty { + EndpointHandleTy::Ctl => block_on(self.on_write_endp_ctl(port_num, endp_num, buf)), + EndpointHandleTy::Data => { +@@ -2348,6 +2652,54 @@ impl SchemeSync for &Xhci { + } + + impl Xhci { ++ fn ensure_port_active(&self, port_num: PortId) -> Result<()> { ++ let port_state = self.port_states.get(&port_num).ok_or(Error::new(EBADFD))?; ++ ++ match port_state.pm_state { ++ super::PortPmState::Active => Ok(()), ++ super::PortPmState::Suspended => { ++ info!( ++ "xhcid: port {} rejected routable operation while suspended", ++ port_num ++ ); ++ Err(Error::new(EBUSY)) ++ } ++ } ++ } ++ ++ pub async fn suspend_device(&self, port_num: PortId) -> Result<()> { ++ let mut port_state = self.port_states.get_mut(&port_num).ok_or(Error::new(EBADFD))?; ++ ++ if port_state.pm_state != super::PortPmState::Active { ++ return Err(Error::new(EBUSY)); ++ } ++ ++ port_state.pm_state = super::PortPmState::Suspended; ++ info!("xhcid: suspended port {}", port_num); ++ Ok(()) ++ } ++ ++ pub async fn resume_device(&self, port_num: PortId) -> Result<()> { ++ let mut port_state = self.port_states.get_mut(&port_num).ok_or(Error::new(EBADFD))?; ++ ++ if port_state.pm_state == super::PortPmState::Active { ++ return Ok(()); ++ } ++ ++ let slot_state = self.slot_state(port_state.slot as usize); ++ if slot_state != SlotState::Addressed as u8 && slot_state != SlotState::Configured as u8 { ++ warn!( ++ "refusing to resume port {} while slot {} is in controller state {}", ++ port_num, port_state.slot, slot_state ++ ); ++ return Err(Error::new(EIO)); ++ } ++ ++ port_state.pm_state = super::PortPmState::Active; ++ info!("xhcid: resumed port {}", port_num); ++ Ok(()) ++ } ++ + pub fn get_endp_status(&self, port_num: PortId, endp_num: u8) -> Result { + let port_state = self.port_states.get(&port_num).ok_or(Error::new(EBADFD))?; + +@@ -2398,6 +2750,8 @@ impl Xhci { + endp_num: u8, + clear_feature: bool, + ) -> Result<()> { ++ self.ensure_port_active(port_num)?; ++ + if self.get_endp_status(port_num, endp_num)? != EndpointStatus::Halted { + return Err(Error::new(EPROTO)); + } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 306bdf6f..aa9a9742 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2025-10-03" +channel = "nightly-2026-04-01" components = ["rust-src", "rustfmt", "clippy"] profile = "minimal"