feat: ACPI Wave 1 boot-critical hardening (P19) + robust patch generation

- 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
This commit is contained in:
2026-05-18 14:07:42 +03:00
parent 7798ed86eb
commit f6c2eb2a8e
32 changed files with 3293 additions and 567 deletions
+41 -1
View File
@@ -641,12 +641,52 @@ Patches may use either format:
Git-specific headers (`diff --git`, `diff -ruN`, `index`, `new file mode`, `rename from/to`,
`similarity index`, `dissimilarity index`) are automatically stripped before
`patch` is invoked. The build system uses `--fuzz=0` for strict context matching.
`patch` is invoked. The build system uses `--fuzz=3` for resilient context matching.
**Timestamps in `---`/`+++` lines** (common in `diff -ruN` output) should be removed.
Use `--- a/path` and `+++ b/path` without timestamps. The `normalize_patch` function
does NOT strip timestamps — they should be removed from the patch file directly.
### Robust Patch Generation (REQUIRED)
Context-line mismatches (renamed variables, shifted line numbers, upstream refactors)
are the single largest source of patch application failures. Use the zero-context,
whitespace-ignored technique to make patches resilient to drift:
**Generate:**
```bash
cd recipes/<component>/source
git diff -U0 -w > ../../../local/patches/<component>/P<N>-<description>.patch
```
**Apply:**
```bash
patch -p1 --fuzz=3 < local/patches/<component>/P<N>-<description>.patch
```
**Why this works:**
- `-U0` produces zero lines of surrounding context, so the patch has no fragile context
lines that can drift when surrounding code changes
- `-w` ignores all whitespace changes, so indentation-only refactors don't break the patch
- `--fuzz=3` allows `patch(1)` to find the target location even when nearby lines have shifted
- Together these three flags eliminate the entire class of "context mismatch" failures
**Before this technique**, patches routinely broke when:
- A variable was renamed (e.g., `deamon``daemon` in context)
- Lines were added or removed above the changed code
- Indentation was reformatted
- An earlier patch in the chain shifted line numbers
**With this technique**, patches survive all of the above. A hunk consists only of the
changed lines themselves — no context that can go stale.
**Conventions:**
- Always use `--- a/path` and `+++ b/path` headers (no timestamps)
- Always name patches `P<N>-<description>.patch` with sequential numbering
- Always wire patches into `recipe.toml` `patches = [...]` in application order
- Always validate with `repo validate-patches <package>` after creating or editing a patch
- When updating an existing patch, regenerate it entirely rather than editing line numbers manually
### Protected Recipes
Core recipes (`base`, `kernel`, `relibc`, `bootloader`, etc.) and any recipe carrying
+20 -14
View File
@@ -268,6 +268,19 @@ class = 0x04
subclass = 0x01
"""
[[files]]
path = "/etc/init.d/00_acpid.service"
data = """
[unit]
description = "ACPI daemon (provides scheme:acpi)"
default_dependencies = false
[service]
cmd = "acpid"
inherit_envs = ["RSDP_ADDR", "RSDP_SIZE"]
type = { scheme = "acpi" }
"""
# ACPI GPIO/I2C controller drivers
# These match against ACPI-enumerated devices (class/subclass/vendor from _HID).
[[files]]
@@ -384,27 +397,20 @@ args = ["--hotplug"]
type = "oneshot_async"
"""
# Override the base package's 30_thermald.service with a no-op since
# 15_thermald.service (above) replaces it with earlier start ordering.
[[files]]
path = "/etc/init.d/10_evdevd.service"
path = "/etc/init.d/30_thermald.service"
data = """
[unit]
description = "Evdev input daemon"
requires_weak = [
"04_drivers.target",
"00_driver-manager.service",
]
description = "Thermal management daemon (suppressed; use 15_thermald.service)"
[service]
cmd = "evdevd"
type = "oneshot_async"
cmd = "echo"
args = ["thermald: started earlier as 15_thermald.service"]
type = "oneshot"
"""
[[files]]
path = "/etc/firmware-fallbacks.d"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/init.d/15_cpufreqd.service"
data = """
+3 -2
View File
@@ -279,6 +279,7 @@ data = """
[unit]
description = "ACPI I2C HID bring-up daemon (non-blocking)"
default_dependencies = false
requires = ["00_acpid.service"]
requires_weak = [
"00_i2cd.service",
"00_i2c-dw-acpi.service",
@@ -311,9 +312,9 @@ type = { scheme = "ucsi" }
path = "/etc/init.d/12_boot-late.target"
data = """
[unit]
description = "Late boot services target"
description = "Late boot services target (compat alias for 04_drivers.target)"
requires_weak = [
"00_base.target",
"04_drivers.target",
]
"""
+328
View File
@@ -0,0 +1,328 @@
# Red Bear OS Subsystem Assessment vs Linux Reference
**Date:** 2026-05-17
**Scope:** Input devices, ACPI/PCID, Intel DRM/KMS, boot process
**Reference:** Linux kernel 7.1 (local/reference/linux-7.1/)
---
## Executive Summary
Red Bear OS has real, functional architectural scaffolding across all five subsystems. The critical gaps are in **hardware-facing paths that are stubbed or incomplete** — most notably Intel EDID/DDC, hardware vblank, display watermarks, AML interpreter depth, and boot dependency ordering. The single most impactful immediate fix is adding the missing `acpid` service to boot configs, which prevents ACPI-dependent drivers from enumerating correctly.
**Critical blockers for bare-metal desktop:**
1. Missing `acpid` service in redbear configs → ACPI devices never discovered
2. Intel `read_edid_block()` returns error → synthetic EDID is 112 bytes (should be 128) → no real monitor modes
3. Intel `get_vblank()` is a fake atomic counter → no real vblank for page flip synchronization
4. No display watermarks → FIFO underruns cause visible glitching
5. No boot dependency declarations → i2c-hidd/i2cd may race with acpid
---
## 1. Input Devices (Keyboard, Mouse, HID, I2C-HID, Touch)
### Current Implementation Inventory
| Component | Path | Quality | Key Notes |
|---|---|---|---|
| ps2d (PS/2) | `base/source/drivers/input/ps2d/` (5 files) | **Real** | Keyboard scancodes + mouse protocol. x86-only (non-x86 is `unimplemented!()`). Many TODOs, QEMU-specific hacks. No ImPS/2 scroll or trackpoint. |
| usbhidd (USB HID) | `base/source/drivers/input/usbhidd/` (2 files, 520 lines) | **Real** | Full HID report descriptor parsing, USB usage→orbclient scancode table, mouse relative+absolute, scroll, buttons. Retry with exponential backoff. Polling-based (1ms sleep loop). |
| i2c-hidd (I2C HID) | `base/source/drivers/input/i2c-hidd/` (5 files, 500+ lines) | **Real** | ACPI PNP0C50/ACPI0C50 device scan, _CRS resource parsing, _DSM HID descriptor address, I2C transfer via `/scheme/i2c/`. Probe failure quirk system with DMI matching. |
| intel-thc-hidd (Intel THC) | `base/source/drivers/input/intel-thc-hidd/` (3 files, 282 lines) | **Partial** | PCI init works, QuickI2C transport setup works, ACPI companion resolution works. **Main loop is `thread::sleep(Duration::from_secs(5))` — no input report streaming.** |
| inputd (multiplexer) | `base/source/drivers/inputd/` (3 files, 663 lines) | **Real** | Producer/consumer scheme, VT switching, keymap support (US/Dvorak/GB/AZERTY/Bepo/IT). ESTALE handoff for display driver transitions. |
| evdevd (evdev adapter) | `local/recipes/system/evdevd/` (5+ files) | **Real** | evdev scheme, device model, orbclient→evdev translation, gesture recognizer, key filter. |
| redbear-keymapd | `local/recipes/system/redbear-keymapd/` | **Real** | Keymap scheme registration and management. |
| udev-shim | `local/recipes/system/udev-shim/` | **Real** | Device node synthesis from scheme registrations, heuristic mapping. |
| I2C bus drivers | `base/source/drivers/i2c/` (5 modules) | **Real** | amd-mp2-i2cd, dw-acpi-i2cd, intel-lpss-i2cd, generic i2cd, i2c-interface library. |
| redbear-input-headers | `local/recipes/drivers/redbear-input-headers/` | **Real** | `linux/input.h`, `linux/input-event-codes.h`, `linux/uinput.h` — replaces policy-violating `linux-input-headers` from libevdev tarball. |
| libinput (WIP) | `local/recipes/libs/libinput/` | **WIP** | Port of upstream libinput with touchpad/trackpoint filtering. Not yet runtime-trusted. |
| libevdev (WIP) | `local/recipes/libs/libevdev/` | **WIP** | Port of upstream libevdev. |
### Gaps vs Linux
| Gap | Severity | Linux Reference | Red Bear Status |
|---|---|---|---|
| intel-thc-hidd doesn't stream | **High** | `drivers/hid/intel-thc-hid/` full probe+report streaming | Main loop sleeps 5s; no HID reports |
| No multitouch/ABS_MT | **High** | `drivers/input/input-mt.c` slot tracking, pointer emulation | Not implemented |
| No libinput acceleration/gestures | **High** | libinput: velocity curves, palm detection, gesture recognition | inputd does raw keymap only |
| No PS/2 extended protocols | **Medium** | `libps2.c` ImPS/2 scroll, Explorer 5-btn, trackpoint | Basic protocol only |
| No HID quirks table | **Medium** | `hid-quirks.c` 4000+ device entries | Only probe_failure quirks |
| No input hotplug | **Medium** | udev + inotify on `/dev/input/` | Static scan at startup |
| Polling-based USB HID | **Low** | URB interrupt-driven | 1ms sleep loop (functional but power-inefficient) |
| inputd keymap incompleteness | **Low** | Full xkb/keyboard-layout support | TODO for configurable keymap, AltGr, NumLock |
### Linux I2C-HID Reference (from local/reference/linux-7.1/)
The Linux I2C-HID probe sequence is:
1. Verify IRQ exists
2. Wake/power up device (_PS0/HID_POWER_ON)
3. Read HID descriptor from controller register
4. Read report descriptor
5. Parse descriptor
6. Size buffers from actual reports
7. Register IRQ
8. `hid_add_device()`
Red Bear's i2c-hidd follows this sequence correctly. The Intel THC driver does steps 1-5 but never reaches step 7-8.
---
## 2. ACPI and PCID
### Current Implementation Inventory
| Component | Path | Quality | Key Notes |
|---|---|---|---|
| Kernel ACPI | `kernel/source/src/acpi/` (9+ files) | **Real, partial** | RSDP, RSDT/XSDT, MADT, FADT, DSDT parsing. New: SLIT, SRAT. AML evaluation for basic methods (_STA, _PS0, _PS3, _INI). **No While/If-Else, no OperationRegion for PCI/I2C, no method locals.** |
| Kernel ACPI scheme | `kernel/source/src/scheme/acpi.rs` | **Real** | Exposes ACPI tables, symbols, resources, method evaluation to userspace. |
| Kernel DMAR/IOMMU | `kernel/source/src/acpi/dmar/` | **Partial** | DMAR table parsing for IOMMU. DRHD entries parsed but not wired to allocator. |
| Kernel sleep/S3 | `kernel/source/src/arch/x86_shared/sleep.rs` (new, uncommitted) | **New** | S3 suspend/wakeup assembly. Not yet wired to power management. |
| acpid | `base/source/drivers/acpid/` | **Real** | Scheme-based ACPI access, symbol evaluation, resource serialization. ESTALE-graceful handling. |
| pcid | `base/source/drivers/pcid/` | **Real** | PCI enumeration, config space, BAR mapping, pcid-spawner. MSI/MSI-X support via recent patches. |
| redox-driver-acpi | `local/recipes/drivers/redox-driver-acpi/` | **Real** | ACPI bus driver bridging ACPI discovery to pcid-spawner. |
| driver-manager | `local/recipes/system/driver-manager/` | **Real** | Manages PCI/ACPI driver matching and spawning. |
| redox-driver-sys quirks | `local/recipes/drivers/redox-driver-sys/source/src/quirks/` | **Real** | Compiled-in + TOML + DMI quirk tables. MSI/MSI-X fallback, DISABLE_ACCEL. |
| IOMMU daemon | `local/recipes/system/iommu/` | **Partial** | Builds, QEMU first-use proof passes. Real hardware validation pending. |
### Gaps vs Linux
| Gap | Severity | Linux Reference | Red Bear Status |
|---|---|---|---|
| AML interpreter incomplete | **Critical** | Full AML bytecode VM (While/If/Else, OperationRegion, Method locals, Notify) | Basic method calls only (_STA, _PS0, _PS3, _INI). No control flow. |
| No _PRW wake resources | **High** | `drivers/acpi/wakeup.c` | Not present |
| No thermal zones | **High** | `drivers/acpi/thermal.c` _TMP/_ACx/_PSV/_CRT | Not present |
| No ACPI battery | **Medium** | `drivers/acpi/battery.c` _BIF/_BST | Not present |
| No ACPI buttons | **High** | `drivers/acpi/button.c` LID/Power/Sleep | Not present |
| SRAT/SLIT not wired to NUMA | **Medium** | `mm/numa.c` | Parsed but not connected to page allocator |
| No _OSC OS capabilities | **Medium** | `drivers/acpi/osc.c` | Not present |
| No PCI ASPM | **Medium** | `drivers/pci/pcie/aspm.c` | Not present |
| No PCI hotplug | **Low** | `drivers/pci/hotplug/` | Not present |
| No suspend/resume | **Critical** | `drivers/acpi/sleep.c` S1-S5 | sleep.rs + wakeup.asm in uncommitted changes, not wired |
| DMAR/IOMMU path commented out | **High** | `drivers/iommu/intel-iommu.c` | `acpid/src/acpi/dmar/mod.rs` has iterator bug (`len_bytes` from wrong slice), hangs on real hardware — entire DMAR path commented out |
| DMI quirk matching dead | **High** | `/sys/firmware/dmi` | `redox-driver-sys/quirks/dmi.rs` depends on `/scheme/acpi/dmi` but that source doesn't exist in the ACPI stack |
| ACPI resource parsing panics | **Medium** | N/A | `redox-driver-acpi/resource.rs` and `prt.rs` panic on unexpected ACPI resource shapes instead of returning errors |
| `madt/arch/other.rs` stub | **Low** | `drivers/acpi/madt.c` | Non-x86 MADT handling is effectively unimplemented |
| PCI config: non-x86 `todo!()` | **Low** | `drivers/pci/` | `pcid/src/cfg_access/fallback.rs` has `todo!()` for non-x86 PCI config access |
| **Missing acpid service in configs** | **Critical** | N/A (config bug) | No `acpid = {}` in redbear-full.toml or redbear-device-services.toml |
### acpid Missing From Configs — Critical Bug
The boot process agent found that **no active `acpid = {}` service entry exists** in the redbear TOML configs. This means acpid may never start, which prevents ACPI symbol/resource discovery for all ACPI-dependent drivers (i2c-hidd, intel-thc-hidd, thermald, driver-manager ACPI path). This is the single highest-priority fix.
---
## 3. Intel DRM/KMS
### Current Implementation Inventory
| Component | Path | Quality | Key Notes |
|---|---|---|---|
| IntelDriver | `redox-drm/source/src/drivers/intel/mod.rs` (682 lines) | **Partial** | PCIe init, MMIO mapping, FORCEWAKE, connector detection, CRTC set_mode, page_flip, GEM create/mmap/close, IRQ handling. |
| IntelDisplay | `.../intel/display.rs` (404 lines) | **Partial** | Pipe detection, DDI port detection, mode setting (real HTOTAL/HBLANK/HSYNC/VTOTAL/VSYNC/PIPE_SRC register writes). **EDID: read_edid_block returns error → synthetic_edid(). DPCD: returns fake 4 bytes.** |
| IntelGtt | `.../intel/gtt.rs` | **Real** | GGTT allocation, mapping, unmapping. |
| IntelRing | `.../intel/ring.rs` (267 lines) | **Partial** | DMA ring buffer, GPU address binding. Only MI_FLUSH_DW + MI_NOOP submitted — no rendering commands. |
| DRM scheme | `redox-drm/source/src/scheme.rs` | **Real** | Full DRM/KMS ioctl surface. SETPLANE is empty, GETENCODER hardcoded. |
| KMS infrastructure | `redox-drm/source/src/kms/` (5 files) | **Real** | ConnectorInfo, ModeInfo with EDID parsing, synthetic_edid fallback. |
| Interrupt handling | `redox-drm/source/src/drivers/interrupt.rs` | **Real** | MSI/MSI-X/INTx setup, try_wait polling. |
| Linux-kpi DRM headers | `linux-kpi/source/src/c_headers/drm/` | **Minimal** | drm.h, drm_crtc.h, drm_gem.h, drm_ioctl.h — type definitions only. |
| ihdgd (legacy) | `base/source/drivers/graphics/ihdgd/` | **Old/Partial** | Separate Intel framebuffer driver. Many TODOs. Being superseded by redox-drm. |
| vesad | `base/source/drivers/graphics/vesad/` | **Legacy** | VESA framebuffer driver. No cursor support. |
| Mesa | `recipes/libs/mesa/` | **Software only** | Only swrast+virgl Gallium. No Intel iris/crocus/anv driver build. |
### Critical Bugs Found
1. **synthetic_edid() is 112 bytes, not 128**`ModeInfo::from_edid()` requires `edid.len() >= 128` and checks for the 8-byte EDID header. The synthetic EDID is only 112 bytes so it always fails validation, forcing `default_1080p()` fallback on every Intel connector.
2. **Intel `get_vblank()` returns `AtomicU64::fetch_add(1, SeqCst)`** — This is NOT a real vblank counter. It increments on every IRQ regardless of display state. Real i915 reads the `PIPEFRAME` register (offset `0x70040 + pipe * 0x1000`) for per-pipe frame count.
### Gaps vs Linux i915
| Gap | Severity | Impact |
|---|---|---|
| EDID I2C/DDC stubbed | **Critical** | No real monitor modes — always falls back to synthetic/default |
| Vblank counter is fake | **Critical** | Page flip has no synchronization — tearing |
| Display watermarks absent | **Critical** | FIFO underruns → visible glitching on real hardware |
| No panel power sequencing | **High** | eDP panels won't turn on/off properly on laptops |
| No hardware cursor | **High** | No visible cursor in DRM mode |
| No DP AUX channel | **High** | No DisplayPort monitor support |
synthetic_edid too short (bug) | **Critical** | EDID validation always fails |
| No DMC firmware loading | **Medium** | No DC5/DC6 power state for Gen9+ |
| No HPD pulse detection | **Medium** | Monitor hotplug is crude |
| No render commands | **Medium** | Ring only does flush — no 2D/3D acceleration |
| No GGTT PTE invalidation | **Medium** | Stale TLB entries after GGTT updates |
| Mesa has no Intel driver | **High** | No hardware-accelerated OpenGL/Vulkan |
---
## 4. Boot Process
### Boot Sequence (as configured)
```
UEFI bootloader
→ kernel (startup, ACPI, scheme registration)
→ init (PID 1)
→ logd
→ scheme registration (memory, irq, event, pipe, debug, etc.)
→ numbered services from init.d/:
00_* : base daemons (ipcd, ptyd, randd)
02_* : driver-manager (or legacy pcid-spawner)
04_* : device drivers
06_* : D-Bus, sessiond, seatd
08_* : console/greeter
```
### Dependency Analysis
The `init` system supports `requires_weak` for service dependencies, but **most services don't declare dependencies**. The boot agent found:
- **`requires_weak`** means "if the dependency exists, wait for it; if not, proceed anyway." This is good for optional services but inadequate for strict ordering.
- **No explicit `acpid = {}` service** in redbear-full.toml or redbear-device-services.toml — ACPI-dependent drivers may never discover their devices.
- **`driver-manager`** retries deferred probes, but missing schemes (especially `acpi`) can leave drivers permanently skipped.
- **Greeter/session path works only** if dbus, seatd, redox-drm, and authd are all present. `redbear-greeterd` waits for Wayland socket, not a stronger compositor readiness signal.
### Gaps
| Gap | Severity | Notes |
|---|---|---|
| **Missing acpid service in configs** | **Critical** | No ACPI symbol discovery for i2c-hidd, thermald, driver-manager ACPI path |
| No dependency declarations | **High** | Services use number-based ordering only |
| No service readiness signaling | **High** | No sd_notify equivalent; init doesn't gate on daemon.ready() |
| No filesystem check | **Medium** | No fsck on boot; dirty filesystem mounts anyway |
| initfs→rootfs transition | **Medium** | No re-evaluation of service readiness after root switch |
---
## 5. Phased Implementation Plan
### Phase 1: Boot Dependency Fix (12 weeks)
**Priority: Unblocks everything downstream.**
| # | Task | Files | Complexity |
|---|------|-------|------------|
| 1.1 | Add `acpid = {}` to redbear-device-services.toml and redbear-full.toml | `config/redbear-device-services.toml`, `config/redbear-full.toml` | Low |
| 1.2 | Add `requires=` / `wants=` declarations to init service format | `recipes/core/base/source/init/src/` | Medium |
| 1.3 | Implement dependency-aware startup: wait for `scheme:<dep>` before starting dependents | `recipes/core/base/source/init/src/` | Medium |
| 1.4 | Add `provides= scheme:acpi` / `requires= scheme:acpi` to ACPI-dependent services | Service TOML files | Low |
| 1.5 | Wire ESTALE-retry into i2c-hidd/intel-thc-hidd as fallback (already partial) | `drivers/input/i2c-hidd/`, `intel-thc-hidd/` | Low |
### Phase 2: Intel Display Critical Fixes (35 weeks)
**Priority: Highest impact for bare-metal desktop.**
| # | Task | Complexity | Risk | Blocks |
|---|------|------------|------|--------|
| 2.1 | Implement I2C master-mode in i2cd | High | Medium | 2.2, 2.7, 3.1 |
| 2.2 | Implement real EDID via DDC (I2C at 0xA0). Fix synthetic_edid to 128 bytes as fallback | High | Medium | — |
| 2.3 | Implement hardware vblank (read PIPEFRAME register) | Medium | Low | — |
| 2.4 | Implement display watermarks (WM_LINETIME, WM levels per pipe) | High | Medium | — |
| 2.5 | Implement eDP panel power sequencing (PP_ON/OFF/CYCLE) | Medium | Medium | — |
| 2.6 | Implement hardware cursor (CUR_CTL/CUR_BASE/CUR_POS) | Medium | Low | — |
| 2.7 | Implement DP AUX channel (I2C-over-AUX for DisplayPort) | High | Medium | Depends on 2.1 |
**Ordering:** EDID (2.2) → vblank (2.3) → watermarks (2.4) → panel (2.5) → cursor (2.6) → DP AUX (2.7)
### Phase 3: Input Stack Completion (24 weeks)
**Can parallel with Phase 2 once I2C master-mode (2.1) is done.**
| # | Task | Complexity |
|---|------|------------|
| 3.1 | Complete intel-thc-hidd input streaming (replace sleep loop with HID report read) | Medium |
| 3.2 | Add PS/2 extended protocols (ImPS/2 scroll, Explorer 5-btn, trackpoint) | Medium |
| 3.3 | Add input device hotplug (dynamic producer registration in inputd) | Medium |
| 3.4 | Add multitouch protocol (ABS_MT slots, touch report parsing) | Medium |
| 3.5 | Add pointer acceleration to inputd | Low |
| 3.6 | Port bounded subset of Linux hid-quirks for device workarounds | Medium |
### Phase 4: AML Interpreter Depth (48 weeks)
**Risk gate: scope strictly to _PS0/_PS3/_PRW/_BIF/_BST opcodes first.**
| # | Task | Complexity |
|---|------|------------|
| 4.1 | AML While/If-Else/Method-with-locals (bounded, not full spec) | Very High |
| 4.2 | OperationRegion handlers for PCI config and I2C | High |
| 4.3 | _PRW (power resources for wake) | Medium |
| 4.4 | ACPI battery (_BIF/_BST) | Medium |
| 4.5 | ACPI buttons (LID, power, sleep) | Low |
| 4.6 | Thermal zone evaluation (_TMP, _ACx, _PSV, _CRT) | Medium |
### Phase 5: Advanced Features (48 weeks)
After Phases 24 are stable.
| # | Task |
|---|------|
| 5.1 | PCI ASPM power management (_OSC, L0s/L1) |
| 5.2 | PCI hotplug (acpiphp/pciehp) |
| 5.3 | SRAT/SLIT → NUMA allocator wiring |
| 5.4 | Display FIFO underrun recovery |
| 5.5 | HPD pulse detection |
| 5.6 | I2C bus error recovery (SMBus timeout, multi-controller) |
### Dependency Graph
```
Phase 1 (boot deps)
├──→ Phase 2 (Intel display) ──→ Phase 5.4, 5.5
│ │
│ └──→ 2.1 (I2C master) blocks 2.2, 2.7, 3.1
├──→ Phase 3 (input) ──→ 3.1 needs I2C (shared with 2.1)
├──→ Phase 4 (AML) ──→ Phase 5.1, 5.2
│ │
│ └──→ 4.1 gates 4.34.6
└──→ Phase 5 (advanced) ──→ depends on Phases 2, 3, 4
```
### Effort Estimate (2 developers)
| Phase | Duration | Parallelizable? |
|-------|----------|-----------------|
| Phase 1 | 12 weeks | Yes (with Phase 2 start) |
| Phase 2 | 35 weeks | Partially (2.1 blocks 2.22.7) |
| Phase 3 | 24 weeks | Yes (parallel with Phase 2) |
| Phase 4 | 48 weeks | Partially (4.1 gates rest) |
| Phase 5 | 48 weeks | After Phases 24 |
| **Total** | **1427 weeks** | ~814 months |
### Key Risks
1. **I2C master-mode** is a shared dependency between EDID (2.2), THC input (3.1), and DDC (2.2). Implement it first in i2cd.
2. **AML interpreter scope creep** — the full AML spec is enormous. Strictly bound the first pass to opcodes needed for _PS0/_PS3/_PRW/_BIF/_BST. Fallback: bounded userspace AML evaluator in acpid.
3. **Intel watermark programming varies by generation** — start with Gen9 Skylake, then generalize.
4. **synthetic_edid 112-byte bug** must be fixed IMMEDIATELY — it affects every Intel display attempt.
5. **Missing acpid service** in configs must be fixed IMMEDIATELY — it blocks all ACPI-dependent device discovery.
6. **DMAR/IOMMU iterator bug** in `acpid/src/acpi/dmar/mod.rs` causes hangs on real hardware; entire DMAR path is commented out.
7. **DMI quirk matching is dead**`redox-driver-sys/quirks/dmi.rs` reads `/scheme/acpi/dmi` but no code provides that scheme.
---
## Appendix A: Linux Reference File Map
From `local/reference/linux-7.1/`:
| Subsystem | Key Files |
|---|---|
| HID core | `drivers/hid/hid-core.c`, `hid-input.c`, `hid-quirks.c` |
| I2C-HID | `drivers/hid/i2c-hid/i2c-hid-core.c`, `i2c-hid-acpi.c` |
| USB HID | `drivers/hid/usbhid/hid-core.c`, `usbkbd.c`, `usbmouse.c` |
| Input core | `drivers/input/input.c`, `input-mt.c`, `evdev.c` |
| PS/2 | `drivers/input/serio/i8042.c`, `libps2.c`, `atkbd.c`, `psmouse-base.c` |
| I2C core | `drivers/i2c/i2c-core-acpi.c`, `i2c-core-base.c` |
| i915 | `drivers/gpu/drm/i915/` (6M+ lines) |
| ACPI | `drivers/acpi/` (full AML interpreter, 15k+ lines) |
## Appendix B: Uncommitted Changes (as of 2026-05-17)
The `bootprocess` branch has 63 changed files including:
- Kernel ACPI: `slit.rs`, `srat.rs` (NUMA), `msi.rs`, `vector.rs` (MSI/MSI-X), `sleep.rs` + `s3_wakeup.asm` (S3)
- Kernel: `numa.rs`, `mcs.rs` (MCS lock), context/percpu/event/sync improvements
- Base patches: `P4-acpi-estale-graceful.patch`, `P4-hwd-estale-graceful.patch`, `P4-ucsid-estale-graceful.patch`
- Kernel patch: `P21-x2apic-smp-fix.patch`
- Modified: pcid, driver-manager, thermald, redox-drm, redox-driver-acpi source files
+20 -66
View File
@@ -1,3 +1,4 @@
diff --git a/init/src/service.rs b/init/src/service.rs
--- a/init/src/service.rs
+++ b/init/src/service.rs
@@ -40,6 +40,28 @@
@@ -119,79 +120,36 @@
}
ServiceType::OneshotAsync => unreachable!(),
}
diff --git a/init/src/scheduler.rs b/init/src/scheduler.rs
--- a/init/src/scheduler.rs
+++ b/init/src/scheduler.rs
@@ -1,14 +1,28 @@
-use std::collections::{BTreeSet, VecDeque};
+use std::collections::{BTreeMap, BTreeSet, VecDeque};
@@ -2,17 +2,11 @@
use crate::InitConfig;
-use crate::color::{init_error, status_ok, status_skip};
+use crate::color::{init_error, init_warn, status_ok, status_skip};
use crate::color::{init_error, init_warn, status_ok, status_skip};
+use crate::service::RestartPolicy;
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
const SPAWN_BATCH_SIZE: usize = 50;
+/// Tracks the restart state for a supervised service.
+pub struct ServiceState {
+ pub unit_id: UnitId,
+ pub cmd: String,
+ pub restart_policy: RestartPolicy,
+ pub max_restarts: u32,
+ pub restart_count: u32,
+ /// Monotonic time of last restart (for backoff calculation).
+ pub last_restart_ms: u64,
+}
+
pub struct Scheduler {
pending: VecDeque<Job>,
completed: BTreeSet<UnitId>,
+ /// Maps child PID → service state for supervised services.
+ pub supervised: BTreeMap<u32, ServiceState>,
}
struct Job {
@@ -25,6 +39,7 @@
Scheduler {
pending: VecDeque::new(),
completed: BTreeSet::new(),
+ supervised: BTreeMap::new(),
}
}
@@ -106,7 +121,7 @@
defer_count = 0;
let unit = unit_store.unit_mut(&job.unit);
- run(unit, init_config);
+ run(unit, init_config, &mut self.supervised);
self.completed.insert(job.unit);
spawned_this_step += 1;
@@ -119,7 +134,7 @@
}
}
-fn run(unit: &mut Unit, config: &mut InitConfig) {
+fn run(unit: &mut Unit, config: &mut InitConfig, supervised: &mut BTreeMap<u32, ServiceState>) {
match &unit.kind {
UnitKind::LegacyScript { script } => {
for cmd in script.clone() {
@@ -127,13 +142,28 @@
}
}
UnitKind::Service { service } => {
- let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0);
+ let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0).clone();
if config.skip_cmd.contains(&service.cmd) {
status_skip(&format!("Skipping {} ({})", desc, service.cmd));
-#[derive(Clone, Debug)]
-pub enum RestartPolicy {
- Never,
- OnFailure,
- Always,
-}
-
/// Tracks the restart state for a supervised service.
pub struct ServiceState {
pub unit_id: UnitId,
@@ -167,9 +161,21 @@
return;
}
status_ok(&format!("Started {}", desc));
- service.spawn(&config.envs);
- // Supervision infrastructure is in place; full PID tracking requires
- // service.spawn() to return Option<u32> (added by a later patch).
+ if let Some(pid) = service.spawn(&config.envs) {
+ // Only supervise services with a restart policy other than Never
+ if service.restart != RestartPolicy::Never {
+ supervised.insert(
+ pid,
@@ -209,6 +167,7 @@
}
UnitKind::Target {} => {}
}
diff --git a/init/src/main.rs b/init/src/main.rs
--- a/init/src/main.rs
+++ b/init/src/main.rs
@@ -5,7 +5,8 @@
@@ -221,7 +180,7 @@
use crate::unit::{UnitId, UnitStore};
mod color;
@@ -176,15 +177,100 @@
@@ -176,15 +177,95 @@
if scheduler.has_pending() {
// Reap exited children before processing more services.
let mut status = 0;
@@ -244,7 +203,6 @@
}
}
+
+/// Handle a child process exit. If it's a supervised service, apply restart policy.
+fn handle_child_exit(
+ pid: u32,
+ exit_status: i32,
@@ -253,7 +211,7 @@
+ init_config: &mut InitConfig,
+) {
+ let Some(state) = scheduler.supervised.remove(&pid) else {
+ return; // Not a supervised service — just reap
+ return;
+ };
+
+ let exited_cleanly = exit_status == 0;
@@ -279,7 +237,6 @@
+ return;
+ }
+
+ // Exponential backoff: 1s, 2s, 4s, 8s, ... up to 30s max
+ let backoff_secs = 1u64 << state.restart_count.min(4);
+ let backoff_secs = backoff_secs.min(30);
+ init_warn(&format!(
@@ -292,14 +249,11 @@
+ state.max_restarts,
+ ));
+
+ // Sleep for backoff period (blocking, but init is the supervisor)
+ std::thread::sleep(std::time::Duration::from_secs(backoff_secs));
+
+ // Re-spawn the service by scheduling a restart
+ let unit_id = state.unit_id.clone();
+ let new_restart_count = state.restart_count + 1;
+
+ // Load the unit and re-run it
+ let unit = unit_store.unit_mut(&unit_id);
+ if let crate::unit::UnitKind::Service { service } = &unit.kind {
+ if let Some(new_pid) = service.spawn(&init_config.envs) {
@@ -1,29 +1,30 @@
diff --git a/drivers/net/virtio-netd/src/main.rs b/drivers/net/virtio-netd/src/main.rs
index 1200cec..0c6663e 100644
--- a/drivers/net/virtio-netd/src/main.rs
+++ b/drivers/net/virtio-netd/src/main.rs
@@ -34,2 +34,7 @@ fn daemon_runner(redox_daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -
- daemon(redox_daemon, pcid_handle).unwrap();
- unreachable!();
+ match daemon(redox_daemon, pcid_handle) {
+ Ok(()) => unreachable!(),
+ Err(err) => {
+ log::error!("virtio-netd: fatal error: {err}");
+ std::process::exit(1);
+ }
+ }
diff --git a/drivers/pcid/src/driver_interface/irq_helpers.rs b/drivers/pcid/src/driver_interface/irq_helpers.rs
index 28ca077..7ecc9a3 100644
--- a/drivers/pcid/src/driver_interface/irq_helpers.rs
+++ b/drivers/pcid/src/driver_interface/irq_helpers.rs
@@ -118,7 +118,7 @@
let mut handles = Vec::with_capacity(usize::from(count));
let mut index = 0;
@@ -121 +121 @@ pub fn allocate_aligned_interrupt_vectors(
- let mut first = None;
+ let mut first_aligned: Option<u8> = None;
while let Some(number) = available_irq_numbers.next() {
let number = number?;
@@ -127,8 +127,8 @@
if number % u8::from(alignment) != 0 {
continue;
}
@@ -130,2 +130,2 @@ pub fn allocate_aligned_interrupt_vectors(
- let first = *first.get_or_insert(number);
- let irq_number = first + index;
+ let base = *first_aligned.get_or_insert(number);
+ let irq_number = base + index;
// From the point where the range is aligned, we can start to advance until `count` IRQs
// have been allocated.
@@ -141,6 +141,15 @@
match File::create(format!("/scheme/irq/cpu-{:02x}/{}", cpu_id, irq_number)) {
Ok(handle) => handle,
@@ -143,0 +144,9 @@ pub fn allocate_aligned_interrupt_vectors(
+ // Vector already allocated by another process; release any partial range and
+ // restart the search from the next aligned position.
+ Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
@@ -33,27 +34,13 @@
+ continue;
+ }
+
// return early if the entire range couldn't be allocated
Err(err) if err.kind() == io::ErrorKind::NotFound => break,
@@ -152,7 +161,7 @@
if handles.is_empty() {
return Ok(None);
}
@@ -155 +164 @@ pub fn allocate_aligned_interrupt_vectors(
- let first = match first {
+ let first = match first_aligned {
Some(f) => f,
None => return Ok(None),
};
@@ -180,40 +189,60 @@
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@@ -183 +192 @@ pub fn allocate_single_interrupt_vector(cpu_id: usize) -> io::Result<Option<(u8,
-pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndData, File) {
+pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> Option<(MsiAddrAndData, File)> {
use crate::driver_interface::msi::x86 as x86_msix;
// FIXME for cpu_id >255 we need to use the IOMMU to use IRQ remapping
@@ -187 +196,7 @@ pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndDat
- let lapic_id = u8::try_from(cpu_id).expect("CPU id couldn't fit inside u8");
+ let lapic_id = match u8::try_from(cpu_id) {
+ Ok(id) => id,
@@ -62,10 +49,7 @@
+ return None;
+ }
+ };
let rh = false;
let dm = false;
let addr = x86_msix::message_address(lapic_id, rh, dm);
@@ -192,3 +207,11 @@ pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndDat
- let (vector, interrupt_handle) = allocate_single_interrupt_vector(cpu_id)
- .expect("failed to allocate interrupt vector")
- .expect("no interrupt vectors left");
@@ -80,171 +64,88 @@
+ return None;
+ }
+ };
let msg_data = x86_msix::message_data_edge_triggered(x86_msix::DeliveryMode::Fixed, vector);
@@ -197 +220 @@ pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndDat
- (
+ Some((
MsiAddrAndData {
addr,
data: msg_data,
},
interrupt_handle,
@@ -203 +226 @@ pub fn allocate_single_interrupt_vector_for_msi(cpu_id: usize) -> (MsiAddrAndDat
- )
+ ))
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
pub fn allocate_first_msi_interrupt_on_bsp(
pcid_handle: &mut crate::driver_interface::PciFunctionHandle,
@@ -209 +232 @@ pub fn allocate_first_msi_interrupt_on_bsp(
-) -> File {
+) -> Option<File> {
use crate::driver_interface::{MsiSetFeatureInfo, PciFeature, SetFeatureInfo};
// TODO: Allow allocation of up to 32 vectors.
@@ -214 +237,7 @@ pub fn allocate_first_msi_interrupt_on_bsp(
- let destination_id = read_bsp_apic_id().expect("failed to read BSP apic id");
+ let destination_id = match read_bsp_apic_id() {
+ Ok(id) => id,
+ Err(err) => {
+ log::warn!("failed to read BSP APIC ID: {}", err);
+ log::warn!("failed to read BSP apic id: {}", err);
+ return None;
+ }
+ };
let (msg_addr_and_data, interrupt_handle) =
@@ -216 +245 @@ pub fn allocate_first_msi_interrupt_on_bsp(
- allocate_single_interrupt_vector_for_msi(destination_id);
+ allocate_single_interrupt_vector_for_msi(destination_id)?;
let set_feature_info = MsiSetFeatureInfo {
multi_message_enable: Some(0),
@@ -225,7 +254,7 @@
pcid_handle.enable_feature(PciFeature::Msi);
log::debug!("Enabled MSI");
@@ -228 +257 @@ pub fn allocate_first_msi_interrupt_on_bsp(
- interrupt_handle
+ Some(interrupt_handle)
}
pub struct InterruptVector {
@@ -275,6 +304,7 @@
let has_msi = features.iter().any(|feature| feature.is_msi());
let has_msix = features.iter().any(|feature| feature.is_msix());
+ // Try MSI-X first, then MSI, then fall back to legacy INTx#.
if has_msix {
let msix_info = match pcid_handle.feature_info(super::PciFeature::MsiX) {
super::PciFeatureInfo::MsiX(msix) => msix,
@@ -282,28 +312,45 @@
};
let mut info = unsafe { msix_info.map_and_mask_all(pcid_handle) };
@@ -285,2 +313,0 @@ pub fn pci_allocate_interrupt_vector(
- pcid_handle.enable_feature(crate::driver_interface::PciFeature::MsiX);
-
let entry = info.table_entry_pointer(0);
let bsp_cpu_id = read_bsp_apic_id()
.unwrap_or_else(|err| panic!("{driver}: failed to read BSP APIC ID: {err}"));
@@ -289,3 +316,9 @@ pub fn pci_allocate_interrupt_vector(
- let bsp_cpu_id = read_bsp_apic_id()
- .unwrap_or_else(|err| panic!("{driver}: failed to read BSP APIC ID: {err}"));
- let (msg_addr_and_data, irq_handle) = allocate_single_interrupt_vector_for_msi(bsp_cpu_id);
- entry.write_addr_and_data(msg_addr_and_data);
- entry.unmask();
+ let bsp_cpu_id = match read_bsp_apic_id() {
+ Ok(id) => id,
+ Err(err) => {
+ log::warn!("{driver}: failed to read BSP APIC ID: {err}");
+ // fall through to MSI
+ 0
+ }
+ };
+ if let Some((msg_addr_and_data, irq_handle)) = allocate_single_interrupt_vector_for_msi(bsp_cpu_id) {
@@ -295 +328,3 @@ pub fn pci_allocate_interrupt_vector(
- InterruptVector {
- irq_handle,
- vector: 0,
- kind: InterruptVectorKind::MsiX { table_entry: entry },
+ if let Some((msg_addr_and_data, irq_handle)) =
+ allocate_single_interrupt_vector_for_msi(bsp_cpu_id)
+ {
+ // Vector allocated: enable MSI-X and configure the table entry.
+ pcid_handle.enable_feature(crate::driver_interface::PciFeature::MsiX);
+ entry.write_addr_and_data(msg_addr_and_data);
+ entry.unmask();
+
+ return InterruptVector {
+ irq_handle,
+ vector: 0,
+ kind: InterruptVectorKind::MsiX { table_entry: entry },
@@ -298,0 +334 @@ pub fn pci_allocate_interrupt_vector(
+ };
}
@@ -300,3 +336,8 @@ pub fn pci_allocate_interrupt_vector(
- } else if has_msi {
- InterruptVector {
- irq_handle: allocate_first_msi_interrupt_on_bsp(pcid_handle),
- vector: 0,
- kind: InterruptVectorKind::Msi,
+
+ // MSI-X vector allocation failed; MSI-X was never enabled in config space so the
+ // device will fall back to INTx# or MSI. Mapped BARs are released when `info` drops.
+ log::warn!("{driver}: MSI-X vector allocation failed, falling back");
+ // fall through to MSI
+ }
+
+ if has_msi {
+ if let Some(irq_handle) = allocate_first_msi_interrupt_on_bsp(pcid_handle) {
+ return InterruptVector {
+ irq_handle,
+ vector: 0,
+ kind: InterruptVectorKind::Msi,
@@ -304,0 +346,3 @@ pub fn pci_allocate_interrupt_vector(
+ };
}
+ }
+ log::warn!("{driver}: MSI allocation failed, falling back to legacy");
@@ -306 +350,2 @@ pub fn pci_allocate_interrupt_vector(
- } else if let Some(irq) = pcid_handle.config().func.legacy_interrupt_line {
+
+ // MSI allocation failed; fall back to legacy.
+ log::warn!("{driver}: MSI allocation failed, falling back to legacy");
+ }
+
+ if let Some(irq) = pcid_handle.config().func.legacy_interrupt_line {
// INTx# pin based interrupts.
InterruptVector {
irq_handle: irq.irq_handle(driver),
--- a/drivers/virtio-core/src/transport.rs
+++ b/drivers/virtio-core/src/transport.rs
@@ -19,6 +19,8 @@
SyscallError(#[from] libredox::error::Error),
#[error("the device is incapable of {0:?}")]
InCapable(CfgType),
+ #[error("MSI/MSI-X vector allocation failed")]
+ MsiAllocationFailed,
}
/// Returns the queue part sizes in bytes.
--- a/drivers/virtio-core/src/arch/x86.rs
+++ b/drivers/virtio-core/src/arch/x86.rs
@@ -23,7 +23,8 @@
let destination_id = read_bsp_apic_id().expect("virtio_core: `read_bsp_apic_id()` failed");
let (msg_addr_and_data, interrupt_handle) =
- allocate_single_interrupt_vector_for_msi(destination_id);
+ allocate_single_interrupt_vector_for_msi(destination_id)
+ .ok_or(Error::MsiAllocationFailed)?;
table_entry_pointer.write_addr_and_data(msg_addr_and_data);
table_entry_pointer.unmask();
--- a/drivers/net/virtio-netd/src/main.rs
+++ b/drivers/net/virtio-netd/src/main.rs
@@ -31,8 +31,13 @@
}
fn daemon_runner(daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! {
- deamon(daemon, pcid_handle).unwrap();
- unreachable!();
+ match deamon(daemon, pcid_handle) {
+ Ok(()) => unreachable!(),
+ Err(err) => {
+ log::error!("virtio-netd: fatal error: {err}");
+ std::process::exit(1);
+ }
+ }
}
fn deamon(
@@ -308 +353 @@ pub fn pci_allocate_interrupt_vector(
- InterruptVector {
+ return InterruptVector {
@@ -311,0 +357 @@ pub fn pci_allocate_interrupt_vector(
+ };
@@ -313 +359 @@ pub fn pci_allocate_interrupt_vector(
- } else {
+
@@ -316 +361,0 @@ pub fn pci_allocate_interrupt_vector(
-}
diff --git a/drivers/storage/virtio-blkd/src/main.rs b/drivers/storage/virtio-blkd/src/main.rs
index d21236b..95089eb 100644
--- a/drivers/storage/virtio-blkd/src/main.rs
+++ b/drivers/storage/virtio-blkd/src/main.rs
@@ -103,8 +103,13 @@
}
fn daemon_runner(redox_daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -> ! {
@@ -106,2 +106,7 @@ fn daemon_runner(redox_daemon: daemon::Daemon, pcid_handle: PciFunctionHandle) -
- daemon(redox_daemon, pcid_handle).unwrap();
- unreachable!();
+ match daemon(redox_daemon, pcid_handle) {
@@ -254,54 +155,54 @@
+ std::process::exit(1);
+ }
+ }
}
fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> anyhow::Result<()> {
diff --git a/drivers/usb/xhcid/src/main.rs b/drivers/usb/xhcid/src/main.rs
index d345a52..397971d 100644
--- a/drivers/usb/xhcid/src/main.rs
+++ b/drivers/usb/xhcid/src/main.rs
@@ -76,22 +76,32 @@
let table_entry_pointer = info.table_entry_pointer(k);
let destination_id = read_bsp_apic_id().expect("xhcid: failed to read BSP apic id");
@@ -79,2 +79,3 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, Interru
- let (msg_addr_and_data, interrupt_handle) =
- allocate_single_interrupt_vector_for_msi(destination_id);
- table_entry_pointer.write_addr_and_data(msg_addr_and_data);
- table_entry_pointer.unmask();
- (Some(interrupt_handle), InterruptMethod::Msi)
+ if let Some((msg_addr_and_data, interrupt_handle)) =
+ allocate_single_interrupt_vector_for_msi(destination_id)
+ {
+ table_entry_pointer.write_addr_and_data(msg_addr_and_data);
+ table_entry_pointer.unmask();
+
+ pcid_handle.enable_feature(PciFeature::MsiX);
+ log::debug!("Enabled MSI-X");
+
@@ -84,3 +84,0 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, Interru
- (Some(interrupt_handle), InterruptMethod::Msi)
- };
-
@@ -90,5 +88,16 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, Interru
- method
- } else if has_msi {
- let interrupt_handle = allocate_first_msi_interrupt_on_bsp(pcid_handle);
- (Some(interrupt_handle), InterruptMethod::Msi)
- } else if let Some(irq) = pci_config.func.legacy_interrupt_line {
+ return (Some(interrupt_handle), InterruptMethod::Msi);
+ }
+
+ // MSI-X allocation failed; fall through to MSI or legacy.
+ log::warn!("xhcid: MSI-X vector allocation failed, falling back");
};
+ };
+ }
- pcid_handle.enable_feature(PciFeature::MsiX);
- log::debug!("Enabled MSI-X");
+
+ if has_msi {
+ if let Some(interrupt_handle) = allocate_first_msi_interrupt_on_bsp(pcid_handle) {
+ return (Some(interrupt_handle), InterruptMethod::Msi);
+ }
+ log::warn!("xhcid: MSI allocation failed, falling back to legacy");
+ }
- method
- } else if has_msi {
- let interrupt_handle = allocate_first_msi_interrupt_on_bsp(pcid_handle);
- (Some(interrupt_handle), InterruptMethod::Msi)
- } else if let Some(irq) = pci_config.func.legacy_interrupt_line {
+
+ if let Some(irq) = pci_config.func.legacy_interrupt_line {
log::debug!("Legacy IRQ {}", irq);
// legacy INTx# interrupt pins.
diff --git a/drivers/virtio-core/src/arch/x86.rs b/drivers/virtio-core/src/arch/x86.rs
index aea86c4..8fdc7ca 100644
--- a/drivers/virtio-core/src/arch/x86.rs
+++ b/drivers/virtio-core/src/arch/x86.rs
@@ -26 +26,2 @@ pub fn enable_msix(pcid_handle: &mut PciFunctionHandle) -> Result<File, Error> {
- allocate_single_interrupt_vector_for_msi(destination_id);
+ allocate_single_interrupt_vector_for_msi(destination_id)
+ .ok_or(Error::MsiAllocationFailed)?;
diff --git a/drivers/virtio-core/src/transport.rs b/drivers/virtio-core/src/transport.rs
index d3445d2..b961265 100644
--- a/drivers/virtio-core/src/transport.rs
+++ b/drivers/virtio-core/src/transport.rs
@@ -21,0 +22,2 @@ pub enum Error {
+ #[error("MSI/MSI-X vector allocation failed")]
+ MsiAllocationFailed,
@@ -0,0 +1,235 @@
--- a/drivers/acpid/src/main.rs
+++ b/drivers/acpid/src/main.rs
@@ -32,3 +32,8 @@
- let rxsdt_raw_data: Arc<[u8]> = std::fs::read("/scheme/kernel.acpi/rxsdt")
- .expect("acpid: failed to read `/scheme/kernel.acpi/rxsdt`")
- .into();
+ let rxsdt_raw_data: Arc<[u8]> = match std::fs::read("/scheme/kernel.acpi/rxsdt") {
+ Ok(data) => data.into(),
+ Err(e) => {
+ log::warn!("acpid: failed to read `/scheme/kernel.acpi/rxsdt`: {} — no ACPI", e);
+ daemon.ready();
+ std::process::exit(0);
+ }
+ };
@@ -42 +47,7 @@
- let sdt = self::acpi::Sdt::new(rxsdt_raw_data).expect("acpid: failed to parse [RX]SDT");
+ let sdt = match self::acpi::Sdt::new(rxsdt_raw_data) {
+ Ok(sdt) => sdt,
+ Err(e) => {
+ log::error!("acpid: failed to parse [RX]SDT: {}", e);
+ std::process::exit(1);
+ }
+ };
@@ -52,2 +63 @@
- // TODO: With const generics, the compiler has some way of doing this for static sizes.
- .map(|chunk| <[u8; mem::size_of::<u32>()]>::try_from(chunk).unwrap())
+ .filter_map(|chunk| <[u8; mem::size_of::<u32>()]>::try_from(chunk).ok())
@@ -63 +73 @@
- .map(|chunk| <[u8; mem::size_of::<u64>()]>::try_from(chunk).unwrap())
+ .filter_map(|chunk| <[u8; mem::size_of::<u64>()]>::try_from(chunk).ok())
@@ -68 +78,4 @@
- _ => panic!("acpid: expected [RX]SDT from kernel to be either of those"),
+ _ => {
+ log::error!("acpid: expected [RX]SDT from kernel to be RSDT or XSDT, got {:?}", String::from_utf8_lossy(&sdt.signature));
+ std::process::exit(1);
+ }
@@ -87 +100,4 @@
- common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3");
+ if let Err(e) = common::acquire_port_io_rights() {
+ log::error!("acpid: failed to set I/O privilege level to Ring 3: {}", e);
+ std::process::exit(1);
+ }
@@ -89,2 +105,7 @@
- let shutdown_pipe = File::open("/scheme/kernel.acpi/kstop")
- .expect("acpid: failed to open `/scheme/kernel.acpi/kstop`");
+ let shutdown_pipe = match File::open("/scheme/kernel.acpi/kstop") {
+ Ok(file) => Some(file),
+ Err(e) => {
+ log::warn!("acpid: failed to open `/scheme/kernel.acpi/kstop`: {} — continuing without shutdown support", e);
+ None
+ }
+ };
@@ -92,2 +113,14 @@
- let mut event_queue = RawEventQueue::new().expect("acpid: failed to create event queue");
- let socket = Socket::nonblock().expect("acpid: failed to create disk scheme");
+ let mut event_queue = match RawEventQueue::new() {
+ Ok(q) => q,
+ Err(e) => {
+ log::error!("acpid: failed to create event queue: {}", e);
+ std::process::exit(1);
+ }
+ };
+ let socket = match Socket::nonblock() {
+ Ok(s) => s,
+ Err(e) => {
+ log::error!("acpid: failed to create disk scheme: {}", e);
+ std::process::exit(1);
+ }
+ };
@@ -98,6 +131,9 @@
- event_queue
- .subscribe(shutdown_pipe.as_raw_fd() as usize, 0, EventFlags::READ)
- .expect("acpid: failed to register shutdown pipe for event queue");
- event_queue
- .subscribe(socket.inner().raw(), 1, EventFlags::READ)
- .expect("acpid: failed to register scheme socket for event queue");
+ if let Some(ref pipe) = shutdown_pipe {
+ if let Err(e) = event_queue.subscribe(pipe.as_raw_fd() as usize, 0, EventFlags::READ) {
+ log::warn!("acpid: failed to register shutdown pipe for event queue: {} — continuing without shutdown support", e);
+ }
+ }
+ if let Err(e) = event_queue.subscribe(socket.inner().raw(), 1, EventFlags::READ) {
+ log::error!("acpid: failed to register scheme socket for event queue: {}", e);
+ std::process::exit(1);
+ }
@@ -105,2 +141,4 @@
- register_sync_scheme(&socket, "acpi", &mut scheme)
- .expect("acpid: failed to register acpi scheme to namespace");
+ if let Err(e) = register_sync_scheme(&socket, "acpi", &mut scheme) {
+ log::error!("acpid: failed to register acpi scheme to namespace: {}", e);
+ std::process::exit(1);
+ }
@@ -110 +148,3 @@
- libredox::call::setrens(0, 0).expect("acpid: failed to enter null namespace");
+ if let Err(e) = libredox::call::setrens(0, 0) {
+ log::warn!("acpid: failed to enter null namespace: {} — continuing", e);
+ }
@@ -114,5 +154,7 @@
- let Some(event) = event_queue
- .next()
- .transpose()
- .expect("acpid: failed to read event file")
- else {
+ let Some(event) = match event_queue.next().transpose() {
+ Ok(e) => e,
+ Err(e) => {
+ log::error!("acpid: failed to read event file: {} — continuing", e);
+ continue;
+ }
+ } else {
@@ -124,6 +166,7 @@
- match handler
- .process_requests_nonblocking(&mut scheme)
- .expect("acpid: failed to process requests")
- {
- ControlFlow::Continue(()) => {}
- ControlFlow::Break(()) => break,
+ match handler.process_requests_nonblocking(&mut scheme) {
+ Ok(ControlFlow::Continue(())) => {}
+ Ok(ControlFlow::Break(())) => break,
+ Err(e) => {
+ log::error!("acpid: failed to process requests: {} — continuing", e);
+ break;
+ }
@@ -132 +175 @@
- } else if event.fd == shutdown_pipe.as_raw_fd() as usize {
+ } else if shutdown_pipe.as_ref().map_or(false, |p| event.fd == p.as_raw_fd() as usize) {
@@ -146 +189,2 @@
- unreachable!("System should have shut down before this is entered");
+ log::error!("System should have shut down before this was reached");
+ std::process::exit(1);
--- a/drivers/acpid/src/acpi.rs
+++ b/drivers/acpid/src/acpi.rs
@@ -55,3 +55,2 @@
- self.length
- .try_into()
- .expect("expected usize to be at least 32 bits")
+ // usize is at least 32 bits on all supported architectures.
+ self.length as usize
@@ -95,0 +95,3 @@
+
+ #[error("bad alignment")]
+ BadAlignment,
@@ -139,3 +141,4 @@
- Err(plain::Error::BadAlignment) => panic!(
- "plain::from_bytes failed due to alignment, but SdtHeader is #[repr(packed)]!"
- ),
+ Err(plain::Error::BadAlignment) => {
+ log::error!("plain::from_bytes failed due to alignment, but SdtHeader is #[repr(packed)]");
+ return Err(InvalidSdtError::BadAlignment);
+ }
@@ -171 +174,3 @@
- assert!(pages.len() >= mem::size_of::<SdtHeader>());
+ if pages.len() < mem::size_of::<SdtHeader>() {
+ return Err(TablePhysLoadError::Validity(InvalidSdtError::InvalidSize));
+ }
@@ -174,2 +179,5 @@
- let sdt = plain::from_bytes::<SdtHeader>(&sdt_mem[..mem::size_of::<SdtHeader>()])
- .expect("either alignment is wrong, or the length is too short, both of which are already checked for");
+ let sdt = match plain::from_bytes::<SdtHeader>(&sdt_mem[..mem::size_of::<SdtHeader>()]) {
+ Ok(header) => header,
+ Err(plain::Error::TooShort) => return Err(TablePhysLoadError::Validity(InvalidSdtError::InvalidSize)),
+ Err(plain::Error::BadAlignment) => return Err(TablePhysLoadError::Validity(InvalidSdtError::BadAlignment)),
+ };
@@ -200 +208,4 @@
- assert_eq!(left, 0);
+ if left != 0 {
+ log::error!("SDT physical load left {} bytes remaining after loop", left);
+ return Err(TablePhysLoadError::Validity(InvalidSdtError::InvalidSize));
+ }
@@ -213,2 +224,2 @@
- plain::from_bytes::<SdtHeader>(&self.0)
- .expect("expected already validated Sdt to be able to get its header")
+ // SAFETY: Sdt::new validated the slice length and SdtHeader is #[repr(packed)].
+ unsafe { &*(self.0.as_ptr() as *const SdtHeader) }
@@ -417,3 +428,3 @@
- interpreter
- .release_global_lock()
- .expect("Failed to release GIL!"); //TODO: check if this should panic
+ if let Err(e) = interpreter.release_global_lock() {
+ log::error!("Failed to release AML global lock: {:?}", e);
+ }
@@ -435,4 +446,8 @@
- .map(|physaddr| {
- let physaddr: usize = physaddr
- .try_into()
- .expect("expected ACPI addresses to be compatible with the current word size");
+ .filter_map(|physaddr| {
+ let physaddr: usize = match physaddr.try_into() {
+ Ok(addr) => addr,
+ Err(e) => {
+ log::error!("expected ACPI addresses to be compatible with the current word size: {}", e);
+ return None;
+ }
+ };
@@ -442 +457,7 @@
- Sdt::load_from_physical(physaddr).expect("failed to load physical SDT")
+ match Sdt::load_from_physical(physaddr) {
+ Ok(sdt) => Some(sdt),
+ Err(error) => {
+ log::error!("failed to load physical SDT at {:#x}: {}", physaddr, error);
+ None
+ }
+ }
@@ -838,3 +859,4 @@
- Err(plain::Error::BadAlignment) => unreachable!(
- "plain::from_bytes reported bad alignment, but FadtAcpi2Struct is #[repr(packed)]"
- ),
+ Err(plain::Error::BadAlignment) => {
+ log::error!("plain::from_bytes reported bad alignment for FadtAcpi2Struct, but it is #[repr(packed)]");
+ None
+ }
@@ -849,2 +871,2 @@
- plain::from_bytes::<FadtStruct>(&self.0 .0)
- .expect("expected FADT struct to already be validated in Deref impl")
+ // SAFETY: Fadt::new validated the slice length and FadtStruct is #[repr(packed)].
+ unsafe { &*(self.0 .0.as_ptr() as *const FadtStruct) }
@@ -863,3 +885,7 @@
- let fadt_sdt = context
- .take_single_sdt(*b"FACP")
- .expect("expected ACPI to always have a FADT");
+ let fadt_sdt = match context.take_single_sdt(*b"FACP") {
+ Some(sdt) => sdt,
+ None => {
+ log::error!("expected ACPI to always have a FADT");
+ return;
+ }
+ };
@@ -876,4 +902,2 @@
- Some(fadt2) => usize::try_from(fadt2.x_dsdt).unwrap_or_else(|_| {
- usize::try_from(fadt.dsdt).expect("expected any given u32 to fit within usize")
- }),
- None => usize::try_from(fadt.dsdt).expect("expected any given u32 to fit within usize"),
+ Some(fadt2) => fadt2.x_dsdt as usize,
+ None => fadt.dsdt as usize,
@@ -0,0 +1,34 @@
--- a/init/src/main.rs
+++ b/init/src/main.rs
@@ -167 +167,8 @@
- UnitId(entry.file_name().unwrap().to_str().unwrap().to_owned()),
+ let file_name = match entry.file_name().to_str() {
+ Some(name) => name.to_owned(),
+ None => {
+ init_warn("skipping non-UTF8 service file name");
+ continue;
+ }
+ };
+ UnitId(file_name)
@@ -174 +181,3 @@
- libredox::call::setrens(0, 0).expect("init: failed to enter null namespace");
+ if let Err(err) = libredox::call::setrens(0, 0) {
+ init_error(&format!("init: failed to enter null namespace: {}", err));
+ }
--- a/init/src/service.rs
+++ b/init/src/service.rs
@@ -178,3 +178,11 @@
- let current_namespace_fd = libredox::call::getns().expect("TODO");
- libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd)
- .expect("TODO");
+ let current_namespace_fd = match libredox::call::getns() {
+ Ok(fd) => fd,
+ Err(err) => {
+ init_error(&format!("failed to get namespace for {:?}: {}", command, err));
+ return Some(child.id());
+ }
+ };
+ if let Err(err) = libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd) {
+ init_error(&format!("failed to register scheme {:?} for {:?}: {}", scheme, command, err));
+ return Some(child.id());
+ }
@@ -0,0 +1,126 @@
--- a/drivers/gpio/intel-gpiod/src/main.rs
+++ b/drivers/gpio/intel-gpiod/src/main.rs
@@ -130,6 +130,12 @@
log::debug!("intel-gpiod: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
+ // 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-gpiod: ACPI symbols unavailable ({}), running with no GPIO controllers", err);
+ return Ok(Vec::new());
+ }
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
--- a/drivers/i2c/dw-acpi-i2cd/src/main.rs
+++ b/drivers/i2c/dw-acpi-i2cd/src/main.rs
@@ -117,6 +117,12 @@
log::debug!("dw-acpi-i2cd: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
+ // 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!("dw-acpi-i2cd: ACPI symbols unavailable ({}), running with no I2C controllers", err);
+ return Ok(Vec::new());
+ }
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
--- a/drivers/i2c/intel-lpss-i2cd/src/main.rs
+++ b/drivers/i2c/intel-lpss-i2cd/src/main.rs
@@ -117,6 +117,12 @@
log::debug!("intel-lpss-i2cd: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
+ // 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-lpss-i2cd: ACPI symbols unavailable ({}), running with no I2C controllers", err);
+ return Ok(Vec::new());
+ }
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
--- a/drivers/gpio/i2c-gpio-expanderd/src/main.rs
+++ b/drivers/gpio/i2c-gpio-expanderd/src/main.rs
@@ -121,6 +121,12 @@
log::debug!("i2c-gpio-expanderd: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
+ // 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!("i2c-gpio-expanderd: ACPI symbols unavailable ({}), running with no GPIO expanders", err);
+ return Ok(Vec::new());
+ }
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
--- a/drivers/input/i2c-hidd/src/acpi.rs
+++ b/drivers/input/i2c-hidd/src/acpi.rs
@@ -32,6 +32,12 @@
Err(err) if err.kind() == ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
return Ok(Vec::new());
}
+ // 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() == ErrorKind::NotFound => {
+ log::info!("i2c-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"),
};
--- a/drivers/input/intel-thc-hidd/src/main.rs
+++ b/drivers/input/intel-thc-hidd/src/main.rs
@@ -95,8 +95,20 @@
}
fn resolve_acpi_companion(addr: &pci_types::PciAddress) -> Result<Option<String>> {
- let entries =
- fs::read_dir("/scheme/acpi/symbols").context("failed to read /scheme/acpi/symbols")?;
+ 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 {
@@ -136,8 +148,18 @@
}
fn scan_bound_i2c_hid_devices(companion: Option<&str>) -> Result<Vec<String>> {
- let entries =
- fs::read_dir("/scheme/acpi/symbols").context("failed to read /scheme/acpi/symbols")?;
+ 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 {
@@ -0,0 +1,24 @@
--- a/drivers/hwd/src/backend/acpi.rs
+++ b/drivers/hwd/src/backend/acpi.rs
@@ -16,7 +16,20 @@
fn probe(&mut self) -> Result<(), Box<dyn Error>> {
// Read symbols from acpi scheme
- let entries = fs::read_dir("/scheme/acpi/symbols")?;
+ 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!("hwd: ACPI symbols are not ready yet");
+ return Ok(());
+ }
+ // 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!("hwd: ACPI symbols unavailable ({}), running with no ACPI devices", err);
+ return Ok(());
+ }
+ Err(err) => return Err(err.into()),
+ };
// TODO: Reimplement with getdents?
let symbols_fd = libredox::Fd::open(
"/scheme/acpi/symbols",
@@ -0,0 +1,15 @@
--- a/drivers/usb/ucsid/src/main.rs
+++ b/drivers/usb/ucsid/src/main.rs
@@ -397,6 +397,12 @@
log::debug!("ucsid: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
+ // 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!("ucsid: ACPI symbols unavailable ({}), running with no UCSI devices", err);
+ return Ok(Vec::new());
+ }
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
@@ -0,0 +1,681 @@
diff --git a/drivers/acpid/src/dmi.rs b/drivers/acpid/src/dmi.rs
new file mode 100644
--- /dev/null
+++ b/drivers/acpid/src/dmi.rs
@@ -0,0 +1,350 @@
+use std::fmt;
+use syscall::PAGE_SIZE;
+
+use crate::acpi::PhysmapGuard;
+
+#[derive(Clone, Debug, Default)]
+pub struct DmiStrings {
+ pub sys_vendor: Option<String>,
+ pub board_vendor: Option<String>,
+ pub board_name: Option<String>,
+ pub board_version: Option<String>,
+ pub product_name: Option<String>,
+ pub product_version: Option<String>,
+ pub bios_version: Option<String>,
+}
+
+impl DmiStrings {
+ pub fn to_text(&self) -> String {
+ let mut text = String::new();
+ if let Some(ref v) = self.sys_vendor {
+ text.push_str("sys_vendor=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.board_vendor {
+ text.push_str("board_vendor=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.board_name {
+ text.push_str("board_name=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.board_version {
+ text.push_str("board_version=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.product_name {
+ text.push_str("product_name=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.product_version {
+ text.push_str("product_version=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ if let Some(ref v) = self.bios_version {
+ text.push_str("bios_version=");
+ text.push_str(v);
+ text.push('\n');
+ }
+ text
+ }
+}
+
+#[derive(Debug)]
+pub enum DmiError {
+ InvalidData,
+ Io(std::io::Error),
+}
+
+impl fmt::Display for DmiError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidData => write!(f, "invalid SMBIOS data"),
+ Self::Io(e) => write!(f, "I/O error: {}", e),
+ }
+ }
+}
+
+impl From<std::io::Error> for DmiError {
+ fn from(e: std::io::Error) -> Self {
+ DmiError::Io(e)
+ }
+}
+
+/// Scan physical memory for SMBIOS entry point and parse DMI strings.
+pub fn read_smbios_dmi() -> Result<DmiStrings, DmiError> {
+ // SMBIOS entry point is in the BIOS ROM area 0xF0000-0xFFFFF
+ let scan_start = 0xF_0000usize;
+ let scan_size = 0x10_0000usize - scan_start;
+ let start_page = scan_start / PAGE_SIZE * PAGE_SIZE;
+ let start_offset = scan_start % PAGE_SIZE;
+ let page_count = (start_offset + scan_size).div_ceil(PAGE_SIZE);
+
+ let pages = PhysmapGuard::map(start_page, page_count)?;
+ let bios_region = &pages[start_offset..start_offset + scan_size];
+
+ // Search for 64-bit entry point first (_SM3_), then 32-bit (_SM_)
+ // Entry points must be 16-byte aligned
+ let mut entry_point_addr = None;
+ let mut is_64bit = false;
+
+ for offset in (0..bios_region.len().saturating_sub(5)).step_by(16) {
+ if bios_region[offset..].starts_with(b"_SM3_") {
+ entry_point_addr = Some(scan_start + offset);
+ is_64bit = true;
+ break;
+ }
+ }
+
+ if entry_point_addr.is_none() {
+ for offset in (0..bios_region.len().saturating_sub(4)).step_by(16) {
+ if bios_region[offset..].starts_with(b"_SM_") {
+ // Verify intermediate anchor "_DMI_" at offset 0x10 from anchor start
+ let dmi_offset = offset + 0x10;
+ if dmi_offset + 5 <= bios_region.len()
+ && bios_region[dmi_offset..dmi_offset + 5].starts_with(b"_DMI_")
+ {
+ entry_point_addr = Some(scan_start + offset);
+ is_64bit = false;
+ break;
+ }
+ }
+ }
+ }
+
+ let entry_addr = match entry_point_addr {
+ Some(addr) => addr,
+ None => {
+ log::warn!("SMBIOS entry point not found in 0xF0000-0xFFFFF");
+ return Ok(DmiStrings::default());
+ }
+ };
+
+ log::info!(
+ "Found SMBIOS {} entry point at {:#x}",
+ if is_64bit { "3.0" } else { "2.x" },
+ entry_addr
+ );
+
+ if is_64bit {
+ parse_smbios_64(entry_addr)
+ } else {
+ parse_smbios_32(entry_addr)
+ }
+}
+
+fn parse_smbios_32(entry_addr: usize) -> Result<DmiStrings, DmiError> {
+ // 32-bit entry point is at least 0x1F bytes
+ let page = entry_addr / PAGE_SIZE * PAGE_SIZE;
+ let offset = entry_addr % PAGE_SIZE;
+ let pages = PhysmapGuard::map(page, 1)?;
+
+ if pages.len() < offset + 0x1F {
+ log::warn!("SMBIOS 32-bit entry point truncated");
+ return Err(DmiError::InvalidData);
+ }
+
+ let ep = &pages[offset..];
+ let ep_len = ep[0x05] as usize;
+ if ep_len < 0x1F || pages.len() < offset + ep_len {
+ log::warn!("SMBIOS 32-bit entry point length invalid: {}", ep_len);
+ return Err(DmiError::InvalidData);
+ }
+
+ let checksum = ep[..ep_len].iter().fold(0u8, |sum, &b| sum.wrapping_add(b));
+ if checksum != 0 {
+ log::warn!("SMBIOS 32-bit entry point checksum failed");
+ return Err(DmiError::InvalidData);
+ }
+
+ let table_len = u16::from_le_bytes([ep[0x16], ep[0x17]]) as usize;
+ let table_addr = u32::from_le_bytes([ep[0x18], ep[0x19], ep[0x1A], ep[0x1B]]) as usize;
+
+ log::info!("SMBIOS 32-bit: table at {:#x}, len {}", table_addr, table_len);
+
+ parse_smbios_structures(table_addr, table_len)
+}
+
+fn parse_smbios_64(entry_addr: usize) -> Result<DmiStrings, DmiError> {
+ // 64-bit entry point is at least 0x18 bytes
+ let page = entry_addr / PAGE_SIZE * PAGE_SIZE;
+ let offset = entry_addr % PAGE_SIZE;
+ let pages = PhysmapGuard::map(page, 1)?;
+
+ if pages.len() < offset + 0x18 {
+ log::warn!("SMBIOS 64-bit entry point truncated");
+ return Err(DmiError::InvalidData);
+ }
+
+ let ep = &pages[offset..];
+ let ep_len = ep[0x06] as usize;
+ if ep_len < 0x18 || pages.len() < offset + ep_len {
+ log::warn!("SMBIOS 64-bit entry point length invalid: {}", ep_len);
+ return Err(DmiError::InvalidData);
+ }
+
+ let checksum = ep[..ep_len].iter().fold(0u8, |sum, &b| sum.wrapping_add(b));
+ if checksum != 0 {
+ log::warn!("SMBIOS 64-bit entry point checksum failed");
+ return Err(DmiError::InvalidData);
+ }
+
+ let table_max_size =
+ u32::from_le_bytes([ep[0x0C], ep[0x0D], ep[0x0E], ep[0x0F]]) as usize;
+ let table_addr = u64::from_le_bytes([
+ ep[0x10], ep[0x11], ep[0x12], ep[0x13], ep[0x14], ep[0x15], ep[0x16], ep[0x17],
+ ]) as usize;
+
+ log::info!(
+ "SMBIOS 64-bit: table at {:#x}, max size {}",
+ table_addr,
+ table_max_size
+ );
+
+ parse_smbios_structures(table_addr, table_max_size)
+}
+
+fn parse_smbios_structures(table_addr: usize, table_len: usize) -> Result<DmiStrings, DmiError> {
+ if table_addr == 0 || table_len == 0 {
+ log::warn!("SMBIOS structure table address or length is zero");
+ return Ok(DmiStrings::default());
+ }
+
+ // Map the structure table. It may span multiple pages.
+ let start_page = table_addr / PAGE_SIZE * PAGE_SIZE;
+ let start_offset = table_addr % PAGE_SIZE;
+ let total_needed = start_offset + table_len;
+ let page_count = total_needed.div_ceil(PAGE_SIZE);
+
+ let pages = PhysmapGuard::map(start_page, page_count)?;
+ if pages.len() < total_needed {
+ log::warn!("SMBIOS structure table mapping truncated");
+ return Err(DmiError::InvalidData);
+ }
+
+ let table = &pages[start_offset..start_offset + table_len];
+
+ let mut dmi = DmiStrings::default();
+ let mut pos = 0;
+
+ while pos + 4 <= table.len() {
+ let stype = table[pos];
+ let slen = table[pos + 1] as usize;
+ let _handle = u16::from_le_bytes([table[pos + 2], table[pos + 3]]);
+
+ if slen < 4 {
+ log::warn!(
+ "Malformed SMBIOS structure at offset {}, type {}, len {}",
+ pos,
+ stype,
+ slen
+ );
+ break;
+ }
+
+ if pos + slen > table.len() {
+ log::warn!(
+ "SMBIOS structure at offset {} extends past table end", pos
+ );
+ break;
+ }
+
+ // Parse strings after the formatted section
+ let strings_start = pos + slen;
+ let mut strings = Vec::new();
+ let mut s = strings_start;
+
+ while s < table.len() {
+ // Find null terminator
+ let mut end = s;
+ while end < table.len() && table[end] != 0 {
+ end += 1;
+ }
+
+ if end >= table.len() {
+ log::warn!("Unterminated SMBIOS strings at offset {}", s);
+ break;
+ }
+
+ let string = std::str::from_utf8(&table[s..end]).unwrap_or("").to_string();
+ strings.push(string);
+
+ s = end + 1;
+
+ // Double null terminates the string set
+ if s < table.len() && table[s] == 0 {
+ s += 1;
+ break;
+ }
+ }
+
+ match stype {
+ 0 => {
+ // BIOS Information
+ if slen > 5 {
+ let idx = table[pos + 5] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.bios_version = Some(strings[idx - 1].clone());
+ }
+ }
+ }
+ 1 => {
+ // System Information
+ if slen > 4 {
+ let idx = table[pos + 4] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.sys_vendor = Some(strings[idx - 1].clone());
+ }
+ }
+ if slen > 5 {
+ let idx = table[pos + 5] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.product_name = Some(strings[idx - 1].clone());
+ }
+ }
+ if slen > 6 {
+ let idx = table[pos + 6] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.product_version = Some(strings[idx - 1].clone());
+ }
+ }
+ }
+ 2 => {
+ // Base Board Information
+ if slen > 4 {
+ let idx = table[pos + 4] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.board_vendor = Some(strings[idx - 1].clone());
+ }
+ }
+ if slen > 5 {
+ let idx = table[pos + 5] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.board_name = Some(strings[idx - 1].clone());
+ }
+ }
+ if slen > 6 {
+ let idx = table[pos + 6] as usize;
+ if idx > 0 && idx <= strings.len() {
+ dmi.board_version = Some(strings[idx - 1].clone());
+ }
+ }
+ }
+ 127 => {
+ // End-of-table marker
+ break;
+ }
+ _ => {}
+ }
+
+ pos = s;
+ }
+
+ Ok(dmi)
+}
diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs
--- a/drivers/acpid/src/acpi.rs
+++ b/drivers/acpid/src/acpi.rs
@@ -95,12 +95,12 @@
BadChecksum,
}
-struct PhysmapGuard {
+pub struct PhysmapGuard {
virt: *const u8,
size: usize,
}
impl PhysmapGuard {
- fn map(page: usize, page_count: usize) -> std::io::Result<Self> {
+ pub fn map(page: usize, page_count: usize) -> std::io::Result<Self> {
let size = page_count * PAGE_SIZE;
let virt = unsafe {
common::physmap(page, size, common::Prot::RO, common::MemoryType::default())
@@ -245,26 +245,55 @@
symbol_cache: FxHashMap<String, String>,
page_cache: Arc<Mutex<AmlPageCache>>,
aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>,
+ pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>,
}
impl AmlSymbols {
- pub fn new(aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>) -> Self {
+ pub fn new(aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>, pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>) -> Self {
Self {
aml_context: None,
symbol_cache: FxHashMap::default(),
page_cache: Arc::new(Mutex::new(AmlPageCache::default())),
aml_region_handlers,
+ pci_fd,
}
}
- pub fn init(&mut self, pci_fd: Option<&libredox::Fd>) -> Result<(), Box<dyn Error>> {
+ pub fn init(&mut self) -> Result<(), Box<dyn Error>> {
if self.aml_context.is_some() {
return Err("AML interpreter already initialized".into());
}
let format_err = |err| format!("{:?}", err);
- let handler = AmlPhysMemHandler::new(pci_fd, Arc::clone(&self.page_cache));
+ let handler = AmlPhysMemHandler::new(Arc::clone(&self.pci_fd), Arc::clone(&self.page_cache));
//TODO: use these parsed tables for the rest of acpid
- let rsdp_address = usize::from_str_radix(&std::env::var("RSDP_ADDR")?, 16)?;
+ let rsdp_address = match std::env::var("RSDP_ADDR") {
+ Ok(addr) => usize::from_str_radix(&addr, 16)?,
+ Err(_) => {
+ // RSDP_ADDR not provided — probe BIOS area (0xE00000xFFFFF) for RSDP signature
+ log::info!("RSDP_ADDR not set, probing BIOS area for RSDP...");
+ let mut found = None;
+ for page_base in (0xE_0000..0x10_0000).step_by(16) {
+ let mapped = unsafe {
+ common::physmap(
+ page_base,
+ 16,
+ common::Prot::RW,
+ common::MemoryType::default(),
+ )
+ };
+ if let Ok(virt) = mapped {
+ let sig = unsafe { std::slice::from_raw_parts(virt as *const u8, 8) };
+ if sig == b"RSD PTR " {
+ log::info!("found RSDP at physical {:#x}", page_base);
+ found = Some(page_base);
+ break;
+ }
+ let _ = unsafe { libredox::call::munmap(virt as *mut (), 16) };
+ }
+ }
+ found.ok_or("RSDP not found in BIOS area (0xE0000-0xFFFFF)")?
+ }
+ };
let tables =
unsafe { AcpiTables::from_rsdp(handler.clone(), rsdp_address).map_err(format_err)? };
let platform = AcpiPlatform::new(tables, handler).map_err(format_err)?;
@@ -278,10 +307,9 @@
pub fn aml_context_mut(
&mut self,
- pci_fd: Option<&libredox::Fd>,
) -> Result<&mut Interpreter<AmlPhysMemHandler>, AmlEvalError> {
if self.aml_context.is_none() {
- match self.init(pci_fd) {
+ match self.init() {
Ok(()) => (),
Err(err) => {
log::error!("failed to initialize AML context: {}", err);
@@ -305,8 +333,8 @@
None
}
- pub fn build_cache(&mut self, pci_fd: Option<&libredox::Fd>) {
- let Ok(aml_context) = self.aml_context_mut(pci_fd) else {
+ pub fn build_cache(&mut self) {
+ let Ok(aml_context) = self.aml_context_mut() else {
return;
};
@@ -382,6 +410,8 @@
aml_symbols: RwLock<AmlSymbols>,
+ pub pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>,
+
// TODO: The kernel ACPI code seemed to use load_table quite ubiquitously, however ACPI 5.1
// states that DDBHandles can only be obtained when loading XSDT-pointed tables. So, we'll
// generate an index only for those.
@@ -397,7 +427,7 @@
args: Vec<AmlSerdeValue>,
) -> Result<AmlSerdeValue, AmlEvalError> {
let mut symbols = self.aml_symbols.write();
- let interpreter = symbols.aml_context_mut(None)?;
+ let interpreter = symbols.aml_context_mut()?;
interpreter.acquire_global_lock(16)?;
let args = args
@@ -440,13 +470,17 @@
})
.collect::<Vec<Sdt>>();
+ let pci_fd = Arc::new(parking_lot::RwLock::new(None));
+
let mut this = Self {
tables,
dsdt: None,
fadt: None,
// Temporary values
- aml_symbols: RwLock::new(AmlSymbols::new(ec)),
+ aml_symbols: RwLock::new(AmlSymbols::new(ec, Arc::clone(&pci_fd))),
+
+ pci_fd,
next_ctx: RwLock::new(0),
@@ -526,7 +560,7 @@
}
pub fn aml_lookup(&self, symbol: &str) -> Option<String> {
- if let Ok(aml_symbols) = self.aml_symbols(None) {
+ if let Ok(aml_symbols) = self.aml_symbols() {
aml_symbols.lookup(symbol)
} else {
None
@@ -535,7 +569,6 @@
pub fn aml_symbols(
&self,
- pci_fd: Option<&libredox::Fd>,
) -> Result<RwLockReadGuard<'_, AmlSymbols>, AmlError> {
// return the cached value if it exists
let symbols = self.aml_symbols.read();
@@ -550,7 +583,7 @@
let mut aml_symbols = self.aml_symbols.write();
- aml_symbols.build_cache(pci_fd);
+ aml_symbols.build_cache();
// return the cached value
Ok(RwLockWriteGuard::downgrade(aml_symbols))
diff --git a/drivers/acpid/src/main.rs b/drivers/acpid/src/main.rs
--- a/drivers/acpid/src/main.rs
+++ b/drivers/acpid/src/main.rs
@@ -14,6 +14,7 @@
mod aml_physmem;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod ec;
+mod dmi;
mod scheme;
@@ -73,6 +74,14 @@
];
let acpi_context = self::acpi::AcpiContext::init(physaddrs_iter, region_handlers);
+ let dmi_strings = match self::dmi::read_smbios_dmi() {
+ Ok(strings) => Some(strings),
+ Err(e) => {
+ log::warn!("Failed to read SMBIOS DMI: {}", e);
+ None
+ }
+ };
+
// TODO: I/O permission bitmap?
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3");
@@ -83,7 +92,7 @@
let mut event_queue = RawEventQueue::new().expect("acpid: failed to create event queue");
let socket = Socket::nonblock().expect("acpid: failed to create disk scheme");
- let mut scheme = self::scheme::AcpiScheme::new(&acpi_context, &socket);
+ let mut scheme = self::scheme::AcpiScheme::new(&acpi_context, &socket, dmi_strings);
let mut handler = Blocking::new(&socket, 16);
event_queue
diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs
--- a/drivers/acpid/src/scheme.rs
+++ b/drivers/acpid/src/scheme.rs
@@ -22,12 +22,13 @@
use syscall::{EOVERFLOW, EPERM};
use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature};
+use crate::dmi::DmiStrings;
pub struct AcpiScheme<'acpi, 'sock> {
ctx: &'acpi AcpiContext,
handles: HandleMap<Handle<'acpi>>,
- pci_fd: Option<Fd>,
socket: &'sock Socket,
+ dmi_text: Option<String>,
}
struct Handle<'a> {
@@ -43,6 +44,7 @@
Symbol { name: String, description: String },
SchemeRoot,
RegisterPci,
+ Dmi(String),
}
impl HandleKind<'_> {
@@ -55,6 +57,7 @@
Self::Symbol { .. } => false,
Self::SchemeRoot => false,
Self::RegisterPci => false,
+ Self::Dmi(_) => false,
}
}
fn len(&self, acpi_ctx: &AcpiContext) -> Result<usize> {
@@ -65,6 +68,7 @@
.ok_or(Error::new(EBADFD))?
.length(),
Self::Symbol { description, .. } => description.len(),
+ Self::Dmi(text) => text.len(),
// Directories
Self::TopLevel | Self::Symbols(_) | Self::Tables => 0,
Self::SchemeRoot | Self::RegisterPci => return Err(Error::new(EBADF)),
@@ -73,12 +77,12 @@
}
impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> {
- pub fn new(ctx: &'acpi AcpiContext, socket: &'sock Socket) -> Self {
+ pub fn new(ctx: &'acpi AcpiContext, socket: &'sock Socket, dmi: Option<DmiStrings>) -> Self {
Self {
ctx,
handles: HandleMap::new(),
- pci_fd: None,
socket,
+ dmi_text: dmi.map(|d| d.to_text()),
}
}
}
@@ -196,6 +200,7 @@
match &*components {
[""] => HandleKind::TopLevel,
["register_pci"] => HandleKind::RegisterPci,
+ ["dmi"] => HandleKind::Dmi(self.dmi_text.clone().unwrap_or_default()),
["tables"] => HandleKind::Tables,
["tables", table] => {
@@ -204,7 +209,7 @@
}
["symbols"] => {
- if let Ok(aml_symbols) = self.ctx.aml_symbols(self.pci_fd.as_ref()) {
+ if let Ok(aml_symbols) = self.ctx.aml_symbols() {
HandleKind::Symbols(aml_symbols)
} else {
return Err(Error::new(EIO));
@@ -309,6 +314,7 @@
.ok_or(Error::new(EBADFD))?
.as_slice(),
HandleKind::Symbol { description, .. } => description.as_bytes(),
+ HandleKind::Dmi(ref text) => text.as_bytes(),
_ => return Err(Error::new(EINVAL)),
};
@@ -332,9 +338,13 @@
match &handle.kind {
HandleKind::TopLevel => {
- const TOPLEVEL_ENTRIES: &[&str] = &["tables", "symbols"];
+ const TOPLEVEL_ENTRIES: &[(DirentKind, &str)] = &[
+ (DirentKind::Regular, "dmi"),
+ (DirentKind::Directory, "tables"),
+ (DirentKind::Directory, "symbols"),
+ ];
- for (idx, name) in TOPLEVEL_ENTRIES
+ for (idx, (kind, name)) in TOPLEVEL_ENTRIES
.iter()
.enumerate()
.skip(opaque_offset as usize)
@@ -343,7 +353,7 @@
inode: 0,
next_opaque_id: idx as u64 + 1,
name,
- kind: DirentKind::Directory,
+ kind: *kind,
})?;
}
}
@@ -470,10 +480,12 @@
}
let new_fd = libredox::Fd::new(new_fd);
- if self.pci_fd.is_some() {
- return Err(Error::new(EINVAL));
- } else {
- self.pci_fd = Some(new_fd);
+ {
+ let mut pci_fd = self.ctx.pci_fd.write();
+ if pci_fd.is_some() {
+ return Err(Error::new(EINVAL));
+ }
+ *pci_fd = Some(new_fd);
}
Ok(num_fds)
@@ -0,0 +1,408 @@
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('/', ".")
+}
@@ -0,0 +1,240 @@
diff --git a/init/src/scheduler.rs b/init/src/scheduler.rs
--- a/init/src/scheduler.rs
+++ b/init/src/scheduler.rs
@@ -1,14 +1,34 @@
-use std::collections::{BTreeSet, VecDeque};
+use std::collections::{BTreeMap, BTreeSet, VecDeque};
use crate::InitConfig;
-use crate::color::{init_error, status_ok, status_skip};
+use crate::color::{init_error, init_warn, status_ok, status_skip};
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
const SPAWN_BATCH_SIZE: usize = 50;
+#[derive(Clone, Debug)]
+pub enum RestartPolicy {
+ Never,
+ OnFailure,
+ Always,
+}
+
+/// Tracks the restart state for a supervised service.
+pub struct ServiceState {
+ pub unit_id: UnitId,
+ pub cmd: String,
+ pub restart_policy: RestartPolicy,
+ pub max_restarts: u32,
+ pub restart_count: u32,
+ /// Monotonic time of last restart (for backoff calculation).
+ pub last_restart_ms: u64,
+}
+
pub struct Scheduler {
pending: VecDeque<Job>,
completed: BTreeSet<UnitId>,
+ /// Maps child PID → service state for supervised services.
+ pub supervised: BTreeMap<u32, ServiceState>,
}
struct Job {
@@ -25,6 +45,7 @@
Scheduler {
pending: VecDeque::new(),
completed: BTreeSet::new(),
+ supervised: BTreeMap::new(),
}
}
@@ -75,25 +96,38 @@
match job.kind {
JobKind::Start => {
- let deps_ok = {
+ let (deps_ok, hard_deps_met) = {
let unit = unit_store.unit(&job.unit);
- let mut ok = true;
- for dep in &unit.info.requires_weak {
+ let mut hard_deps_met = true;
+ for dep in &unit.info.requires {
if self.completed.contains(dep) {
continue;
}
if !unit_store.has_unit(dep) {
- continue;
- }
- let in_pending = self.pending.iter().any(|pj| &pj.unit == dep);
- if in_pending {
- ok = false;
+ init_error(&format!(
+ "{}: hard dependency '{}' not found, skipping",
+ job.unit.0, dep.0
+ ));
+ hard_deps_met = false;
break;
}
+ hard_deps_met = false;
+ break;
}
- ok
+ let weak_ok = unit.info.requires_weak.iter().all(|dep| {
+ self.completed.contains(dep)
+ || !unit_store.has_unit(dep)
+ || self.pending.iter().any(|pj| &pj.unit == dep)
+ });
+ (weak_ok, hard_deps_met)
};
+ if !hard_deps_met {
+ init_warn(&format!("{}: hard dependency not met, skipping", job.unit.0));
+ self.completed.insert(job.unit);
+ continue 'a;
+ }
+
if !deps_ok {
defer_count += 1;
self.pending.push_back(job);
@@ -106,7 +140,7 @@
defer_count = 0;
let unit = unit_store.unit_mut(&job.unit);
- run(unit, init_config);
+ run(unit, init_config, &mut self.supervised);
self.completed.insert(job.unit);
spawned_this_step += 1;
@@ -119,7 +153,7 @@
}
}
-fn run(unit: &mut Unit, config: &mut InitConfig) {
+fn run(unit: &mut Unit, config: &mut InitConfig, supervised: &mut BTreeMap<u32, ServiceState>) {
match &unit.kind {
UnitKind::LegacyScript { script } => {
for cmd in script.clone() {
@@ -127,13 +161,15 @@
}
}
UnitKind::Service { service } => {
- let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0);
+ let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0).clone();
if config.skip_cmd.contains(&service.cmd) {
status_skip(&format!("Skipping {} ({})", desc, service.cmd));
return;
}
status_ok(&format!("Started {}", desc));
service.spawn(&config.envs);
+ // Supervision infrastructure is in place; full PID tracking requires
+ // service.spawn() to return Option<u32> (added by a later patch).
}
UnitKind::Target {} => {}
}
diff --git a/init/src/script.rs b/init/src/script.rs
--- a/init/src/script.rs
+++ b/init/src/script.rs
@@ -12,12 +12,13 @@
}
}
-pub struct Script(pub Vec<Command>, pub Vec<UnitId>);
+pub struct Script(pub Vec<Command>, pub Vec<UnitId>, pub Vec<UnitId>);
impl Script {
pub fn from_str(config: &str, errors: &mut Vec<String>) -> io::Result<Script> {
let mut cmds = vec![];
let mut requires_weak = vec![];
+ let mut requires = vec![];
for line_raw in config.lines() {
let line = line_raw.trim();
@@ -27,14 +28,14 @@
let args = line.split(' ').map(subst_env);
- match Command::parse(args, &mut requires_weak) {
+ match Command::parse(args, &mut requires_weak, &mut requires) {
Ok(None) => {}
Ok(Some(cmd)) => cmds.push(cmd),
Err(err) => errors.push(err),
}
}
- Ok(Script(cmds, requires_weak))
+ Ok(Script(cmds, requires_weak, requires))
}
}
@@ -54,12 +55,17 @@
fn parse(
mut args: impl Iterator<Item = String>,
requires_weak: &mut Vec<UnitId>,
+ requires: &mut Vec<UnitId>,
) -> Result<Option<Command>, String> {
let Some(cmd) = args.next() else {
return Ok(None);
};
match cmd.as_str() {
+ "requires" => {
+ requires.extend(args.map(UnitId));
+ Ok(None)
+ }
"requires_weak" => {
requires_weak.extend(args.map(UnitId));
Ok(None)
diff --git a/init/src/unit.rs b/init/src/unit.rs
--- a/init/src/unit.rs
+++ b/init/src/unit.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::{fs, io};
@@ -76,7 +76,9 @@
pub fn load_units(&mut self, root_unit: UnitId, errors: &mut Vec<String>) -> Vec<UnitId> {
let mut loaded_units = vec![];
- let mut pending_units = vec![root_unit];
+ let mut pending_units = vec![root_unit.clone()];
+ let mut seen = BTreeSet::new();
+ seen.insert(root_unit);
while let Some(unit_id) = pending_units.pop() {
if self.units.contains_key(&unit_id) {
@@ -85,7 +87,16 @@
let unit = self.load_single_unit(unit_id, errors);
if let Some(unit) = unit {
loaded_units.push(unit.clone());
+ for dep in &self.unit(&unit).info.requires {
+ if !seen.insert(dep.clone()) {
+ continue;
+ }
+ pending_units.push(dep.clone());
+ }
for dep in &self.unit(&unit).info.requires_weak {
+ if !seen.insert(dep.clone()) {
+ continue;
+ }
pending_units.push(dep.clone());
}
}
@@ -125,6 +136,8 @@
#[serde(default = "true_bool")]
pub default_dependencies: bool,
#[serde(default)]
+ pub requires: Vec<UnitId>,
+ #[serde(default)]
pub requires_weak: Vec<UnitId>,
pub condition_architecture: Option<Vec<String>>,
// FIXME replace this with hwd reading from the devicetree
@@ -191,6 +204,7 @@
info: UnitInfo {
description: None,
default_dependencies: true,
+ requires: script.2,
requires_weak: script.1,
condition_architecture: None,
condition_board: None,
+1 -162
View File
@@ -1,3 +1,4 @@
diff --git a/drivers/acpid/src/aml_physmem.rs b/drivers/acpid/src/aml_physmem.rs
--- a/drivers/acpid/src/aml_physmem.rs
+++ b/drivers/acpid/src/aml_physmem.rs
@@ -143,7 +143,7 @@
@@ -48,165 +49,3 @@
Some(pci_fd) => match pci_fd.call_wo(value, syscall::CallFlags::empty(), &metadata) {
Ok(_) => {}
Err(err) => {
--- a/drivers/acpid/src/acpi.rs
+++ b/drivers/acpid/src/acpi.rs
@@ -245,24 +245,26 @@
symbol_cache: FxHashMap<String, String>,
page_cache: Arc<Mutex<AmlPageCache>>,
aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>,
+ pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>,
}
impl AmlSymbols {
- pub fn new(aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>) -> Self {
+ pub fn new(aml_region_handlers: Vec<(RegionSpace, Box<dyn RegionHandler>)>, pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>) -> Self {
Self {
aml_context: None,
symbol_cache: FxHashMap::default(),
page_cache: Arc::new(Mutex::new(AmlPageCache::default())),
aml_region_handlers,
+ pci_fd,
}
}
- pub fn init(&mut self, pci_fd: Option<&libredox::Fd>) -> Result<(), Box<dyn Error>> {
+ pub fn init(&mut self) -> Result<(), Box<dyn Error>> {
if self.aml_context.is_some() {
return Err("AML interpreter already initialized".into());
}
let format_err = |err| format!("{:?}", err);
- let handler = AmlPhysMemHandler::new(pci_fd, Arc::clone(&self.page_cache));
+ let handler = AmlPhysMemHandler::new(Arc::clone(&self.pci_fd), Arc::clone(&self.page_cache));
//TODO: use these parsed tables for the rest of acpid
let rsdp_address = usize::from_str_radix(&std::env::var("RSDP_ADDR")?, 16)?;
let tables =
@@ -278,10 +280,9 @@
pub fn aml_context_mut(
&mut self,
- pci_fd: Option<&libredox::Fd>,
) -> Result<&mut Interpreter<AmlPhysMemHandler>, AmlEvalError> {
if self.aml_context.is_none() {
- match self.init(pci_fd) {
+ match self.init() {
Ok(()) => (),
Err(err) => {
log::error!("failed to initialize AML context: {}", err);
@@ -305,8 +306,8 @@
None
}
- pub fn build_cache(&mut self, pci_fd: Option<&libredox::Fd>) {
- let Ok(aml_context) = self.aml_context_mut(pci_fd) else {
+ pub fn build_cache(&mut self) {
+ let Ok(aml_context) = self.aml_context_mut() else {
return;
};
@@ -382,6 +383,8 @@
aml_symbols: RwLock<AmlSymbols>,
+ pub pci_fd: Arc<parking_lot::RwLock<Option<libredox::Fd>>>,
+
// TODO: The kernel ACPI code seemed to use load_table quite ubiquitously, however ACPI 5.1
// states that DDBHandles can only be obtained when loading XSDT-pointed tables. So, we'll
// generate an index only for those.
@@ -397,7 +400,7 @@
args: Vec<AmlSerdeValue>,
) -> Result<AmlSerdeValue, AmlEvalError> {
let mut symbols = self.aml_symbols.write();
- let interpreter = symbols.aml_context_mut(None)?;
+ let interpreter = symbols.aml_context_mut()?;
interpreter.acquire_global_lock(16)?;
let args = args
@@ -440,13 +443,17 @@
})
.collect::<Vec<Sdt>>();
+ let pci_fd = Arc::new(parking_lot::RwLock::new(None));
+
let mut this = Self {
tables,
dsdt: None,
fadt: None,
// Temporary values
- aml_symbols: RwLock::new(AmlSymbols::new(ec)),
+ aml_symbols: RwLock::new(AmlSymbols::new(ec, Arc::clone(&pci_fd))),
+
+ pci_fd,
next_ctx: RwLock::new(0),
@@ -526,7 +533,7 @@
}
pub fn aml_lookup(&self, symbol: &str) -> Option<String> {
- if let Ok(aml_symbols) = self.aml_symbols(None) {
+ if let Ok(aml_symbols) = self.aml_symbols() {
aml_symbols.lookup(symbol)
} else {
None
@@ -535,7 +542,6 @@
pub fn aml_symbols(
&self,
- pci_fd: Option<&libredox::Fd>,
) -> Result<RwLockReadGuard<'_, AmlSymbols>, AmlError> {
// return the cached value if it exists
let symbols = self.aml_symbols.read();
@@ -550,7 +556,7 @@
let mut aml_symbols = self.aml_symbols.write();
- aml_symbols.build_cache(pci_fd);
+ aml_symbols.build_cache();
// return the cached value
Ok(RwLockWriteGuard::downgrade(aml_symbols))
--- a/drivers/acpid/src/scheme.rs
+++ b/drivers/acpid/src/scheme.rs
@@ -26,7 +26,6 @@
pub struct AcpiScheme<'acpi, 'sock> {
ctx: &'acpi AcpiContext,
handles: HandleMap<Handle<'acpi>>,
- pci_fd: Option<Fd>,
socket: &'sock Socket,
}
@@ -77,7 +76,6 @@
Self {
ctx,
handles: HandleMap::new(),
- pci_fd: None,
socket,
}
}
@@ -204,7 +202,7 @@
}
["symbols"] => {
- if let Ok(aml_symbols) = self.ctx.aml_symbols(self.pci_fd.as_ref()) {
+ if let Ok(aml_symbols) = self.ctx.aml_symbols() {
HandleKind::Symbols(aml_symbols)
} else {
return Err(Error::new(EIO));
@@ -470,10 +468,12 @@
}
let new_fd = libredox::Fd::new(new_fd);
- if self.pci_fd.is_some() {
- return Err(Error::new(EINVAL));
- } else {
- self.pci_fd = Some(new_fd);
+ {
+ let mut pci_fd = self.ctx.pci_fd.write();
+ if pci_fd.is_some() {
+ return Err(Error::new(EINVAL));
+ }
+ *pci_fd = Some(new_fd);
}
Ok(num_fds)
@@ -0,0 +1,757 @@
diff --git a/drivers/input/ps2d/src/controller.rs b/drivers/input/ps2d/src/controller.rs
index d7af4cba..061ef2cf 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 {
@@ -129,7 +137,15 @@ impl Ps2 {
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
pub fn new() -> Self {
- unimplemented!()
+ // PS/2 controller is x86-only hardware. On other architectures, construct
+ // a zeroed struct; init() will fail at the controller self-test and the
+ // daemon will log an error and stop attempting keyboard/mouse operations.
+ Ps2 {
+ data: Mmio::new(0),
+ status: ReadOnly::new(Mmio::new(0)),
+ command: WriteOnly::new(Mmio::new(0)),
+ mouse_resets: 0,
+ }
}
fn status(&mut self) -> StatusFlags {
@@ -261,6 +277,30 @@ impl Ps2 {
self.write(command as u8)
}
+ pub fn set_leds(&mut self, caps: bool, num: bool, scroll: bool) {
+ let mut led_byte = 0u8;
+ if scroll { led_byte |= 1; }
+ if num { led_byte |= 2; }
+ if caps { led_byte |= 4; }
+ if let Err(err) = self.keyboard_command_inner(0xED) {
+ log::debug!("ps2d: LED command 0xED not supported: {:?}", err);
+ return;
+ }
+ match self.read_timeout(DEFAULT_TIMEOUT) {
+ Ok(0xFA) => {
+ if let Err(err) = self.write(led_byte) {
+ log::debug!("ps2d: failed to send LED byte {:02X}: {:?}", led_byte, err);
+ }
+ }
+ Ok(val) => {
+ log::debug!("ps2d: LED command ACK expected 0xFA, got {:02X}", val);
+ }
+ Err(err) => {
+ log::debug!("ps2d: LED command ACK timeout: {:?}", err);
+ }
+ }
+ }
+
pub fn next(&mut self) -> Option<(bool, u8)> {
let status = self.status();
if status.contains(StatusFlags::OUTPUT_FULL) {
@@ -271,6 +311,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 +392,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..86f903bf 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,8 @@ 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 keyboard input");
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
user_data! {
enum Source {
@@ -93,7 +94,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..23099493 100644
--- a/drivers/input/ps2d/src/mouse.rs
+++ b/drivers/input/ps2d/src/mouse.rs
@@ -1,8 +1,8 @@
use crate::controller::Ps2;
use std::time::Duration;
-pub const RESET_RETRIES: usize = 10;
-pub const RESET_TIMEOUT: Duration = Duration::from_millis(1000);
+pub const RESET_RETRIES: usize = 3;
+pub const RESET_TIMEOUT: Duration = Duration::from_millis(250);
pub const COMMAND_TIMEOUT: Duration = Duration::from_millis(100);
#[derive(Clone, Copy, Debug)]
@@ -61,6 +61,10 @@ impl MouseTx {
if data == 0xFA {
self.write_i += 1;
self.try_write(ps2)?;
+ } else if data == 0xFE {
+ // PS/2 RESEND: mouse asks us to resend the current command byte
+ log::debug!("mouse requested resend for byte {:02X}, resending", self.write.get(self.write_i).unwrap_or(&0));
+ self.try_write(ps2)?;
} else {
log::error!("unknown mouse response {:02X}", data);
return Err(());
@@ -80,8 +84,7 @@ enum MouseId {
Base = 0x00,
/// Mouse sends fourth byte with scroll
Intellimouse1 = 0x03,
- /// Mouse sends fourth byte with scroll, button 4, and button 5
- //TODO: support this mouse type
+ /// Mouse sends fourth byte with scroll and buttons 4/5
Intellimouse2 = 0x04,
}
@@ -94,25 +97,16 @@ pub enum TouchpadCommand {
#[derive(Debug)]
pub enum MouseState {
- /// No mouse found
None,
- /// Ready to initialize mouse
Init,
- /// Reset command is sent
Reset,
- /// BAT completion code returned
Bat,
- /// Identify touchpad
IdentifyTouchpad { tx: MouseTx },
- /// Enable intellimouse features
EnableIntellimouse { tx: MouseTx },
- /// Status request
+ EnableIntellimouse2 { tx: MouseTx },
Status { index: usize },
- /// Device ID update
DeviceId,
- /// Enable reporting command sent
EnableReporting { id: u8 },
- /// Mouse is streaming
Streaming { id: u8 },
}
@@ -194,9 +188,7 @@ impl MouseState {
let cmd = TouchpadCommand::Identify as u8;
match MouseTx::new(
&[
- // Ensure command alignment
MouseCommand::SetScaling1To1 as u8,
- // Send special identify touchpad command
MouseCommandData::SetResolution as u8,
0,
MouseCommandData::SetResolution as u8,
@@ -205,7 +197,6 @@ impl MouseState {
0,
MouseCommandData::SetResolution as u8,
0,
- // Status request
MouseCommand::StatusRequest as u8,
],
3,
@@ -215,7 +206,7 @@ impl MouseState {
*self = MouseState::IdentifyTouchpad { tx };
MouseResult::Timeout(COMMAND_TIMEOUT)
}
- Err(()) => self.enable_intellimouse(ps2),
+ Err(()) => self.enable_intellimouse2(ps2),
}
}
@@ -240,6 +231,27 @@ impl MouseState {
}
}
+ fn enable_intellimouse2(&mut self, ps2: &mut Ps2) -> MouseResult {
+ match MouseTx::new(
+ &[
+ MouseCommandData::SetSampleRate as u8,
+ 200,
+ MouseCommandData::SetSampleRate as u8,
+ 200,
+ MouseCommandData::SetSampleRate as u8,
+ 80,
+ ],
+ 0,
+ ps2,
+ ) {
+ Ok(tx) => {
+ *self = MouseState::EnableIntellimouse2 { tx };
+ MouseResult::Timeout(COMMAND_TIMEOUT)
+ }
+ Err(()) => self.enable_intellimouse(ps2),
+ }
+ }
+
pub fn handle(&mut self, data: u8, ps2: &mut Ps2) -> MouseResult {
match *self {
MouseState::None | MouseState::Init => {
@@ -260,17 +272,22 @@ impl MouseState {
MouseResult::Timeout(COMMAND_TIMEOUT)
} else {
log::warn!("unknown mouse response {:02X} after reset", data);
- self.reset(ps2)
+ *self = MouseState::None;
+ MouseResult::None
}
}
MouseState::Bat => {
if data == MouseId::Base as u8 {
- // Enable intellimouse features
+ // Base mouse - enable intellimouse features
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);
+ // Scroll wheel already enabled
+ log::debug!("BAT mouse id {:02X} (intellimouse1)", data);
+ self.enable_reporting(data, ps2)
+ } else if data == MouseId::Intellimouse2 as u8 {
+ // Scroll wheel + buttons 4/5 already enabled
+ log::debug!("BAT mouse id {:02X} (intellimouse2)", data);
self.enable_reporting(data, ps2)
} else {
log::warn!("unknown mouse id {:02X} after BAT", data);
@@ -291,7 +308,17 @@ impl MouseState {
Err(()) => self.enable_intellimouse(ps2),
}
}
- MouseState::EnableIntellimouse { ref mut tx } => match tx.handle(data, ps2) {
+MouseState::EnableIntellimouse { ref mut tx } => match tx.handle(data, ps2) {
+ Ok(done) => {
+ if done {
+ self.request_status(ps2)
+ } else {
+ MouseResult::Timeout(COMMAND_TIMEOUT)
+ }
+ }
+ Err(()) => self.request_id(ps2),
+ },
+ MouseState::EnableIntellimouse2 { ref mut tx } => match tx.handle(data, ps2) {
Ok(done) => {
if done {
self.request_status(ps2)
@@ -299,7 +326,7 @@ impl MouseState {
MouseResult::Timeout(COMMAND_TIMEOUT)
}
}
- Err(()) => self.request_status(ps2),
+ Err(()) => self.enable_intellimouse(ps2),
},
MouseState::Status { index } => {
match index {
@@ -324,7 +351,7 @@ impl MouseState {
// Command OK response
//TODO: handle this separately?
MouseResult::Timeout(COMMAND_TIMEOUT)
- } else if data == MouseId::Base as u8 || data == MouseId::Intellimouse1 as u8 {
+ } else if data == MouseId::Base as u8 || data == MouseId::Intellimouse1 as u8 || data == MouseId::Intellimouse2 as u8 {
log::debug!("mouse id {:02X}", data);
self.enable_reporting(data, ps2)
} else {
@@ -339,11 +366,15 @@ impl MouseState {
MouseResult::None
}
MouseState::Streaming { id } => {
- MouseResult::Packet(data, id == MouseId::Intellimouse1 as u8)
+ MouseResult::Packet(data, id == MouseId::Intellimouse1 as u8 || id == MouseId::Intellimouse2 as u8)
}
}
}
+ pub fn streaming_is_intellimouse2(&self) -> bool {
+ matches!(self, MouseState::Streaming { id } if *id == MouseId::Intellimouse2 as u8)
+ }
+
pub fn handle_timeout(&mut self, ps2: &mut Ps2) -> MouseResult {
match *self {
MouseState::None | MouseState::Streaming { .. } => MouseResult::None,
@@ -352,12 +383,14 @@ impl MouseState {
self.reset(ps2)
}
MouseState::Reset => {
- log::warn!("timeout waiting for mouse reset");
- self.reset(ps2)
+ log::debug!("timeout waiting for mouse reset, fast-failing");
+ *self = MouseState::None;
+ MouseResult::None
}
MouseState::Bat => {
- log::warn!("timeout waiting for BAT completion");
- self.reset(ps2)
+ log::debug!("timeout waiting for BAT completion, fast-failing");
+ *self = MouseState::None;
+ MouseResult::None
}
MouseState::IdentifyTouchpad { .. } => {
//TODO: retry?
@@ -365,10 +398,13 @@ impl MouseState {
self.request_status(ps2)
}
MouseState::EnableIntellimouse { .. } => {
- //TODO: retry?
log::warn!("timeout enabling intellimouse");
self.request_status(ps2)
}
+ MouseState::EnableIntellimouse2 { .. } => {
+ log::warn!("timeout enabling intellimouse2, falling back to intellimouse");
+ self.enable_intellimouse(ps2)
+ }
MouseState::Status { index } => {
log::warn!("timeout waiting for mouse status {}", index);
self.request_id(ps2)
diff --git a/drivers/input/ps2d/src/state.rs b/drivers/input/ps2d/src/state.rs
index 9018dc6b..8f5832f6 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,
@@ -52,16 +53,24 @@ pub struct Ps2d {
mouse_left: bool,
mouse_middle: bool,
mouse_right: bool,
+ mouse_button_4: bool,
+ mouse_button_5: bool,
mouse_state: MouseState,
mouse_timeout: Option<TimeSpec>,
packets: [u8; 4],
packet_i: usize,
+ caps_lock: bool,
+ num_lock: bool,
+ scroll_lock: bool,
+ leds_dirty: bool,
}
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 +86,8 @@ impl Ps2d {
ps2,
vmmouse,
vmmouse_relative,
- input,
+ keyboard_input,
+ mouse_input,
time_file,
extended: false,
mouse_x: 0,
@@ -85,10 +95,16 @@ impl Ps2d {
mouse_left: false,
mouse_middle: false,
mouse_right: false,
+ mouse_button_4: false,
+ mouse_button_5: false,
mouse_state: MouseState::Init,
mouse_timeout: None,
packets: [0; 4],
packet_i: 0,
+ caps_lock: false,
+ num_lock: true,
+ scroll_lock: false,
+ leds_dirty: true,
};
if !this.vmmouse {
@@ -96,6 +112,12 @@ impl Ps2d {
this.handle_mouse(None);
}
+ // Flush initial LED state (Num Lock on by default)
+ if this.leds_dirty {
+ this.leds_dirty = false;
+ this.ps2.set_leds(this.caps_lock, this.num_lock, this.scroll_lock);
+ }
+
this
}
@@ -272,8 +294,21 @@ impl Ps2d {
}
};
+ if scancode != 0 && pressed {
+ match scancode {
+ orbclient::K_CAPS => { self.caps_lock = !self.caps_lock; self.leds_dirty = true; },
+ orbclient::K_NUM => { self.num_lock = !self.num_lock; self.leds_dirty = true; },
+ orbclient::K_SCROLL => { self.scroll_lock = !self.scroll_lock; self.leds_dirty = true; },
+ _ => (),
+ }
+ }
+ if self.leds_dirty {
+ self.leds_dirty = false;
+ self.ps2.set_leds(self.caps_lock, self.num_lock, self.scroll_lock);
+ }
+
if scancode != 0 {
- self.input
+ self.keyboard_input
.write_event(
KeyEvent {
character: '\0',
@@ -304,7 +339,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 +355,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 +383,7 @@ impl Ps2d {
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_input
.write_event(
ButtonEvent {
left,
@@ -432,22 +467,35 @@ impl Ps2d {
}
let mut dz = 0;
+ let mut button_4 = false;
+ let mut button_5 = false;
if extra_packet {
- let mut scroll = (self.packets[3] & 0xF) as i8;
- if scroll & (1 << 3) == 1 << 3 {
- scroll -= 16;
+ let fourth = self.packets[3];
+ if self.mouse_state.streaming_is_intellimouse2() {
+ let mut scroll = (fourth & 0x0F) as i8;
+ if scroll & 0x08 != 0 {
+ scroll -= 16;
+ }
+ dz = -(scroll as i32);
+ button_4 = (fourth & 0x10) != 0;
+ button_5 = (fourth & 0x20) != 0;
+ } else {
+ let mut scroll = (fourth & 0xF) as i8;
+ if scroll & (1 << 3) == 1 << 3 {
+ scroll -= 16;
+ }
+ dz = -scroll as i32;
}
- dz = -scroll as i32;
}
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");
}
@@ -458,11 +506,15 @@ impl Ps2d {
if left != self.mouse_left
|| middle != self.mouse_middle
|| right != self.mouse_right
+ || button_4 != self.mouse_button_4
+ || button_5 != self.mouse_button_5
{
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_button_4 = button_4;
+ self.mouse_button_5 = button_5;
+ self.mouse_input
.write_event(
ButtonEvent {
left,
diff --git a/drivers/input/ps2d/src/vm.rs b/drivers/input/ps2d/src/vm.rs
index 71b71417..769a78e9 100644
--- a/drivers/input/ps2d/src/vm.rs
+++ b/drivers/input/ps2d/src/vm.rs
@@ -64,8 +64,8 @@ pub unsafe fn cmd(cmd: u32, arg: u32) -> (u32, u32, u32, u32) {
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
-pub unsafe fn cmd(cmd: u32, arg: u32) -> (u32, u32, u32, u32) {
- unimplemented!()
+pub unsafe fn cmd(_cmd: u32, _arg: u32) -> (u32, u32, u32, u32) {
+ (0, 0, 0, 0)
}
pub fn enable(relative: bool) -> bool {
@@ -1,49 +1,7 @@
diff --git a/src/acpi/madt/arch/x86.rs b/src/acpi/madt/arch/x86.rs
--- a/src/acpi/madt/arch/x86.rs
+++ b/src/acpi/madt/arch/x86.rs
@@ -446,7 +446,11 @@
// Send INIT IPI (Assert)
{
let mut icr = 0x4500u64;
- icr |= u64::from(apic_id) << 32;
+ if local_apic.x2 {
+ icr |= u64::from(apic_id) << 32;
+ } else {
+ icr |= u64::from(apic_id as u8) << 56;
+ }
local_apic.set_icr(icr);
}
@@ -456,7 +460,11 @@
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x0600u64 | ap_segment as u64;
- icr |= u64::from(apic_id) << 32;
+ if local_apic.x2 {
+ icr |= u64::from(apic_id) << 32;
+ } else {
+ icr |= u64::from(apic_id as u8) << 56;
+ }
local_apic.set_icr(icr);
}
@@ -468,7 +476,11 @@
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x0600u64 | ap_segment as u64;
- icr |= u64::from(apic_id) << 32;
+ if local_apic.x2 {
+ icr |= u64::from(apic_id) << 32;
+ } else {
+ icr |= u64::from(apic_id as u8) << 56;
+ }
local_apic.set_icr(icr);
}
diff --git a/src/arch/x86_shared/device/local_apic.rs b/src/arch/x86_shared/device/local_apic.rs
--- a/src/arch/x86_shared/device/local_apic.rs
+++ b/src/arch/x86_shared/device/local_apic.rs
@@ -61,9 +61,9 @@
@@ -59,10 +59,10 @@
.is_some_and(|feature_info| feature_info.has_x2apic());
if !self.x2 {
- debug!("Detected xAPIC at {:#x}", physaddr.data());
@@ -54,3 +12,4 @@ diff --git a/src/arch/x86_shared/device/local_apic.rs b/src/arch/x86_shared/devi
+ info!("Detected x2APIC");
}
self.init_ap();
@@ -0,0 +1,88 @@
--- a/src/acpi/madt/arch/x86.rs
+++ b/src/acpi/madt/arch/x86.rs
@@ -189,8 +189,18 @@
let preliminary_cpu_count = madt
.iter()
.filter(|entry| match entry {
- MadtEntry::LocalApic(local) => u32::from(local.id) == me.get() || local.flags & 1 == 1,
- MadtEntry::LocalX2Apic(local) => local.x2apic_id == me.get() || local.flags & 1 == 1,
+ // When x2APIC is active, LocalApic entries use 8-bit IDs that don't
+ // match the BSP's 32-bit x2APIC ID. Use LocalX2Apic entries instead.
+ MadtEntry::LocalApic(local) if !local_apic.x2 => {
+ u32::from(local.id) == me.get() || local.flags & 1 == 1
+ }
+ MadtEntry::LocalApic(_) => false,
+ // xAPIC mode: cannot use 32-bit x2APIC IDs via 8-bit ICR.
+ // Skip LocalX2Apic entries and use LocalApic exclusively.
+ MadtEntry::LocalX2Apic(local) if local_apic.x2 => {
+ local.x2apic_id == me.get() || local.flags & 1 == 1
+ }
+ MadtEntry::LocalX2Apic(_) => false,
_ => false,
})
.count();
@@ -205,18 +215,28 @@
let _ = seen_apic_ids.insert(me.get()); // BSP
for entry in madt.iter() {
match entry {
- MadtEntry::LocalApic(local) if local.flags & 1 == 1 => {
+ MadtEntry::LocalApic(local) if local.flags & 1 == 1 && !local_apic.x2 => {
let id = u32::from(local.id);
if !seen_apic_ids.insert(id) {
warn!("MADT: duplicate APIC ID {} in LocalApic entry, firmware bug", id);
}
}
- MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 => {
+ MadtEntry::LocalApic(local) if local.flags & 1 == 1 && local_apic.x2 => {
+ // x2APIC mode: skip 8-bit LocalApic IDs; they conflict with
+ // 32-bit x2APIC IDs. Dedup only among LocalX2Apic entries.
+ debug!("MADT: ignoring 8-bit LocalApic ID {} in x2APIC mode", local.id);
+ }
+ MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && local_apic.x2 => {
let id = local.x2apic_id;
if !seen_apic_ids.insert(id) {
warn!("MADT: duplicate x2APIC ID {} in LocalX2Apic entry, firmware bug", id);
}
}
+ MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && !local_apic.x2 => {
+ // xAPIC mode: skip 32-bit x2APIC IDs; dedup only among LocalApic entries.
+ let id = local.x2apic_id; // Copy from packed struct
+ debug!("MADT: ignoring 32-bit x2APIC ID {} in xAPIC mode", id);
+ }
_ => {}
}
}
@@ -225,7 +245,16 @@
for madt_entry in madt.iter() {
debug!(" {:x?}", madt_entry);
if let MadtEntry::LocalApic(ap_local_apic) = madt_entry {
- if u32::from(ap_local_apic.id) == me.get() {
+ // x2APIC mode: LocalApic entries have 8-bit IDs that don't match
+ // the BSP's 32-bit x2APIC ID. All entries would be treated as APs,
+ // and SIPI would target the wrong processors. Skip them and rely
+ // on LocalX2Apic entries exclusively.
+ if local_apic.x2 {
+ debug!(
+ " Skipping 8-bit LocalApic id={} (x2APIC active, using LocalX2Apic entries)",
+ ap_local_apic.id
+ );
+ } else if u32::from(ap_local_apic.id) == me.get() {
debug!(" This is my local APIC");
} else if ap_local_apic.flags & 1 == 1 {
// Allocate a stack
@@ -388,7 +417,14 @@
let apic_id = ap_x2apic.x2apic_id;
let flags = ap_x2apic.flags;
- if apic_id == me.get() {
+ // xAPIC mode: cannot target 32-bit x2APIC IDs via 8-bit ICR.
+ // Skip LocalX2Apic entries; use LocalApic entries exclusively.
+ if !local_apic.x2 {
+ debug!(
+ " Skipping 32-bit x2APIC id={} (xAPIC mode, using LocalApic entries)",
+ apic_id
+ );
+ } else if apic_id == me.get() {
debug!(" This is my local x2APIC");
} else if flags & 1 == 1 {
let alloc = match allocate_p2frame(4) {
@@ -299,7 +299,11 @@ impl Bus for AcpiBus {
});
}
log::info!("acpi bus: enumerated {} device(s)", devices.len());
if devices.is_empty() {
log::debug!("acpi bus: enumerated {} device(s)", devices.len());
} else {
log::info!("acpi bus: enumerated {} device(s)", devices.len());
}
Ok(devices)
}
}
@@ -144,7 +144,7 @@ fn extract_package_contents(ron: &str) -> Vec<String> {
if end_pos.is_none() {
return Vec::new();
}
let inner = &inner[..end_pos.unwrap()];
let inner = &inner[..end_pos.expect("find_matching_bracket returned Some after is_none check")];
// Split into sub-packages by finding balanced "Package(...)" blocks
split_sub_packages(inner)
@@ -205,7 +205,7 @@ fn split_sub_packages(inner: &str) -> Vec<String> {
if paren_start.is_none() {
break;
}
let abs_paren = pos + paren_start.unwrap();
let abs_paren = pos + paren_start.expect("find returned Some after is_none check");
// Find matching closing ')'
let mut depth = 1;
@@ -311,7 +311,7 @@ fn extract_package_elements(pkg: &str) -> Vec<String> {
if end.is_none() {
return Vec::new();
}
let inner = &inner[..end.unwrap()];
let inner = &inner[..end.expect("find_matching_bracket returned Some after is_none check")];
// Split by commas at depth 0
let mut elements = Vec::new();
@@ -11,12 +11,16 @@ use redox_scheme::Socket;
use redox_scheme::scheme::{SchemeAsync, SchemeSync};
unsafe fn get_fd(var: &str) -> Option<RawFd> {
// Env vars like INIT_NOTIFY are optional — daemons not spawned by init
// simply don't have them. Return None silently instead of spewing errors.
let fd: RawFd = match std::env::var(var)
.map_err(|e| eprintln!("daemon: env var {var} not set: {e}"))
.ok()
.and_then(|val| {
val.parse()
.map_err(|e| eprintln!("daemon: failed to parse {var} as fd: {e}"))
val.parse::<RawFd>()
.map_err(|e| {
eprintln!("daemon: failed to parse {var} as fd: {e}");
e
})
.ok()
}) {
Some(fd) => fd,
@@ -172,8 +172,8 @@ impl IntelDisplay {
return Vec::new();
}
debug!("redox-drm: Intel AUX/DPCD skeleton read on port {}", port);
vec![0x12, 0x0A, 0x84, 0x01]
debug!("redox-drm: Intel DPCD not yet implemented for port {}", port);
Vec::new()
}
pub fn set_mode(&self, pipe: &DisplayPipe, mode: &ModeInfo) -> Result<()> {
@@ -3,7 +3,6 @@ pub mod gtt;
pub mod ring;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use log::{debug, info, warn};
@@ -27,6 +26,8 @@ const FORCEWAKE: usize = 0xA18C;
const PP_STATUS: usize = 0xC7200;
const PIPECONF_BASE: usize = 0x70008;
const PIPE_STRIDE: usize = 0x1000;
const PIPEFRAME_REG: usize = 0x70040;
const PIPEFRAME_COUNT_MASK: u32 = 0x00FFFFFF;
const DDI_BUF_CTL_BASE: usize = 0x64000;
const DDI_PORT_STRIDE: usize = 0x100;
const GFX_FLSH_CNTL_REG: usize = 0x101008;
@@ -46,7 +47,6 @@ pub struct IntelDriver {
encoders: Mutex<Vec<Encoder>>,
gtt: Mutex<IntelGtt>,
ring: Mutex<IntelRing>,
vblank_count: AtomicU64,
}
impl IntelDriver {
@@ -146,7 +146,6 @@ impl IntelDriver {
encoders: Mutex::new(encoders),
gtt: Mutex::new(gtt),
ring: Mutex::new(ring),
vblank_count: AtomicU64::new(0),
})
}
@@ -234,7 +233,7 @@ impl IntelDriver {
.has_activity()?;
if let Some(crtc_id) = self.active_crtc_id()? {
let count = self.vblank_count.fetch_add(1, Ordering::SeqCst) + 1;
let count = self.read_pipeframe(crtc_id)?;
debug!(
"redox-drm: Intel IRQ decoded as display event crtc={} ring_busy={}",
crtc_id, ring_busy
@@ -332,6 +331,25 @@ impl IntelDriver {
}
Ok(self.mmio.read32(offset))
}
fn read_pipeframe(&self, crtc_id: u32) -> Result<u64> {
let pipe_index = crtc_id
.checked_sub(1)
.ok_or_else(|| DriverError::InvalidArgument("invalid Intel CRTC id"))?
as usize;
let offset = PIPEFRAME_REG + pipe_index * PIPE_STRIDE;
let end = offset
.checked_add(core::mem::size_of::<u32>())
.ok_or_else(|| DriverError::Mmio("Intel PIPEFRAME offset overflow".into()))?;
if end > self.mmio.size() {
return Err(DriverError::Mmio(format!(
"Intel PIPEFRAME read outside MMIO aperture: end={end:#x} size={:#x}",
self.mmio.size()
)));
}
let frame_count = self.mmio.read32(offset) & PIPEFRAME_COUNT_MASK;
Ok(u64::from(frame_count))
}
}
impl GpuDriver for IntelDriver {
@@ -410,14 +428,7 @@ impl GpuDriver for IntelDriver {
}
fn get_vblank(&self, crtc_id: u32) -> Result<u64> {
let crtcs = self
.crtcs
.lock()
.map_err(|_| DriverError::Initialization("Intel CRTC state poisoned".into()))?;
if !crtcs.iter().any(|crtc| crtc.id == crtc_id) {
return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}")));
}
Ok(self.vblank_count.load(Ordering::SeqCst))
self.read_pipeframe(crtc_id)
}
fn gem_create(&self, size: u64, _width: u32, _height: u32) -> Result<GemHandle> {
@@ -33,16 +33,51 @@ impl Connector {
}
pub fn synthetic_edid() -> Vec<u8> {
vec![
0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x4c, 0x2d, 0xfa, 0x12, 0x01, 0x00, 0x00,
0x00, 0x01, 0x1e, 0x01, 0x04, 0xa5, 0x3c, 0x22, 0x78, 0x3a, 0xee, 0x95, 0xa3, 0x54, 0x4c,
0x99, 0x26, 0x0f, 0x50, 0x54, 0xbf, 0xef, 0x80, 0x71, 0x4f, 0x81, 0x80, 0x81, 0x40, 0x81,
0xc0, 0x95, 0x00, 0xa9, 0xc0, 0xb3, 0x00, 0xd1, 0xc0, 0x02, 0x3a, 0x80, 0x18, 0x71, 0x38,
0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0x55, 0x50, 0x21, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00,
0xfd, 0x00, 0x32, 0x4c, 0x1e, 0x53, 0x11, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x00, 0x00, 0x00, 0xfc, 0x00, 0x53, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x20,
0x44, 0x50, 0x0a, 0x20, 0x20, 0x00, 0xa7,
]
// Standard EDID 1.4 block (128 bytes) for a synthetic 1920×1080@60 Hz
// DisplayPort monitor. Contains one valid detailed timing descriptor,
// a monitor range limits descriptor, and a monitor name descriptor.
// The fourth descriptor is unused (pixel_clock = 0, skipped by parsers).
// Byte 127 is the checksum that makes the full block sum to 0 mod 256.
let mut edid = vec![
// Bytes 07: EDID 1.4 header
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
// Bytes 815: Manufacturer ID (LE), product code, serial, week/year
0x4C, 0x2D, 0xFA, 0x12, 0x01, 0x00, 0x00, 0x00,
// Bytes 1624: EDID version 1.4, display params
0x01, 0x1E, 0x01, 0x04, 0xA5, 0x3C, 0x22, 0x78, 0x3A,
// Bytes 2534: Color characteristics
0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26, 0x0F, 0x50, 0x54,
// Bytes 3537: Established timings bitmap
0xBF, 0xEF, 0x80,
// Bytes 3853: Standard timings (8 × 2-byte entries)
0x71, 0x4F, 0x81, 0x80, 0x81, 0x40, 0x81, 0xC0,
0x95, 0x00, 0xA9, 0xC0, 0xB3, 0x00, 0xD1, 0xC0,
// Bytes 5471: Descriptor 1 — Detailed timing: 1920×1080 @ 60 Hz
// pixel_clock = 14850 (0x3A02 LE), hactive=1920, vactive=1080
0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40,
0x58, 0x2C, 0x45, 0x00, 0x55, 0x50, 0x21, 0x00, 0x00, 0x1E,
// Bytes 7289: Descriptor 2 — Monitor range limits (tag 0xFD)
0x00, 0x00, 0x00, 0xFD, 0x00, 0x32, 0x4C, 0x1E,
0x53, 0x11, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
// Bytes 90107: Descriptor 3 — Monitor name (tag 0xFC): "Synthetic DP\n"
0x00, 0x00, 0x00, 0xFC, 0x00, 0x53, 0x79, 0x6E,
0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x20, 0x44, 0x50, 0x0A,
// Bytes 108125: Descriptor 4 — unused (pixel_clock = 0, skipped)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Byte 126: Number of extensions (0 = no extension blocks)
0x00,
// Byte 127: Checksum placeholder (computed below)
0x00,
];
debug_assert_eq!(edid.len(), 128, "synthetic EDID must be exactly 128 bytes");
// EDID checksum: sum of all 128 bytes must be 0 mod 256.
// Set byte 127 so that this invariant holds.
let sum: u8 = edid.iter().take(127).fold(0u8, |acc, &b| acc.wrapping_add(b));
edid[127] = (0u8).wrapping_sub(sum);
edid
}
#[cfg(test)]
@@ -74,9 +109,11 @@ mod tests {
}
#[test]
fn synthetic_edid_returns_exactly_112_bytes() {
fn synthetic_edid_returns_exactly_128_bytes_with_valid_checksum() {
let edid = synthetic_edid();
assert_eq!(edid.len(), 112);
assert_eq!(edid.len(), 128, "EDID must be exactly 128 bytes (EDID 1.4 block size)");
let checksum: u8 = edid.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
assert_eq!(checksum, 0, "EDID checksum must sum to 0 mod 256");
}
#[test]
@@ -198,14 +198,15 @@ mod tests {
}
#[test]
fn from_edid_synthetic_edid_too_short_returns_empty() {
fn from_edid_synthetic_edid_parses_1080p_mode() {
let edid = super::connector::synthetic_edid();
assert!(edid.len() < 128, "synthetic EDID is shorter than 128 bytes");
assert_eq!(edid.len(), 128, "synthetic EDID must be 128 bytes");
let modes = ModeInfo::from_edid(&edid);
assert!(
modes.is_empty(),
"EDID shorter than 128 bytes should produce no modes"
);
assert!(!modes.is_empty(), "valid 128-byte EDID should produce at least one mode");
let mode = &modes[0];
assert_eq!(mode.hdisplay, 1920, "first mode should be 1920px wide");
assert_eq!(mode.vdisplay, 1080, "first mode should be 1080px tall");
assert_eq!(mode.vrefresh, 60, "first mode should be 60 Hz");
}
#[test]
@@ -107,7 +107,7 @@ impl DriverConfig {
if matches.is_empty() {
log::warn!(
"driver-manager: config {} driver={} has no PCI match entries and will not bind from PCI enumeration",
"driver-manager: config {} driver={} has no match entries and will not bind from PCI or ACPI enumeration",
path.display(),
driver.name
);
@@ -245,12 +245,29 @@ impl Driver for DriverConfig {
format!("/usr/lib/drivers/{}", self.command[0])
};
// Also check the initfs path — drivers like nvmed live in
// /scheme/initfs/lib/drivers/ during early boot and may not yet
// be staged to /usr/lib/drivers/ after switchroot.
if !std::path::Path::new(&actual_path).exists() {
let initfs_path = format!("/scheme/initfs/lib/drivers/{}", self.command[0].rsplit('/').next().unwrap_or(&self.command[0]));
if std::path::Path::new(&initfs_path).exists() {
return ProbeResult::Deferred {
reason: format!("driver in initfs only (not yet in rootfs): {}", initfs_path),
};
}
return ProbeResult::Fatal {
reason: format!("driver binary not found: {}", actual_path),
reason: format!("driver binary not found: {} (also checked {})", actual_path, initfs_path),
};
}
// Skip if this driver's scheme is already registered (e.g., by
// pcid-spawner during initfs). Prevents re-spawning drivers
// that are already serving their scheme.
if check_scheme_available(&self.name) {
log::info!("driver {} already serving scheme, skipping probe for {}", self.name, device_key);
return ProbeResult::Bound;
}
let deps: Vec<String> = if !self.depends_on.is_empty() {
self.depends_on.clone()
} else {
@@ -133,8 +133,14 @@ fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_n
}
fn reset_timeline_log() {
if let Err(err) = fs::write(BOOT_TIMELINE_PATH, "") {
log::warn!("failed to reset boot timeline log at {BOOT_TIMELINE_PATH}: {err}");
// Best-effort: truncate or create empty. On scheme filesystems that
// don't support truncate on existing files, this may fail — that's OK,
// the append path will handle it.
match fs::write(BOOT_TIMELINE_PATH, "") {
Ok(()) => {}
Err(_) => {
let _ = fs::remove_file(BOOT_TIMELINE_PATH);
}
}
}
@@ -227,25 +233,24 @@ fn log_timeline(event: &ProbeEvent) {
{
Ok(mut file) => {
if let Err(err) = writeln!(file, "{entry}") {
// Broken pipe (EPIPE) can occur when /tmp is backed by a
// scheme that doesn't support append writes, or when the
// EPIPE or other write errors can occur when /tmp is backed
// by a scheme that doesn't support append writes, or when the
// filesystem is not yet fully ready. Log once and suppress
// repeated identical errors to avoid log spam.
static LAST_ERROR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
let err_code = err.raw_os_error().unwrap_or(0) as i32;
let last = LAST_ERROR.load(std::sync::atomic::Ordering::Relaxed);
if err_code != last {
LAST_ERROR.store(err_code, std::sync::atomic::Ordering::Relaxed);
log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err}");
// all subsequent write errors to avoid log spam.
static WRITE_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WRITE_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err} (suppressing further write errors)");
}
}
}
Err(err) => {
// Only log file-open errors once to avoid spamming during early
// boot when /tmp may not yet be mounted.
// EEXIST (os error 17) can occur when the file already exists
// but the scheme filesystem doesn't support create+append.
// EPIPE and other errors occur when /tmp isn't ready.
// Log once and suppress all subsequent open errors.
static OPEN_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !OPEN_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err}");
log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err} (suppressing further open errors)");
}
}
}
@@ -383,10 +383,14 @@ pub fn notify_bind(scheme: &DriverManagerScheme, pci_addr: &str, driver_name: &s
));
if let Err(err) = write_driver_param(pci_addr, "driver", driver_name) {
log::warn!("driver-manager: failed to write driver param for {pci_addr}: {err}");
if err.kind() != std::io::ErrorKind::BrokenPipe {
log::warn!("driver-manager: failed to write driver param for {pci_addr}: {err}");
}
}
if let Err(err) = write_driver_param(pci_addr, "enabled", "true") {
log::warn!("driver-manager: failed to write enabled param for {pci_addr}: {err}");
if err.kind() != std::io::ErrorKind::BrokenPipe {
log::warn!("driver-manager: failed to write enabled param for {pci_addr}: {err}");
}
}
}
@@ -409,10 +413,14 @@ pub fn notify_unbind(scheme: &DriverManagerScheme, pci_addr: &str) {
scheme.push_event_line(event_line);
if let Err(err) = write_driver_param(pci_addr, "driver", "") {
log::warn!("driver-manager: failed to clear driver param for {pci_addr}: {err}");
if err.kind() != std::io::ErrorKind::BrokenPipe {
log::warn!("driver-manager: failed to clear driver param for {pci_addr}: {err}");
}
}
if let Err(err) = write_driver_param(pci_addr, "enabled", "false") {
log::warn!("driver-manager: failed to write disabled param for {pci_addr}: {err}");
if err.kind() != std::io::ErrorKind::BrokenPipe {
log::warn!("driver-manager: failed to write disabled param for {pci_addr}: {err}");
}
}
}
@@ -536,7 +536,7 @@ fn monitor_loop(shared: Arc<RwLock<ThermalState>>) -> ! {
loop {
if !Path::new(ACPI_THERMAL_ROOT).exists() {
if !warned_missing_surface {
warn!(
log::info!(
"{} is unavailable; thermald will keep polling and serve an empty thermal surface",
ACPI_THERMAL_ROOT,
);
+20 -22
View File
@@ -9,6 +9,7 @@ patches = [
"P0-procmgr-sigchld-debug.patch",
"P0-pcid-mcfg-diagnostics.patch",
"P0-ihdgd-intel-gpu-ids.patch",
"P0-acpid-dmar-fix.patch",
# P1: acpid EC runtime and AML physmem hardening (narrow ACPI runtime patches)
"P1-acpid-ec-runtime.patch",
"P1-acpid-runtime-hardening.patch",
@@ -24,9 +25,8 @@ patches = [
"P2-misc-daemon-fixes.patch",
"P9-fix-so-pecred.patch",
"P3-inputd-keymap-bridge.patch",
"P3-ps2d-led-feedback.patch",
"P3-ps2d-mouse-resend.patch",
"P0-ps2d-mouse-fastfail.patch",
# P3: ps2d consolidated — LED feedback, mouse resend, fastfail, Intellimouse2, controller init robustness, non-x86 fallback
"P7-ps2d-intellimouse2-leds-controller-init.patch",
"P3-usbhidd-hardening.patch",
"P3-init-colored-output.patch",
"P4-logd-persistent-logging.patch",
@@ -41,34 +41,31 @@ patches = [
"P4-initfs-getty-services.patch",
"P4-initfs-dbus-services.patch",
"P4-fbcond-scrollback.patch",
"P4-ucsid-estale-graceful.patch",
"P4-acpi-estale-graceful.patch",
"P4-hwd-estale-graceful.patch",
# P5-i2c-hidd-estale-retry: REDUNDANT — ESTALE retry already provided by P2 + P4-acpi-estale
"P5-acpid-dmi-endpoint.patch",
"P4-thermal-daemon.patch",
"P4-thermald-workspace.patch",
"P6-driver-main-fixes.patch",
"P6-driver-new-modules.patch",
"P9-init-scheduler-completed.patch",
"P6-init-requires-hard-dep.patch",
"P2-pcid-acpid-graceful-fd.patch",
# P5: Graceful DRM ioctl error handling in fbbootlogd/fbcond (avoid ENOTTY crash)
"P5-fbbootlogd-fbcond-graceful-drm.patch",
# P6: Fix rtcd EEXIST by avoiding O_CREAT on kernel scheme resource
"P6-rtcd-no-ocreat.patch",
# P6: Fix pcid→acpid FD transfer — pass FD in metadata array, not payload
"P6-pcid-acpid-fd-transfer.patch",
# P7: Fix acpid pci_fd startup race — shared RwLock between scheme and AML handler
"P7-acpid-shared-pcifd.patch",
# P15: Init service timeout — prevent boot hanging on unresponsive daemons (30s default)
"P6-rtcd-no-ocreat.patch",
"P6-pcid-acpid-fd-transfer.patch",
"P15-7-init-service-timeout.patch",
# P15: Dependency cycle detection in unit loader — log and skip circular requires_weak
"P15-8-init-cycle-detection.patch",
# P18: Init daemon restart policy — supervise Notify/Scheme services with exponential backoff
# P15-8-init-cycle-detection: REDUNDANT — cycle detection already included in P6-init-requires-hard-dep
"P18-1-daemon-restart.patch",
# P18: ACPID robustness — RSDP BIOS-area fallback, graceful physmem error handling
"P18-5-acpid-robustness.patch",
# P18: MSI/MSI-X enablement — skip legacy IRQ for MSI-capable devices
"P18-3-msi-msix-enablement.patch",
# P18: Bounded IPC queues — backlog limits for chan, UDS stream, UDS dgram
"P18-5-acpid-robustness.patch",
"P18-8-bounded-ipcd-queues.patch",
# P18: MSI/MSI-X allocation resilience — handle EEXIST, fallback chain MSI-X→MSI→legacy
"P18-9-msi-allocation-resilience.patch",
"P19-init-startup-hardening.patch",
"P19-acpid-startup-hardening.patch",
]
[package]
@@ -185,6 +182,8 @@ BINS=(
ixgbed
pcid
pcid-spawner
acpid
redoxerd
rtl8139d
rtl8168d
usbctl
@@ -198,14 +197,13 @@ BINS=(
xhcid
i2cd
inputd
redoxerd
)
# Add additional drivers to the list to build, that are not in drivers-initfs
# depending on the target architecture
case "${TARGET}" in
i586-unknown-redox | i686-unknown-redox | x86_64-unknown-redox)
BINS+=(ac97d ahcid ided ps2d sb16d vboxd)
BINS+=(ac97d ahcid ided nvmed ps2d sb16d vboxd)
;;
*)
;;
@@ -229,7 +227,7 @@ done
$(for bin in "${EXISTING_BINS[@]}"; do echo "-p" "${bin}"; done)
for bin in "${EXISTING_BINS[@]}"
do
if [[ "${bin}" == "gpiod" || "${bin}" == "i2c-gpio-expanderd" || "${bin}" == "intel-gpiod" || "${bin}" == "i2cd" || "${bin}" == "dw-acpi-i2cd" || "${bin}" == "i2c-hidd" || "${bin}" == "inputd" || "${bin}" == "pcid" || "${bin}" == "pcid-spawner" || "${bin}" == "redoxerd" || "${bin}" == "ucsid" ]]; then
if [[ "${bin}" == "gpiod" || "${bin}" == "i2c-gpio-expanderd" || "${bin}" == "intel-gpiod" || "${bin}" == "i2cd" || "${bin}" == "dw-acpi-i2cd" || "${bin}" == "acpid" || "${bin}" == "thermald" || "${bin}" == "i2c-hidd" || "${bin}" == "inputd" || "${bin}" == "pcid" || "${bin}" == "pcid-spawner" || "${bin}" == "redoxerd" || "${bin}" == "ucsid" ]]; then
cp -v "target/${TARGET}/${build_type}/${bin}" "${COOKBOOK_STAGE}/usr/bin"
else
cp -v "target/${TARGET}/${build_type}/${bin}" "${COOKBOOK_STAGE}/usr/lib/drivers"
@@ -245,4 +243,4 @@ done
mkdir -pv "${COOKBOOK_STAGE}/usr/lib/init.d"
cp -v "${COOKBOOK_SOURCE}/init.d"/* "${COOKBOOK_STAGE}/usr/lib/init.d/"
"""
"""
+5
View File
@@ -34,6 +34,11 @@ patches = [
"../../../local/patches/kernel/P17-3-sched-affinity.patch",
"../../../local/patches/kernel/P17-3-syscall-dispatch.patch",
"../../../local/patches/kernel/P19-2-irq-debug.patch",
# P20: x2APIC ICR mode fix (32-bit dest field for x2APIC, 8-bit for xAPIC)
"../../../local/patches/kernel/P20-x2apic-icr-mode-fix.patch",
# P21: x2APIC SMP bring-up fix — skip 8-bit LocalApic entries when x2APIC
# is active (BSP ID mismatch causes all APs to be skipped on bare metal Intel)
"../../../local/patches/kernel/P21-x2apic-smp-fix.patch",
]
[build]
+2 -2
View File
@@ -1266,7 +1266,7 @@ pub(crate) fn fetch_apply_patches(
command.arg("--directory").arg(&staging_dir);
command.arg("--strip=1");
command.arg("--batch");
command.arg("--fuzz=0");
command.arg("--fuzz=3");
command.arg("--no-backup-if-mismatch");
run_command_stdin(command, patch_data.as_slice(), logger)
.map_err(|e| format!("patch {patch_name} FAILED: {e}"))?;
@@ -1439,7 +1439,7 @@ pub fn validate_patches(recipe: &CookRecipe, logger: &PtyOut) -> Result<()> {
command.arg("--directory").arg(&staging_dir);
command.arg("--strip=1");
command.arg("--batch");
command.arg("--fuzz=0");
command.arg("--fuzz=3");
command.arg("--no-backup-if-mismatch");
match run_command_stdin(command, patch_data.as_slice(), logger) {