Files
punktfunk/packaging/windows/drivers/pf-umdf-util/src/wdf.rs
T
enricobuehler 00c29f82f2 fix(gamepad/windows): a pad enumerates as the controller it IS, not always a DualSense
A Steam Deck client streaming to a Windows host had a stuck stick and a stuck
d-pad. The virtual pad was enumerating with the DualSense VID/PID **and the
DualSense report descriptor**, so Windows parsed the 64-byte Deck frame as
DualSense report 0x01: LX = report[1] = 0x00 (stick hard left), LY = report[2] =
0x09 (hard up), and a d-pad hat of 0 — which is UP, held forever.

The driver picked its identity from `device_type` in the sealed section, but
hidclass asks for the descriptors and attributes while it STARTS the device, and
the section can only be delivered over the HID device interface — which does not
exist until those queries are answered. The channel was structurally unavailable
at the only moment it was needed, so `device_type()`'s bounded wait always timed
out and every identity fell back to DualSense. Not a race: DualShock 4 and the
Edge enumerated as DualSenses too (verified on .173 — both report 054C:0CE6 with
a 64-byte DualSense input report, while their on-demand strings read correctly).

The devnode's own hardware ids carry the identity and are readable at
EvtDeviceAdd, before anything is asked, so resolve it there. The section stays
authoritative once attached; the old wait survives only for a devnode whose ids
match nothing.

`hwid_devtype_table_matches_the_driver` pins the host's hwid → device_type
mapping against the driver's table, including the ordering trap that
`pf_dualsense` is a prefix of `pf_dualsenseedge`.
2026-07-30 23:40:35 +02:00

267 lines
12 KiB
Rust

//! 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;
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyHardwareID — the devnode's `REG_MULTI_SZ` hardware-id
/// list (same caveat: stable WDM value, not re-exported).
const DEVICE_PROPERTY_HARDWARE_ID: i32 = 1;
#[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 }
}
/// Read the devnode's hardware-id list (`pszzHardwareIds`) as one lowercase ASCII string, ids
/// separated by `;` (e.g. `"pf_steamdeck;usb\\vid_28de&pid_1205&rev_0100&mi_02;…"`). Empty if the
/// property is absent.
///
/// This is the ONE identity surface available at `EvtDeviceAdd` — before hidclass asks for the
/// report descriptor and attributes, and long before the sealed channel can deliver (delivery goes
/// through the HID device interface, which only exists once those very queries are answered). A
/// driver that must know *which* device it is at descriptor time has to read it here.
///
/// # Safety
/// `device` must be the live `WDFDEVICE` created in the current `EvtDeviceAdd`.
pub unsafe fn query_hardware_ids(device: WDFDEVICE) -> String {
let mut mem: wdk_sys::WDFMEMORY = core::ptr::null_mut();
// SAFETY: `device` is live per this fn's contract; property = HardwareID; pool ignored in UMDF;
// `mem` receives the handle (device-parented — the framework frees it at device teardown).
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfDeviceAllocAndQueryProperty,
device,
DEVICE_PROPERTY_HARDWARE_ID,
0,
WDF_NO_OBJECT_ATTRIBUTES,
&mut mem
)
};
if !nt_success(st) || mem.is_null() {
return String::new();
}
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 String::new();
}
// SAFETY: `buf` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read exactly `len / 2` u16.
let chars = unsafe { core::slice::from_raw_parts(buf, len / 2) };
// REG_MULTI_SZ: NUL-separated, double-NUL-terminated. Ids are ASCII in practice; anything else
// is dropped rather than lossily transliterated (this string is only ever substring-matched).
let mut out = String::with_capacity(chars.len());
for &c in chars {
match c {
0 => {
if out.ends_with(';') || out.is_empty() {
break; // the terminating second NUL
}
out.push(';');
}
0x20..=0x7E => out.push((c as u8 as char).to_ascii_lowercase()),
_ => {}
}
}
out
}