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,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 }
|
||||
}
|
||||
Reference in New Issue
Block a user