Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fallible-streaming-iterator = { version = "0.1.9" }
fixedbitset = { version = "^0.5", default-features = false }
gdbstub = { version = "0.7.3", default-features = false }
goblin = { version = "~0.10.2", default-features = false }
hidparser = { version = "1" }
indoc = { version = "2.0" }
lazy_static = { version = "^1" }
linked_list_allocator = { version = "^0.10" }
Expand All @@ -31,6 +32,7 @@ log = { version = "0.4", default-features = false }
memoffset = {version = "0.9.1" }
mu_rust_helpers = { version = "3.0.2" }
num-traits = { version = "0.2", default-features = false }
num_enum = { version = "0.7", default-features = false }
patina = { version = "20.1.2", path = "sdk/patina" }
patina_debugger = { version = "20.1.2", path = "core/patina_debugger" }
patina_ffs = { version = "20.1.2", path = "sdk/patina_ffs" }
Expand Down
27 changes: 27 additions & 0 deletions components/uefi_hid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
[package]
name = "uefi_hid"
version.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
description = "UEFI HID (Human Interface Device) support as a Patina component."

[lints]
workspace = true

[features]
default = ["ctrl-alt-del"]
ctrl-alt-del = []

[dependencies]
hidparser = { workspace = true }
log = { workspace = true }
num_enum = { workspace = true }
patina = { workspace = true }
r-efi = { workspace = true }
scroll = { workspace = true }

[dev-dependencies]
mockall = { workspace = true }
patina = { workspace = true, features = ["mockall"] }
98 changes: 98 additions & 0 deletions components/uefi_hid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
# UEFI HID

## Overview

