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,208 @@
|
||||
//! Safe(ly-contracted) helpers over the WDF request/memory/property DDIs the pad drivers use. The
|
||||
//! pattern: a framework callback converts its raw `WDFREQUEST` into a [`Request`] token **once**
|
||||
//! (`unsafe`, the framework's validity guarantee is the contract); every operation after that is a
|
||||
//! safe method, and completion consumes the token so a request cannot be completed twice or used
|
||||
//! after completion from safe code.
|
||||
|
||||
use wdk_sys::{
|
||||
NTSTATUS, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
||||
call_unsafe_wdf_function_binding,
|
||||
};
|
||||
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
||||
/// wdk_sys root; the value is stable WDM).
|
||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
|
||||
/// A validity token for one framework-delivered `WDFREQUEST`. Not `Copy`/`Clone`: completing or
|
||||
/// forwarding consumes it, so safe code cannot touch a request the framework already owns again.
|
||||
pub struct Request(WDFREQUEST);
|
||||
|
||||
impl Request {
|
||||
/// Wrap the raw request handed to the current framework callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `raw` must be the live, framework-provided `WDFREQUEST` of the callback invocation this is
|
||||
/// called from (WDF owns handle validity; a forged/dangling handle is framework UB).
|
||||
pub unsafe fn new(raw: WDFREQUEST) -> Request {
|
||||
Request(raw)
|
||||
}
|
||||
|
||||
/// Complete the request with `status` (consumes the token — the framework owns it afterwards).
|
||||
pub fn complete(self, status: NTSTATUS) {
|
||||
// SAFETY: `self.0` is the live callback request per `Request::new`'s contract, not yet
|
||||
// completed or forwarded (both consume the token).
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, self.0, status) };
|
||||
}
|
||||
|
||||
/// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
||||
/// Returns the status to complete with (`STATUS_INVALID_BUFFER_SIZE` if the buffer is short).
|
||||
pub fn copy_to_output(&self, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; `mem` receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: `mem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: `mem` is valid and at least `src.len()` bytes; `src` is a live borrow.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut core::ffi::c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: `self.0` is the live callback request.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, src.len() as u64)
|
||||
};
|
||||
0 // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
/// The request's input buffer: up to `cap` bytes copied out, plus the buffer's TRUE length.
|
||||
/// `Err(status)` if the input memory can't be retrieved (propagate as the completion status).
|
||||
pub fn input_bytes(&self, cap: usize) -> Result<(Vec<u8>, usize), NTSTATUS> {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; `inmem` receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, self.0, &mut inmem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return Err(st);
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: `inmem` is the valid memory object just retrieved; `len` receives its size.
|
||||
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
||||
as *const u8;
|
||||
if p.is_null() {
|
||||
return Ok((Vec::new(), 0));
|
||||
}
|
||||
let n = len.min(cap);
|
||||
// SAFETY: `p` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `n <= len`.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(p, n) }.to_vec();
|
||||
Ok((bytes, len))
|
||||
}
|
||||
|
||||
/// The request's output-buffer LENGTH (0 if unavailable) — UMDF HID marshalling carries the
|
||||
/// output-report id in it.
|
||||
pub fn output_buffer_len(&self) -> usize {
|
||||
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; output memory is optional here.
|
||||
if !nt_success(unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut outmem)
|
||||
}) {
|
||||
return 0;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: `outmem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||
let _ =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
||||
outlen
|
||||
}
|
||||
|
||||
/// Set the completed-bytes information field (for paths that complete with a length but no
|
||||
/// output copy, e.g. echoing an output report's length).
|
||||
pub fn set_information(&self, info: u64) {
|
||||
// SAFETY: `self.0` is the live callback request.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, info) };
|
||||
}
|
||||
|
||||
/// Forward the request to a manual queue. On success the framework owns it (the token is
|
||||
/// consumed by value — the caller cannot touch the request again); on failure the token is
|
||||
/// handed back with the status so the caller completes it. (`Request` has no `Drop`, so the
|
||||
/// consumed-on-success token simply falls out of scope — nothing to run.)
|
||||
///
|
||||
/// # Safety
|
||||
/// `queue` must be a live manual `WDFQUEUE` of the same device (e.g. the one created in
|
||||
/// `EvtDeviceAdd` and stashed in a static).
|
||||
pub unsafe fn forward_to_queue(self, queue: WDFQUEUE) -> Result<(), (Request, NTSTATUS)> {
|
||||
// SAFETY: `self.0` is the live callback request; `queue` is live per this fn's contract.
|
||||
let st =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, self.0, queue) };
|
||||
if nt_success(st) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((self, st))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop the next pended request off a manual queue (`None` when empty).
|
||||
///
|
||||
/// # Safety
|
||||
/// `queue` must be a live manual `WDFQUEUE` (e.g. the timer's parent object).
|
||||
pub unsafe fn retrieve_next_request(queue: WDFQUEUE) -> Option<Request> {
|
||||
let mut request: WDFREQUEST = core::ptr::null_mut();
|
||||
// SAFETY: `queue` is live per this fn's contract; `request` receives the next pended request.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
||||
};
|
||||
// SAFETY: on success `request` is a live framework request this caller now services — the
|
||||
// exact contract `Request::new` requires.
|
||||
nt_success(st).then(|| unsafe { Request::new(request) })
|
||||
}
|
||||
|
||||
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), a
|
||||
/// NUL-terminated UTF-16 decimal string. Defaults to 0 (single-pad) if absent. (The WDFMEMORY is
|
||||
/// device-parented and freed by the framework at device teardown — one small alloc per device add.)
|
||||
///
|
||||
/// # Safety
|
||||
/// `device` must be the live `WDFDEVICE` created in the current `EvtDeviceAdd`.
|
||||
pub unsafe fn query_location_index(device: WDFDEVICE) -> u32 {
|
||||
let mut mem: wdk_sys::WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `device` is live per this fn's contract; property = LocationInformation; pool ignored
|
||||
// in UMDF; `mem` receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceAllocAndQueryProperty,
|
||||
device,
|
||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
||||
0,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut mem
|
||||
)
|
||||
};
|
||||
if !nt_success(st) || mem.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: `mem` is the valid memory object just allocated; `len` receives its size.
|
||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
||||
as *const u16;
|
||||
if buf.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let units = (len / 2).min(8);
|
||||
// SAFETY: `buf` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `units * 2 <= len`.
|
||||
let chars = unsafe { core::slice::from_raw_parts(buf, units) };
|
||||
let mut idx: u32 = 0;
|
||||
let mut any = false;
|
||||
for &c in chars {
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
if (0x30..=0x39).contains(&c) {
|
||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any { idx } else { 0 }
|
||||
}
|
||||
Reference in New Issue
Block a user