feat: atomic patch application, colored init output, XKB bridge, USB HID hardening

Build system (src/cook/fetch.rs):
- Atomic patch application: applies patches to staging directory (cp -al),
  atomically swaps on success, discards on failure — source tree is never
  left in a partially-patched state
- normalize_patch(): strips diff --git/index/new-file-mode headers that the
  build system's patch command does not recognize
- cleanup_workspace_pollution(): removes orphaned recipes/Cargo.toml and
  recipes/Cargo.lock to prevent workspace conflicts
- Added --allow-protected CLI flag to repo binary

Input stack (local/patches/base/P3-*.patch):
- P3-ps2d-led-feedback: PS/2 LED state handling + InputProducer migration
- P3-inputd-keymap-bridge: InputProducer enum, keymap bridge query
- P3-usbhidd-hardening: HID descriptor validation, static lookup table,
  8-button mouse support, transfer retry with exponential backoff
- P3-init-colored-output: ANSI-color coded init daemon output (green OK,
  red FAILED, yellow SKIP/WARN)

XKB bridge (local/recipes/system/redbear-keymapd/source/src/xkb.rs):
- Parses X11 xkb/symbols/* format, maps XKB keycodes to PS/2 scancodes,
  80+ X11 keysym names to Unicode, 4-level key support

Patch governance (local/patches/base/absorbed/README.md):
- Documents consolidation of P0-P3 patches into redox.patch
This commit is contained in:
2026-05-03 08:21:54 +01:00
parent 7b48083a14
commit aca2f2913d
13 changed files with 2920 additions and 40 deletions
@@ -0,0 +1,461 @@
--- a/drivers/input/ps2d/src/controller.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/controller.rs 2026-05-02 02:50:19.907179957 +0100
@@ -97,6 +97,14 @@
const DEFAULT_TIMEOUT: u64 = 50_000;
// Reset timeout in microseconds
const RESET_TIMEOUT: u64 = 1_000_000;
+// Maximum bytes to drain during flush (Linux: I8042_BUFFER_SIZE)
+const FLUSH_LIMIT: usize = 4096;
+// Controller self-test pass value (Linux: I8042_RET_CTL_TEST)
+const SELFTEST_PASS: u8 = 0x55;
+// Controller self-test retries (Linux: 5 attempts)
+const SELFTEST_RETRIES: usize = 5;
+// AUX port test pass value (Linux returns 0x00 on success)
+const AUX_TEST_PASS: u8 = 0x00;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
pub struct Ps2 {
@@ -261,6 +269,30 @@
self.write(command as u8)
}
+ pub fn set_leds(&mut self, caps: bool, num: bool, scroll: bool) {
+ let mut led_byte = 0u8;
+ if scroll { led_byte |= 1; }
+ if num { led_byte |= 2; }
+ if caps { led_byte |= 4; }
+ if let Err(err) = self.keyboard_command_inner(0xED) {
+ warn!("ps2d: failed to send LED command 0xED: {:?}", err);
+ return;
+ }
+ match self.read_timeout(DEFAULT_TIMEOUT) {
+ Ok(0xFA) => {
+ if let Err(err) = self.write(led_byte) {
+ warn!("ps2d: failed to send LED byte {:02X}: {:?}", led_byte, err);
+ }
+ }
+ Ok(val) => {
+ warn!("ps2d: LED command ACK expected 0xFA, got {:02X}", val);
+ }
+ Err(err) => {
+ warn!("ps2d: LED command ACK timeout: {:?}", err);
+ }
+ }
+ }
+
pub fn next(&mut self) -> Option<(bool, u8)> {
let status = self.status();
if status.contains(StatusFlags::OUTPUT_FULL) {
@@ -271,6 +303,50 @@
}
}
+ /// Drain all pending bytes from the controller output buffer.
+ /// Borrowed from Linux i8042_flush(): stale firmware/BIOS bytes can be
+ /// misinterpreted as device responses during initialization.
+ fn flush(&mut self) -> usize {
+ let mut count = 0;
+ while self.status().contains(StatusFlags::OUTPUT_FULL) {
+ if count >= FLUSH_LIMIT {
+ warn!("flush: exceeded limit, controller may be stuck");
+ break;
+ }
+ let data = self.data.read();
+ trace!("flush: discarded {:02X}", data);
+ count += 1;
+ }
+ if count > 0 {
+ debug!("flushed {} stale bytes from controller", count);
+ }
+ count
+ }
+
+ /// Test the AUX (mouse) port via controller command 0xA9.
+ /// Borrowed from Linux: verifies electrical connectivity before
+ /// attempting to talk to the mouse. Returns true if the port passed.
+ fn test_aux_port(&mut self) -> bool {
+ if let Err(err) = self.command(Command::TestSecond) {
+ warn!("aux port test command failed: {:?}", err);
+ return false;
+ }
+ match self.read() {
+ Ok(AUX_TEST_PASS) => {
+ debug!("aux port test passed");
+ true
+ }
+ Ok(val) => {
+ warn!("aux port test failed: {:02X}", val);
+ false
+ }
+ Err(err) => {
+ warn!("aux port test read timeout: {:?}", err);
+ false
+ }
+ }
+ }
+
pub fn init_keyboard(&mut self) -> Result<(), Error> {
let mut b;
@@ -308,66 +384,125 @@
}
pub fn init(&mut self) -> Result<(), Error> {
+ // Linux i8042_controller_check(): verify controller is present by
+ // flushing any stale data. A stuck output buffer means no controller.
+ self.flush();
+
+ // Bare-metal controllers may be slow after firmware handoff.
+ // Give the controller a moment to finish POST before sending commands.
+ std::thread::sleep(std::time::Duration::from_millis(50));
+
{
- // Disable devices
- self.command(Command::DisableFirst)?;
- self.command(Command::DisableSecond)?;
+ // Disable both ports first — use retry because the controller
+ // may still be settling or temporarily unresponsive.
+ // Failure here is non-fatal: we continue and attempt the rest
+ // of initialization. A truly absent controller will fail later
+ // at self-test or keyboard reset.
+ if let Err(err) = self.retry(
+ format_args!("disable first port"),
+ 3,
+ |x| x.command(Command::DisableFirst),
+ ) {
+ warn!("disable first port failed: {:?}", err);
+ }
+ if let Err(err) = self.retry(
+ format_args!("disable second port"),
+ 3,
+ |x| x.command(Command::DisableSecond),
+ ) {
+ warn!("disable second port failed: {:?}", err);
+ }
}
- // Disable clocks, disable interrupts, and disable translate
+ // Flush again after disabling — firmware may have queued more bytes
+ self.flush();
+
+ // Linux i8042_controller_init() step 1: write a known-safe config
+ // (interrupts off, both ports disabled) so stale config can't cause
+ // spurious interrupts during the rest of init.
{
- // Since the default config may have interrupts enabled, and the kernel may eat up
- // our data in that case, we will write a config without reading the current one
let config = ConfigFlags::POST_PASSED
| ConfigFlags::FIRST_DISABLED
| ConfigFlags::SECOND_DISABLED;
self.set_config(config)?;
}
- // The keyboard seems to still collect bytes even when we disable
- // the port, so we must disable the keyboard too
+ // Linux i8042_controller_selftest(): retry up to 5 times with delay.
+ // "On some really fragile systems this does not take the first time."
+ {
+ let mut passed = false;
+ for attempt in 0..SELFTEST_RETRIES {
+ if let Err(err) = self.command(Command::TestController) {
+ warn!("self-test command failed (attempt {}): {:?}", attempt + 1, err);
+ continue;
+ }
+ match self.read() {
+ Ok(SELFTEST_PASS) => {
+ passed = true;
+ break;
+ }
+ Ok(val) => {
+ warn!(
+ "self-test unexpected value {:02X} (attempt {}/{})",
+ val,
+ attempt + 1,
+ SELFTEST_RETRIES
+ );
+ }
+ Err(err) => {
+ warn!("self-test read timeout (attempt {}): {:?}", attempt + 1, err);
+ }
+ }
+ // Linux: msleep(50) between retries
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+ if !passed {
+ // Linux on x86: "giving up on controller selftest, continuing anyway"
+ warn!("controller self-test did not pass after {} attempts, continuing", SELFTEST_RETRIES);
+ }
+ }
+
+ // Flush any bytes the self-test may have left behind
+ self.flush();
+
+ // Linux i8042_controller_init() step 2: set keyboard defaults
+ // (disable scanning so keyboard doesn't send scancodes during init)
self.retry(format_args!("keyboard defaults"), 4, |x| {
- // Set defaults and disable scanning
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
if b != 0xFA {
error!("keyboard failed to set defaults: {:02X}", b);
return Err(Error::CommandRetry);
}
-
Ok(b)
})?;
- {
- // Perform the self test
- self.command(Command::TestController)?;
- let r = self.read()?;
- if r != 0x55 {
- warn!("self test unexpected value: {:02X}", r);
- }
- }
-
// Initialize keyboard
if let Err(err) = self.init_keyboard() {
error!("failed to initialize keyboard: {:?}", err);
return Err(err);
}
- // Enable second device
- let enable_mouse = match self.command(Command::EnableSecond) {
- Ok(()) => true,
- Err(err) => {
- error!("failed to initialize mouse: {:?}", err);
- false
+ // Linux: test AUX port (command 0xA9) before enabling.
+ // Skips mouse init entirely if the port is not electrically present.
+ let aux_ok = self.test_aux_port();
+
+ // Enable second device (mouse) only if AUX port tested OK
+ let enable_mouse = if aux_ok {
+ match self.command(Command::EnableSecond) {
+ Ok(()) => true,
+ Err(err) => {
+ warn!("failed to enable aux port after test passed: {:?}", err);
+ false
+ }
}
+ } else {
+ info!("skipping mouse init: aux port test did not pass");
+ false
};
{
- // Enable keyboard data reporting
- // Use inner function to prevent retries
- // Response is ignored since scanning is now on
if let Err(err) = self.keyboard_command_inner(KeyboardCommand::EnableReporting as u8) {
error!("failed to initialize keyboard reporting: {:?}", err);
- //TODO: fix by using interrupts?
}
}
--- a/drivers/input/ps2d/src/state.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/state.rs 2026-05-02 04:22:27.342569199 +0100
@@ -1,4 +1,4 @@
-use inputd::ProducerHandle;
+use inputd::InputProducer;
use log::{error, warn};
use orbclient::{ButtonEvent, KeyEvent, MouseEvent, MouseRelativeEvent, ScrollEvent};
use std::{
@@ -44,7 +44,8 @@
ps2: Ps2,
vmmouse: bool,
vmmouse_relative: bool,
- input: ProducerHandle,
+ keyboard_input: InputProducer,
+ mouse_input: InputProducer,
time_file: File,
extended: bool,
mouse_x: i32,
@@ -56,12 +57,18 @@
mouse_timeout: Option<TimeSpec>,
packets: [u8; 4],
packet_i: usize,
+ caps_lock: bool,
+ num_lock: bool,
+ scroll_lock: bool,
+ leds_dirty: bool,
}
impl Ps2d {
- pub fn new(input: ProducerHandle, time_file: File) -> Self {
+ pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
let mut ps2 = Ps2::new();
- ps2.init().expect("failed to initialize");
+ if let Err(err) = ps2.init() {
+ log::error!("ps2d: controller init failed: {:?}", err);
+ }
// FIXME add an option for orbital to disable this when an app captures the mouse.
let vmmouse_relative = false;
@@ -77,7 +84,8 @@
ps2,
vmmouse,
vmmouse_relative,
- input,
+ keyboard_input,
+ mouse_input,
time_file,
extended: false,
mouse_x: 0,
@@ -89,6 +97,10 @@
mouse_timeout: None,
packets: [0; 4],
packet_i: 0,
+ caps_lock: false,
+ num_lock: true,
+ scroll_lock: false,
+ leds_dirty: true,
};
if !this.vmmouse {
@@ -96,6 +108,12 @@
this.handle_mouse(None);
}
+ // Flush initial LED state (Num Lock on by default)
+ if this.leds_dirty {
+ this.leds_dirty = false;
+ this.ps2.set_leds(this.caps_lock, this.num_lock, this.scroll_lock);
+ }
+
this
}
@@ -272,8 +290,21 @@
}
};
+ if scancode != 0 && pressed {
+ match scancode {
+ orbclient::K_CAPS => { self.caps_lock = !self.caps_lock; self.leds_dirty = true; },
+ orbclient::K_NUM => { self.num_lock = !self.num_lock; self.leds_dirty = true; },
+ orbclient::K_SCROLL => { self.scroll_lock = !self.scroll_lock; self.leds_dirty = true; },
+ _ => (),
+ }
+ }
+ if self.leds_dirty {
+ self.leds_dirty = false;
+ self.ps2.set_leds(self.caps_lock, self.num_lock, self.scroll_lock);
+ }
+
if scancode != 0 {
- self.input
+ self.keyboard_input
.write_event(
KeyEvent {
character: '\0',
@@ -304,7 +335,7 @@
if self.vmmouse_relative {
if dx != 0 || dy != 0 {
- self.input
+ self.mouse_input
.write_event(
MouseRelativeEvent {
dx: dx as i32,
@@ -320,14 +351,14 @@
if x != self.mouse_x || y != self.mouse_y {
self.mouse_x = x;
self.mouse_y = y;
- self.input
+ self.mouse_input
.write_event(MouseEvent { x, y }.to_event())
.expect("ps2d: failed to write mouse event");
}
};
if dz != 0 {
- self.input
+ self.mouse_input
.write_event(
ScrollEvent {
x: 0,
@@ -348,7 +379,7 @@
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_input
.write_event(
ButtonEvent {
left,
@@ -441,13 +472,13 @@
}
if dx != 0 || dy != 0 {
- self.input
+ self.mouse_input
.write_event(MouseRelativeEvent { dx, dy }.to_event())
.expect("ps2d: failed to write mouse event");
}
if dz != 0 {
- self.input
+ self.mouse_input
.write_event(ScrollEvent { x: 0, y: dz }.to_event())
.expect("ps2d: failed to write scroll event");
}
@@ -462,7 +493,7 @@
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_input
.write_event(
ButtonEvent {
left,
diff --git a/drivers/input/ps2d/src/main.rs b/drivers/input/ps2d/src/main.rs
index db17de2a..86f903bf 100644
--- a/drivers/input/ps2d/src/main.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/main.rs
@@ -14,4 +14,4 @@
-use inputd::ProducerHandle;
+use inputd::InputProducer;
use crate::state::Ps2d;
@@ -31,7 +31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
user_data! {
enum Source {
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
- let mut ps2d = Ps2d::new(input, time_file);
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut data = [0; 256];
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {
+31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
user_data! {
enum Source {
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
- let mut ps2d = Ps2d::new(input, time_file);
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut data = [0; 256];
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {