feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)
Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\ objects (openable by any sibling LocalService) to UNNAMED sections/events whose handles the host DuplicateHandles into the driver's verified WUDFHost with least access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL, pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded; HID minidrivers have no control device). Driver-validated pad_index kills cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides. Sibling-LocalService denial proven empirically (design/idd-push-security.md, design/gamepad-channel-sealing.md). Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI. driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created SwDevice REVIVES the old devnode with its previously-bound driver (never re-ranks), so an upgrade otherwise leaves the old driver serving — or, across the v1→v2 fence, a dead pad (found live on the RTX box). On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via both the test harness and a real streaming session; phantom-sweep repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
//! The sealed pad channel, driver side (`design/gamepad-channel-sealing.md`, gamepad proto v2):
|
||||
//! poll the named bootstrap mailbox by index, publish our pid (iff the host's proto version
|
||||
//! matches), adopt the host-delivered DATA-section handle, and validate the mapped section's magic
|
||||
//! and `pad_index` before use. One implementation shared by `pf-xusb` and `pf-dualsense` (they used
|
||||
//! to hand-duplicate it), parameterized by [`ChannelConfig`].
|
||||
//!
|
||||
//! This module **forbids `unsafe`**: the entire state machine is safe Rust over
|
||||
//! [`section`](crate::section)'s checked accessors — the memory-safety surface of the sealed
|
||||
//! channel lives in that module alone.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::section::{MappedView, ViewCell, close_handle_value};
|
||||
use core::mem::offset_of;
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use pf_driver_proto::gamepad::{BOOT_MAGIC, GAMEPAD_PROTO_VERSION, PadBootstrap};
|
||||
|
||||
// PadBootstrap field offsets (the mailbox handshake; pinned by pf_driver_proto's asserts).
|
||||
const BOOT_OFF_MAGIC: usize = offset_of!(PadBootstrap, magic);
|
||||
const BOOT_OFF_HOST_PROTO: usize = offset_of!(PadBootstrap, host_proto);
|
||||
const BOOT_OFF_DRIVER_PID: usize = offset_of!(PadBootstrap, driver_pid);
|
||||
const BOOT_OFF_DRIVER_PROTO: usize = offset_of!(PadBootstrap, driver_proto);
|
||||
const BOOT_OFF_DATA_HANDLE: usize = offset_of!(PadBootstrap, data_handle);
|
||||
const BOOT_OFF_HANDLE_PID: usize = offset_of!(PadBootstrap, handle_pid);
|
||||
const BOOT_OFF_HANDLE_SEQ: usize = offset_of!(PadBootstrap, handle_seq);
|
||||
const BOOT_SIZE: usize = core::mem::size_of::<PadBootstrap>();
|
||||
|
||||
/// What varies between the two pad drivers.
|
||||
pub struct ChannelConfig {
|
||||
/// Log-line prefix (`"pf-xusb"` / `"pf-ds"`).
|
||||
pub tag: &'static str,
|
||||
/// Mailbox name prefix, completed with the pad index (`"Global\\pfxusb-boot-"` / `"Global\\pfds-boot-"`).
|
||||
pub boot_name_prefix: &'static str,
|
||||
/// The DATA section's magic (`XUSB_MAGIC` / `PAD_MAGIC`).
|
||||
pub data_magic: u32,
|
||||
/// The DATA section's size (`size_of::<XusbShm>()` / `size_of::<PadShm>()`).
|
||||
pub data_size: usize,
|
||||
/// `offset_of!(…Shm, pad_index)` in the DATA section.
|
||||
pub pad_index_off: usize,
|
||||
/// The driver's logger (each driver tees to its own debug file).
|
||||
pub log: fn(&str),
|
||||
}
|
||||
|
||||
/// Per-pad channel state (a `static` in each driver — per-pad because
|
||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` gives each pad its own WUDFHost).
|
||||
pub struct ChannelClient {
|
||||
/// The pad index from the devnode Location (which mailbox to poll + the `pad_index` the
|
||||
/// delivered DATA section must carry).
|
||||
index: AtomicU32,
|
||||
/// The adopted DATA view; leaked-on-publish (see [`ViewCell`]) so a re-delivery can never
|
||||
/// unmap a view a concurrent callback still reads through.
|
||||
data: ViewCell,
|
||||
/// The last `handle_seq` consumed (CAS-guarded so concurrent pumps adopt a delivery exactly
|
||||
/// once). Reset to 0 when the mailbox disappears, so a NEW host session's delivery is always
|
||||
/// fresh even if its (per-host-process) seq counter collides with the previous session's.
|
||||
consumed_seq: AtomicU32,
|
||||
logged_proto_mismatch: AtomicBool,
|
||||
logged_pid: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for ChannelClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
pub const fn new() -> ChannelClient {
|
||||
ChannelClient {
|
||||
index: AtomicU32::new(0),
|
||||
data: ViewCell::new(),
|
||||
consumed_seq: AtomicU32::new(0),
|
||||
logged_proto_mismatch: AtomicBool::new(false),
|
||||
logged_pid: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the pad index (from the devnode Location, in `EvtDeviceAdd`).
|
||||
pub fn set_index(&self, idx: u32) {
|
||||
self.index.store(idx, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The adopted DATA view regardless of mailbox liveness — for write paths where acting on a
|
||||
/// stale section is harmless (the pump owns the detach semantics).
|
||||
pub fn data(&self) -> Option<&'static MappedView> {
|
||||
self.data.get()
|
||||
}
|
||||
|
||||
/// One tick of the sealed-channel state machine: publish our pid (+ proto version) in the
|
||||
/// mailbox, adopt a delivered DATA handle, and return the attached DATA view — `None` while
|
||||
/// unattached, on a host/driver version mismatch (fail closed), or when the mailbox is gone
|
||||
/// (host gone). The mailbox is re-opened by name on every call: the name existing doubles as
|
||||
/// host-liveness (the host closes it when the pad is torn down).
|
||||
pub fn pump(&self, cfg: &ChannelConfig) -> Option<&'static MappedView> {
|
||||
let name = format!("{}{}", cfg.boot_name_prefix, self.index());
|
||||
let boot = match MappedView::open_named(&name, BOOT_SIZE) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
// Mailbox gone → the host (or this pad) is gone. Forget the consumed seq so the
|
||||
// NEXT host session's first delivery always reads as fresh.
|
||||
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Acquire pairs with the host's Release magic store, so a valid magic implies `host_proto`
|
||||
// is visible. A missing/garbled magic reads as "no usable mailbox" (same as absent).
|
||||
if boot.load_u32(BOOT_OFF_MAGIC, Ordering::Acquire) != BOOT_MAGIC {
|
||||
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
// Publish our proto version first (idempotent) — the host logs a mismatch even when we
|
||||
// refuse to publish a pid below.
|
||||
boot.store_u32(
|
||||
BOOT_OFF_DRIVER_PROTO,
|
||||
GAMEPAD_PROTO_VERSION,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
let host_proto = boot.load_u32(BOOT_OFF_HOST_PROTO, Ordering::Relaxed);
|
||||
if host_proto != GAMEPAD_PROTO_VERSION {
|
||||
if !self.logged_proto_mismatch.swap(true, Ordering::Relaxed) {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] host proto {host_proto} != driver proto {GAMEPAD_PROTO_VERSION} — \
|
||||
refusing the handshake (update host + drivers together)",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
return None; // version mismatch — fail closed
|
||||
}
|
||||
let mypid = std::process::id();
|
||||
if boot.load_u32(BOOT_OFF_DRIVER_PID, Ordering::Relaxed) != mypid {
|
||||
boot.store_u32(BOOT_OFF_DRIVER_PID, mypid, Ordering::Release);
|
||||
if !self.logged_pid.swap(true, Ordering::Relaxed) {
|
||||
(cfg.log)(&format!("[{}] bootstrap: published pid {mypid}", cfg.tag));
|
||||
}
|
||||
}
|
||||
// A delivery addressed to us we haven't consumed? CAS so concurrent pumps (worker thread /
|
||||
// timer + IOCTL paths) adopt exactly once.
|
||||
let seq = boot.load_u32(BOOT_OFF_HANDLE_SEQ, Ordering::Acquire);
|
||||
let cur = self.consumed_seq.load(Ordering::Relaxed);
|
||||
if seq != 0
|
||||
&& seq != cur
|
||||
&& boot.load_u32(BOOT_OFF_HANDLE_PID, Ordering::Relaxed) == mypid
|
||||
&& self
|
||||
.consumed_seq
|
||||
.compare_exchange(cur, seq, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
self.adopt(cfg, boot.load_u64(BOOT_OFF_DATA_HANDLE, Ordering::Relaxed));
|
||||
}
|
||||
self.data()
|
||||
}
|
||||
|
||||
/// Map + validate a delivered DATA-section handle VALUE (untrusted until the mapped section
|
||||
/// carries our magic AND our pad index). On success we own the handle (adopt-on-success) and
|
||||
/// close it — the view keeps the section alive. On validation failure the handle is
|
||||
/// deliberately NOT closed: a tampered value could name an unrelated handle in our own table.
|
||||
fn adopt(&self, cfg: &ChannelConfig, value: u64) {
|
||||
let Some(view) = MappedView::from_handle_value(value, cfg.data_size) else {
|
||||
if value != 0 {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] delivered DATA handle 0x{value:x} did not map — ignoring",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
return;
|
||||
};
|
||||
let magic = view.load_u32(0, Ordering::Relaxed);
|
||||
let idx = view.load_u32(cfg.pad_index_off, Ordering::Relaxed);
|
||||
let want = self.index();
|
||||
if magic != cfg.data_magic || idx != want {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] delivered DATA section failed validation (magic 0x{magic:08x}, pad_index \
|
||||
{idx}, want {want}) — ignoring",
|
||||
cfg.tag
|
||||
));
|
||||
// `view` drops here → unmapped; the handle stays open (see above).
|
||||
return;
|
||||
}
|
||||
// The value resolved to OUR pad's section, so it is the handle the host duplicated for us —
|
||||
// we own it; the (about-to-be-leaked) view keeps the section alive after the close.
|
||||
close_handle_value(value);
|
||||
self.data.set(view);
|
||||
(cfg.log)(&format!(
|
||||
"[{}] sealed pad channel mapped (index {want})",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user