From 65acab85bbfa020531b473a0834117db76d6a377 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Sat, 25 Apr 2026 19:10:00 +0100 Subject: [PATCH] Split base cumulative patch and add relibc AIO stubs, KDE recipes Base patch extraction (8 topic-grouped patches from the 17k-line monolith): - P2-ps2d-improvements: PS/2 controller flush/retry, mouse state machine, named producers - P2-storage-error-handling: AHCI/IDE/NVMe/VirtIO unwrap/expect removal - P2-usb-pm-and-drivers: suspend/resume, SCSI enablement, staged port fallback - P2-network-error-handling: e1000/ixgbe/rtl8139/rtl8168d/virtio-net error propagation - P2-pcid-cfg-access: PCI config I/O port and ECAM graceful fallbacks - P2-ihdad-hda-stream: InputStream support, public stream types, Debug derives - P2-init-acpid-wiring: acpid weak dependency on drivers/hwd/pcid-spawner - P2-misc-daemon-fixes: audiod/usbhidd/zerod graceful degradation relibc P3-aio.patch: synchronous POSIX AIO fallback (aio_read, aio_write, aio_error, aio_return, aio_cancel, aio_suspend, aio_fsync, lio_listio) for Qt6 QIODevice compatibility. 36 patches total in relibc recipe. KDE recipes: breeze (widget style, decorations disabled), kde-cli-tools (kioclient, kreadconfig, etc., kdesu disabled). --- local/patches/base/P2-ihdad-hda-stream.patch | 148 +++++ local/patches/base/P2-init-acpid-wiring.patch | 45 ++ local/patches/base/P2-misc-daemon-fixes.patch | 126 ++++ .../base/P2-network-error-handling.patch | 118 ++++ local/patches/base/P2-pcid-cfg-access.patch | 169 +++++ local/patches/base/P2-ps2d-improvements.patch | 516 +++++++++++++++ .../base/P2-storage-error-handling.patch | 601 ++++++++++++++++++ .../patches/base/P2-usb-pm-and-drivers.patch | 158 +++++ local/patches/relibc/P3-aio.patch | 336 ++++++++++ local/recipes/kde/breeze/recipe.toml | 61 ++ local/recipes/kde/kde-cli-tools/recipe.toml | 56 ++ recipes/core/relibc/recipe.toml | 1 + 12 files changed, 2335 insertions(+) create mode 100644 local/patches/base/P2-ihdad-hda-stream.patch create mode 100644 local/patches/base/P2-init-acpid-wiring.patch create mode 100644 local/patches/base/P2-misc-daemon-fixes.patch create mode 100644 local/patches/base/P2-network-error-handling.patch create mode 100644 local/patches/base/P2-pcid-cfg-access.patch create mode 100644 local/patches/base/P2-ps2d-improvements.patch create mode 100644 local/patches/base/P2-storage-error-handling.patch create mode 100644 local/patches/base/P2-usb-pm-and-drivers.patch create mode 100644 local/patches/relibc/P3-aio.patch create mode 100644 local/recipes/kde/breeze/recipe.toml create mode 100644 local/recipes/kde/kde-cli-tools/recipe.toml diff --git a/local/patches/base/P2-ihdad-hda-stream.patch b/local/patches/base/P2-ihdad-hda-stream.patch new file mode 100644 index 00000000..37ae03dd --- /dev/null +++ b/local/patches/base/P2-ihdad-hda-stream.patch @@ -0,0 +1,148 @@ +# P2-ihdad-hda-stream.patch +# +# Intel HDA stream handling: add InputStream support, make stream types public, +# add digital/dispatch/fixup/parser modules, derive Debug for node types. +# +# Covers: +# - ihdad/hda/mod.rs: add digital, dispatch, fixup, parser modules +# - ihdad/hda/node.rs: derive Debug for HDANode +# - ihdad/hda/stream.rs: public SampleRate fields, Debug+Copy derives for BitsPerSample, +# new InputStream struct, StreamBuffer::read_block method +# +diff --git a/drivers/audio/ihdad/src/hda/mod.rs b/drivers/audio/ihdad/src/hda/mod.rs +index 7f01daf8..82ba89ae 100644 +--- a/drivers/audio/ihdad/src/hda/mod.rs ++++ b/drivers/audio/ihdad/src/hda/mod.rs +@@ -2,7 +2,11 @@ + pub mod cmdbuff; + pub mod common; + pub mod device; ++pub mod digital; ++pub mod dispatch; ++pub mod fixup; + pub mod node; ++pub mod parser; + pub mod stream; + + pub use self::node::*; +@@ -10,6 +14,8 @@ pub use self::stream::*; + + pub use self::cmdbuff::*; + pub use self::device::IntelHDA; ++pub use self::fixup::FixupEngine; ++pub use self::parser::AutoPinConfig; + pub use self::stream::BitsPerSample; + pub use self::stream::BufferDescriptorListEntry; + pub use self::stream::StreamBuffer; +diff --git a/drivers/audio/ihdad/src/hda/node.rs b/drivers/audio/ihdad/src/hda/node.rs +index 06c5121f..c1f9c31f 100644 +--- a/drivers/audio/ihdad/src/hda/node.rs ++++ b/drivers/audio/ihdad/src/hda/node.rs +@@ -1,7 +1,7 @@ + use super::common::*; + use std::{fmt, mem}; + +-#[derive(Clone)] ++#[derive(Clone, Debug)] + pub struct HDANode { + pub addr: WidgetAddr, + +diff --git a/drivers/audio/ihdad/src/hda/stream.rs b/drivers/audio/ihdad/src/hda/stream.rs +index caa3c364..a3f5ed73 100644 +--- a/drivers/audio/ihdad/src/hda/stream.rs ++++ b/drivers/audio/ihdad/src/hda/stream.rs +@@ -14,9 +14,9 @@ pub enum BaseRate { + } + + pub struct SampleRate { +- base: BaseRate, +- mult: u16, +- div: u16, ++ pub base: BaseRate, ++ pub mult: u16, ++ pub div: u16, + } + + use self::BaseRate::{BR44_1, BR48}; +@@ -78,6 +78,7 @@ pub const SR_192: SampleRate = SampleRate { + div: 1, + }; + ++#[derive(Debug, Clone, Copy)] + #[repr(u8)] + pub enum BitsPerSample { + Bits8 = 0, +@@ -271,6 +272,52 @@ impl OutputStream { + } + } + ++pub struct InputStream { ++ buff: StreamBuffer, ++ desc_regs: &'static mut StreamDescriptorRegs, ++} ++ ++impl InputStream { ++ pub fn new( ++ block_count: usize, ++ block_length: usize, ++ regs: &'static mut StreamDescriptorRegs, ++ ) -> InputStream { ++ InputStream { ++ buff: StreamBuffer::new(block_length, block_count).unwrap(), ++ desc_regs: regs, ++ } ++ } ++ ++ pub fn read_block(&mut self, buf: &mut [u8]) -> Result { ++ self.buff.read_block(buf) ++ } ++ ++ pub fn block_size(&self) -> usize { ++ self.buff.block_size() ++ } ++ ++ pub fn block_count(&self) -> usize { ++ self.buff.block_count() ++ } ++ ++ pub fn current_block(&self) -> usize { ++ self.buff.current_block() ++ } ++ ++ pub fn addr(&self) -> usize { ++ self.buff.addr() ++ } ++ ++ pub fn phys(&self) -> usize { ++ self.buff.phys() ++ } ++ ++ pub fn regs(&mut self) -> &mut StreamDescriptorRegs { ++ self.desc_regs ++ } ++} ++ + #[repr(C, packed)] + pub struct BufferDescriptorListEntry { + addr_low: Mmio, +@@ -379,6 +426,20 @@ impl StreamBuffer { + + Ok(len) + } ++ ++ pub fn read_block(&mut self, buf: &mut [u8]) -> Result { ++ let len = min(self.block_size(), buf.len()); ++ unsafe { ++ copy_nonoverlapping( ++ (self.addr() + self.current_block() * self.block_size()) as *const u8, ++ buf.as_mut_ptr(), ++ len, ++ ); ++ } ++ self.cur_pos += 1; ++ self.cur_pos %= self.block_count(); ++ Ok(len) ++ } + } + impl Drop for StreamBuffer { + fn drop(&mut self) { diff --git a/local/patches/base/P2-init-acpid-wiring.patch b/local/patches/base/P2-init-acpid-wiring.patch new file mode 100644 index 00000000..60922bfb --- /dev/null +++ b/local/patches/base/P2-init-acpid-wiring.patch @@ -0,0 +1,45 @@ +# P2-init-acpid-wiring.patch +# +# Init service acpid dependency wiring: add 41_acpid.service as a weak +# dependency to the drivers target, hardware manager, and PCI spawner so +# acpid starts reliably during boot. +# +# Covers: +# - 40_drivers.target: add 41_acpid.service to requires_weak +# - 40_hwd.service: add 41_acpid.service to requires_weak +# - 40_pcid-spawner-initfs.service: add 41_acpid.service to requires_weak +# +diff --git a/init.initfs.d/40_drivers.target b/init.initfs.d/40_drivers.target +index 8ddb4795..029583a1 100644 +--- a/init.initfs.d/40_drivers.target ++++ b/init.initfs.d/40_drivers.target +@@ -7,4 +7,5 @@ requires_weak = [ + "40_bcm2835-sdhcid.service", + "40_hwd.service", + "40_pcid-spawner-initfs.service", ++ "41_acpid.service", + ] +diff --git a/init.initfs.d/40_hwd.service b/init.initfs.d/40_hwd.service +index cba12dde..cf34a51b 100644 +--- a/init.initfs.d/40_hwd.service ++++ b/init.initfs.d/40_hwd.service +@@ -1,6 +1,6 @@ + [unit] + description = "Hardware manager" +-requires_weak = ["10_inputd.service", "10_lived.service", "20_graphics.target"] ++requires_weak = ["10_inputd.service", "10_lived.service", "20_graphics.target", "41_acpid.service"] + + [service] + cmd = "hwd" +diff --git a/init.initfs.d/40_pcid-spawner-initfs.service b/init.initfs.d/40_pcid-spawner-initfs.service +index 6945b9ea..ba1ee0bb 100644 +--- a/init.initfs.d/40_pcid-spawner-initfs.service ++++ b/init.initfs.d/40_pcid-spawner-initfs.service +@@ -1,6 +1,6 @@ + [unit] + description = "PCI driver spawner" +-requires_weak = ["10_inputd.service", "20_graphics.target", "40_hwd.service"] ++requires_weak = ["10_inputd.service", "20_graphics.target", "40_hwd.service", "41_acpid.service"] + + [service] + cmd = "pcid-spawner" diff --git a/local/patches/base/P2-misc-daemon-fixes.patch b/local/patches/base/P2-misc-daemon-fixes.patch new file mode 100644 index 00000000..18a8db86 --- /dev/null +++ b/local/patches/base/P2-misc-daemon-fixes.patch @@ -0,0 +1,126 @@ +# P2-misc-daemon-fixes.patch +# +# Various daemon error handling and robustness fixes: +# graceful degradation when audio hardware is absent, InputProducer migration +# for USB HID, zerod argument handling and request loop resilience. +# +# Covers: +# - audiod/main.rs: handle ENODEV when no audio hardware present +# - usbhidd/main.rs: migrate ProducerHandle→InputProducer with named producer +# - zerod/main.rs: derive Copy for Ty, graceful argument parsing, resilient request loop +# +diff --git a/audiod/src/main.rs b/audiod/src/main.rs +index 51b103af..2354cf5f 100644 +--- a/audiod/src/main.rs ++++ b/audiod/src/main.rs +@@ -48,7 +48,14 @@ fn daemon(daemon: SchemeDaemon) -> anyhow::Result<()> { + + let pid = libredox::call::getpid()?; + +- let hw_file = Fd::open("/scheme/audiohw", flag::O_WRONLY | flag::O_CLOEXEC, 0)?; ++ let hw_file = match Fd::open("/scheme/audiohw", flag::O_WRONLY | flag::O_CLOEXEC, 0) { ++ Ok(fd) => fd, ++ Err(err) if err.errno() == syscall::ENODEV => { ++ eprintln!("audiod: no audio hardware detected"); ++ return Ok(()); ++ } ++ Err(err) => return Err(err).context("failed to open /scheme/audiohw"), ++ }; + + let socket = Socket::create().context("failed to create scheme")?; + + +diff --git a/drivers/input/usbhidd/src/main.rs b/drivers/input/usbhidd/src/main.rs +index 15c5b778..706c4008 100644 +--- a/drivers/input/usbhidd/src/main.rs ++++ b/drivers/input/usbhidd/src/main.rs +@@ -1,7 +1,7 @@ + use anyhow::{Context, Result}; + use std::{env, thread, time}; + +-use inputd::ProducerHandle; ++use inputd::InputProducer; + use orbclient::KeyEvent as OrbKeyEvent; + use rehid::{ + report_desc::{ReportTy, REPORT_DESC_TY}, +@@ -15,7 +15,7 @@ use xhcid_interface::{ + + mod reqs; + +-fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pressed: bool) { ++fn send_key_event(display: &mut InputProducer, usage_page: u16, usage: u16, pressed: bool) { + let scancode = match usage_page { + 0x07 => match usage { + 0x04 => orbclient::K_A, +@@ -272,7 +272,9 @@ fn main() -> Result<()> { + let report_ty = ReportTy::Input; + let report_id = 0; + +- let mut display = ProducerHandle::new().context("Failed to open input socket")?; ++ let producer_name = format!("usb-{}-if{}", port, interface_num); ++ let mut display = InputProducer::new_named_or_fallback(&producer_name) ++ .context("Failed to open input socket")?; + let mut endpoint_opt = match endp_desc_opt { + Some((endp_num, _endp_desc)) => match handle.open_endpoint(endp_num as u8) { + Ok(ok) => Some(ok), + +diff --git a/zerod/src/main.rs b/zerod/src/main.rs +index c9bd1465..59f6b97c 100644 +--- a/zerod/src/main.rs ++++ b/zerod/src/main.rs +@@ -5,6 +5,7 @@ use scheme_utils::Blocking; + + mod scheme; + ++#[derive(Clone, Copy)] + enum Ty { + Null, + Zero, +@@ -15,21 +16,36 @@ fn main() { + } + + fn daemon(daemon: daemon::SchemeDaemon) -> ! { +- let ty = match &*std::env::args().nth(1).unwrap() { +- "null" => Ty::Null, +- "zero" => Ty::Zero, +- _ => panic!("needs to be called with either null or zero as argument"), ++ let ty = match std::env::args().nth(1).as_deref() { ++ Some("null") => Ty::Null, ++ Some("zero") | None => Ty::Zero, ++ Some(other) => { ++ eprintln!("zerod: unknown argument '{other}', use 'null' or 'zero'"); ++ std::process::exit(1); ++ } + }; + +- let socket = Socket::create().expect("zerod: failed to create zero scheme"); ++ let socket = match Socket::create() { ++ Ok(s) => s, ++ Err(e) => { ++ eprintln!("zerod: failed to create zero scheme: {e}"); ++ std::process::exit(1); ++ } ++ }; + let mut zero_scheme = ZeroScheme(ty); +- let zero_handler = Blocking::new(&socket, 16); + + let _ = daemon.ready_sync_scheme(&socket, &mut zero_scheme); + +- libredox::call::setrens(0, 0).expect("zerod: failed to enter null namespace"); +- +- zero_handler +- .process_requests_blocking(zero_scheme) +- .expect("zerod: failed to process events from zero scheme"); ++ if let Err(e) = libredox::call::setrens(0, 0) { ++ eprintln!("zerod: failed to enter null namespace: {e}"); ++ } ++ ++ loop { ++ let zero_handler = Blocking::new(&socket, 16); ++ let scheme = ZeroScheme(ty); ++ match zero_handler.process_requests_blocking(scheme) { ++ Ok(never) => never, ++ Err(e) => eprintln!("zerod: error processing requests: {e}"), ++ } ++ } + } diff --git a/local/patches/base/P2-network-error-handling.patch b/local/patches/base/P2-network-error-handling.patch new file mode 100644 index 00000000..159a9c05 --- /dev/null +++ b/local/patches/base/P2-network-error-handling.patch @@ -0,0 +1,118 @@ +# P2-network-error-handling.patch +# +# Network driver error handling: replace unwrap()/expect()/panic!() with proper +# error propagation and graceful exits across e1000, ixgbe, rtl8139, rtl8168d, +# and virtio-net drivers. +# +# Covers: +# - e1000d/device.rs: replace unreachable!() in DMA array conversion +# - ixgbed/Cargo.toml: add log dependency +# - rtl8139d/device.rs: replace unreachable!() with EIO error +# - rtl8168d/device.rs: replace unreachable!() with EIO error +# - virtio-netd/scheme.rs: DMA allocation error handling for rx buffers +# +diff --git a/drivers/net/e1000d/src/device.rs b/drivers/net/e1000d/src/device.rs +index 4c518f30..0e42d72b 100644 +--- a/drivers/net/e1000d/src/device.rs ++++ b/drivers/net/e1000d/src/device.rs +@@ -3,7 +3,7 @@ use std::{cmp, mem, ptr, slice, thread, time}; + + use driver_network::NetworkAdapter; + +-use syscall::error::Result; ++use syscall::error::{Error, Result, EIO}; + + use common::dma::Dma; + +@@ -207,11 +207,10 @@ impl NetworkAdapter for Intel8254x { + } + + fn dma_array() -> Result<[Dma; N]> { +- Ok((0..N) ++ let vec: Vec> = (0..N) + .map(|_| Ok(unsafe { Dma::zeroed()?.assume_init() })) +- .collect::>>()? +- .try_into() +- .unwrap_or_else(|_| unreachable!())) ++ .collect::>>()?; ++ vec.try_into().map_err(|_| Error::new(EIO)) + } + impl Intel8254x { + pub unsafe fn new(base: usize) -> Result { + +diff --git a/drivers/net/ixgbed/Cargo.toml b/drivers/net/ixgbed/Cargo.toml +index d97ff398..fcaf4b19 100644 +--- a/drivers/net/ixgbed/Cargo.toml ++++ b/drivers/net/ixgbed/Cargo.toml +@@ -7,6 +7,7 @@ edition = "2021" + [dependencies] + bitflags.workspace = true + libredox.workspace = true ++log.workspace = true + redox_event.workspace = true + redox_syscall.workspace = true + + +diff --git a/drivers/net/rtl8139d/src/device.rs b/drivers/net/rtl8139d/src/device.rs +index 37167ee2..d7428132 100644 +--- a/drivers/net/rtl8139d/src/device.rs ++++ b/drivers/net/rtl8139d/src/device.rs +@@ -215,7 +215,7 @@ impl Rtl8139 { + .map(|_| Ok(Dma::zeroed()?.assume_init())) + .collect::>>()? + .try_into() +- .unwrap_or_else(|_| unreachable!()), ++ .map_err(|_| Error::new(EIO))?, + transmit_i: 0, + mac_address: [0; 6], + }; + +diff --git a/drivers/net/rtl8168d/src/device.rs b/drivers/net/rtl8168d/src/device.rs +index ae545ec4..7229a52d 100644 +--- a/drivers/net/rtl8168d/src/device.rs ++++ b/drivers/net/rtl8168d/src/device.rs +@@ -177,7 +177,7 @@ impl Rtl8168 { + .map(|_| Ok(Dma::zeroed()?.assume_init())) + .collect::>>()? + .try_into() +- .unwrap_or_else(|_| unreachable!()), ++ .map_err(|_| Error::new(EIO))?, + + receive_ring: Dma::zeroed()?.assume_init(), + receive_i: 0, +@@ -185,7 +185,7 @@ impl Rtl8168 { + .map(|_| Ok(Dma::zeroed()?.assume_init())) + .collect::>>()? + .try_into() +- .unwrap_or_else(|_| unreachable!()), ++ .map_err(|_| Error::new(EIO))?, + transmit_ring: Dma::zeroed()?.assume_init(), + transmit_i: 0, + transmit_buffer_h: [Dma::zeroed()?.assume_init()], + +diff --git a/drivers/net/virtio-netd/src/scheme.rs b/drivers/net/virtio-netd/src/scheme.rs +index 59b3b93e..d0acb2ba 100644 +--- a/drivers/net/virtio-netd/src/scheme.rs ++++ b/drivers/net/virtio-netd/src/scheme.rs +@@ -27,11 +27,16 @@ impl<'a> VirtioNet<'a> { + // Populate all of the `rx_queue` with buffers to maximize performence. + let mut rx_buffers = vec![]; + for i in 0..(rx.descriptor_len() as usize) { +- rx_buffers.push(unsafe { +- Dma::<[u8]>::zeroed_slice(MAX_BUFFER_LEN) +- .unwrap() +- .assume_init() +- }); ++ let buf = unsafe { ++ match Dma::<[u8]>::zeroed_slice(MAX_BUFFER_LEN) { ++ Ok(dma) => dma.assume_init(), ++ Err(err) => { ++ log::error!("virtio-netd: failed to allocate rx buffer: {err}"); ++ continue; ++ } ++ } ++ }; ++ rx_buffers.push(buf); + + let chain = ChainBuilder::new() + .chain(Buffer::new_unsized(&rx_buffers[i]).flags(DescriptorFlags::WRITE_ONLY)) diff --git a/local/patches/base/P2-pcid-cfg-access.patch b/local/patches/base/P2-pcid-cfg-access.patch new file mode 100644 index 00000000..53ea9766 --- /dev/null +++ b/local/patches/base/P2-pcid-cfg-access.patch @@ -0,0 +1,169 @@ +# P2-pcid-cfg-access.patch +# +# PCI config access error handling: replace unwrap()/expect()/assert!() with +# proper error returns and graceful fallbacks in PCI configuration space access +# (both I/O port fallback and ECAM/DTB paths). +# +# Covers: +# - pcid/cfg_access/fallback.rs: I/O port rights error logging, offset overflow returns +# - pcid/cfg_access/mod.rs: DTB property access with proper error propagation, +# bus-range validation, interrupt-map phandle/property fallbacks +# +diff --git a/drivers/pcid/src/cfg_access/fallback.rs b/drivers/pcid/src/cfg_access/fallback.rs +index 671d17f7..ea8f69f8 100644 +--- a/drivers/pcid/src/cfg_access/fallback.rs ++++ b/drivers/pcid/src/cfg_access/fallback.rs +@@ -33,7 +33,12 @@ impl Pci { + "PCI: couldn't find or access PCIe extended configuration, \ + and thus falling back to PCI 3.0 io ports" + ); +- common::acquire_port_io_rights().expect("pcid: failed to get IO port rights"); ++ common::acquire_port_io_rights() ++ .map_err(|err| { ++ log::error!("pcid: failed to get IO port rights: {err}"); ++ err ++ }) ++ .expect("pcid: IO port rights required for PCI 3.0 fallback"); + } + }); + } +@@ -61,8 +66,9 @@ impl ConfigRegionAccess for Pci { + + Self::set_iopl(); + +- let offset = +- u8::try_from(offset).expect("offset too large for PCI 3.0 configuration space"); ++ let Ok(offset) = u8::try_from(offset) else { ++ return 0xFFFFFFFF; ++ }; + let address = Self::address(address, offset); + + Pio::::new(0xCF8).write(address); +@@ -74,8 +80,9 @@ impl ConfigRegionAccess for Pci { + + Self::set_iopl(); + +- let offset = +- u8::try_from(offset).expect("offset too large for PCI 3.0 configuration space"); ++ let Ok(offset) = u8::try_from(offset) else { ++ return; ++ }; + let address = Self::address(address, offset); + + Pio::::new(0xCF8).write(address); +diff --git a/drivers/pcid/src/cfg_access/mod.rs b/drivers/pcid/src/cfg_access/mod.rs +index c2552448..0fe215a6 100644 +--- a/drivers/pcid/src/cfg_access/mod.rs ++++ b/drivers/pcid/src/cfg_access/mod.rs +@@ -38,42 +38,57 @@ fn locate_ecam_dtb( + ) + })?; + +- let address = node.reg().unwrap().next().unwrap().starting_address as u64; ++ let mut reg = node.reg().ok_or_else(|| { ++ io::Error::new(io::ErrorKind::NotFound, "pci-host-ecam-generic missing 'reg' property") ++ })?; ++ let address = reg.next().ok_or_else(|| { ++ io::Error::new(io::ErrorKind::NotFound, "pci-host-ecam-generic 'reg' has no entries") ++ })?.starting_address as u64; + +- let bus_range = node.property("bus-range").unwrap(); +- assert_eq!(bus_range.value.len(), 8); +- let start_bus = u32::from_be_bytes(<[u8; 4]>::try_from(&bus_range.value[0..4]).unwrap()); +- let end_bus = u32::from_be_bytes(<[u8; 4]>::try_from(&bus_range.value[4..8]).unwrap()); ++ let bus_range = node.property("bus-range").ok_or_else(|| { ++ io::Error::new(io::ErrorKind::NotFound, "pci-host-ecam-generic missing 'bus-range' property") ++ })?; ++ if bus_range.value.len() != 8 { ++ return Err(io::Error::new(io::ErrorKind::InvalidData, "pci-host-ecam-generic 'bus-range' not 8 bytes")); ++ } ++ let start_bus = u32::from_be_bytes(<[u8; 4]>::try_from(&bus_range.value[0..4]).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bus-range start parse failed"))?); ++ let end_bus = u32::from_be_bytes(<[u8; 4]>::try_from(&bus_range.value[4..8]).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bus-range end parse failed"))?); + +- // address-cells == 3, size-cells == 2, interrupt-cells == 1 +- let mut interrupt_map_data = node +- .property("interrupt-map") +- .unwrap() ++ let interrupt_map_prop = node.property("interrupt-map").ok_or_else(|| { ++ io::Error::new(io::ErrorKind::NotFound, "pci-host-ecam-generic missing 'interrupt-map' property") ++ })?; ++ let mut interrupt_map_data = interrupt_map_prop + .value + .chunks_exact(4) +- .map(|x| u32::from_be_bytes(<[u8; 4]>::try_from(x).unwrap())); ++ .map(|x| u32::from_be_bytes(<[u8; 4]>::try_from(x).unwrap_or([0, 0, 0, 0]))); + let mut interrupt_map = Vec::::new(); + while let Ok([addr1, addr2, addr3, int1, phandle]) = interrupt_map_data.next_chunk::<5>() { +- let parent = dt.find_phandle(phandle).unwrap(); +- let parent_address_cells = u32::from_be_bytes( +- parent.property("#address-cells").unwrap().value[..4] +- .try_into() +- .unwrap(), +- ); ++ let Some(parent) = dt.find_phandle(phandle) else { ++ log::warn!("pcid: DTB interrupt-map references phandle {phandle} not found, skipping"); ++ continue; ++ }; ++ let parent_address_cells = match parent.property("#address-cells") { ++ Some(prop) => u32::from_be_bytes( ++ prop.value[..4] ++ .try_into() ++ .unwrap_or([0, 0, 0, 0]), ++ ), ++ None => 0, ++ }; + match parent_address_cells { + 0 => {} + 1 => { +- assert_eq!(interrupt_map_data.next().unwrap(), 0); ++ let _ = interrupt_map_data.next(); + } + 2 => { +- assert_eq!(interrupt_map_data.next_chunk::<2>().unwrap(), [0, 0]); ++ let _ = interrupt_map_data.next_chunk::<2>(); + } + 3 => { +- assert_eq!(interrupt_map_data.next_chunk::<3>().unwrap(), [0, 0, 0]); ++ let _ = interrupt_map_data.next_chunk::<3>(); + } + _ => break, + }; +- let parent_interrupt_cells = parent.interrupt_cells().unwrap(); ++ let parent_interrupt_cells = parent.interrupt_cells().unwrap_or(1); + let parent_interrupt = match parent_interrupt_cells { + 1 if let Some(a) = interrupt_map_data.next() => [a, 0, 0], + 2 if let Ok([a, b]) = interrupt_map_data.next_chunk::<2>() => [a, b, 0], +@@ -94,8 +109,8 @@ fn locate_ecam_dtb( + let mut cells = interrupt_mask_node + .value + .chunks_exact(4) +- .map(|x| u32::from_be_bytes(<[u8; 4]>::try_from(x).unwrap())); +- cells.next_chunk::<4>().unwrap().to_owned() ++ .map(|x| u32::from_be_bytes(<[u8; 4]>::try_from(x).unwrap_or([0, 0, 0, 0]))); ++ cells.next_chunk::<4>().unwrap_or([u32::MAX, u32::MAX, u32::MAX, u32::MAX]).to_owned() + } else { + [u32::MAX, u32::MAX, u32::MAX, u32::MAX] + }; +@@ -104,8 +119,8 @@ fn locate_ecam_dtb( + PcieAllocs(&[PcieAlloc { + base_addr: address, + seg_group_num: 0, +- start_bus: start_bus.try_into().unwrap(), +- end_bus: end_bus.try_into().unwrap(), ++ start_bus: start_bus.try_into().map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "start_bus overflow"))?, ++ end_bus: end_bus.try_into().map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "end_bus overflow"))?, + _rsvd: [0; 4], + }]), + interrupt_map, +@@ -165,7 +180,10 @@ impl Mcfg { + // crashing. `as_encoded_bytes()` returns some superset + // of ASCII, so directly comparing it with an ASCII name + // is fine. +- let table_filename = table_path.file_name().unwrap().as_encoded_bytes(); ++ let table_filename = match table_path.file_name() { ++ Some(name) => name.as_encoded_bytes(), ++ None => continue, ++ }; + if table_filename.get(0..4) == Some(&MCFG_NAME) { + let bytes = fs::read(table_path)?.into_boxed_slice(); + match Mcfg::parse(&*bytes) { diff --git a/local/patches/base/P2-ps2d-improvements.patch b/local/patches/base/P2-ps2d-improvements.patch new file mode 100644 index 00000000..2eb47326 --- /dev/null +++ b/local/patches/base/P2-ps2d-improvements.patch @@ -0,0 +1,516 @@ +# P2-ps2d-improvements.patch +# +# PS/2 controller improvements: flush/retry logic, mouse state machine, +# separate keyboard/mouse input producers. +# +# Covers: +# - ps2d/controller.rs: flush stale bytes, self-test with retry, AUX port test +# - ps2d/main.rs: separate InputProducer for keyboard and mouse +# - ps2d/mouse.rs: ACK/RESEND/BAT constant names, resend handling, state machine fixes +# - ps2d/state.rs: dual InputProducer fields, non-fatal init error handling +# +diff --git a/drivers/input/ps2d/src/controller.rs b/drivers/input/ps2d/src/controller.rs +index d7af4cba..638b7cc1 100644 +--- a/drivers/input/ps2d/src/controller.rs ++++ b/drivers/input/ps2d/src/controller.rs +@@ -97,6 +97,14 @@ enum KeyboardCommandData { + const DEFAULT_TIMEOUT: u64 = 50_000; + // Reset timeout in microseconds + const RESET_TIMEOUT: u64 = 1_000_000; ++// Maximum bytes to drain during flush (Linux: I8042_BUFFER_SIZE) ++const FLUSH_LIMIT: usize = 4096; ++// Controller self-test pass value (Linux: I8042_RET_CTL_TEST) ++const SELFTEST_PASS: u8 = 0x55; ++// Controller self-test retries (Linux: 5 attempts) ++const SELFTEST_RETRIES: usize = 5; ++// AUX port test pass value (Linux returns 0x00 on success) ++const AUX_TEST_PASS: u8 = 0x00; + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + pub struct Ps2 { +@@ -271,6 +279,50 @@ impl Ps2 { + } + } + ++ /// Drain all pending bytes from the controller output buffer. ++ /// Borrowed from Linux i8042_flush(): stale firmware/BIOS bytes can be ++ /// misinterpreted as device responses during initialization. ++ fn flush(&mut self) -> usize { ++ let mut count = 0; ++ while self.status().contains(StatusFlags::OUTPUT_FULL) { ++ if count >= FLUSH_LIMIT { ++ warn!("flush: exceeded limit, controller may be stuck"); ++ break; ++ } ++ let data = self.data.read(); ++ trace!("flush: discarded {:02X}", data); ++ count += 1; ++ } ++ if count > 0 { ++ debug!("flushed {} stale bytes from controller", count); ++ } ++ count ++ } ++ ++ /// Test the AUX (mouse) port via controller command 0xA9. ++ /// Borrowed from Linux: verifies electrical connectivity before ++ /// attempting to talk to the mouse. Returns true if the port passed. ++ fn test_aux_port(&mut self) -> bool { ++ if let Err(err) = self.command(Command::TestSecond) { ++ warn!("aux port test command failed: {:?}", err); ++ return false; ++ } ++ match self.read() { ++ Ok(AUX_TEST_PASS) => { ++ debug!("aux port test passed"); ++ true ++ } ++ Ok(val) => { ++ warn!("aux port test failed: {:02X}", val); ++ false ++ } ++ Err(err) => { ++ warn!("aux port test read timeout: {:?}", err); ++ false ++ } ++ } ++ } ++ + pub fn init_keyboard(&mut self) -> Result<(), Error> { + let mut b; + +@@ -308,66 +360,125 @@ impl Ps2 { + } + + pub fn init(&mut self) -> Result<(), Error> { ++ // Linux i8042_controller_check(): verify controller is present by ++ // flushing any stale data. A stuck output buffer means no controller. ++ self.flush(); ++ ++ // Bare-metal controllers may be slow after firmware handoff. ++ // Give the controller a moment to finish POST before sending commands. ++ std::thread::sleep(std::time::Duration::from_millis(50)); ++ + { +- // Disable devices +- self.command(Command::DisableFirst)?; +- self.command(Command::DisableSecond)?; ++ // Disable both ports first — use retry because the controller ++ // may still be settling or temporarily unresponsive. ++ // Failure here is non-fatal: we continue and attempt the rest ++ // of initialization. A truly absent controller will fail later ++ // at self-test or keyboard reset. ++ if let Err(err) = self.retry( ++ format_args!("disable first port"), ++ 3, ++ |x| x.command(Command::DisableFirst), ++ ) { ++ warn!("disable first port failed: {:?}", err); ++ } ++ if let Err(err) = self.retry( ++ format_args!("disable second port"), ++ 3, ++ |x| x.command(Command::DisableSecond), ++ ) { ++ warn!("disable second port failed: {:?}", err); ++ } + } + +- // Disable clocks, disable interrupts, and disable translate ++ // Flush again after disabling — firmware may have queued more bytes ++ self.flush(); ++ ++ // Linux i8042_controller_init() step 1: write a known-safe config ++ // (interrupts off, both ports disabled) so stale config can't cause ++ // spurious interrupts during the rest of init. + { +- // Since the default config may have interrupts enabled, and the kernel may eat up +- // our data in that case, we will write a config without reading the current one + let config = ConfigFlags::POST_PASSED + | ConfigFlags::FIRST_DISABLED + | ConfigFlags::SECOND_DISABLED; + self.set_config(config)?; + } + +- // The keyboard seems to still collect bytes even when we disable +- // the port, so we must disable the keyboard too ++ // Linux i8042_controller_selftest(): retry up to 5 times with delay. ++ // "On some really fragile systems this does not take the first time." ++ { ++ let mut passed = false; ++ for attempt in 0..SELFTEST_RETRIES { ++ if let Err(err) = self.command(Command::TestController) { ++ warn!("self-test command failed (attempt {}): {:?}", attempt + 1, err); ++ continue; ++ } ++ match self.read() { ++ Ok(SELFTEST_PASS) => { ++ passed = true; ++ break; ++ } ++ Ok(val) => { ++ warn!( ++ "self-test unexpected value {:02X} (attempt {}/{})", ++ val, ++ attempt + 1, ++ SELFTEST_RETRIES ++ ); ++ } ++ Err(err) => { ++ warn!("self-test read timeout (attempt {}): {:?}", attempt + 1, err); ++ } ++ } ++ // Linux: msleep(50) between retries ++ std::thread::sleep(std::time::Duration::from_millis(50)); ++ } ++ if !passed { ++ // Linux on x86: "giving up on controller selftest, continuing anyway" ++ warn!("controller self-test did not pass after {} attempts, continuing", SELFTEST_RETRIES); ++ } ++ } ++ ++ // Flush any bytes the self-test may have left behind ++ self.flush(); ++ ++ // Linux i8042_controller_init() step 2: set keyboard defaults ++ // (disable scanning so keyboard doesn't send scancodes during init) + self.retry(format_args!("keyboard defaults"), 4, |x| { +- // Set defaults and disable scanning + let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?; + if b != 0xFA { + error!("keyboard failed to set defaults: {:02X}", b); + return Err(Error::CommandRetry); + } +- + Ok(b) + })?; + +- { +- // Perform the self test +- self.command(Command::TestController)?; +- let r = self.read()?; +- if r != 0x55 { +- warn!("self test unexpected value: {:02X}", r); +- } +- } +- + // Initialize keyboard + if let Err(err) = self.init_keyboard() { + error!("failed to initialize keyboard: {:?}", err); + return Err(err); + } + +- // Enable second device +- let enable_mouse = match self.command(Command::EnableSecond) { +- Ok(()) => true, +- Err(err) => { +- error!("failed to initialize mouse: {:?}", err); +- false ++ // Linux: test AUX port (command 0xA9) before enabling. ++ // Skips mouse init entirely if the port is not electrically present. ++ let aux_ok = self.test_aux_port(); ++ ++ // Enable second device (mouse) only if AUX port tested OK ++ let enable_mouse = if aux_ok { ++ match self.command(Command::EnableSecond) { ++ Ok(()) => true, ++ Err(err) => { ++ warn!("failed to enable aux port after test passed: {:?}", err); ++ false ++ } + } ++ } else { ++ info!("skipping mouse init: aux port test did not pass"); ++ false + }; + + { +- // Enable keyboard data reporting +- // Use inner function to prevent retries +- // Response is ignored since scanning is now on + if let Err(err) = self.keyboard_command_inner(KeyboardCommand::EnableReporting as u8) { + error!("failed to initialize keyboard reporting: {:?}", err); +- //TODO: fix by using interrupts? + } + } + +diff --git a/drivers/input/ps2d/src/main.rs b/drivers/input/ps2d/src/main.rs +index db17de2a..1ae055e4 100644 +--- a/drivers/input/ps2d/src/main.rs ++++ b/drivers/input/ps2d/src/main.rs +@@ -11,7 +11,7 @@ use std::process; + + use common::acquire_port_io_rights; + use event::{user_data, EventQueue}; +-use inputd::ProducerHandle; ++use inputd::InputProducer; + + use crate::state::Ps2d; + +@@ -31,7 +31,10 @@ fn daemon(daemon: daemon::Daemon) -> ! { + + acquire_port_io_rights().expect("ps2d: failed to get I/O permission"); + +- let input = ProducerHandle::new().expect("ps2d: failed to open input producer"); ++ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard") ++ .expect("ps2d: failed to open input producer"); ++ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse") ++ .expect("ps2d: failed to open input producer"); + + user_data! { + enum Source { +@@ -93,7 +96,7 @@ fn daemon(daemon: daemon::Daemon) -> ! { + + daemon.ready(); + +- let mut ps2d = Ps2d::new(input, time_file); ++ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file); + + let mut data = [0; 256]; + for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) { +diff --git a/drivers/input/ps2d/src/mouse.rs b/drivers/input/ps2d/src/mouse.rs +index 9e95ab88..8087c8c4 100644 +--- a/drivers/input/ps2d/src/mouse.rs ++++ b/drivers/input/ps2d/src/mouse.rs +@@ -5,6 +5,11 @@ pub const RESET_RETRIES: usize = 10; + pub const RESET_TIMEOUT: Duration = Duration::from_millis(1000); + pub const COMMAND_TIMEOUT: Duration = Duration::from_millis(100); + ++const CMD_ACK: u8 = 0xFA; ++const CMD_RESEND: u8 = 0xFE; ++const BAT_COMPLETE: u8 = 0xAA; ++const BAT_FAIL: u8 = 0xFC; ++ + #[derive(Clone, Copy, Debug)] + #[repr(u8)] + #[allow(dead_code)] +@@ -58,9 +63,11 @@ impl MouseTx { + + fn handle(&mut self, data: u8, ps2: &mut Ps2) -> Result { + if self.write_i < self.write.len() { +- if data == 0xFA { ++ if data == CMD_ACK { + self.write_i += 1; + self.try_write(ps2)?; ++ } else if data == CMD_RESEND { ++ self.try_write(ps2)?; + } else { + log::error!("unknown mouse response {:02X}", data); + return Err(()); +@@ -251,25 +258,43 @@ impl MouseState { + MouseResult::None + } + MouseState::Reset => { +- if data == 0xFA { +- log::debug!("mouse reset ok"); ++ if data == CMD_ACK { ++ log::debug!("mouse reset ack"); + MouseResult::Timeout(RESET_TIMEOUT) +- } else if data == 0xAA { ++ } else if data == BAT_COMPLETE { + log::debug!("BAT completed"); + *self = MouseState::Bat; + MouseResult::Timeout(COMMAND_TIMEOUT) ++ } else if data == CMD_RESEND { ++ // Device asks us to resend the reset command (0xFF). ++ // Resend WITHOUT incrementing the retry counter — 0xFE is ++ // a normal protocol response, not a failure. ++ log::debug!("mouse requests resend during reset, resending 0xFF"); ++ match ps2.mouse_command_async(MouseCommand::Reset as u8) { ++ Ok(()) => MouseResult::Timeout(RESET_TIMEOUT), ++ Err(err) => { ++ log::error!("failed to resend mouse reset: {:?}", err); ++ self.reset(ps2) ++ } ++ } ++ } else if data == BAT_FAIL { ++ log::warn!("mouse BAT failed (0xFC)"); ++ self.reset(ps2) + } else { + log::warn!("unknown mouse response {:02X} after reset", data); + self.reset(ps2) + } + } + MouseState::Bat => { +- if data == MouseId::Base as u8 { +- // Enable intellimouse features ++ if data == CMD_RESEND { ++ // 0xFE after BAT is unusual — the device may be re-issuing ++ // BAT. Wait for the next byte (device ID or another BAT). ++ log::debug!("mouse resend (0xFE) during BAT, waiting"); ++ MouseResult::Timeout(COMMAND_TIMEOUT) ++ } else if data == MouseId::Base as u8 { + log::debug!("BAT mouse id {:02X} (base)", data); + self.identify_touchpad(ps2) + } else if data == MouseId::Intellimouse1 as u8 { +- // Extra packet already enabled + log::debug!("BAT mouse id {:02X} (intellimouse)", data); + self.enable_reporting(data, ps2) + } else { +@@ -320,10 +345,17 @@ impl MouseState { + } + } + MouseState::DeviceId => { +- if data == 0xFA { +- // Command OK response +- //TODO: handle this separately? ++ if data == CMD_ACK { + MouseResult::Timeout(COMMAND_TIMEOUT) ++ } else if data == CMD_RESEND { ++ log::debug!("mouse resend during DeviceId, resending GetDeviceId"); ++ match ps2.mouse_command_async(MouseCommand::GetDeviceId as u8) { ++ Ok(()) => MouseResult::Timeout(COMMAND_TIMEOUT), ++ Err(err) => { ++ log::error!("failed to resend GetDeviceId: {:?}", err); ++ self.reset(ps2) ++ } ++ } + } else if data == MouseId::Base as u8 || data == MouseId::Intellimouse1 as u8 { + log::debug!("mouse id {:02X}", data); + self.enable_reporting(data, ps2) +@@ -333,10 +365,28 @@ impl MouseState { + } + } + MouseState::EnableReporting { id } => { +- log::debug!("mouse id {:02X} enable reporting {:02X}", id, data); +- //TODO: handle response ok/error +- *self = MouseState::Streaming { id }; +- MouseResult::None ++ if data == CMD_ACK { ++ log::debug!("mouse id {:02X} reporting enabled", id); ++ *self = MouseState::Streaming { id }; ++ MouseResult::None ++ } else if data == CMD_RESEND { ++ log::debug!("mouse resend during EnableReporting, resending 0xF4"); ++ match ps2.mouse_command_async(MouseCommand::EnableReporting as u8) { ++ Ok(()) => MouseResult::Timeout(COMMAND_TIMEOUT), ++ Err(err) => { ++ log::error!("failed to resend EnableReporting: {:?}", err); ++ *self = MouseState::Streaming { id }; ++ MouseResult::None ++ } ++ } ++ } else { ++ log::warn!( ++ "unexpected mouse response {:02X} during enable reporting, streaming anyway", ++ data ++ ); ++ *self = MouseState::Streaming { id }; ++ MouseResult::None ++ } + } + MouseState::Streaming { id } => { + MouseResult::Packet(data, id == MouseId::Intellimouse1 as u8) +diff --git a/drivers/input/ps2d/src/state.rs b/drivers/input/ps2d/src/state.rs +index 9018dc6b..da304e05 100644 +--- a/drivers/input/ps2d/src/state.rs ++++ b/drivers/input/ps2d/src/state.rs +@@ -1,4 +1,4 @@ +-use inputd::ProducerHandle; ++use inputd::InputProducer; + use log::{error, warn}; + use orbclient::{ButtonEvent, KeyEvent, MouseEvent, MouseRelativeEvent, ScrollEvent}; + use std::{ +@@ -44,7 +44,8 @@ pub struct Ps2d { + ps2: Ps2, + vmmouse: bool, + vmmouse_relative: bool, +- input: ProducerHandle, ++ keyboard_input: InputProducer, ++ mouse_input: InputProducer, + time_file: File, + extended: bool, + mouse_x: i32, +@@ -59,9 +60,11 @@ pub struct Ps2d { + } + + impl Ps2d { +- pub fn new(input: ProducerHandle, time_file: File) -> Self { ++ pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self { + let mut ps2 = Ps2::new(); +- ps2.init().expect("failed to initialize"); ++ if let Err(err) = ps2.init() { ++ log::error!("ps2d: controller init failed: {:?}", err); ++ } + + // FIXME add an option for orbital to disable this when an app captures the mouse. + let vmmouse_relative = false; +@@ -77,7 +80,8 @@ impl Ps2d { + ps2, + vmmouse, + vmmouse_relative, +- input, ++ keyboard_input, ++ mouse_input, + time_file, + extended: false, + mouse_x: 0, +@@ -273,7 +277,7 @@ impl Ps2d { + }; + + if scancode != 0 { +- self.input ++ self.keyboard_input + .write_event( + KeyEvent { + character: '\0', +@@ -304,7 +308,7 @@ impl Ps2d { + + if self.vmmouse_relative { + if dx != 0 || dy != 0 { +- self.input ++ self.mouse_input + .write_event( + MouseRelativeEvent { + dx: dx as i32, +@@ -320,14 +324,14 @@ impl Ps2d { + if x != self.mouse_x || y != self.mouse_y { + self.mouse_x = x; + self.mouse_y = y; +- self.input ++ self.mouse_input + .write_event(MouseEvent { x, y }.to_event()) + .expect("ps2d: failed to write mouse event"); + } + }; + + if dz != 0 { +- self.input ++ self.mouse_input + .write_event( + ScrollEvent { + x: 0, +@@ -348,7 +352,7 @@ impl Ps2d { + self.mouse_left = left; + self.mouse_middle = middle; + self.mouse_right = right; +- self.input ++ self.mouse_input + .write_event( + ButtonEvent { + left, +@@ -441,13 +445,13 @@ impl Ps2d { + } + + if dx != 0 || dy != 0 { +- self.input ++ self.mouse_input + .write_event(MouseRelativeEvent { dx, dy }.to_event()) + .expect("ps2d: failed to write mouse event"); + } + + if dz != 0 { +- self.input ++ self.mouse_input + .write_event(ScrollEvent { x: 0, y: dz }.to_event()) + .expect("ps2d: failed to write scroll event"); + } +@@ -462,7 +466,7 @@ impl Ps2d { + self.mouse_left = left; + self.mouse_middle = middle; + self.mouse_right = right; +- self.input ++ self.mouse_input + .write_event( + ButtonEvent { + left, diff --git a/local/patches/base/P2-storage-error-handling.patch b/local/patches/base/P2-storage-error-handling.patch new file mode 100644 index 00000000..cde8c72f --- /dev/null +++ b/local/patches/base/P2-storage-error-handling.patch @@ -0,0 +1,601 @@ +# P2-storage-error-handling.patch +# +# Storage driver error handling: replace unwrap()/expect()/panic!() with proper +# error propagation and graceful exits across AHCI, IDE, NVMe, and VirtIO block drivers. +# +# Covers: +# - ahcid/disk_ata.rs: replace unreachable!() with EIO error +# - ahcid/disk_atapi.rs: replace unreachable!() with EBADF error +# - ahcid/hba.rs: DMA allocation error handling +# - ided/ide.rs: assert→debug_assert, try_into error handling +# - nvmed/executor.rs: executor initialization error handling +# - nvmed/identify.rs: DMA allocation, unreachable!() fallback +# - nvmed/mod.rs: assert→debug_assert, unwrap→proper error/exit +# - nvmed/queues.rs: unreachable!()→safe fallback +# - virtio-blkd/scheme.rs: DMA allocation error handling, assert→if check +# +diff --git a/drivers/storage/ahcid/src/ahci/disk_ata.rs b/drivers/storage/ahcid/src/ahci/disk_ata.rs +index 4f83c51d..7423603b 100644 +--- a/drivers/storage/ahcid/src/ahci/disk_ata.rs ++++ b/drivers/storage/ahcid/src/ahci/disk_ata.rs +@@ -1,7 +1,7 @@ + use std::convert::TryInto; + use std::ptr; + +-use syscall::error::Result; ++use syscall::error::{Error, Result, EIO}; + + use common::dma::Dma; + +@@ -39,7 +39,7 @@ impl DiskATA { + .map(|_| Ok(unsafe { Dma::zeroed()?.assume_init() })) + .collect::>>()? + .try_into() +- .unwrap_or_else(|_| unreachable!()); ++ .map_err(|_| Error::new(EIO))?; + + let mut fb = unsafe { Dma::zeroed()?.assume_init() }; + let buf = unsafe { Dma::zeroed()?.assume_init() }; +diff --git a/drivers/storage/ahcid/src/ahci/disk_atapi.rs b/drivers/storage/ahcid/src/ahci/disk_atapi.rs +index a0e75c09..8fbdfbef 100644 +--- a/drivers/storage/ahcid/src/ahci/disk_atapi.rs ++++ b/drivers/storage/ahcid/src/ahci/disk_atapi.rs +@@ -37,7 +37,7 @@ impl DiskATAPI { + .map(|_| Ok(unsafe { Dma::zeroed()?.assume_init() })) + .collect::>>()? + .try_into() +- .unwrap_or_else(|_| unreachable!()); ++ .map_err(|_| Error::new(EBADF))?; + + let mut fb = unsafe { Dma::zeroed()?.assume_init() }; + let mut buf = unsafe { Dma::zeroed()?.assume_init() }; +diff --git a/drivers/storage/ahcid/src/ahci/hba.rs b/drivers/storage/ahcid/src/ahci/hba.rs +index bea8792c..11a3d4ae 100644 +--- a/drivers/storage/ahcid/src/ahci/hba.rs ++++ b/drivers/storage/ahcid/src/ahci/hba.rs +@@ -178,7 +178,10 @@ impl HbaPort { + clb: &mut Dma<[HbaCmdHeader; 32]>, + ctbas: &mut [Dma; 32], + ) -> Result { +- let dest: Dma<[u16; 256]> = Dma::new([0; 256]).unwrap(); ++ let dest: Dma<[u16; 256]> = Dma::new([0; 256]).map_err(|err| { ++ error!("ahcid: failed to allocate DMA buffer: {err}"); ++ Error::new(EIO) ++ })?; + + let slot = self + .ata_start(clb, ctbas, |cmdheader, cmdfis, prdt_entries, _acmd| { + +diff --git a/drivers/storage/ided/src/ide.rs b/drivers/storage/ided/src/ide.rs +index 5faf3250..094e5889 100644 +--- a/drivers/storage/ided/src/ide.rs ++++ b/drivers/storage/ided/src/ide.rs +@@ -184,10 +184,10 @@ impl Disk for AtaDisk { + let block = start_block + (count as u64) / 512; + + //TODO: support other LBA modes +- assert!(block < 0x1_0000_0000_0000); ++ debug_assert!(block < 0x1_0000_0000_0000); + + let sectors = (chunk.len() + 511) / 512; +- assert!(sectors <= 128); ++ debug_assert!(sectors <= 128); + + log::trace!( + "IDE read chan {} dev {} block {:#x} count {:#x}", +@@ -205,7 +205,7 @@ impl Disk for AtaDisk { + // Make PRDT EOT match chunk size + for i in 0..sectors { + chan.prdt[i] = PrdtEntry { +- phys: (chan.buf.physical() + i * 512).try_into().unwrap(), ++ phys: (chan.buf.physical() + i * 512).try_into().map_err(|_| Error::new(EIO))?, + size: 512, + flags: if i + 1 == sectors { + 1 << 15 // End of table +@@ -216,7 +216,7 @@ impl Disk for AtaDisk { + } + // Set PRDT + let prdt = chan.prdt.physical(); +- chan.busmaster_prdt.write(prdt.try_into().unwrap()); ++ chan.busmaster_prdt.write(prdt.try_into().map_err(|_| Error::new(EIO))?); + // Set to read + chan.busmaster_command.writef(1 << 3, true); + // Clear interrupt and error bits +@@ -325,10 +325,10 @@ impl Disk for AtaDisk { + let block = start_block + (count as u64) / 512; + + //TODO: support other LBA modes +- assert!(block < 0x1_0000_0000_0000); ++ debug_assert!(block < 0x1_0000_0000_0000); + + let sectors = (chunk.len() + 511) / 512; +- assert!(sectors <= 128); ++ debug_assert!(sectors <= 128); + + log::trace!( + "IDE write chan {} dev {} block {:#x} count {:#x}", +@@ -346,7 +346,7 @@ impl Disk for AtaDisk { + // Make PRDT EOT match chunk size + for i in 0..sectors { + chan.prdt[i] = PrdtEntry { +- phys: (chan.buf.physical() + i * 512).try_into().unwrap(), ++ phys: (chan.buf.physical() + i * 512).try_into().map_err(|_| Error::new(EIO))?, + size: 512, + flags: if i + 1 == sectors { + 1 << 15 // End of table +@@ -357,7 +357,7 @@ impl Disk for AtaDisk { + } + // Set PRDT + let prdt = chan.prdt.physical(); +- chan.busmaster_prdt.write(prdt.try_into().unwrap()); ++ chan.busmaster_prdt.write(prdt.try_into().map_err(|_| Error::new(EIO))?); + // Set to write + chan.busmaster_command.writef(1 << 3, false); + // Clear interrupt and error bits + +diff --git a/drivers/storage/nvmed/src/nvme/executor.rs b/drivers/storage/nvmed/src/nvme/executor.rs +index 6242fa98..c1435e88 100644 +--- a/drivers/storage/nvmed/src/nvme/executor.rs ++++ b/drivers/storage/nvmed/src/nvme/executor.rs +@@ -34,7 +34,12 @@ impl Hardware for NvmeHw { + &VTABLE + } + fn current() -> std::rc::Rc> { +- THE_EXECUTOR.with(|exec| Rc::clone(exec.borrow().as_ref().unwrap())) ++ THE_EXECUTOR.with(|exec| { ++ Rc::clone(exec.borrow().as_ref().unwrap_or_else(|| { ++ log::error!("nvmed: internal error: executor not initialized"); ++ std::process::exit(1); ++ })) ++ }) + } + fn try_submit( + nvme: &Arc, +diff --git a/drivers/storage/nvmed/src/nvme/identify.rs b/drivers/storage/nvmed/src/nvme/identify.rs +index 05e5b9b2..b1b6e959 100644 +--- a/drivers/storage/nvmed/src/nvme/identify.rs ++++ b/drivers/storage/nvmed/src/nvme/identify.rs +@@ -126,7 +126,7 @@ impl LbaFormat { + 0b01 => RelativePerformance::Better, + 0b10 => RelativePerformance::Good, + 0b11 => RelativePerformance::Degraded, +- _ => unreachable!(), ++ _ => RelativePerformance::Degraded, + } + } + pub fn is_available(&self) -> bool { +@@ -153,7 +153,14 @@ impl Nvme { + /// Returns the serial number, model, and firmware, in that order. + pub async fn identify_controller(&self) { + // TODO: Use same buffer +- let data: Dma = unsafe { Dma::zeroed().unwrap().assume_init() }; ++ let data: Dma = unsafe { ++ Dma::zeroed() ++ .map(|dma| dma.assume_init()) ++ .unwrap_or_else(|err| { ++ log::error!("nvmed: failed to allocate identify DMA: {err}"); ++ std::process::exit(1); ++ }) ++ }; + + // println!(" - Attempting to identify controller"); + let comp = self +@@ -182,7 +189,14 @@ impl Nvme { + } + pub async fn identify_namespace_list(&self, base: u32) -> Vec { + // TODO: Use buffer +- let data: Dma<[u32; 1024]> = unsafe { Dma::zeroed().unwrap().assume_init() }; ++ let data: Dma<[u32; 1024]> = unsafe { ++ Dma::zeroed() ++ .map(|dma| dma.assume_init()) ++ .unwrap_or_else(|err| { ++ log::error!("nvmed: failed to allocate namespace list DMA: {err}"); ++ std::process::exit(1); ++ }) ++ }; + + // println!(" - Attempting to retrieve namespace ID list"); + let comp = self +@@ -198,7 +212,14 @@ impl Nvme { + } + pub async fn identify_namespace(&self, nsid: u32) -> NvmeNamespace { + //TODO: Use buffer +- let data: Dma = unsafe { Dma::zeroed().unwrap().assume_init() }; ++ let data: Dma = unsafe { ++ Dma::zeroed() ++ .map(|dma| dma.assume_init()) ++ .unwrap_or_else(|err| { ++ log::error!("nvmed: failed to allocate namespace DMA: {err}"); ++ std::process::exit(1); ++ }) ++ }; + + log::debug!("Attempting to identify namespace {nsid}"); + let comp = self +@@ -216,7 +237,10 @@ impl Nvme { + let block_size = data + .formatted_lba_size() + .lba_data_size() +- .expect("nvmed: error: size outside 512-2^64 range"); ++ .unwrap_or_else(|| { ++ log::error!("nvmed: error: size outside 512-2^64 range"); ++ std::process::exit(1); ++ }); + log::debug!("NVME block size: {}", block_size); + + NvmeNamespace { +diff --git a/drivers/storage/nvmed/src/nvme/mod.rs b/drivers/storage/nvmed/src/nvme/mod.rs +index 682ee933..90a25d5b 100644 +--- a/drivers/storage/nvmed/src/nvme/mod.rs ++++ b/drivers/storage/nvmed/src/nvme/mod.rs +@@ -160,7 +160,15 @@ impl Nvme { + } + fn cur_thread_ctxt(&self) -> Arc> { + // TODO: multi-threading +- Arc::clone(self.thread_ctxts.read().get(&0).unwrap()) ++ Arc::clone( ++ self.thread_ctxts ++ .read() ++ .get(&0) ++ .unwrap_or_else(|| { ++ log::error!("nvmed: internal error: thread context 0 missing"); ++ std::process::exit(1); ++ }), ++ ) + } + + pub unsafe fn submission_queue_tail(&self, qid: u16, tail: u16) { +@@ -208,10 +216,22 @@ impl Nvme { + } + + for (qid, iv) in self.cq_ivs.get_mut().iter_mut() { +- let ctxt = thread_ctxts.get(&0).unwrap().lock(); ++ let ctxt = match thread_ctxts.get(&0) { ++ Some(c) => c.lock(), ++ None => { ++ log::error!("nvmed: internal error: thread context 0 missing"); ++ return Err(Error::new(EIO)); ++ } ++ }; + let queues = ctxt.queues.borrow(); + +- let &(ref cq, ref sq) = queues.get(qid).unwrap(); ++ let (cq, sq) = match queues.get(qid) { ++ Some(pair) => pair, ++ None => { ++ log::error!("nvmed: internal error: queue {qid} missing"); ++ return Err(Error::new(EIO)); ++ } ++ }; + log::debug!( + "iv {iv} [cq {qid}: {:X}, {}] [sq {qid}: {:X}, {}]", + cq.data.physical(), +@@ -222,7 +242,13 @@ impl Nvme { + } + + { +- let main_ctxt = thread_ctxts.get(&0).unwrap().lock(); ++ let main_ctxt = match thread_ctxts.get(&0) { ++ Some(c) => c.lock(), ++ None => { ++ log::error!("nvmed: internal error: thread context 0 missing"); ++ return Err(Error::new(EIO)); ++ } ++ }; + + for (i, prp) in main_ctxt.buffer_prp.borrow_mut().iter_mut().enumerate() { + *prp = (main_ctxt.buffer.borrow_mut().physical() + i * 4096) as u64; +@@ -231,7 +257,13 @@ impl Nvme { + let regs = self.regs.get_mut(); + + let mut queues = main_ctxt.queues.borrow_mut(); +- let (asq, acq) = queues.get_mut(&0).unwrap(); ++ let (asq, acq) = match queues.get_mut(&0) { ++ Some(pair) => pair, ++ None => { ++ log::error!("nvmed: internal error: admin queue pair missing"); ++ return Err(Error::new(EIO)); ++ } ++ }; + regs.aqa + .write(((acq.data.len() as u32 - 1) << 16) | (asq.data.len() as u32 - 1)); + regs.asq_low.write(asq.data.physical() as u32); +@@ -281,14 +313,14 @@ impl Nvme { + let vector = vector as u8; + + if masked { +- assert_ne!( ++ debug_assert_ne!( + to_clear & (1 << vector), + (1 << vector), + "nvmed: internal error: cannot both mask and set" + ); + to_mask |= 1 << vector; + } else { +- assert_ne!( ++ debug_assert_ne!( + to_mask & (1 << vector), + (1 << vector), + "nvmed: internal error: cannot both mask and set" +@@ -326,22 +358,27 @@ impl Nvme { + cmd_init: impl FnOnce(CmdId) -> NvmeCmd, + fail: impl FnOnce(), + ) -> Option<(CqId, CmdId)> { +- match ctxt.queues.borrow_mut().get_mut(&sq_id).unwrap() { +- (sq, _cq) => { +- if sq.is_full() { +- fail(); +- return None; +- } +- let cmd_id = sq.tail; +- let tail = sq.submit_unchecked(cmd_init(cmd_id)); +- +- // TODO: Submit in bulk +- unsafe { +- self.submission_queue_tail(sq_id, tail); +- } +- Some((sq_id, cmd_id)) ++ let mut queues_ref = ctxt.queues.borrow_mut(); ++ let (sq, _cq) = match queues_ref.get_mut(&sq_id) { ++ Some(pair) => pair, ++ None => { ++ log::error!("nvmed: internal error: submission queue {sq_id} missing"); ++ fail(); ++ return None; + } ++ }; ++ if sq.is_full() { ++ fail(); ++ return None; ++ } ++ let cmd_id = sq.tail; ++ let tail = sq.submit_unchecked(cmd_init(cmd_id)); ++ ++ // TODO: Submit in bulk ++ unsafe { ++ self.submission_queue_tail(sq_id, tail); + } ++ Some((sq_id, cmd_id)) + } + + pub async fn create_io_completion_queue( +@@ -349,13 +386,19 @@ impl Nvme { + io_cq_id: CqId, + vector: Option, + ) -> NvmeCompQueue { +- let queue = NvmeCompQueue::new().expect("nvmed: failed to allocate I/O completion queue"); +- +- let len = u16::try_from(queue.data.len()) +- .expect("nvmed: internal error: I/O CQ longer than 2^16 entries"); +- let raw_len = len +- .checked_sub(1) +- .expect("nvmed: internal error: CQID 0 for I/O CQ"); ++ let queue = NvmeCompQueue::new().unwrap_or_else(|err| { ++ log::error!("nvmed: failed to allocate I/O completion queue: {err}"); ++ std::process::exit(1); ++ }); ++ ++ let len = u16::try_from(queue.data.len()).unwrap_or_else(|_| { ++ log::error!("nvmed: internal error: I/O CQ longer than 2^16 entries"); ++ std::process::exit(1); ++ }); ++ let raw_len = len.checked_sub(1).unwrap_or_else(|| { ++ log::error!("nvmed: internal error: CQID 0 for I/O CQ"); ++ std::process::exit(1); ++ }); + + let comp = self + .submit_and_complete_admin_command(|cid| { +@@ -370,22 +413,28 @@ impl Nvme { + .await; + + /*match comp.status.specific { +- 1 => panic!("invalid queue identifier"), +- 2 => panic!("invalid queue size"), +- 8 => panic!("invalid interrupt vector"), ++ 1 => { log::error!("nvmed: invalid queue identifier"); std::process::exit(1); } ++ 2 => { log::error!("nvmed: invalid queue size"); std::process::exit(1); } ++ 8 => { log::error!("nvmed: invalid interrupt vector"); std::process::exit(1); } + _ => (), + }*/ + + queue + } + pub async fn create_io_submission_queue(&self, io_sq_id: SqId, io_cq_id: CqId) -> NvmeCmdQueue { +- let q = NvmeCmdQueue::new().expect("failed to create submission queue"); +- +- let len = u16::try_from(q.data.len()) +- .expect("nvmed: internal error: I/O SQ longer than 2^16 entries"); +- let raw_len = len +- .checked_sub(1) +- .expect("nvmed: internal error: SQID 0 for I/O SQ"); ++ let q = NvmeCmdQueue::new().unwrap_or_else(|err| { ++ log::error!("nvmed: failed to create submission queue: {err}"); ++ std::process::exit(1); ++ }); ++ ++ let len = u16::try_from(q.data.len()).unwrap_or_else(|_| { ++ log::error!("nvmed: internal error: I/O SQ longer than 2^16 entries"); ++ std::process::exit(1); ++ }); ++ let raw_len = len.checked_sub(1).unwrap_or_else(|| { ++ log::error!("nvmed: internal error: SQID 0 for I/O SQ"); ++ std::process::exit(1); ++ }); + + let comp = self + .submit_and_complete_admin_command(|cid| { +@@ -399,9 +448,9 @@ impl Nvme { + }) + .await; + /*match comp.status.specific { +- 0 => panic!("completion queue invalid"), +- 1 => panic!("invalid queue identifier"), +- 2 => panic!("invalid queue size"), ++ 0 => { log::error!("nvmed: completion queue invalid"); std::process::exit(1); } ++ 1 => { log::error!("nvmed: invalid queue identifier"); std::process::exit(1); } ++ 2 => { log::error!("nvmed: invalid queue size"); std::process::exit(1); } + _ => (), + }*/ + +@@ -431,7 +480,10 @@ impl Nvme { + self.thread_ctxts + .read() + .get(&0) +- .unwrap() ++ .unwrap_or_else(|| { ++ log::error!("nvmed: internal error: thread context 0 missing"); ++ std::process::exit(1); ++ }) + .lock() + .queues + .borrow_mut() +@@ -497,8 +549,8 @@ impl Nvme { + for chunk in buf.chunks_mut(/* TODO: buf len */ 8192) { + let blocks = (chunk.len() + block_size - 1) / block_size; + +- assert!(blocks > 0); +- assert!(blocks <= 0x1_0000); ++ debug_assert!(blocks > 0); ++ debug_assert!(blocks <= 0x1_0000); + + self.namespace_rw(&*ctxt, namespace, lba, (blocks - 1) as u16, false) + .await?; +@@ -525,8 +577,8 @@ impl Nvme { + for chunk in buf.chunks(/* TODO: buf len */ 8192) { + let blocks = (chunk.len() + block_size - 1) / block_size; + +- assert!(blocks > 0); +- assert!(blocks <= 0x1_0000); ++ debug_assert!(blocks > 0); ++ debug_assert!(blocks <= 0x1_0000); + + ctxt.buffer.borrow_mut()[..chunk.len()].copy_from_slice(chunk); + +diff --git a/drivers/storage/nvmed/src/nvme/queues.rs b/drivers/storage/nvmed/src/nvme/queues.rs +index a3712aeb..438c905c 100644 +--- a/drivers/storage/nvmed/src/nvme/queues.rs ++++ b/drivers/storage/nvmed/src/nvme/queues.rs +@@ -145,7 +145,7 @@ impl Status { + 3 => Self::PathRelatedStatus(code), + 4..=6 => Self::Rsvd(code), + 7 => Self::Vendor(code), +- _ => unreachable!(), ++ _ => Self::Vendor(code), + } + } + } + +diff --git a/drivers/storage/virtio-blkd/src/scheme.rs b/drivers/storage/virtio-blkd/src/scheme.rs +index ec4ecf73..39fb24a8 100644 +--- a/drivers/storage/virtio-blkd/src/scheme.rs ++++ b/drivers/storage/virtio-blkd/src/scheme.rs +@@ -15,19 +15,34 @@ trait BlkExtension { + + impl BlkExtension for Queue<'_> { + async fn read(&self, block: u64, target: &mut [u8]) -> usize { +- let req = Dma::new(BlockVirtRequest { ++ let req = match Dma::new(BlockVirtRequest { + ty: BlockRequestTy::In, + reserved: 0, + sector: block, +- }) +- .unwrap(); ++ }) { ++ Ok(req) => req, ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate read request DMA: {err}"); ++ return 0; ++ } ++ }; + + let result = unsafe { +- Dma::<[u8]>::zeroed_slice(target.len()) +- .unwrap() +- .assume_init() ++ match Dma::<[u8]>::zeroed_slice(target.len()) { ++ Ok(dma) => dma.assume_init(), ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate read buffer DMA: {err}"); ++ return 0; ++ } ++ } ++ }; ++ let status = match Dma::new(u8::MAX) { ++ Ok(s) => s, ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate read status DMA: {err}"); ++ return 0; ++ } + }; +- let status = Dma::new(u8::MAX).unwrap(); + + let chain = ChainBuilder::new() + .chain(Buffer::new(&req)) +@@ -37,28 +52,46 @@ impl BlkExtension for Queue<'_> { + + // XXX: Subtract 1 because the of status byte. + let written = self.send(chain).await as usize - 1; +- assert_eq!(*status, 0); ++ if *status != 0 { ++ log::error!("virtio-blkd: read failed with status {}", *status); ++ return 0; ++ } + + target[..written].copy_from_slice(&result); + written + } + + async fn write(&self, block: u64, target: &[u8]) -> usize { +- let req = Dma::new(BlockVirtRequest { ++ let req = match Dma::new(BlockVirtRequest { + ty: BlockRequestTy::Out, + reserved: 0, + sector: block, +- }) +- .unwrap(); ++ }) { ++ Ok(req) => req, ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate write request DMA: {err}"); ++ return 0; ++ } ++ }; + + let mut result = unsafe { +- Dma::<[u8]>::zeroed_slice(target.len()) +- .unwrap() +- .assume_init() ++ match Dma::<[u8]>::zeroed_slice(target.len()) { ++ Ok(dma) => dma.assume_init(), ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate write buffer DMA: {err}"); ++ return 0; ++ } ++ } + }; + result.copy_from_slice(target.as_ref()); + +- let status = Dma::new(u8::MAX).unwrap(); ++ let status = match Dma::new(u8::MAX) { ++ Ok(s) => s, ++ Err(err) => { ++ log::error!("virtio-blkd: failed to allocate write status DMA: {err}"); ++ return 0; ++ } ++ }; + + let chain = ChainBuilder::new() + .chain(Buffer::new(&req)) +@@ -67,7 +100,10 @@ impl BlkExtension for Queue<'_> { + .build(); + + self.send(chain).await as usize; +- assert_eq!(*status, 0); ++ if *status != 0 { ++ log::error!("virtio-blkd: write failed with status {}", *status); ++ return 0; ++ } + + target.len() + } diff --git a/local/patches/base/P2-usb-pm-and-drivers.patch b/local/patches/base/P2-usb-pm-and-drivers.patch new file mode 100644 index 00000000..83f8d745 --- /dev/null +++ b/local/patches/base/P2-usb-pm-and-drivers.patch @@ -0,0 +1,158 @@ +# P2-usb-pm-and-drivers.patch +# +# USB power management and driver interface improvements: +# suspend/resume commands, SCSI driver enablement, PortPmState type, +# IRQ reactor staged port state fallback. +# +# Covers: +# - usbctl/main.rs: pm-state, suspend, resume subcommands +# - xhcid/drivers.toml: enable SCSI over USB driver (was commented out) +# - xhcid/driver_interface.rs: PortPmState enum, suspend/resume/port_pm_state methods +# - xhcid/irq_reactor.rs: staged_port_states fallback in with_ring/with_ring_mut +# +diff --git a/drivers/usb/usbctl/src/main.rs b/drivers/usb/usbctl/src/main.rs +index 9b5773d9..232f7cfc 100644 +--- a/drivers/usb/usbctl/src/main.rs ++++ b/drivers/usb/usbctl/src/main.rs +@@ -15,6 +15,9 @@ fn main() { + Command::new("port") + .arg(Arg::new("PORT").num_args(1).required(true)) + .subcommand(Command::new("status")) ++ .subcommand(Command::new("pm-state")) ++ .subcommand(Command::new("suspend")) ++ .subcommand(Command::new("resume")) + .subcommand( + Command::new("endpoint") + .arg(Arg::new("ENDPOINT_NUM").num_args(1).required(true)) +@@ -38,6 +41,15 @@ fn main() { + if let Some(_status_scmd_matches) = port_scmd_matches.subcommand_matches("status") { + let state = handle.port_state().expect("Failed to get port state"); + println!("{}", state.as_str()); ++ } else if let Some(_pm_state_scmd_matches) = port_scmd_matches.subcommand_matches("pm-state") { ++ let state = handle ++ .port_pm_state() ++ .expect("Failed to get port power-management state"); ++ println!("{}", state.as_str()); ++ } else if let Some(_suspend_scmd_matches) = port_scmd_matches.subcommand_matches("suspend") { ++ handle.suspend_device().expect("Failed to suspend device"); ++ } else if let Some(_resume_scmd_matches) = port_scmd_matches.subcommand_matches("resume") { ++ handle.resume_device().expect("Failed to resume device"); + } else if let Some(endp_scmd_matches) = port_scmd_matches.subcommand_matches("endpoint") { + let endp_num = endp_scmd_matches + + .get_one::("ENDPOINT_NUM") +diff --git a/drivers/usb/xhcid/drivers.toml b/drivers/usb/xhcid/drivers.toml +index 83c90e23..470ec063 100644 +--- a/drivers/usb/xhcid/drivers.toml ++++ b/drivers/usb/xhcid/drivers.toml +@@ -1,9 +1,8 @@ +-#TODO: causes XHCI errors +-#[[drivers]] +-#name = "SCSI over USB" +-#class = 8 # Mass Storage class +-#subclass = 6 # SCSI transparent command set +-#command = ["usbscsid", "$SCHEME", "$PORT", "$IF_PROTO"] ++[[drivers]] ++name = "SCSI over USB" ++class = 8 # Mass Storage class ++subclass = 6 # SCSI transparent command set ++command = ["usbscsid", "$SCHEME", "$PORT", "$IF_PROTO"] + + [[drivers]] + name = "USB HUB" + +diff --git a/drivers/usb/xhcid/src/driver_interface.rs b/drivers/usb/xhcid/src/driver_interface.rs +index 727f8d7e..82f839ae 100644 +--- a/drivers/usb/xhcid/src/driver_interface.rs ++++ b/drivers/usb/xhcid/src/driver_interface.rs +@@ -444,6 +444,33 @@ impl str::FromStr for PortState { + } + } + ++#[repr(u8)] ++#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] ++pub enum PortPmState { ++ Active, ++ Suspended, ++} ++impl PortPmState { ++ pub fn as_str(&self) -> &'static str { ++ match self { ++ Self::Active => "active", ++ Self::Suspended => "suspended", ++ } ++ } ++} ++ ++impl str::FromStr for PortPmState { ++ type Err = Invalid; ++ ++ fn from_str(s: &str) -> result::Result { ++ Ok(match s { ++ "active" => Self::Active, ++ "suspended" => Self::Suspended, ++ _ => return Err(Invalid("read reserved port PM state")), ++ }) ++ } ++} ++ + #[repr(u8)] + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] + pub enum EndpointStatus { +@@ -560,6 +587,16 @@ impl XhciClientHandle { + let _bytes_written = file.write(&[])?; + Ok(()) + } ++ pub fn suspend_device(&self) -> result::Result<(), XhciClientHandleError> { ++ let file = self.fd.openat("suspend", libredox::flag::O_WRONLY, 0)?; ++ let _bytes_written = file.write(&[])?; ++ Ok(()) ++ } ++ pub fn resume_device(&self) -> result::Result<(), XhciClientHandleError> { ++ let file = self.fd.openat("resume", libredox::flag::O_WRONLY, 0)?; ++ let _bytes_written = file.write(&[])?; ++ Ok(()) ++ } + pub fn get_standard_descs(&self) -> result::Result { + let json = self.read("descriptors")?; + Ok(serde_json::from_slice(&json)?) +@@ -582,6 +619,10 @@ impl XhciClientHandle { + let string = self.read_to_string("state")?; + Ok(string.parse()?) + } ++ pub fn port_pm_state(&self) -> result::Result { ++ let string = self.read_to_string("pm_state")?; ++ Ok(string.parse()?) ++ } + pub fn open_endpoint_ctl(&self, num: u8) -> result::Result { + let path = format!("endpoints/{}/ctl", num); + let fd = self.fd.openat(&path, libredox::flag::O_RDWR, 0)?; + +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/local/patches/relibc/P3-aio.patch b/local/patches/relibc/P3-aio.patch new file mode 100644 index 00000000..67daa872 --- /dev/null +++ b/local/patches/relibc/P3-aio.patch @@ -0,0 +1,336 @@ +diff -ruN a/src/header/_aio/mod.rs b/src/header/_aio/mod.rs +--- a/src/header/_aio/mod.rs ++++ b/src/header/_aio/mod.rs +@@ -1,75 +1,283 @@ + //! `aio.h` implementation. + //! +-//! See . ++//! Synchronous emulation of POSIX AIO. All operations complete immediately ++//! in the calling thread. This provides sufficient compatibility for software ++//! (such as Qt6's QIODevice) that uses aio as an optional fallback path. + +-use crate::{ +- header::{bits_timespec::timespec, signal::sigevent}, +- platform::types::{c_int, c_void}, +-}; ++use core::slice; ++ ++use crate::{ ++ error::Errno, ++ header::{ ++ bits_timespec::timespec, ++ errno::{EFAULT, EINVAL, EINPROGRESS, EIO}, ++ fcntl::O_SYNC, ++ signal::sigevent, ++ }, ++ platform::{ ++ Sys, ++ types::{c_int, c_void, off_t, size_t, ssize_t}, ++ ERRNO, ++ }, ++}; ++ ++// POSIX lio_listio operation codes ++pub const LIO_READ: c_int = 0; ++pub const LIO_WRITE: c_int = 1; ++pub const LIO_NOP: c_int = 2; ++ ++// lio_listio modes ++pub const LIO_WAIT: c_int = 0; ++pub const LIO_NOWAIT: c_int = 1; ++ ++// aio_cancel return values ++pub const AIO_CANCELED: c_int = 0; ++pub const AIO_NOTCANCELED: c_int = 1; ++pub const AIO_ALLDONE: c_int = 2; ++ ++// O_DSYNC is not yet defined in relibc's fcntl module. ++// Accept it in aio_fsync by matching the Linux x86_64 value. ++// TODO: import from fcntl when O_DSYNC is added there. ++const _O_DSYNC: c_int = 0x0001_0000; ++ ++// Internal operation states for synchronous emulation ++const _AIO_IDLE: c_int = 0; ++const _AIO_DONE: c_int = 2; + + /// See . ++#[repr(C)] + pub struct aiocb { + pub aio_fildes: c_int, ++ pub aio_offset: off_t, + pub aio_lio_opcode: c_int, + pub aio_reqprio: c_int, + pub aio_buf: *mut c_void, +- pub aio_nbytes: usize, ++ pub aio_nbytes: size_t, + pub aio_sigevent: sigevent, ++ // Private emulation state ++ pub __state: c_int, ++ pub __error_code: c_int, ++ pub __return_value: ssize_t, + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_read(aiocbp: *mut aiocb) -> c_int { +- unimplemented!(); ++/// Perform a synchronous pread and store the result in the aiocb. ++/// ++/// Returns 0 on success, -1 on error (with errno set). ++unsafe fn aio_do_read(cb: &mut aiocb) -> c_int { ++ let buf = unsafe { slice::from_raw_parts_mut(cb.aio_buf.cast::(), cb.aio_nbytes) }; ++ match Sys::pread(cb.aio_fildes, buf, cb.aio_offset) { ++ Ok(n) => { ++ cb.__error_code = 0; ++ cb.__return_value = n as ssize_t; ++ cb.__state = _AIO_DONE; ++ 0 ++ } ++ Err(Errno(e)) => { ++ cb.__error_code = e; ++ cb.__return_value = -1; ++ cb.__state = _AIO_DONE; ++ ERRNO.set(e); ++ -1 ++ } ++ } + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_write(aiocbp: *mut aiocb) -> c_int { +- unimplemented!(); ++/// Perform a synchronous pwrite and store the result in the aiocb. ++/// ++/// Returns 0 on success, -1 on error (with errno set). ++unsafe fn aio_do_write(cb: &mut aiocb) -> c_int { ++ let buf = unsafe { slice::from_raw_parts(cb.aio_buf.cast::(), cb.aio_nbytes) }; ++ match Sys::pwrite(cb.aio_fildes, buf, cb.aio_offset) { ++ Ok(n) => { ++ cb.__error_code = 0; ++ cb.__return_value = n as ssize_t; ++ cb.__state = _AIO_DONE; ++ 0 ++ } ++ Err(Errno(e)) => { ++ cb.__error_code = e; ++ cb.__return_value = -1; ++ cb.__state = _AIO_DONE; ++ ERRNO.set(e); ++ -1 ++ } ++ } + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn lio_listio( +- mode: c_int, +- list: *const *const aiocb, +- nent: c_int, +- sig: *mut sigevent, +-) -> c_int { +- unimplemented!(); +-} ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_read(aiocbp: *mut aiocb) -> c_int { ++ if aiocbp.is_null() { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ let cb = unsafe { &mut *aiocbp }; ++ if cb.aio_buf.is_null() && cb.aio_nbytes > 0 { ++ ERRNO.set(EFAULT); ++ cb.__state = _AIO_DONE; ++ cb.__error_code = EFAULT; ++ cb.__return_value = -1; ++ return -1; ++ } ++ unsafe { aio_do_read(cb) } ++} + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_error(aiocbp: *const aiocb) -> c_int { +- unimplemented!(); ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_write(aiocbp: *mut aiocb) -> c_int { ++ if aiocbp.is_null() { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ let cb = unsafe { &mut *aiocbp }; ++ if cb.aio_buf.is_null() && cb.aio_nbytes > 0 { ++ ERRNO.set(EFAULT); ++ cb.__state = _AIO_DONE; ++ cb.__error_code = EFAULT; ++ cb.__return_value = -1; ++ return -1; ++ } ++ unsafe { aio_do_write(cb) } + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_return(aiocbp: *mut aiocb) -> usize { +- unimplemented!(); +-} ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_fsync(operation: c_int, aiocbp: *mut aiocb) -> c_int { ++ if aiocbp.is_null() { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ // Validate operation: O_SYNC from fcntl, or _O_DSYNC (Linux compat value). ++ if operation != O_SYNC && operation != _O_DSYNC { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ let cb = unsafe { &mut *aiocbp }; ++ match Sys::fsync(cb.aio_fildes) { ++ Ok(()) => { ++ cb.__error_code = 0; ++ cb.__return_value = 0; ++ cb.__state = _AIO_DONE; ++ 0 ++ } ++ Err(Errno(e)) => { ++ cb.__error_code = e; ++ cb.__return_value = -1; ++ cb.__state = _AIO_DONE; ++ ERRNO.set(e); ++ -1 ++ } ++ } ++} + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_cancel(fildes: c_int, aiocbp: *mut aiocb) -> c_int { +- unimplemented!(); ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_error(aiocbp: *const aiocb) -> c_int { ++ if aiocbp.is_null() { ++ return EINVAL; ++ } ++ let cb = unsafe { &*aiocbp }; ++ match cb.__state { ++ _AIO_IDLE => 0, // Never submitted -- no error ++ _AIO_DONE => cb.__error_code, ++ _ => EINPROGRESS, // Should not occur with sync emulation ++ } + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_suspend( +- list: *const *const aiocb, +- nent: c_int, +- timeout: *const timespec, +-) -> c_int { +- unimplemented!(); ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_return(aiocbp: *mut aiocb) -> ssize_t { ++ if aiocbp.is_null() { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ let cb = unsafe { &*aiocbp }; ++ if cb.__state != _AIO_DONE { ++ ERRNO.set(EINPROGRESS); ++ return -1; ++ } ++ cb.__return_value + } + +-/// See . +-// #[unsafe(no_mangle)] +-pub extern "C" fn aio_fsync(operation: c_int, aiocbp: *mut aiocb) -> c_int { +- unimplemented!(); ++/// See . ++/// ++/// With synchronous emulation, all operations are already complete when ++/// aio_suspend is called, so this is effectively a no-op that returns 0. ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_suspend( ++ list: *const *const aiocb, ++ nent: c_int, ++ timeout: *const timespec, ++) -> c_int { ++ let _ = timeout; ++ if list.is_null() || nent < 0 { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ // All operations complete synchronously, so just return success. ++ 0 ++} ++ ++/// See . ++/// ++/// With synchronous emulation, operations complete before aio_cancel can be ++/// called, so this always returns AIO_ALLDONE. ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn aio_cancel(fildes: c_int, aiocbp: *mut aiocb) -> c_int { ++ if !aiocbp.is_null() { ++ let cb = unsafe { &*aiocbp }; ++ if cb.aio_fildes != fildes { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ } ++ AIO_ALLDONE ++} ++ ++/// See . ++#[unsafe(no_mangle)] ++pub unsafe extern "C" fn lio_listio( ++ mode: c_int, ++ list: *const *const aiocb, ++ nent: c_int, ++ sig: *mut sigevent, ++) -> c_int { ++ let _ = sig; ++ if (mode != LIO_WAIT && mode != LIO_NOWAIT) || list.is_null() || nent < 0 { ++ ERRNO.set(EINVAL); ++ return -1; ++ } ++ let mut any_failed = false; ++ for i in 0..nent { ++ let entry = unsafe { *list.add(i as usize) }; ++ if entry.is_null() { ++ continue; ++ } ++ let cb = unsafe { &mut *(entry as *mut aiocb) }; ++ match cb.aio_lio_opcode { ++ LIO_READ => { ++ if unsafe { aio_read(cb) } != 0 { ++ any_failed = true; ++ } ++ } ++ LIO_WRITE => { ++ if unsafe { aio_write(cb) } != 0 { ++ any_failed = true; ++ } ++ } ++ LIO_NOP => {} ++ _ => { ++ cb.__state = _AIO_DONE; ++ cb.__error_code = EINVAL; ++ cb.__return_value = -1; ++ ERRNO.set(EINVAL); ++ any_failed = true; ++ } ++ } ++ } ++ if any_failed { ++ ERRNO.set(EIO); ++ return -1; ++ } ++ 0 + } diff --git a/local/recipes/kde/breeze/recipe.toml b/local/recipes/kde/breeze/recipe.toml new file mode 100644 index 00000000..a2f714e7 --- /dev/null +++ b/local/recipes/kde/breeze/recipe.toml @@ -0,0 +1,61 @@ +#TODO: Breeze — KDE theme engine (widget style, colors, window decoration). +# Builds the Breeze Qt widget style plugin for consistent theming. +# The KWin decoration plugin is disabled (needs kwin at build time for the decoration API). +# Cursor and icon themes are not built here — those are separate upstream packages. +# Blockers: the window decoration subdirectory needs KDecoration3::Decoration which requires +# kwin headers at configure time; disabled via CMake option. The widget style plugin should +# build independently with just qtbase + KF6 deps. +[source] +tar = "https://invent.kde.org/plasma/breeze/-/archive/v6.3.4/breeze-v6.3.4.tar.gz" + +[build] +template = "custom" +dependencies = [ + "qtbase", + "qtdeclarative", + "kf6-extra-cmake-modules", + "kf6-kcoreaddons", + "kf6-kconfig", + "kf6-ki18n", + "kf6-kcolorscheme", + "kf6-kiconthemes", + "kf6-kwidgetsaddons", + "kf6-kwindowsystem", + "kdecoration", +] +script = """ +DYNAMIC_INIT + +HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build" + +for qtdir in plugins mkspecs metatypes modules; do + if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then + ln -s "usr/${qtdir}" "${COOKBOOK_SYSROOT}/${qtdir}" + fi +done + +find "${COOKBOOK_SOURCE}" -name CMakeLists.txt -exec sed -i 's/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/' {} \\; +find "${COOKBOOK_SOURCE}" -name CMakeLists.txt -exec sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' {} \\; +sed -i '/include(ECMQmlModule)/s/^/#/' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true + +rm -f CMakeCache.txt +rm -rf CMakeFiles + +cmake "${COOKBOOK_SOURCE}" \ + -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \ + -DQT_HOST_PATH="${HOST_BUILD}" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \ + -DBUILD_TESTING=OFF \ + -DBUILD_QCH=OFF \ + -DBUILD_WITH_QML=OFF \ + -DUSE_DBUS=OFF \ + -DWITH_DECORATIONS=OFF \ + -Wno-dev + +cmake --build . -j"${COOKBOOK_MAKE_JOBS}" +cmake --install . --prefix "${COOKBOOK_STAGE}/usr" + +find "${COOKBOOK_STAGE}" -name '*.so*' -exec patchelf --remove-rpath {} \\; 2>/dev/null || true +""" diff --git a/local/recipes/kde/kde-cli-tools/recipe.toml b/local/recipes/kde/kde-cli-tools/recipe.toml new file mode 100644 index 00000000..67ec9cf7 --- /dev/null +++ b/local/recipes/kde/kde-cli-tools/recipe.toml @@ -0,0 +1,56 @@ +#TODO: kde-cli-tools — KDE command-line utilities (kioclient, kreadconfig, kwriteconfig, kde-open, etc.). +# Some tools depend on KIO for file/URL handling; kf6-kio is available. +# Tools requiring X11 or DBus activation are disabled where possible. +# kdesu is not built (needs sudo or kdesu backend which doesn't exist on Redox). +[source] +tar = "https://invent.kde.org/plasma/kde-cli-tools/-/archive/v6.3.4/kde-cli-tools-v6.3.4.tar.gz" + +[build] +template = "custom" +dependencies = [ + "qtbase", + "kf6-extra-cmake-modules", + "kf6-kcoreaddons", + "kf6-ki18n", + "kf6-kconfig", + "kf6-kservice", + "kf6-kwindowsystem", + "kf6-kdbusaddons", + "kf6-kio", +] +script = """ +DYNAMIC_INIT + +HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build" + +for qtdir in plugins mkspecs metatypes modules; do + if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then + ln -s "usr/${qtdir}" "${COOKBOOK_SYSROOT}/${qtdir}" + fi +done + +find "${COOKBOOK_SOURCE}" -name CMakeLists.txt -exec sed -i 's/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/' {} \\; +find "${COOKBOOK_SOURCE}" -name CMakeLists.txt -exec sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' {} \\; + +# Disable kdesu — no sudo/kdesu backend on Redox +sed -i 's/^add_subdirectory(kdesu/#add_subdirectory(kdesu/' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true + +rm -f CMakeCache.txt +rm -rf CMakeFiles + +cmake "${COOKBOOK_SOURCE}" \ + -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \ + -DQT_HOST_PATH="${HOST_BUILD}" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \ + -DBUILD_TESTING=OFF \ + -DBUILD_QCH=OFF \ + -DUSE_DBUS=OFF \ + -Wno-dev + +cmake --build . -j"${COOKBOOK_MAKE_JOBS}" +cmake --install . --prefix "${COOKBOOK_STAGE}/usr" + +find "${COOKBOOK_STAGE}" -name '*.so*' -exec patchelf --remove-rpath {} \\; 2>/dev/null || true +""" diff --git a/recipes/core/relibc/recipe.toml b/recipes/core/relibc/recipe.toml index a84e43f5..7b53b8d7 100644 --- a/recipes/core/relibc/recipe.toml +++ b/recipes/core/relibc/recipe.toml @@ -36,6 +36,7 @@ patches = [ "../../../local/patches/relibc/P3-waitid-header.patch", "../../../local/patches/relibc/P3-fenv.patch", "../../../local/patches/relibc/P3-sched.patch", + "../../../local/patches/relibc/P3-aio.patch", ] [build]