This Patina component provides Human Interface Device (HID) support for UEFI. It consumes the
[HidIo](https://github.com/microsoft/mu_plus/blob/release/202502/HidPkg/Include/Protocol/HidIo.h) protocol and
produces standard UEFI input protocols for keyboard and pointer HID devices:

- **SimpleTextInput** (`EFI_SIMPLE_TEXT_INPUT_PROTOCOL`)
- **SimpleTextInputEx** (`EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL`)
- **AbsolutePointer** (`EFI_ABSOLUTE_POINTER_PROTOCOL`)

## Architecture

The component installs a UEFI Driver Binding that manages HID device instances. When the driver is
started on a controller that exposes the HidIo protocol, it:

1. Opens the HidIo protocol on the controller.
2. Parses the HID report descriptor to identify keyboard and pointer usages.
3. Creates the appropriate input protocol handlers (keyboard and/or pointer).
4. Installs the corresponding UEFI input protocols on the controller handle:
- **SimpleTextInput** and **SimpleTextInputEx** for keyboard devices.
- **AbsolutePointer** for pointer and touch devices.
5. Registers a report callback to receive asynchronous HID input reports.

### Report Processing

Incoming HID reports are buffered through a `ReportQueue` rather than being processed inline from the
HidIo producer's callback. This ensures all report processing occurs at a consistent `TPL_CALLBACK`
regardless of the producer's calling TPL:

1. **Report callback** (any TPL): pushes raw HID report bytes onto a queue and signals a `TPL_CALLBACK` event.
2. **Event handler** (`TPL_CALLBACK`): dequeues all pending reports and dispatches them to receivers.

## Modules

| Module | Description |
|---|---|
| `hid` | Driver binding implementation that manages HID instances on controllers. |
| `hid_io` | HidIo protocol FFI bindings, report queue, and receiver traits (`HidIo`, `HidReportReceiver`). |
| `keyboard` | Keyboard HID handler — translates HID key reports into UEFI keystrokes using HII keyboard layouts, and produces SimpleTextInput / SimpleTextInputEx protocol interfaces. |
| `pointer` | Pointer HID handler — translates HID pointer/touch reports into absolute pointer state and produces the AbsolutePointer protocol interface. |

## Features

| Feature | Default | Description |
|---|---|---|
| `ctrl-alt-del` | ✅ | Enables Ctrl+Alt+Delete to trigger a system reset via UEFI Runtime Services. |

## Dependencies

Key crate dependencies (see `Cargo.toml` for the full list):

- [`hidparser`](https://crates.io/crates/hidparser) — HID report descriptor parsing.
- [`patina`](https://crates.io/crates/patina) — Patina component SDK (boot services, driver binding, protocol interfaces).
- [`r-efi`](https://crates.io/crates/r-efi) — Rust UEFI type definitions.

## Platform Integration

To include `uefi_hid` in a Patina binary, add the crate as a dependency and register the component
in the platform's `ComponentInfo` implementation.

1. Add the dependency to the binary crate's `Cargo.toml`:

```toml
[dependencies]
uefi_hid = { version = "20" }
```

2. Register the component in the `components` function:

```rust
impl ComponentInfo for MyPlatform {
fn components(mut add: Add<Component>) {
// ...other components...
add.component(uefi_hid::UefiHidComponent);
}
}
```

The driver binding will automatically attach to any controller that exposes the HidIo protocol. A
HidIo producer (e.g. a USB HID driver) must be present in the platform firmware for this component
to be functional.

The `ctrl-alt-del` feature is enabled by default. To disable it:

```toml
uefi_hid = { path = "../components/uefi_hid", default-features = false }
```

## Testing

Unit tests use `mockall` and `patina`'s mock boot services:

```sh
cargo test -p uefi_hid
```
203 changes: 203 additions & 0 deletions components/uefi_hid/src/hid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! HID driver binding implementation.
//!
//! The [`HidDriver`] implements [`patina::driver_binding::DriverBinding`] to
//! manage HID instances on controllers that support the HidIo protocol.
//!
//! When started on a controller, it creates a [`crate::hid_io::UefiHidIo`]
//! instance, instantiates keyboard and pointer receivers, initializes them
//! via `&dyn HidIo`, and starts report reception through the device.
//!
//! ## License
//!
//! Copyright (c) Microsoft Corporation. All rights reserved.
//!

use alloc::{boxed::Box, vec::Vec};
use core::{ffi::c_void, ptr::NonNull};

use r_efi::{efi, protocols::device_path::Protocol as EfiDevicePathProtocol};

use patina::{
BinaryGuid, boot_services::BootServices, driver_binding::DriverBinding, uefi_protocol::ProtocolInterface,
};

use crate::{
hid_io::{HidReportReceiver, ReceiverFactory, UefiHidIo},
keyboard::KeyboardHidHandler,
pointer::PointerHidHandler,
};

/// Per-controller context installed as a private protocol to track the HID instance.
struct HidInstance<T: BootServices + Clone + 'static> {
_device: UefiHidIo<T>,
}

// SAFETY: HidInstance GUID uniquely identifies this private protocol.
unsafe impl<T: BootServices + Clone + 'static> ProtocolInterface for HidInstance<T> {
const PROTOCOL_GUID: BinaryGuid = BinaryGuid::from_string("0a87cfdb-c482-48e4-ade7-d9f99620e169");
}

impl<T: BootServices + Clone + 'static> HidInstance<T> {
// Creates a new HidInstance wrapping the given device.
fn new(device: UefiHidIo<T>) -> Self {
Self { _device: device }
}
}

/// HID driver that implements [`DriverBinding`].
///
/// Creates and manages HID instances on controllers that expose the HidIo
/// protocol. Directly constructs [`UefiHidIo`] devices and creates keyboard
/// and pointer handlers as report receivers.
pub struct HidDriver<T: BootServices + Clone + 'static> {
boot_services: &'static T,
agent: efi::Handle,
}

impl<T: BootServices + Clone + 'static> HidDriver<T> {
/// Creates a new HID driver bound to the given agent handle.
///
/// `agent` is the image handle for this driver, used for protocol operations.
pub fn new(boot_services: &'static T, agent: efi::Handle) -> Self {
Self { boot_services, agent }
}

// Creates factory functions for HID report receivers.
fn new_receiver_factories(&self) -> Vec<ReceiverFactory> {
let bs = self.boot_services;
alloc::vec![
Box::new(move |controller, hid_io| {
Ok(PointerHidHandler::new(bs, controller, hid_io)? as Box<dyn HidReportReceiver>)
}),
Box::new(move |controller, hid_io| {
Ok(KeyboardHidHandler::new(bs, controller, hid_io)? as Box<dyn HidReportReceiver>)
}),
]
}
}

// controller is an efi::Handle (raw pointer) from the DriverBinding trait. efi::Handle is defined as *mut c_void, but
// essentially an opaque type that happens to be a pointer. The unsafe deref warning will be resolved once latest
// r_efi with unsafe API is integrated.
#[allow(clippy::not_unsafe_ptr_arg_deref)]
// This is a wrapper trait to abstract driver binding for FFI; core logic is all tested elsewhere.
#[coverage(off)]
impl<T: BootServices + Clone + 'static> DriverBinding for HidDriver<T> {
/// Tests if the given controller supports the HidIo protocol.
fn driver_binding_supported<U: BootServices + 'static>(
&self,
_boot_services: &'static U,
controller: efi::Handle,
_remaining_device_path: Option<NonNull<EfiDevicePathProtocol>>,
) -> Result<bool, efi::Status> {
Ok(UefiHidIo::supports(self.boot_services, self.agent, controller))
}

