f6c2eb2a8e
- P19-init-startup-hardening: Replace panic-grade expect/unwrap in init startup paths (getns, register_scheme_to_ns, setrens, filename parsing) with graceful error handling and logging - P19-acpid-startup-hardening: Replace panic-grade calls in acpid with graceful degradation (rxsdt read failure → warn + exit 0, SDT parse → error + exit 1, I/O privilege → fatal, scheme registration → fatal, setrens → warn + continue, event loop errors → log + continue) - P18-9-msi-allocation-resilience: Regenerate with git diff -U0 -w format for maximum context resilience - fetch.rs: Change --fuzz=0 to --fuzz=3 for resilient patch application - AGENTS.md: Document robust patch generation technique as mandatory - Add P4/P5/P6/P7 patches (estale, dmi, i2c, ps2d hardening) - Add P21 kernel x2apic SMP fix patch - Multiple local recipe source improvements (redox-drm, driver-manager, driver-acpi, thermald) - Config updates for redbear-mini and redbear-device-services - Subsystem assessment document
409 lines
14 KiB
Diff
409 lines
14 KiB
Diff
diff --git a/drivers/input/i2c-hidd/src/main.rs b/drivers/input/i2c-hidd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..88270e37
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/main.rs
|
|
@@ -0,0 +1,114 @@
|
|
+use std::process;
|
|
+use std::thread;
|
|
+use std::time::Duration;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+
|
|
+mod acpi;
|
|
+mod hid;
|
|
+mod input;
|
|
+mod quirks;
|
|
+
|
|
+use acpi::{
|
|
+ hid_descriptor_address, prepare_acpi_device, read_decoded_resources, recover_acpi_device,
|
|
+ scan_acpi_i2c_hid_devices,
|
|
+};
|
|
+use hid::{fetch_hid_descriptor, fetch_report_descriptor, stream_input_reports, I2cAdapterClient};
|
|
+use input::InputForwarder;
|
|
+use quirks::match_probe_failure_quirk;
|
|
+
|
|
+fn main() {
|
|
+ daemon::Daemon::new(daemon);
|
|
+}
|
|
+
|
|
+fn daemon(daemon: daemon::Daemon) -> ! {
|
|
+ common::setup_logging(
|
|
+ "input",
|
|
+ "i2c-hid",
|
|
+ "i2c-hidd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ if let Err(err) = run(daemon) {
|
|
+ log::error!("RB_I2C_HIDD_BLOCKER stage=startup error={err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn run(daemon: daemon::Daemon) -> Result<()> {
|
|
+ log::info!("RB_I2C_HIDD_SCHEMA version=1");
|
|
+
|
|
+ let devices = scan_acpi_i2c_hid_devices().context("failed to scan ACPI I2C HID devices")?;
|
|
+ if devices.is_empty() {
|
|
+ log::warn!("RB_I2C_HIDD_BLOCKER stage=scan error=no PNP0C50/ACPI0C50 devices found");
|
|
+ }
|
|
+
|
|
+ let mut workers = Vec::new();
|
|
+ for device in devices {
|
|
+ log::info!("RB_I2C_HIDD_SNAPSHOT device={device}");
|
|
+ workers.push(thread::spawn(move || {
|
|
+ if let Err(err) = bind_device(&device) {
|
|
+ log::error!("RB_I2C_HIDD_BLOCKER device={} error={:#}", device, err);
|
|
+ }
|
|
+ }));
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+
|
|
+ if workers.is_empty() {
|
|
+ loop {
|
|
+ thread::sleep(Duration::from_secs(5));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for worker in workers {
|
|
+ let _ = worker.join();
|
|
+ }
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+pub fn bind_device(device_path: &str) -> Result<()> {
|
|
+ prepare_acpi_device(device_path)
|
|
+ .with_context(|| format!("failed to prepare ACPI device {device_path}"))?;
|
|
+
|
|
+ let resources = read_decoded_resources(device_path)
|
|
+ .with_context(|| format!("failed to decode _CRS for {device_path}"))?;
|
|
+ log::info!(
|
|
+ "RB_I2C_HIDD_SNAPSHOT device={} adapter={} addr={:04x} irq={:?} gpio_int={} gpio_io={}",
|
|
+ device_path,
|
|
+ resources.i2c.adapter,
|
|
+ resources.i2c.address,
|
|
+ resources.irq,
|
|
+ resources.gpio_int.len(),
|
|
+ resources.gpio_io.len()
|
|
+ );
|
|
+
|
|
+ let hid_desc_addr = hid_descriptor_address(device_path)
|
|
+ .with_context(|| format!("failed to evaluate _DSM for {device_path}"))?;
|
|
+ let adapter = I2cAdapterClient::new(resources.i2c.clone());
|
|
+ let hid_desc = fetch_hid_descriptor(&adapter, resources.i2c.address, hid_desc_addr)
|
|
+ .with_context(|| format!("failed to fetch HID descriptor for {device_path}"))?;
|
|
+ let report_desc = fetch_report_descriptor(&adapter, resources.i2c.address, &hid_desc)
|
|
+ .with_context(|| format!("failed to fetch report descriptor for {device_path}"))?;
|
|
+ let mut forwarder = InputForwarder::new().context("failed to connect to inputd producer")?;
|
|
+
|
|
+ match stream_input_reports(
|
|
+ &adapter,
|
|
+ resources.i2c.address,
|
|
+ &hid_desc,
|
|
+ &report_desc,
|
|
+ &mut forwarder,
|
|
+ ) {
|
|
+ Ok(()) => Ok(()),
|
|
+ Err(err) => {
|
|
+ let quirk =
|
|
+ match_probe_failure_quirk().context("failed to evaluate DMI recovery quirks")?;
|
|
+ recover_acpi_device(device_path, &resources, quirk.as_ref())
|
|
+ .with_context(|| format!("failed ACPI recovery for {device_path}"))?;
|
|
+ Err(err).with_context(|| format!("streaming input reports failed for {device_path}"))
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/drivers/input/intel-thc-hidd/src/main.rs b/drivers/input/intel-thc-hidd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..c5cda29e
|
|
--- /dev/null
|
|
+++ b/drivers/input/intel-thc-hidd/src/main.rs
|
|
@@ -0,0 +1,282 @@
|
|
+use std::collections::BTreeSet;
|
|
+use std::fs::{self, OpenOptions};
|
|
+use std::io::Read;
|
|
+use std::process;
|
|
+use std::thread;
|
|
+use std::time::Duration;
|
|
+
|
|
+use acpi_resource::ResourceDescriptor;
|
|
+use amlserde::{AmlSerde, AmlSerdeValue};
|
|
+use anyhow::{bail, Context, Result};
|
|
+use libredox::flag::{O_CLOEXEC, O_RDWR};
|
|
+use pcid_interface::PciFunctionHandle;
|
|
+
|
|
+mod quicki2c;
|
|
+mod thc;
|
|
+
|
|
+use quicki2c::QuickI2cTransport;
|
|
+use thc::{ThcController, SUPPORTED_PCI_IDS};
|
|
+
|
|
+fn main() {
|
|
+ pcid_interface::pci_daemon(daemon);
|
|
+}
|
|
+
|
|
+fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
|
+ common::setup_logging(
|
|
+ "input",
|
|
+ "intel-thc",
|
|
+ "intel-thc-hidd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ if let Err(err) = run(daemon, &mut pcid_handle) {
|
|
+ log::error!("RB_THC_HIDD_FATAL error={err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn run(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> Result<()> {
|
|
+ log::info!("RB_THC_HIDD_SCHEMA version=1");
|
|
+
|
|
+ let pci_config = pcid_handle.config();
|
|
+ let id = (
|
|
+ pci_config.func.full_device_id.vendor_id,
|
|
+ pci_config.func.full_device_id.device_id,
|
|
+ );
|
|
+ if !SUPPORTED_PCI_IDS.contains(&id) {
|
|
+ bail!("unsupported Intel THC PCI device {:04x}:{:04x}", id.0, id.1);
|
|
+ }
|
|
+
|
|
+ pcid_handle.enable_device();
|
|
+ let bar = unsafe { pcid_handle.map_bar(0) };
|
|
+ let controller = ThcController::new(bar.ptr.as_ptr(), bar.bar_size)
|
|
+ .context("failed to create THC controller")?;
|
|
+
|
|
+ let companion = resolve_acpi_companion(&pci_config.func.addr)
|
|
+ .context("failed to resolve ACPI companion for THC device")?;
|
|
+ let override_address = companion
|
|
+ .as_deref()
|
|
+ .map(companion_slave_address_override)
|
|
+ .transpose()
|
|
+ .context("failed to evaluate THC slave-address override")?
|
|
+ .flatten();
|
|
+ let hid_devices = scan_bound_i2c_hid_devices(companion.as_deref())
|
|
+ .context("failed to scan PNP0C50 devices for THC controller")?;
|
|
+
|
|
+ let effective_address = override_address.unwrap_or(0x0015);
|
|
+ let transport = QuickI2cTransport::new(controller, effective_address);
|
|
+ transport.prime_controller();
|
|
+ transport.emulate_transfer(&[]);
|
|
+ log::debug!("RB_THC_HIDD status={:#x}", transport.status());
|
|
+
|
|
+ match transport.register_with_i2cd(companion.as_deref(), override_address) {
|
|
+ Ok(()) => {}
|
|
+ Err(err) => {
|
|
+ log::warn!("RB_THC_HIDD registration error={err:#}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ log::info!(
|
|
+ "RB_THC_HIDD pci={} companion={:?} override={:?} hid_devices={}",
|
|
+ pci_config.func.name(),
|
|
+ companion,
|
|
+ override_address,
|
|
+ hid_devices.len()
|
|
+ );
|
|
+
|
|
+ daemon.ready();
|
|
+
|
|
+ loop {
|
|
+ thread::sleep(Duration::from_secs(5));
|
|
+ }
|
|
+}
|
|
+
|
|
+fn resolve_acpi_companion(addr: &pci_types::PciAddress) -> Result<Option<String>> {
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ log::debug!("intel-thc-hidd: ACPI symbols are not ready yet");
|
|
+ return Ok(None);
|
|
+ }
|
|
+ // ESTALE (116): filesystem handle stale during initfs-to-rootfs transition
|
|
+ // ENOENT (2): /scheme/acpi not mounted yet (e.g., acpid not started)
|
|
+ Err(err) if err.raw_os_error() == Some(116) || err.kind() == std::io::ErrorKind::NotFound => {
|
|
+ log::info!("intel-thc-hidd: ACPI symbols unavailable ({}), skipping companion resolution", err);
|
|
+ return Ok(None);
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+ let expected_adr = (u64::from(addr.device()) << 16) | u64::from(addr.function());
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to enumerate ACPI symbol entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("._ADR") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let symbol = read_aml_symbol(&file_name)?;
|
|
+ if !matches!(symbol.value, AmlSerdeValue::Integer(value) if value == expected_adr) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = symbol
|
|
+ .name
|
|
+ .strip_suffix("._ADR")
|
|
+ .unwrap_or(&symbol.name)
|
|
+ .trim_start_matches('\\')
|
|
+ .replace('/', ".");
|
|
+ return Ok(Some(device));
|
|
+ }
|
|
+
|
|
+ Ok(None)
|
|
+}
|
|
+
|
|
+fn companion_slave_address_override(path: &str) -> Result<Option<u16>> {
|
|
+ let icrs = evaluate_integer_method(path, "ICRS").ok();
|
|
+ let isub = evaluate_integer_method(path, "ISUB").ok();
|
|
+ Ok(icrs
|
|
+ .or(isub)
|
|
+ .map(|value| u16::try_from(value))
|
|
+ .transpose()
|
|
+ .context("THC ACPI override out of range")?)
|
|
+}
|
|
+
|
|
+fn scan_bound_i2c_hid_devices(companion: Option<&str>) -> Result<Vec<String>> {
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ log::debug!("intel-thc-hidd: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) if err.raw_os_error() == Some(116) || err.kind() == std::io::ErrorKind::NotFound => {
|
|
+ log::info!("intel-thc-hidd: ACPI symbols unavailable ({}), running with no HID devices", err);
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+ let mut devices = BTreeSet::new();
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to enumerate ACPI HID entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("._HID") && !file_name.ends_with("._CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let symbol = read_aml_symbol(&file_name)?;
|
|
+ let is_hid = matches!(
|
|
+ decode_hardware_id(&symbol.value).as_deref(),
|
|
+ Some("PNP0C50" | "ACPI0C50")
|
|
+ );
|
|
+ if !is_hid {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = symbol
|
|
+ .name
|
|
+ .strip_suffix("._HID")
|
|
+ .or_else(|| symbol.name.strip_suffix("._CID"))
|
|
+ .unwrap_or(&symbol.name)
|
|
+ .trim_start_matches('\\')
|
|
+ .replace('/', ".");
|
|
+ if let Some(companion) = companion {
|
|
+ if !is_bound_to_companion(&device, companion)? {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ devices.insert(device);
|
|
+ }
|
|
+
|
|
+ Ok(devices.into_iter().collect())
|
|
+}
|
|
+
|
|
+fn is_bound_to_companion(device: &str, companion: &str) -> Result<bool> {
|
|
+ let resource_path = format!("/scheme/acpi/resources/{device}");
|
|
+ let serialized = match fs::read_to_string(&resource_path) {
|
|
+ Ok(serialized) => serialized,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
|
+ Err(err) => return Err(err).with_context(|| format!("failed to read {resource_path}")),
|
|
+ };
|
|
+
|
|
+ let resources: Vec<ResourceDescriptor> =
|
|
+ ron::from_str(&serialized).with_context(|| format!("failed to decode {resource_path}"))?;
|
|
+ Ok(resources.into_iter().any(|resource| match resource {
|
|
+ ResourceDescriptor::I2cSerialBus(bus) => bus
|
|
+ .resource_source
|
|
+ .as_ref()
|
|
+ .map(|source| source.source == companion)
|
|
+ .unwrap_or(false),
|
|
+ _ => false,
|
|
+ }))
|
|
+}
|
|
+
|
|
+fn evaluate_integer_method(path: &str, method: &str) -> Result<u64> {
|
|
+ let symbol_name = format!("{}.{}", normalize_device_path(path), method);
|
|
+ let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}");
|
|
+ let fd = libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0)
|
|
+ .with_context(|| format!("failed to open {symbol_path}"))?;
|
|
+
|
|
+ let mut payload = ron::to_string(&Vec::<AmlSerdeValue>::new())
|
|
+ .context("failed to serialize ACPI call arguments")?
|
|
+ .into_bytes();
|
|
+ payload.resize(payload.len() + 2048, 0);
|
|
+ let used = libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[])
|
|
+ .with_context(|| format!("ACPI evaluation failed for {symbol_name}"))?;
|
|
+ let response = std::str::from_utf8(&payload[..used])
|
|
+ .with_context(|| format!("invalid UTF-8 ACPI response for {symbol_name}"))?;
|
|
+ match ron::from_str::<AmlSerdeValue>(response)
|
|
+ .with_context(|| format!("failed to decode ACPI response for {symbol_name}"))?
|
|
+ {
|
|
+ AmlSerdeValue::Integer(value) => Ok(value),
|
|
+ other => bail!("{}.{} returned non-integer value {other:?}", path, method),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn read_aml_symbol(file_name: &str) -> Result<AmlSerde> {
|
|
+ let path = format!("/scheme/acpi/symbols/{file_name}");
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .open(&path)
|
|
+ .with_context(|| format!("failed to open {path}"))?;
|
|
+ let mut ron_text = String::new();
|
|
+ file.read_to_string(&mut ron_text)
|
|
+ .with_context(|| format!("failed to read {path}"))?;
|
|
+ ron::from_str(&ron_text).with_context(|| format!("failed to decode {path}"))
|
|
+}
|
|
+
|
|
+fn decode_hardware_id(value: &AmlSerdeValue) -> Option<String> {
|
|
+ match value {
|
|
+ AmlSerdeValue::String(value) => Some(value.clone()),
|
|
+ AmlSerdeValue::Integer(integer) => {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1f) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+ Some(format!(
|
|
+ "{}{}{}{:01X}{:01X}{:01X}{:01X}",
|
|
+ vendor_1, vendor_2, vendor_3, device_1, device_2, device_3, device_4
|
|
+ ))
|
|
+ }
|
|
+ _ => None,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn normalize_device_path(path: &str) -> String {
|
|
+ path.trim_start_matches('\\')
|
|
+ .trim_matches('/')
|
|
+ .replace('/', ".")
|
|
+}
|