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:
2026-07-03 12:08:56 +00:00
parent a3e1ea2b44
commit 95a08e99c3
37 changed files with 2985 additions and 1174 deletions
@@ -0,0 +1,17 @@
# pf-umdf-util - the audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers.
# Everything a pad driver does with raw pointers or Win32/WDF FFI lives HERE, behind small safe
# (or explicitly-contracted unsafe) APIs, so the driver crates' business logic is 100% safe Rust:
# section - MappedView: bounds+alignment-checked shared-memory access (atomics for sync fields)
# channel - ChannelClient: the sealed pad channel's driver-side state machine (a SAFE module)
# wdf - Request/queue/device-property helpers over call_unsafe_wdf_function_binding
[package]
name = "pf-umdf-util"
edition.workspace = true
version.workspace = true
license.workspace = true
publish = false
description = "punktfunk UMDF driver util: safe shared-memory + sealed-channel + WDF request primitives"
[dependencies]
wdk-sys.workspace = true
pf-driver-proto.workspace = true
@@ -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
));
}
}
@@ -0,0 +1,35 @@
//! The audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers (`pf-xusb`,
//! `pf-dualsense`).
//!
//! A UMDF driver cannot be literally free of `unsafe` — WDF dispatch, Win32 section mapping and
//! cross-process shared memory are FFI by nature. What Rust *can* buy is confining every raw
//! operation to one small, reviewed layer with explicit contracts, so the drivers' business logic
//! (the sealed-channel state machine, report plumbing, IOCTL policy) is **100 % safe code** and a
//! memory-safety bug can only live in this crate. Three modules:
//!
//! * [`section`] — [`section::MappedView`]: bounds- and alignment-checked access to a mapped shared
//! section (atomics for the cross-process sync fields), plus the leaked-view [`section::ViewCell`].
//! * [`channel`] — [`channel::ChannelClient`]: the sealed pad channel's driver side
//! (`design/gamepad-channel-sealing.md`), a **`#[forbid(unsafe_code)]` module** — the entire
//! handshake/validation/adoption state machine is safe Rust over [`section`]'s API.
//! * [`wdf`] — [`wdf::Request`] + queue/device-property helpers: each framework callback converts
//! its raw `WDFREQUEST` into a token exactly once (`unsafe`, with the framework's validity as the
//! contract); everything after that is safe.
//!
//! Lint gates (mirrored in every driver crate, enforced by the drivers CI clippy step):
//! `unsafe_op_in_unsafe_fn` + `clippy::undocumented_unsafe_blocks` — every remaining `unsafe {}`
//! must carry a `// SAFETY:` proof.
#![deny(unsafe_op_in_unsafe_fn)]
#![deny(clippy::undocumented_unsafe_blocks)]
pub mod channel;
pub mod section;
pub mod wdf;
/// `NT_SUCCESS` — an NTSTATUS is an error iff negative.
#[inline]
#[must_use]
pub const fn nt_success(status: wdk_sys::NTSTATUS) -> bool {
status >= 0
}
@@ -0,0 +1,241 @@
//! Safe access to Win32 shared-memory sections: [`MappedView`] wraps a mapped view of a known
//! length and exposes bounds- and alignment-checked accessors, so callers never touch the raw base
//! pointer. Cross-process sync fields (seqs, pids, handle values) go through real atomics; bulk
//! report regions use plain unaligned copies, guarded by the channel protocol's seq fields — the
//! same access discipline the host side uses (`inject/windows/gamepad_raii.rs`).
use core::ffi::c_void;
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering};
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
unsafe extern "system" {
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
fn UnmapViewOfFile(addr: *const c_void) -> i32;
fn CloseHandle(h: *mut c_void) -> i32;
}
/// A read/write view over a mapped shared section of exactly `len` bytes. Every accessor
/// bounds-checks (and, for the atomic ones, alignment-checks) its offset, so no caller can read or
/// write outside the mapping — the offsets are `offset_of!` constants from `pf_driver_proto`, making
/// a failed check a compile-shaped logic bug (it aborts the WUDFHost rather than corrupting).
///
/// Concurrency: the peer process writes the section concurrently. Fields used for cross-process
/// synchronization must be accessed through the `load_*`/`store_*` atomic accessors; the bulk
/// byte/scalar accessors are plain unaligned accesses whose consistency is guarded by the channel
/// protocol (seq-fenced publishes), exactly as on the host side.
pub struct MappedView {
base: *mut u8,
len: usize,
}
// SAFETY: `MappedView` is a pointer + length over an OS mapping that stays valid until
// `UnmapViewOfFile` in `Drop` (or forever, once leaked into a `ViewCell`). All access goes through
// the checked accessors — atomics for shared sync fields, unaligned reads/writes for bulk data —
// none of which require a single-thread owner, so sharing/sending the view across the driver's
// callback threads is sound.
unsafe impl Send for MappedView {}
// SAFETY: as above — `&MappedView` only exposes accessors that are safe under concurrent use.
unsafe impl Sync for MappedView {}
impl MappedView {
/// Open the named section `name` and map its first `len` bytes read/write. `None` if the name
/// does not exist (e.g. the host is gone) or the mapping fails. The section handle is closed
/// immediately — the view keeps the section alive.
pub fn open_named(name: &str, len: usize) -> Option<MappedView> {
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
// SAFETY: `wide` is a valid NUL-terminated UTF-16 string for the duration of the call.
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, wide.as_ptr()) };
if h.is_null() {
return None;
}
// SAFETY: `h` is the valid mapping handle just opened; map `len` bytes read/write. The view
// keeps the section alive, so the handle can be closed right away.
let base = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, len) } as *mut u8;
// SAFETY: `h` is the valid handle from `OpenFileMappingW`, owned solely by this function.
unsafe { CloseHandle(h) };
if base.is_null() {
return None;
}
Some(MappedView { base, len })
}
/// Map `len` bytes of a section from a raw handle VALUE (the sealed channel's delivery — a
/// handle the host duplicated into this process). `None` if the value does not resolve to a
/// mappable section. The handle itself is NOT consumed — the caller decides after validating
/// the mapped content (see [`close_handle_value`]).
pub fn from_handle_value(value: u64, len: usize) -> Option<MappedView> {
if value == 0 {
return None;
}
// SAFETY: `MapViewOfFile` on an arbitrary handle value is safe — it fails (returns null)
// unless the value resolves to a section handle in this process's table with RW access.
let base = unsafe { MapViewOfFile(value as usize as *mut c_void, FILE_MAP_RW, 0, 0, len) }
as *mut u8;
if base.is_null() {
return None;
}
Some(MappedView { base, len })
}
/// Assert `off..off+n` is inside the view and, for atomics, `align`-aligned. The view base is
/// page-aligned (`MapViewOfFile`), so field alignment reduces to offset alignment.
#[inline]
fn check(&self, off: usize, n: usize, align: usize) {
assert!(
off.is_multiple_of(align) && off.checked_add(n).is_some_and(|end| end <= self.len),
"MappedView access out of bounds/alignment (off={off}, n={n}, len={})",
self.len
);
}
/// Atomic `u32` load at `off` (must be 4-aligned) — the cross-process sync accessor.
#[inline]
pub fn load_u32(&self, off: usize, order: Ordering) -> u32 {
self.check(off, 4, 4);
// SAFETY: `off` is in-bounds + 4-aligned per `check`, and the page-aligned mapping stays
// valid while `&self` lives; an `AtomicU32` view over shared memory is the defined way to
// race the peer process.
unsafe { (*(self.base.add(off) as *const AtomicU32)).load(order) }
}
/// Atomic `u32` store at `off` (must be 4-aligned).
#[inline]
pub fn store_u32(&self, off: usize, v: u32, order: Ordering) {
self.check(off, 4, 4);
// SAFETY: as `load_u32` — in-bounds, aligned, valid for `&self`'s lifetime.
unsafe { (*(self.base.add(off) as *const AtomicU32)).store(v, order) }
}
/// Atomic `u64` load at `off` (must be 8-aligned).
#[inline]
pub fn load_u64(&self, off: usize, order: Ordering) -> u64 {
self.check(off, 8, 8);
// SAFETY: as `load_u32`, with 8-byte size/alignment checked.
unsafe { (*(self.base.add(off) as *const AtomicU64)).load(order) }
}
/// Plain byte read at `off` (bulk-region accessor — protocol-guarded, see the type docs).
#[inline]
pub fn read_u8(&self, off: usize) -> u8 {
self.check(off, 1, 1);
// SAFETY: in-bounds per `check`; a one-byte read cannot tear.
unsafe { *self.base.add(off) }
}
/// Plain byte write at `off`.
#[inline]
pub fn write_u8(&self, off: usize, v: u8) {
self.check(off, 1, 1);
// SAFETY: in-bounds per `check`; a one-byte write cannot tear.
unsafe { *self.base.add(off) = v }
}
/// Plain (unaligned) `u16` read at `off`.
#[inline]
pub fn read_u16(&self, off: usize) -> u16 {
self.check(off, 2, 1);
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u16) }
}
/// Plain (unaligned) `u32` read at `off` — the bulk-region accessor for a DATA-section scalar
/// (host-written state / a driver-written publish counter; consistency comes from the channel
/// protocol's seq fences, not from this access, exactly as on the host side).
#[inline]
pub fn read_u32(&self, off: usize) -> u32 {
self.check(off, 4, 1);
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u32) }
}
/// Plain (unaligned) `u32` write at `off` (bulk-region accessor).
#[inline]
pub fn write_u32(&self, off: usize, v: u32) {
self.check(off, 4, 1);
// SAFETY: in-bounds per `check`; `write_unaligned` has no alignment requirement.
unsafe { core::ptr::write_unaligned(self.base.add(off) as *mut u32, v) }
}
/// Plain (unaligned) `i16` read at `off`.
#[inline]
pub fn read_i16(&self, off: usize) -> i16 {
self.check(off, 2, 1);
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const i16) }
}
/// Copy `dst.len()` bytes out of the view starting at `off`.
pub fn read_bytes(&self, off: usize, dst: &mut [u8]) {
self.check(off, dst.len(), 1);
// SAFETY: the source range is in-bounds per `check`; `dst` is a live exclusive borrow of
// `dst.len()` writable bytes and cannot overlap the foreign mapping.
unsafe { core::ptr::copy_nonoverlapping(self.base.add(off), dst.as_mut_ptr(), dst.len()) }
}
/// Copy `src` into the view starting at `off`.
pub fn write_bytes(&self, off: usize, src: &[u8]) {
self.check(off, src.len(), 1);
// SAFETY: the destination range is in-bounds per `check`; `src` is a live borrow that
// cannot overlap the foreign mapping.
unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), self.base.add(off), src.len()) }
}
}
impl Drop for MappedView {
fn drop(&mut self) {
// SAFETY: `base` is the live view from `MapViewOfFile`, unmapped exactly once (here).
unsafe {
UnmapViewOfFile(self.base as *const c_void);
}
}
}
/// Close a raw handle VALUE owned by this process — the sealed channel's adopt-on-success step
/// (the mapped view keeps the section alive after the close). Closing a value that is not a live
/// handle of this process is a logic error the OS rejects (returns FALSE); it is not memory-unsafe.
pub fn close_handle_value(value: u64) {
if value == 0 {
return;
}
// SAFETY: `CloseHandle` validates the value against this process's handle table; no memory is
// dereferenced through it.
unsafe { CloseHandle(value as usize as *mut c_void) };
}
/// A lock-free cell holding the driver's adopted DATA view as a **leaked** `&'static MappedView`.
/// [`set`](Self::set) leaks the new view (and abandons the old one) instead of ever unmapping:
/// a concurrent framework callback may still be reading through a previously-returned reference, so
/// the mapping must never be torn down — a deliberate, bounded leak (one small view per delivery,
/// at most a handful per pad lifetime).
pub struct ViewCell(AtomicPtr<MappedView>);
impl Default for ViewCell {
fn default() -> Self {
Self::new()
}
}
impl ViewCell {
pub const fn new() -> ViewCell {
ViewCell(AtomicPtr::new(core::ptr::null_mut()))
}
/// The current view, if one was published. The `'static` lifetime is real: published views are
/// leaked and never unmapped.
pub fn get(&self) -> Option<&'static MappedView> {
let p = self.0.load(Ordering::Acquire);
// SAFETY: `p` is either null or a `Box::leak`ed `MappedView` published by `set`, which is
// never dropped or unmapped — so the reference is valid for the process lifetime.
(!p.is_null()).then(|| unsafe { &*p })
}
/// Publish `view`, leaking it (and abandoning — NOT freeing — any previous view; see the type
/// docs for why the old mapping must stay alive).
pub fn set(&self, view: MappedView) {
let leaked: &'static mut MappedView = Box::leak(Box::new(view));
self.0.swap(leaked, Ordering::Release);
}
}
@@ -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 }
}