/// Starts HID support for the given controller.
///
/// Creates a UefiHidIo device with keyboard and pointer receivers, and
/// installs a private protocol to track the instance context.
fn driver_binding_start<U: BootServices + 'static>(
&mut self,
boot_services: &'static U,
controller: efi::Handle,
_remaining_device_path: Option<NonNull<EfiDevicePathProtocol>>,
) -> Result<(), efi::Status> {
log::trace!("driver_binding_start: starting HID on controller {:?}", controller);
let device = UefiHidIo::new(self.boot_services, self.agent, controller, self.new_receiver_factories())?;

let hid_instance = Box::new(HidInstance::new(device));
boot_services.install_protocol_interface(Some(controller), hid_instance)?;

Ok(())
}

/// Stops HID support for the given controller.
///
/// Retrieves and drops the HID instance context, reclaiming all resources.
fn driver_binding_stop<U: BootServices + 'static>(
&mut self,
boot_services: &'static U,
controller: efi::Handle,
_number_of_children: usize,
_child_handle_buffer: Option<NonNull<efi::Handle>>,
) -> Result<(), efi::Status> {
log::trace!("driver_binding_stop: stopping HID on controller {:?}", controller);
// SAFETY: The private protocol was installed on this controller by start.
let hid_instance = unsafe {
boot_services.open_protocol_unchecked(
controller,
&HidInstance::<T>::PROTOCOL_GUID,
self.agent,
controller,
efi::OPEN_PROTOCOL_GET_PROTOCOL,
)
}? as *mut HidInstance<T>;

// SAFETY: Uninstalling our private protocol interface.
if let Err(status) = unsafe {
boot_services.uninstall_protocol_interface_unchecked(
controller,
&HidInstance::<T>::PROTOCOL_GUID,
hid_instance as *mut c_void,
)
} {
log::error!("hid::driver_binding_stop: failed to uninstall protocol: {status:x?}");
return Err(status);
}

// SAFETY: hid_instance was created via Box::into_raw (through install_protocol_interface) in start.
drop(unsafe { Box::from_raw(hid_instance) });
Ok(())
}
}

#[cfg(test)]
mod test {
use super::HidDriver;
use crate::hid_io::protocol::HidIoProtocol;
use patina::{boot_services::MockBootServices, driver_binding::DriverBinding};
use r_efi::efi;

fn mock_boot_services() -> &'static mut MockBootServices {
let mut mock = MockBootServices::new();
mock.expect_raise_tpl().returning(|_| patina::boot_services::tpl::Tpl::APPLICATION);
mock.expect_restore_tpl().returning(|_| ());
// SAFETY: Leaked mock for test use with 'static lifetime requirement.
unsafe { Box::into_raw(Box::new(mock)).as_mut().unwrap() }
}

#[test]
fn supported_returns_true_when_hid_io_present() {
let boot_services = mock_boot_services();
boot_services
.expect_open_protocol::<HidIoProtocol>()
.withf_st(|controller, _, _, _| *controller == 0x3 as efi::Handle)
.returning(|_, _, _, _| {
// SAFETY: supports() never dereferences the protocol; zeroed is fine.
Ok(HidIoProtocol::stub())
});
boot_services.expect_open_protocol::<HidIoProtocol>().returning(|_, _, _, _| Err(efi::Status::NOT_FOUND));

let hid_driver = HidDriver::new(boot_services, 0x1 as efi::Handle);

assert_eq!(hid_driver.driver_binding_supported(boot_services, 0x2 as efi::Handle, None), Ok(false));
assert_eq!(hid_driver.driver_binding_supported(boot_services, 0x3 as efi::Handle, None), Ok(true));
}

#[test]
fn start_returns_unsupported_with_no_receivers() {
let boot_services = mock_boot_services();
// open_protocol succeeds (protocol exists) but receiver factories fail → UNSUPPORTED.
boot_services.expect_open_protocol::<HidIoProtocol>().returning(|_, _, _, _| Ok(HidIoProtocol::stub()));
boot_services.expect_close_protocol().returning(|_, _, _, _| Ok(()));

let mut hid_driver = HidDriver::new(boot_services, 0x1 as efi::Handle);

// All receiver factories fail (stub protocol has no real descriptor) → UNSUPPORTED.
assert_eq!(
hid_driver.driver_binding_start(boot_services, 0x2 as efi::Handle, None),
Err(efi::Status::UNSUPPORTED)
);
}
}
Loading
Loading