diff --git a/AGENTS.md b/AGENTS.md index 9d3cad69b3..97a30ae0f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//source +git diff -U0 -w > ../../../local/patches//P-.patch +``` + +**Apply:** +```bash +patch -p1 --fuzz=3 < local/patches//P-.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-.patch` with sequential numbering +- Always wire patches into `recipe.toml` `patches = [...]` in application order +- Always validate with `repo validate-patches ` 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 diff --git a/config/redbear-device-services.toml b/config/redbear-device-services.toml index 2693fc5dc1..c5c6d0912e 100644 --- a/config/redbear-device-services.toml +++ b/config/redbear-device-services.toml @@ -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 = """ diff --git a/config/redbear-mini.toml b/config/redbear-mini.toml index f86588909a..328b1b4425 100644 --- a/config/redbear-mini.toml +++ b/config/redbear-mini.toml @@ -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", ] """ diff --git a/local/docs/SUBSYSTEM-ASSESSMENT-2026-05.md b/local/docs/SUBSYSTEM-ASSESSMENT-2026-05.md new file mode 100644 index 0000000000..497287ef59 --- /dev/null +++ b/local/docs/SUBSYSTEM-ASSESSMENT-2026-05.md @@ -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 (1–2 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:` 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 (3–5 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 (2–4 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 (4–8 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 (4–8 weeks) + +After Phases 2–4 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.3–4.6 + │ + └──→ Phase 5 (advanced) ──→ depends on Phases 2, 3, 4 +``` + +### Effort Estimate (2 developers) + +| Phase | Duration | Parallelizable? | +|-------|----------|-----------------| +| Phase 1 | 1–2 weeks | Yes (with Phase 2 start) | +| Phase 2 | 3–5 weeks | Partially (2.1 blocks 2.2–2.7) | +| Phase 3 | 2–4 weeks | Yes (parallel with Phase 2) | +| Phase 4 | 4–8 weeks | Partially (4.1 gates rest) | +| Phase 5 | 4–8 weeks | After Phases 2–4 | +| **Total** | **14–27 weeks** | ~8–14 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 \ No newline at end of file diff --git a/local/patches/base/P18-1-daemon-restart.patch b/local/patches/base/P18-1-daemon-restart.patch index 1bf7af8877..4fe19c00e7 100644 --- a/local/patches/base/P18-1-daemon-restart.patch +++ b/local/patches/base/P18-1-daemon-restart.patch @@ -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, - completed: BTreeSet, -+ /// Maps child PID → service state for supervised services. -+ pub supervised: BTreeMap, - } - - 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) { - 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 (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) { diff --git a/local/patches/base/P18-9-msi-allocation-resilience.patch b/local/patches/base/P18-9-msi-allocation-resilience.patch index c1a932a3e1..f83f3801a0 100644 --- a/local/patches/base/P18-9-msi-allocation-resilience.patch +++ b/local/patches/base/P18-9-msi-allocation-resilience.patch @@ -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 = 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 (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 { - 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, 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, Interru +- (Some(interrupt_handle), InterruptMethod::Msi) +- }; +- +@@ -90,5 +88,16 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option, 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 { +- 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, diff --git a/local/patches/base/P19-acpid-startup-hardening.patch b/local/patches/base/P19-acpid-startup-hardening.patch new file mode 100644 index 0000000000..a7a68dbd37 --- /dev/null +++ b/local/patches/base/P19-acpid-startup-hardening.patch @@ -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::()]>::try_from(chunk).unwrap()) ++ .filter_map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).ok()) +@@ -63 +73 @@ +- .map(|chunk| <[u8; mem::size_of::()]>::try_from(chunk).unwrap()) ++ .filter_map(|chunk| <[u8; mem::size_of::()]>::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::()); ++ if pages.len() < mem::size_of::() { ++ return Err(TablePhysLoadError::Validity(InvalidSdtError::InvalidSize)); ++ } +@@ -174,2 +179,5 @@ +- let sdt = plain::from_bytes::(&sdt_mem[..mem::size_of::()]) +- .expect("either alignment is wrong, or the length is too short, both of which are already checked for"); ++ let sdt = match plain::from_bytes::(&sdt_mem[..mem::size_of::()]) { ++ 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::(&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::(&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, diff --git a/local/patches/base/P19-init-startup-hardening.patch b/local/patches/base/P19-init-startup-hardening.patch new file mode 100644 index 0000000000..48509f0a8b --- /dev/null +++ b/local/patches/base/P19-init-startup-hardening.patch @@ -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()); ++ } diff --git a/local/patches/base/P4-acpi-estale-graceful.patch b/local/patches/base/P4-acpi-estale-graceful.patch new file mode 100644 index 0000000000..7632ef51f0 --- /dev/null +++ b/local/patches/base/P4-acpi-estale-graceful.patch @@ -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> { +- 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> { +- 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 { diff --git a/local/patches/base/P4-hwd-estale-graceful.patch b/local/patches/base/P4-hwd-estale-graceful.patch new file mode 100644 index 0000000000..a7ec59f49f --- /dev/null +++ b/local/patches/base/P4-hwd-estale-graceful.patch @@ -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> { + // 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", diff --git a/local/patches/base/P4-ucsid-estale-graceful.patch b/local/patches/base/P4-ucsid-estale-graceful.patch new file mode 100644 index 0000000000..8beddb963e --- /dev/null +++ b/local/patches/base/P4-ucsid-estale-graceful.patch @@ -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"), + }; + diff --git a/local/patches/base/P5-acpid-dmi-endpoint.patch b/local/patches/base/P5-acpid-dmi-endpoint.patch new file mode 100644 index 0000000000..996f2f91bc --- /dev/null +++ b/local/patches/base/P5-acpid-dmi-endpoint.patch @@ -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, ++ pub board_vendor: Option, ++ pub board_name: Option, ++ pub board_version: Option, ++ pub product_name: Option, ++ pub product_version: Option, ++ pub bios_version: Option, ++} ++ ++impl 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 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 { ++ // 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 { ++ // 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 { ++ // 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 { ++ 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 { ++ pub fn map(page: usize, page_count: usize) -> std::io::Result { + 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, + page_cache: Arc>, + aml_region_handlers: Vec<(RegionSpace, Box)>, ++ pci_fd: Arc>>, + } + + impl AmlSymbols { +- pub fn new(aml_region_handlers: Vec<(RegionSpace, Box)>) -> Self { ++ pub fn new(aml_region_handlers: Vec<(RegionSpace, Box)>, pci_fd: Arc>>) -> 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> { ++ pub fn init(&mut self) -> Result<(), Box> { + 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 (0xE0000–0xFFFFF) 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, 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, + ++ pub pci_fd: Arc>>, ++ + // 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, + ) -> Result { + 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::>(); + ++ 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 { +- 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, 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>, +- pci_fd: Option, + socket: &'sock Socket, ++ dmi_text: Option, + } + + 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 { +@@ -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) -> 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) diff --git a/local/patches/base/P5-i2c-hidd-estale-retry.patch b/local/patches/base/P5-i2c-hidd-estale-retry.patch new file mode 100644 index 0000000000..6ae98c4cbe --- /dev/null +++ b/local/patches/base/P5-i2c-hidd-estale-retry.patch @@ -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> { ++ 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> { ++ 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> { ++ 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 { ++ 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 = ++ 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 { ++ 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::::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::(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 { ++ 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 { ++ 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('/', ".") ++} diff --git a/local/patches/base/P6-init-requires-hard-dep.patch b/local/patches/base/P6-init-requires-hard-dep.patch new file mode 100644 index 0000000000..4ff849fe2c --- /dev/null +++ b/local/patches/base/P6-init-requires-hard-dep.patch @@ -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, + completed: BTreeSet, ++ /// Maps child PID → service state for supervised services. ++ pub supervised: BTreeMap, + } + + 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) { + 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 (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, pub Vec); ++pub struct Script(pub Vec, pub Vec, pub Vec); + + impl Script { + pub fn from_str(config: &str, errors: &mut Vec) -> io::Result