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:
@@ -2,11 +2,17 @@
|
||||
//!
|
||||
//! Two planes:
|
||||
//! * [`control`] — the low-frequency `DeviceIoControl` plane (add/remove a virtual monitor, pin the
|
||||
//! render adapter, keepalive, info, clear-all). Owned, clean, versioned — NOT the SudoVDA ABI.
|
||||
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of shared keyed-mutex textures
|
||||
//! (+ a header + a frame-ready event) and the driver opens them and publishes composited frames into
|
||||
//! them. This crate owns the [`frame::SharedHeader`] layout, the [`frame::FrameToken`] packing, the
|
||||
//! `Global\` object-name scheme, and the driver-status codes.
|
||||
//! render adapter, keepalive, info, clear-all, deliver the frame channel). Owned, clean, versioned —
|
||||
//! NOT the SudoVDA ABI.
|
||||
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of **unnamed** shared
|
||||
//! keyed-mutex textures (+ a header + a frame-ready event), duplicates their handles into the
|
||||
//! driver's WUDFHost process and delivers the handle VALUES over
|
||||
//! [`control::IOCTL_SET_FRAME_CHANNEL`]; the driver publishes composited frames into them. There is
|
||||
//! deliberately no object-name scheme: an unnamed object cannot be enumerated, opened by name, or
|
||||
//! pre-created ("squatted") — only the two endpoint processes ever hold a handle to any frame object
|
||||
//! (the sealed channel, `design/idd-push-security.md`). This crate owns the [`frame::SharedHeader`]
|
||||
//! layout, the [`frame::FrameToken`] packing, the channel-delivery struct, and the driver-status
|
||||
//! codes.
|
||||
//!
|
||||
//! Both planes were previously hand-duplicated, byte-for-byte, across `idd_push.rs`/`frame_transport.rs`
|
||||
//! and `vdisplay/sudovda.rs`/`control.rs` with only "must match" comments guarding them. Defining them
|
||||
@@ -43,16 +49,22 @@ pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
|
||||
|
||||
/// Bumped on any incompatible change to either plane. Exchanged via [`control::IOCTL_GET_INFO`]; host
|
||||
/// and driver assert a match at startup so a mismatched pair fails loudly instead of corrupting.
|
||||
pub const PROTOCOL_VERSION: u32 = 1;
|
||||
/// v2: the sealed frame channel — the frame objects are unnamed and delivered by handle duplication
|
||||
/// ([`control::IOCTL_SET_FRAME_CHANNEL`]), and [`control::AddReply`] grew `wudf_pid` (the duplication
|
||||
/// target). A v1 driver has no channel-delivery IOCTL and expects named objects, so the pairing is
|
||||
/// incompatible by design.
|
||||
pub const PROTOCOL_VERSION: u32 = 2;
|
||||
|
||||
/// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`.
|
||||
pub const fn ctl_code(func: u32) -> u32 {
|
||||
(0x22u32 << 16) | (func << 2)
|
||||
}
|
||||
|
||||
/// The control (`DeviceIoControl`) plane: add/remove a virtual monitor + adapter pin + keepalive.
|
||||
/// The control (`DeviceIoControl`) plane: add/remove a virtual monitor + adapter pin + keepalive +
|
||||
/// frame-channel delivery.
|
||||
pub mod control {
|
||||
use super::ctl_code;
|
||||
use super::frame::RING_LEN;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
// Contiguous op space at 0x900 — distinct from SudoVDA's gappy 0x800/0x888/0x8FF numbering.
|
||||
@@ -69,6 +81,10 @@ pub mod control {
|
||||
/// Tear down every virtual monitor (host-startup orphan reap). No payload. First-class op — NOT the
|
||||
/// SudoVDA "send-and-hope-it's-ignored" hack.
|
||||
pub const IOCTL_CLEAR_ALL: u32 = ctl_code(0x905);
|
||||
/// Deliver a monitor's IDD-push frame channel: the handle VALUES of the unnamed shared objects the
|
||||
/// host duplicated into the driver's WUDFHost process. Input [`SetFrameChannelRequest`]. Sent once
|
||||
/// after the ring is created and again on every mid-session ring recreate (HDR-mode flip).
|
||||
pub const IOCTL_SET_FRAME_CHANNEL: u32 = ctl_code(0x906);
|
||||
|
||||
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
|
||||
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
|
||||
@@ -103,6 +119,11 @@ pub mod control {
|
||||
/// `_reserved` (offset 12): an un-upgraded driver leaves it `0`, so the host can tell its
|
||||
/// preference was ignored (stale driver) and log it instead of silently losing per-client config.
|
||||
pub resolved_monitor_id: u32,
|
||||
/// The driver's own process id (the WUDFHost hosting `pf_vdisplay`) — the target the host
|
||||
/// duplicates the unnamed frame-object handles INTO (`OpenProcess(PROCESS_DUP_HANDLE)` +
|
||||
/// `DuplicateHandle`, then [`IOCTL_SET_FRAME_CHANNEL`]). Reported per-ADD, not per-open, so a
|
||||
/// WUDFHost restart between sessions can never leave the host duplicating into a dead process.
|
||||
pub wudf_pid: u32,
|
||||
}
|
||||
|
||||
/// `IOCTL_REMOVE` input.
|
||||
@@ -129,6 +150,39 @@ pub mod control {
|
||||
pub watchdog_timeout_s: u32,
|
||||
}
|
||||
|
||||
/// `IOCTL_SET_FRAME_CHANNEL` input — the sealed frame channel's bootstrap. Every handle field is a
|
||||
/// handle VALUE already duplicated into the driver's WUDFHost process by the host; receiving it, the
|
||||
/// driver OWNS those handles (it closes whatever it doesn't consume — a replaced, invalid, or
|
||||
/// unmatched delivery must not leak entries in its own handle table).
|
||||
///
|
||||
/// Handle values are only meaningful inside the target process's handle table, so this struct is
|
||||
/// harmless to any third party: reading it leaks nothing openable, and spoofing it (were the control
|
||||
/// device reachable — it is ACL'd to SYSTEM + admins) could at worst feed the driver values that
|
||||
/// don't resolve, a DoS of the attacker's own session. The frame objects themselves are unnamed and
|
||||
/// therefore unreachable by any process that isn't one of the two endpoints.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||||
pub struct SetFrameChannelRequest {
|
||||
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
|
||||
pub target_id: u32,
|
||||
/// The ring generation these textures belong to (must match the shared header's generation at
|
||||
/// attach time; a stale delivery is dropped by the driver — a fresh one follows every recreate).
|
||||
pub generation: u32,
|
||||
/// How many leading entries of `texture_handles` are valid (`1..=`[`RING_LEN`]).
|
||||
pub ring_len: u32,
|
||||
pub _pad: u32,
|
||||
/// The shared-header file-mapping handle (the driver maps it and writes status/publish tokens).
|
||||
pub header_handle: u64,
|
||||
/// The frame-ready auto-reset event handle (the driver signals it after each publish).
|
||||
pub event_handle: u64,
|
||||
/// The ring textures' shared NT handles (opened via `ID3D11Device1::OpenSharedResource1`).
|
||||
pub texture_handles: [u64; RING_LEN_USIZE],
|
||||
}
|
||||
|
||||
/// [`RING_LEN`] as a usize for the `texture_handles` array length (the wire struct sizes the array
|
||||
/// at the compile-time maximum; `ring_len` says how many entries are live).
|
||||
pub const RING_LEN_USIZE: usize = RING_LEN as usize;
|
||||
|
||||
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
|
||||
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
|
||||
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
|
||||
@@ -142,11 +196,20 @@ pub mod control {
|
||||
assert!(offset_of!(AddRequest, refresh_hz) == 16);
|
||||
assert!(offset_of!(AddRequest, preferred_monitor_id) == 20);
|
||||
|
||||
assert!(size_of::<AddReply>() == 16);
|
||||
assert!(size_of::<AddReply>() == 20);
|
||||
assert!(offset_of!(AddReply, adapter_luid_low) == 0);
|
||||
assert!(offset_of!(AddReply, adapter_luid_high) == 4);
|
||||
assert!(offset_of!(AddReply, target_id) == 8);
|
||||
assert!(offset_of!(AddReply, resolved_monitor_id) == 12);
|
||||
assert!(offset_of!(AddReply, wudf_pid) == 16);
|
||||
|
||||
assert!(size_of::<SetFrameChannelRequest>() == 32 + 8 * RING_LEN_USIZE);
|
||||
assert!(offset_of!(SetFrameChannelRequest, target_id) == 0);
|
||||
assert!(offset_of!(SetFrameChannelRequest, generation) == 4);
|
||||
assert!(offset_of!(SetFrameChannelRequest, ring_len) == 8);
|
||||
assert!(offset_of!(SetFrameChannelRequest, header_handle) == 16);
|
||||
assert!(offset_of!(SetFrameChannelRequest, event_handle) == 24);
|
||||
assert!(offset_of!(SetFrameChannelRequest, texture_handles) == 32);
|
||||
|
||||
assert!(size_of::<RemoveRequest>() == 8);
|
||||
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
||||
@@ -161,11 +224,12 @@ pub mod control {
|
||||
};
|
||||
}
|
||||
|
||||
/// The IDD-push frame transport: the host-created shared ring header, the publish token, the names, and
|
||||
/// the driver-status codes. The texture ring itself is host-created D3D11 keyed-mutex textures (opened
|
||||
/// by name on the driver side); only the *layout/contract* lives here.
|
||||
/// The IDD-push frame transport: the host-created shared ring header, the publish token, and the
|
||||
/// driver-status codes. The texture ring itself is host-created **unnamed** D3D11 keyed-mutex textures;
|
||||
/// the driver reaches them (and the header + event) only through handles the host duplicated into its
|
||||
/// process and delivered via [`crate::control::IOCTL_SET_FRAME_CHANNEL`] — the sealed channel. Only the
|
||||
/// *layout/contract* lives here.
|
||||
pub mod frame {
|
||||
use alloc::string::String;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
/// Header magic (`"PFVD"` LE). The host stamps it LAST (after the ring textures exist) so the driver
|
||||
@@ -195,8 +259,10 @@ pub mod frame {
|
||||
pub struct SharedHeader {
|
||||
pub magic: u32,
|
||||
pub version: u32,
|
||||
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format/names). The driver
|
||||
/// re-attaches when it changes; a publish carries it so the host rejects a stale-ring publish.
|
||||
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format + a fresh
|
||||
/// [`control::IOCTL_SET_FRAME_CHANNEL`](crate::control::IOCTL_SET_FRAME_CHANNEL) delivery). The
|
||||
/// driver re-attaches when it changes; a publish carries it so the host rejects a stale-ring
|
||||
/// publish.
|
||||
pub generation: u32,
|
||||
pub ring_len: u32,
|
||||
pub width: u32,
|
||||
@@ -245,21 +311,6 @@ pub mod frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Global\pfvd-hdr-<target>` — the shared metadata header mapping name.
|
||||
pub fn header_name(target_id: u32) -> String {
|
||||
alloc::format!("Global\\pfvd-hdr-{target_id}")
|
||||
}
|
||||
/// `Global\pfvd-evt-<target>` — the frame-ready auto-reset event name.
|
||||
pub fn event_name(target_id: u32) -> String {
|
||||
alloc::format!("Global\\pfvd-evt-{target_id}")
|
||||
}
|
||||
/// `Global\pfvd-tex-<target>-<generation>-<slot>` — a ring texture's shared-handle name. The
|
||||
/// generation in the name means a recreate's new textures never collide with the old ring's
|
||||
/// not-yet-released handles.
|
||||
pub fn texture_name(target_id: u32, generation: u32, slot: u32) -> String {
|
||||
alloc::format!("Global\\pfvd-tex-{target_id}-{generation}-{slot}")
|
||||
}
|
||||
|
||||
// Size + per-field offsets are load-bearing: both sides access these via raw atomic views over the
|
||||
// mapping, so a same-size field reorder would silently corrupt. Pin every offset. The `_pad` after
|
||||
// `dxgi_format` is what 8-aligns the `u64 latest` at offset 32 — assert that too.
|
||||
@@ -292,8 +343,10 @@ pub mod frame {
|
||||
/// (`design/windows-host-rewrite.md` §2.7). Owning them here with `Pod` derives + `offset_of!`
|
||||
/// asserts makes a one-sided edit a compile error.
|
||||
///
|
||||
/// The host creates the section (privileged, permissive DACL so the restricted WUDFHost token can
|
||||
/// open it) and the driver maps it. Layout only; the section itself is host-created shared memory.
|
||||
/// Since v2 the channel is **sealed** (`design/gamepad-channel-sealing.md`, mirroring the frame
|
||||
/// channel): the host creates the DATA section ([`XusbShm`]/[`PadShm`]) UNNAMED (SYSTEM-only DACL)
|
||||
/// and duplicates its handle into the driver's WUDFHost; only the tiny [`PadBootstrap`] mailbox
|
||||
/// stays named (it carries nothing exploitable). Layout only; the sections are host-created.
|
||||
pub mod gamepad {
|
||||
use alloc::string::String;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
@@ -316,15 +369,68 @@ pub mod gamepad {
|
||||
/// The section starts zeroed, so `0` always means "no driver has attached (yet)"; a pre-health
|
||||
/// driver never writes the field and reads as not-attached, which the host log line calls out
|
||||
/// (the remedy is the same: reinstall the drivers). Bump on a gamepad-layout change.
|
||||
pub const GAMEPAD_PROTO_VERSION: u32 = 1;
|
||||
///
|
||||
/// v2: the **sealed pad channel** (`design/gamepad-channel-sealing.md`) — the DATA section
|
||||
/// ([`XusbShm`]/[`PadShm`]) is UNNAMED and reaches the driver only as a handle the host duplicated
|
||||
/// into its WUDFHost, bootstrapped through the named [`PadBootstrap`] mailbox; the DATA section
|
||||
/// gained `pad_index` (carved from reserved space) so the driver rejects a cross-pad delivery.
|
||||
/// A v1 driver opens `Global\pf…-shm-<i>` (which no longer exists) and a v1 host never creates
|
||||
/// the mailbox a v2 driver polls, so a mixed pairing fails closed either way.
|
||||
pub const GAMEPAD_PROTO_VERSION: u32 = 2;
|
||||
|
||||
/// `Global\pfxusb-shm-<index>` — the virtual Xbox 360 (XInput) shared section.
|
||||
pub fn xusb_shm_name(index: u8) -> String {
|
||||
alloc::format!("Global\\pfxusb-shm-{index}")
|
||||
/// Bootstrap-mailbox magic (`"PFBT"` LE) — the host stamps it LAST (after `host_proto`), so a
|
||||
/// driver only trusts a fully-initialized mailbox.
|
||||
pub const BOOT_MAGIC: u32 = 0x5442_4650;
|
||||
|
||||
/// `Global\pfxusb-boot-<index>` — the virtual Xbox 360 pad's bootstrap mailbox ([`PadBootstrap`]).
|
||||
pub fn xusb_boot_name(index: u8) -> String {
|
||||
alloc::format!("Global\\pfxusb-boot-{index}")
|
||||
}
|
||||
/// `Global\pfds-shm-<index>` — the virtual DualSense / DualShock 4 shared section.
|
||||
pub fn pad_shm_name(index: u8) -> String {
|
||||
alloc::format!("Global\\pfds-shm-{index}")
|
||||
/// `Global\pfds-boot-<index>` — the DualSense / DualShock 4 pad's bootstrap mailbox
|
||||
/// ([`PadBootstrap`]).
|
||||
pub fn pad_boot_name(index: u8) -> String {
|
||||
alloc::format!("Global\\pfds-boot-{index}")
|
||||
}
|
||||
|
||||
/// The per-pad bootstrap mailbox (32 B, named `Global\pf…-boot-<index>`, SY+LS DACL) — the ONLY
|
||||
/// named object left on the gamepad channel. It exists because the pad drivers are UMDF HID
|
||||
/// minidrivers with no control device (hidclass owns the stack), so there is no IOCTL to hand the
|
||||
/// driver a duplicated handle or learn its WUDFHost pid; this mailbox is the late-bound handshake:
|
||||
///
|
||||
/// 1. host creates it (zeroed), stamps `host_proto` then `magic` (in that order);
|
||||
/// 2. driver opens it by name (pad index from `pszDeviceLocation`), writes `driver_proto`, and —
|
||||
/// iff `host_proto` matches its own version — publishes `driver_pid`;
|
||||
/// 3. host polls `driver_pid`, verifies the pid is a genuine WUDFHost, duplicates the unnamed DATA
|
||||
/// section into it, then writes `data_handle` + `handle_pid` and bumps `handle_seq` LAST;
|
||||
/// 4. driver sees a fresh `handle_seq` addressed to its own pid, maps `data_handle`, and validates
|
||||
/// the mapped section's magic + `pad_index` before use.
|
||||
///
|
||||
/// Deliberately safe to leave named + LS-openable: it carries only pids (not sensitive) and a
|
||||
/// handle VALUE (meaningless outside the target WUDFHost's handle table). A sibling LocalService
|
||||
/// that tampers with it can at worst mis-route a delivery — a gamepad DoS, never a read or an
|
||||
/// injection (it cannot place a valid section handle in the WUDFHost, and the driver's
|
||||
/// magic+`pad_index` validation rejects any handle that doesn't resolve to this pad's section).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||||
pub struct PadBootstrap {
|
||||
/// [`BOOT_MAGIC`], host-stamped last at creation.
|
||||
pub magic: u32,
|
||||
/// The host's [`GAMEPAD_PROTO_VERSION`]. A driver whose own version differs must NOT publish
|
||||
/// its pid (fail closed) — it still writes `driver_proto` so the host can log the mismatch.
|
||||
pub host_proto: u32,
|
||||
/// The driver's WUDFHost process id (driver-written; `0` = no driver yet). The duplication
|
||||
/// target the host verifies (`verify_is_wudfhost`) before duplicating the DATA section into it.
|
||||
pub driver_pid: u32,
|
||||
/// The driver's [`GAMEPAD_PROTO_VERSION`] (driver-written; diagnostics only).
|
||||
pub driver_proto: u32,
|
||||
/// The DATA-section handle VALUE the host duplicated into `handle_pid`'s handle table
|
||||
/// (host-written; valid only inside that process).
|
||||
pub data_handle: u64,
|
||||
/// The pid `data_handle` was duplicated for — a driver whose pid differs ignores the delivery.
|
||||
pub handle_pid: u32,
|
||||
/// Bumped by the host (host-global monotonic, never 0) AFTER `data_handle`/`handle_pid` are in
|
||||
/// place — the driver's new-delivery trigger.
|
||||
pub handle_seq: u32,
|
||||
}
|
||||
|
||||
/// Virtual Xbox 360 (XInput) shared section (64 B). The host writes the XInput state (a bumped
|
||||
@@ -356,7 +462,12 @@ pub mod gamepad {
|
||||
/// Bumped by the driver on every serviced XInput IOCTL — proves the game-visible path (it
|
||||
/// only advances while something polls the slot, so a static value is not an error).
|
||||
pub driver_heartbeat: u32,
|
||||
pub _reserved1: [u8; 24],
|
||||
/// The pad index this section serves (host-stamped before the magic). The driver validates it
|
||||
/// against its own `pszDeviceLocation` index when it maps the delivered handle, so a mis-routed
|
||||
/// (or bootstrap-tampered) cross-pad delivery is rejected instead of silently cross-wiring two
|
||||
/// pads. Carved from v1 reserved space (v2).
|
||||
pub pad_index: u32,
|
||||
pub _reserved1: [u8; 20],
|
||||
}
|
||||
|
||||
/// Virtual DualSense / DualShock 4 shared section (256 B). The host writes the `0x01`-style HID
|
||||
@@ -384,7 +495,10 @@ pub mod gamepad {
|
||||
/// Bumped by the driver's ~125 Hz timer each tick — a true liveness heartbeat (unlike the
|
||||
/// XUSB one, this advances whenever the driver is loaded, game or not).
|
||||
pub driver_heartbeat: u32,
|
||||
pub _reserved1: [u8; 104],
|
||||
/// The pad index this section serves (host-stamped before the magic) — see
|
||||
/// [`XusbShm::pad_index`]. Carved from v1 reserved space (v2).
|
||||
pub pad_index: u32,
|
||||
pub _reserved1: [u8; 100],
|
||||
}
|
||||
|
||||
// Offsets are the wire contract the shipped drivers already read by hand — pin every one. A failing
|
||||
@@ -408,6 +522,7 @@ pub mod gamepad {
|
||||
assert!(offset_of!(XusbShm, rumble_small) == 29);
|
||||
assert!(offset_of!(XusbShm, driver_proto) == 32);
|
||||
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
|
||||
assert!(offset_of!(XusbShm, pad_index) == 40);
|
||||
|
||||
assert!(size_of::<PadShm>() == 256);
|
||||
assert!(offset_of!(PadShm, magic) == 0);
|
||||
@@ -417,6 +532,16 @@ pub mod gamepad {
|
||||
assert!(offset_of!(PadShm, device_type) == 140);
|
||||
assert!(offset_of!(PadShm, driver_proto) == 144);
|
||||
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
|
||||
assert!(offset_of!(PadShm, pad_index) == 152);
|
||||
|
||||
assert!(size_of::<PadBootstrap>() == 32);
|
||||
assert!(offset_of!(PadBootstrap, magic) == 0);
|
||||
assert!(offset_of!(PadBootstrap, host_proto) == 4);
|
||||
assert!(offset_of!(PadBootstrap, driver_pid) == 8);
|
||||
assert!(offset_of!(PadBootstrap, driver_proto) == 12);
|
||||
assert!(offset_of!(PadBootstrap, data_handle) == 16);
|
||||
assert!(offset_of!(PadBootstrap, handle_pid) == 24);
|
||||
assert!(offset_of!(PadBootstrap, handle_seq) == 28);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -487,28 +612,71 @@ mod tests {
|
||||
adapter_luid_high: -2,
|
||||
target_id: 262,
|
||||
resolved_monitor_id: 7,
|
||||
wudf_pid: 4242,
|
||||
};
|
||||
let rbytes = bytemuck::bytes_of(&reply);
|
||||
assert_eq!(rbytes.len(), 16);
|
||||
assert_eq!(rbytes.len(), 20);
|
||||
assert_eq!(*bytemuck::from_bytes::<control::AddReply>(rbytes), reply);
|
||||
// resolved_monitor_id occupies the old `_reserved` slot at offset 12 — byte-compatible.
|
||||
assert_eq!(rbytes[12..16], 7u32.to_le_bytes());
|
||||
// The v2 duplication-target pid trails at offset 16.
|
||||
assert_eq!(rbytes[16..20], 4242u32.to_le_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_are_stable() {
|
||||
assert_eq!(frame::header_name(10), "Global\\pfvd-hdr-10");
|
||||
assert_eq!(frame::event_name(10), "Global\\pfvd-evt-10");
|
||||
assert_eq!(frame::texture_name(10, 3, 5), "Global\\pfvd-tex-10-3-5");
|
||||
fn frame_channel_request_roundtrips_through_bytes() {
|
||||
let mut req = control::SetFrameChannelRequest {
|
||||
target_id: 262,
|
||||
generation: 3,
|
||||
ring_len: frame::RING_LEN,
|
||||
_pad: 0,
|
||||
header_handle: 0x0000_0000_0000_1a2c,
|
||||
event_handle: 0x0000_0000_0000_1b30,
|
||||
texture_handles: [0; control::RING_LEN_USIZE],
|
||||
};
|
||||
for (k, t) in req.texture_handles.iter_mut().enumerate() {
|
||||
*t = 0x2000 + k as u64 * 4;
|
||||
}
|
||||
let bytes = bytemuck::bytes_of(&req);
|
||||
assert_eq!(bytes.len(), 32 + 8 * control::RING_LEN_USIZE);
|
||||
assert_eq!(
|
||||
*bytemuck::from_bytes::<control::SetFrameChannelRequest>(bytes),
|
||||
req
|
||||
);
|
||||
// The handle values ride at 8-byte alignment from offset 16 (header, event, then the ring).
|
||||
assert_eq!(bytes[16..24], 0x1a2cu64.to_le_bytes());
|
||||
assert_eq!(bytes[24..32], 0x1b30u64.to_le_bytes());
|
||||
assert_eq!(bytes[32..40], 0x2000u64.to_le_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gamepad_names_and_magics_are_stable() {
|
||||
assert_eq!(gamepad::xusb_shm_name(0), "Global\\pfxusb-shm-0");
|
||||
assert_eq!(gamepad::pad_shm_name(2), "Global\\pfds-shm-2");
|
||||
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
||||
assert_eq!(gamepad::pad_boot_name(2), "Global\\pfds-boot-2");
|
||||
// Lock the exact u32 magics the shipped host/drivers use (inject/{gamepad,dualsense}_windows.rs).
|
||||
assert_eq!(gamepad::XUSB_MAGIC, 0x5558_4650);
|
||||
assert_eq!(gamepad::PAD_MAGIC, 0x5046_4453);
|
||||
// "PFBT" little-endian.
|
||||
assert_eq!(gamepad::BOOT_MAGIC.to_le_bytes(), *b"PFBT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pad_bootstrap_roundtrips_through_bytes() {
|
||||
let b = gamepad::PadBootstrap {
|
||||
magic: gamepad::BOOT_MAGIC,
|
||||
host_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
||||
driver_pid: 1234,
|
||||
driver_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
||||
data_handle: 0x0000_0000_0000_2a4c,
|
||||
handle_pid: 1234,
|
||||
handle_seq: 7,
|
||||
};
|
||||
let bytes = bytemuck::bytes_of(&b);
|
||||
assert_eq!(bytes.len(), 32);
|
||||
assert_eq!(*bytemuck::from_bytes::<gamepad::PadBootstrap>(bytes), b);
|
||||
// The handle value rides 8-aligned at offset 16; the seq trails at 28 (written LAST by the host).
|
||||
assert_eq!(bytes[16..24], 0x2a4cu64.to_le_bytes());
|
||||
assert_eq!(bytes[28..32], 7u32.to_le_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -521,6 +689,7 @@ mod tests {
|
||||
control::IOCTL_PING,
|
||||
control::IOCTL_GET_INFO,
|
||||
control::IOCTL_CLEAR_ALL,
|
||||
control::IOCTL_SET_FRAME_CHANNEL,
|
||||
];
|
||||
for (i, a) in all.iter().enumerate() {
|
||||
for b in &all[i + 1..] {
|
||||
|
||||
@@ -42,6 +42,10 @@ pub struct WinCaptureTarget {
|
||||
pub gdi_name: String,
|
||||
/// Stable SudoVDA target id — re-resolved to the current GDI name on every recovery.
|
||||
pub target_id: u32,
|
||||
/// The pf-vdisplay driver's WUDFHost pid (from the ADD reply) — the process the IDD-push capturer
|
||||
/// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown
|
||||
/// (a pre-v2 pairing can't occur — the version handshake is hard — so this only guards misuse).
|
||||
pub wudf_pid: u32,
|
||||
}
|
||||
|
||||
/// A GPU-resident captured texture (future NVENC-D3D11 zero-copy path).
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
//! P2 direct frame push (kill DDA) — HOST side. The pf-vdisplay driver's WUDFHost canNOT create named
|
||||
//! kernel objects, so — exactly like the gamepad UMDF drivers (`inject/dualsense_windows.rs`) — the
|
||||
//! HOST (privileged) CREATES the shared header + frame-ready event + ring of keyed-mutex textures
|
||||
//! (`Global\` names, scoped `D:(A;;GA;;;SY)(A;;GA;;;LS)` to SYSTEM + the driver's LocalService host —
|
||||
//! see `shared_object_sa`) on the discrete render GPU, and the driver only OPENS them and copies frames in. We then consume the ring
|
||||
//! straight into the zero-copy NVENC path — no DXGI Desktop Duplication, no `win32u` hook. Gated by
|
||||
//! `PUNKTFUNK_IDD_PUSH`. Driver counterpart: `packaging/windows/drivers/pf-vdisplay/src/
|
||||
//! P2 direct frame push (kill DDA) — HOST side, over the **sealed channel**
|
||||
//! (`design/idd-push-security.md`). The frame channel carries whole-desktop pixels, so its protection
|
||||
//! must match DDA's (where capturer and consumer are one process and there is no openable channel at
|
||||
//! all): the HOST (SYSTEM) creates the shared header + frame-ready event + ring of keyed-mutex textures
|
||||
//! **UNNAMED** on the discrete render GPU — nothing to enumerate, open by name, or pre-create
|
||||
//! ("squat") — then DUPLICATES the handles into the pf-vdisplay driver's WUDFHost process
|
||||
//! ([`ChannelBroker`]; SYSTEM can `DuplicateHandle` into the LocalService host, the reverse is
|
||||
//! correctly denied, which is why the HOST is the broker) and delivers the handle VALUES over the
|
||||
//! SYSTEM-only control device (`IOCTL_SET_FRAME_CHANNEL`). A handle value is meaningless outside the
|
||||
//! target process's handle table, so the bootstrap's ACL is not load-bearing; the only way to reach the
|
||||
//! frames is to already be one of the two endpoint processes. The driver copies frames in; we consume
|
||||
//! the ring straight into the zero-copy NVENC path — no DXGI Desktop Duplication, no `win32u` hook.
|
||||
//! Gated by `PUNKTFUNK_IDD_PUSH`. Driver counterpart: `packaging/windows/drivers/pf-vdisplay/src/
|
||||
//! frame_transport.rs`. The shared `SharedHeader` layout, `MAGIC`/`VERSION`/`RING_LEN`, the
|
||||
//! `DRV_STATUS_*` codes, the `Global\` name scheme and the publish token all come from
|
||||
//! [`pf_driver_proto::frame`] (which OWNS the contract, with `const` size asserts) — both sides
|
||||
//! `use` it, so drift is a compile error rather than a "must match" comment.
|
||||
//! `DRV_STATUS_*` codes, the channel-delivery struct and the publish token all come from
|
||||
//! [`pf_driver_proto`] (which OWNS the contract, with `const` size asserts) — both sides `use` it, so
|
||||
//! drift is a compile error rather than a "must match" comment.
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
@@ -16,12 +22,15 @@
|
||||
use super::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget};
|
||||
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use pf_driver_proto::frame;
|
||||
use pf_driver_proto::{control, frame};
|
||||
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use windows::core::{w, Interface, HSTRING};
|
||||
use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, LUID};
|
||||
use windows::core::{w, Interface, PCWSTR, PWSTR};
|
||||
use windows::Win32::Foundation::{
|
||||
DuplicateHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_HANDLE_OPTIONS, DUPLICATE_SAME_ACCESS,
|
||||
HANDLE, INVALID_HANDLE_VALUE, LUID,
|
||||
};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
|
||||
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX,
|
||||
@@ -42,47 +51,43 @@ use windows::Win32::System::Memory::{
|
||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
||||
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
|
||||
};
|
||||
use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject};
|
||||
use windows::Win32::System::Threading::{
|
||||
CreateEventW, GetCurrentProcess, OpenProcess, QueryFullProcessImageNameW, WaitForSingleObject,
|
||||
PROCESS_DUP_HANDLE, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
// The frame-transport contract — `SharedHeader` layout, `MAGIC`/`VERSION`/`RING_LEN`, the
|
||||
// `DRV_STATUS_*` codes and the `Global\` name helpers — lives in `pf_driver_proto::frame`; both sides
|
||||
// `use frame::*`, so a layout/name/code drift is a compile error (the proto has `const` size asserts).
|
||||
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
|
||||
// `use` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
|
||||
use frame::{
|
||||
event_name, header_name, texture_name, SharedHeader, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
|
||||
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
SharedHeader, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN,
|
||||
VERSION,
|
||||
};
|
||||
|
||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` for `CreateSharedHandle`/`OpenSharedResourceByName`. Local (not
|
||||
/// part of the proto contract — it is a DXGI sharing-API arg, mirrored on the driver side).
|
||||
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
|
||||
|
||||
/// Least access the driver needs on the duplicated **header section**: map it read/write (it reads the
|
||||
/// layout + writes `driver_status`/`driver_render_luid`/the publish token). `SECTION_MAP_READ |
|
||||
/// SECTION_MAP_WRITE` (== the driver's `FILE_MAP_READ | FILE_MAP_WRITE` map flag). Duplicating with
|
||||
/// exactly this — instead of `DUPLICATE_SAME_ACCESS`, which would copy the host's full-access creator
|
||||
/// handle — is the "grant least privilege" discipline for unnamed shared objects (Raymond Chen,
|
||||
/// *"unnamed objects aren't safe just because they're unnamed"*): a compromised driver's handle can't
|
||||
/// `WRITE_DAC`/`WRITE_OWNER`/`DELETE` the object, only map it.
|
||||
const SECTION_MAP_RW: u32 = 0x0004 | 0x0002;
|
||||
/// Least access the driver needs on the duplicated **frame-ready event**: it only `SetEvent`s it, which
|
||||
/// requires `EVENT_MODIFY_STATE`. (The host holds `SYNCHRONIZE` on its own handle to wait.)
|
||||
const EVENT_MODIFY_STATE: u32 = 0x0002;
|
||||
|
||||
/// Host-owned output-ring depth: distinct NVENC-input textures rotated per frame so the in-flight
|
||||
/// encode of frame N and the convert/copy of frame N+1 never touch the same texture. 3 covers a
|
||||
/// pipeline depth of 2 with one slot of margin.
|
||||
const OUT_RING: usize = 3;
|
||||
|
||||
/// Bring-up debug block (fixed name) — the host creates it; the driver writes diagnostics into it
|
||||
/// independent of the per-target header. NOT part of `pf_driver_proto` (a host-side bring-up channel,
|
||||
/// not the data path); the matching `DebugBlock` lives in the OLD oracle driver's `frame_transport.rs`.
|
||||
#[repr(C)]
|
||||
struct DebugBlock {
|
||||
magic: u32,
|
||||
run_core_entries: u32,
|
||||
resolved_target_id: u32,
|
||||
header_open_attempts: u32,
|
||||
last_open_error: u32,
|
||||
header_opened: u32,
|
||||
render_luid_low: u32,
|
||||
render_luid_high: i32,
|
||||
frames_acquired: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
const DBG_NAME: &str = "Global\\pfvd-dbg";
|
||||
const DBG_MAGIC: u32 = 0x4742_4450;
|
||||
|
||||
/// Monotonic per-process generation: each capturer instance stamps its ring-texture names with a
|
||||
/// fresh value so a retried/overlapping `open()` never collides with a previous attempt's not-yet-
|
||||
/// released shared-handle names (`DXGI_ERROR_NAME_ALREADY_EXISTS`). The driver reads it from the header.
|
||||
/// Monotonic per-process generation stamped into the header + every publish token, so the host rejects
|
||||
/// a stale-ring publish and the driver detects a recreate. (With unnamed textures there is no name
|
||||
/// collision to avoid — the generation's remaining job is the recreate/stale-publish handshake.)
|
||||
static IDD_GENERATION: AtomicU32 = AtomicU32::new(1);
|
||||
|
||||
fn now_ns() -> u64 {
|
||||
@@ -94,7 +99,7 @@ fn now_ns() -> u64 {
|
||||
|
||||
/// RAII wrapper for a file-mapping object + its mapped view: on drop the view is `UnmapViewOfFile`'d,
|
||||
/// THEN the [`OwnedHandle`] closes the underlying mapping object (order matters — unmap before close).
|
||||
/// A `header`/`dbg_block` raw pointer borrows into the view via [`ptr`](Self::ptr); the section must
|
||||
/// A `header` raw pointer borrows into the view via [`ptr`](Self::ptr); the section must
|
||||
/// outlive it (it's declared before it in [`IddPushCapturer`], and moving the section doesn't move the
|
||||
/// OS mapping, so the borrowed pointer stays valid).
|
||||
struct MappedSection {
|
||||
@@ -122,10 +127,9 @@ impl Drop for MappedSection {
|
||||
struct HostSlot {
|
||||
tex: ID3D11Texture2D,
|
||||
mutex: IDXGIKeyedMutex,
|
||||
/// The named shared-resource handle, held only to keep the resource alive (the driver opens it by
|
||||
/// NAME). An [`OwnedHandle`] so it closes on drop (was a manual `CloseHandle` in a `Drop` impl);
|
||||
/// never read directly — its sole purpose is the RAII close.
|
||||
#[allow(dead_code)]
|
||||
/// The UNNAMED shared-resource NT handle: keeps the resource alive for the session AND is the
|
||||
/// source the [`ChannelBroker`] duplicates into the driver's WUDFHost (the ONLY way the driver can
|
||||
/// reach this texture — there is no name to open). An [`OwnedHandle`] so it closes on drop.
|
||||
shared: OwnedHandle,
|
||||
/// SRV on the slot texture so the HDR path samples the FP16 slot DIRECTLY (no slot→scratch copy);
|
||||
/// the convert pass writes the output ring while holding the slot's keyed mutex. Unused for SDR
|
||||
@@ -168,28 +172,238 @@ impl Drop for KeyedMutexGuard<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm the process is a genuine system WUDFHost — `%SystemRoot%\System32\WUDFHost.exe` — before a
|
||||
/// broker duplicates sensitive handles into it. The pid is driver-reported (the frame channel's
|
||||
/// [`control::AddReply::wudf_pid`], or the gamepad bootstrap's `driver_pid`); a spoofed devnode / a
|
||||
/// tampered mailbox could name an arbitrary process to receive the channel, so this is the
|
||||
/// confused-deputy gate. Best-effort image-path identity is proportionate: a fully-compromised REAL
|
||||
/// driver is already a channel endpoint, and any *other* process (attacker exe, a non-driver pid)
|
||||
/// fails this WUDFHost image check. `what` names the channel in the error (e.g. `"frame-channel"`);
|
||||
/// shared with the gamepad sealed channel (`inject/windows/gamepad_raii.rs`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `process` must be a live process handle carrying `PROCESS_QUERY_LIMITED_INFORMATION`.
|
||||
pub(crate) unsafe fn verify_is_wudfhost(process: HANDLE, wudf_pid: u32, what: &str) -> Result<()> {
|
||||
let mut buf = [0u16; 512];
|
||||
let mut len = buf.len() as u32;
|
||||
// SAFETY: `process` carries QUERY_LIMITED per the contract; `buf`/`len` are a valid out-buffer and
|
||||
// its capacity, and on success `len` is updated to the count of UTF-16 units written (no NUL).
|
||||
unsafe {
|
||||
QueryFullProcessImageNameW(
|
||||
process,
|
||||
PROCESS_NAME_WIN32,
|
||||
PWSTR(buf.as_mut_ptr()),
|
||||
&mut len,
|
||||
)
|
||||
.with_context(|| format!("QueryFullProcessImageNameW on the {what} pid"))?;
|
||||
}
|
||||
let path = String::from_utf16_lossy(&buf[..len as usize]);
|
||||
let got = path.to_ascii_lowercase().replace('/', "\\");
|
||||
let sysroot = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
|
||||
let expected = format!("{}\\system32\\wudfhost.exe", sysroot.to_ascii_lowercase());
|
||||
if got != expected {
|
||||
bail!(
|
||||
"{what} pid {wudf_pid} is not the system WUDFHost (image={path:?}, expected \
|
||||
{expected:?}) — refusing to duplicate the channel's handles into it (spoofed driver / \
|
||||
wrong devnode?)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The sealed channel's handle-duplication broker (`design/idd-push-security.md`): the frame objects
|
||||
/// are unnamed, so the ONLY way the driver can reach them is handles this broker duplicates into its
|
||||
/// WUDFHost process and delivers — as bare handle VALUES — over the SYSTEM-only control device
|
||||
/// (`IOCTL_SET_FRAME_CHANNEL`). Ownership is a strict hand-off: on IOCTL success the DRIVER owns the
|
||||
/// duplicates (it closes them); on any failure [`Self::send`] reaps every duplicate it already made
|
||||
/// (`DUPLICATE_CLOSE_SOURCE`), so a half-delivered channel never leaks handles in WUDFHost.
|
||||
struct ChannelBroker {
|
||||
/// `PROCESS_DUP_HANDLE` handle to the driver's WUDFHost (pid from the ADD reply;
|
||||
/// `ProcessSharingDisabled` makes that process exclusively pf-vdisplay's).
|
||||
process: OwnedHandle,
|
||||
/// The pf-vdisplay control device — owned by the `VirtualDisplayManager`, never closed for the
|
||||
/// process lifetime, so holding the bare `HANDLE` is sound.
|
||||
control: HANDLE,
|
||||
}
|
||||
|
||||
impl ChannelBroker {
|
||||
/// Open the duplication target. Fails when the driver predates the sealed channel (`wudf_pid == 0`
|
||||
/// can't survive the v2 version handshake, but guard anyway) or the WUDFHost is gone (device
|
||||
/// restart mid-open) — either way the caller fails the capture open cleanly.
|
||||
///
|
||||
/// `wudf_pid` comes from the driver's ADD reply, so before we duplicate whole-desktop frame handles
|
||||
/// INTO it we VERIFY it is a genuine system WUDFHost ([`verify_is_wudfhost`]). Without that check a
|
||||
/// spoofed devnode (same interface GUID) could name an arbitrary process and receive the frames; a
|
||||
/// fully-compromised REAL pf_vdisplay driver is already a frame endpoint, so this specifically closes
|
||||
/// the reachable-without-owning-the-driver case (`design/idd-push-security.md` §hardening).
|
||||
fn open(wudf_pid: u32) -> Result<Self> {
|
||||
if wudf_pid == 0 {
|
||||
bail!("driver reported no WUDFHost pid for the frame channel");
|
||||
}
|
||||
let control = crate::vdisplay::manager::control_device_handle().context(
|
||||
"pf-vdisplay control device not open (monitor not created via the manager?)",
|
||||
)?;
|
||||
// SAFETY: plain FFI; `wudf_pid` is a copy. The handle (checked by `?`) is owned solely here and
|
||||
// moved into the `OwnedHandle` (single owner, closes on drop); `verify_is_wudfhost` borrows it
|
||||
// for the duration of the synchronous check and forms no lasting alias.
|
||||
let process = unsafe {
|
||||
let h = OpenProcess(
|
||||
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
false,
|
||||
wudf_pid,
|
||||
)
|
||||
.context("OpenProcess(PROCESS_DUP_HANDLE) on the driver's WUDFHost")?;
|
||||
let process = OwnedHandle::from_raw_handle(h.0 as _);
|
||||
verify_is_wudfhost(HANDLE(process.as_raw_handle()), wudf_pid, "frame-channel")?;
|
||||
process
|
||||
};
|
||||
Ok(Self { process, control })
|
||||
}
|
||||
|
||||
/// Duplicate `h` into the WUDFHost handle table, returning the handle VALUE valid there (and only
|
||||
/// there — the value is meaningless in any other process). `access = Some(rights)` grants the
|
||||
/// driver's handle exactly those rights (least privilege — see [`SECTION_MAP_RW`]);
|
||||
/// `access = None` copies the source handle's access (`DUPLICATE_SAME_ACCESS`), used only where the
|
||||
/// source is already scoped (the DXGI shared-texture handles, minted by `CreateSharedHandle` with
|
||||
/// just `DXGI_SHARED_RESOURCE_READ|WRITE`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle of the current process.
|
||||
unsafe fn dup_into(&self, h: HANDLE, access: Option<u32>) -> Result<u64> {
|
||||
let mut out = HANDLE::default();
|
||||
let (desired, options) = match access {
|
||||
Some(rights) => (rights, DUPLICATE_HANDLE_OPTIONS(0)),
|
||||
None => (0, DUPLICATE_SAME_ACCESS),
|
||||
};
|
||||
// SAFETY: `h` is live per the contract; `self.process` is the live PROCESS_DUP_HANDLE target;
|
||||
// `&mut out` is a valid out-param. Either an explicit least-privilege access mask (options == 0)
|
||||
// or `DUPLICATE_SAME_ACCESS` (desired ignored) — never both.
|
||||
unsafe {
|
||||
DuplicateHandle(
|
||||
GetCurrentProcess(),
|
||||
h,
|
||||
HANDLE(self.process.as_raw_handle()),
|
||||
&mut out,
|
||||
desired,
|
||||
false,
|
||||
options,
|
||||
)
|
||||
}
|
||||
.context("DuplicateHandle into the driver's WUDFHost")?;
|
||||
Ok(out.0 as usize as u64)
|
||||
}
|
||||
|
||||
/// Close a handle VALUE inside the WUDFHost table (the failure-path reaper): `DUPLICATE_CLOSE_SOURCE`
|
||||
/// with no target closes the source handle regardless of the (ignored) result.
|
||||
fn close_remote(&self, value: u64) {
|
||||
if value == 0 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: `self.process` is the live duplication target and `value` is a handle value THIS
|
||||
// broker just created in that process's table (callers only pass back `dup_into` results the
|
||||
// driver never received); closing it there cannot touch any other process's handles.
|
||||
unsafe {
|
||||
let _ = DuplicateHandle(
|
||||
HANDLE(self.process.as_raw_handle()),
|
||||
HANDLE(value as usize as *mut core::ffi::c_void),
|
||||
HANDLE::default(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
false,
|
||||
DUPLICATE_CLOSE_SOURCE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate the whole ring (header + event + every slot texture) into WUDFHost and deliver the
|
||||
/// values via `IOCTL_SET_FRAME_CHANNEL`. All-or-nothing: on any failure every duplicate already
|
||||
/// made is reaped remotely and an error returns (the caller fails the open / logs the recreate).
|
||||
/// The ownership contract with the driver is adopt-on-success only — it closes the handles iff the
|
||||
/// IOCTL succeeded, we reap them iff it didn't, so no value is ever closed twice.
|
||||
///
|
||||
/// # Safety
|
||||
/// `header` and `event` must be live handles of the current process (the capturer's own section +
|
||||
/// event, borrowed for this synchronous call).
|
||||
unsafe fn send(
|
||||
&self,
|
||||
target_id: u32,
|
||||
generation: u32,
|
||||
header: HANDLE,
|
||||
event: HANDLE,
|
||||
slots: &[HostSlot],
|
||||
) -> Result<()> {
|
||||
debug_assert!(slots.len() <= control::RING_LEN_USIZE);
|
||||
let mut req = control::SetFrameChannelRequest {
|
||||
target_id,
|
||||
generation,
|
||||
ring_len: slots.len() as u32,
|
||||
_pad: 0,
|
||||
header_handle: 0,
|
||||
event_handle: 0,
|
||||
texture_handles: [0; control::RING_LEN_USIZE],
|
||||
};
|
||||
// SAFETY: `header`/`event` are live per this fn's contract; each slot's `shared` is the live
|
||||
// `OwnedHandle` the slot keeps for exactly this purpose.
|
||||
let result = unsafe { self.duplicate_and_deliver(&mut req, header, event, slots) };
|
||||
if result.is_err() {
|
||||
// The driver never adopted the delivery — reap every remote duplicate so nothing lingers.
|
||||
self.close_remote(req.header_handle);
|
||||
self.close_remote(req.event_handle);
|
||||
for v in req.texture_handles {
|
||||
self.close_remote(v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// The fallible middle of [`Self::send`]: fill `req` with fresh duplicates, then issue the IOCTL.
|
||||
/// Split out so `send` can reap whatever landed in `req` when any step errors.
|
||||
///
|
||||
/// # Safety
|
||||
/// As [`Self::send`].
|
||||
unsafe fn duplicate_and_deliver(
|
||||
&self,
|
||||
req: &mut control::SetFrameChannelRequest,
|
||||
header: HANDLE,
|
||||
event: HANDLE,
|
||||
slots: &[HostSlot],
|
||||
) -> Result<()> {
|
||||
// SAFETY: forwarded from the caller's contract — `header`/`event`/each `slot.shared` are live
|
||||
// handles of this process, and `self.control` is the manager's control handle, never closed for
|
||||
// the process lifetime (`send_frame_channel`'s precondition).
|
||||
unsafe {
|
||||
// Least privilege per handle: the header maps read/write, the event is only signalled, and
|
||||
// the textures keep their already-scoped `CreateSharedHandle` access (see `dup_into`).
|
||||
req.header_handle = self.dup_into(header, Some(SECTION_MAP_RW))?;
|
||||
req.event_handle = self.dup_into(event, Some(EVENT_MODIFY_STATE))?;
|
||||
for (k, s) in slots.iter().enumerate() {
|
||||
req.texture_handles[k] = self.dup_into(HANDLE(s.shared.as_raw_handle()), None)?;
|
||||
}
|
||||
crate::vdisplay::pf_vdisplay::send_frame_channel(self.control, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates + owns the shared ring; yields the driver's frames as [`FramePayload::D3d11`].
|
||||
pub struct IddPushCapturer {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
target_id: u32,
|
||||
/// Owns the shared-header file mapping + its mapped view (RAII unmap-then-close). Declared BEFORE
|
||||
/// `header`, which is a raw pointer borrowed into this view via [`MappedSection::ptr`]. Never read
|
||||
/// directly (the `header` pointer is) — held purely so the mapping outlives the capturer.
|
||||
#[allow(dead_code)]
|
||||
/// `header`, which is a raw pointer borrowed into this view via [`MappedSection::ptr`]. Also the
|
||||
/// duplication source for the driver's header handle on every [`ChannelBroker::send`].
|
||||
section: MappedSection,
|
||||
header: *mut SharedHeader,
|
||||
event: OwnedHandle,
|
||||
/// Owns the bring-up debug section (mapping + view), or `None` when the debug block wasn't created.
|
||||
/// Never read directly (the `dbg_block` pointer is) — held purely for the RAII unmap/close.
|
||||
#[allow(dead_code)]
|
||||
dbg_section: Option<MappedSection>,
|
||||
dbg_block: *mut DebugBlock,
|
||||
/// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open
|
||||
/// and again on every ring recreate to deliver fresh duplicates.
|
||||
broker: ChannelBroker,
|
||||
width: u32,
|
||||
height: u32,
|
||||
slots: Vec<HostSlot>,
|
||||
/// The ring/texture generation, bumped every time the ring is recreated at a new format (the
|
||||
/// display's HDR mode flipped). Stamped into the texture names + the header so the driver re-attaches.
|
||||
/// display's HDR mode flipped). Stamped into the header + each delivery so the driver re-attaches
|
||||
/// (and so stale-ring publishes are rejected).
|
||||
generation: u32,
|
||||
/// The CLIENT's advertised 10-bit capability (= negotiated `bit_depth >= 10`). Only used at `open`
|
||||
/// to PROACTIVELY enable advanced color (so a 10-bit client gets HDR without a manual toggle); it
|
||||
@@ -228,25 +442,31 @@ pub struct IddPushCapturer {
|
||||
status_logged: bool,
|
||||
_keepalive: Box<dyn Send>,
|
||||
}
|
||||
// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader`/`*mut DebugBlock` raw
|
||||
// pointers (and the COM interfaces). It is created, used, and dropped by a SINGLE thread — the owning
|
||||
// capture/encode thread — never shared: the `ID3D11DeviceContext` is the device's IMMEDIATE context
|
||||
// (single-threaded by D3D11 contract) and is only ever touched from that thread, and the header/
|
||||
// dbg_block pointers (into mappings this struct owns) are only dereferenced there. `Send` transfers
|
||||
// ownership to one thread at a time with NO concurrent access; we do not (and must not) claim `Sync`.
|
||||
// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader` raw pointer (and the
|
||||
// COM interfaces / the broker's bare control `HANDLE`, which is process-global and never closed). It is
|
||||
// created, used, and dropped by a SINGLE thread — the owning capture/encode thread — never shared: the
|
||||
// `ID3D11DeviceContext` is the device's IMMEDIATE context (single-threaded by D3D11 contract) and is
|
||||
// only ever touched from that thread, and the header pointer (into the mapping this struct owns) is
|
||||
// only dereferenced there. `Send` transfers ownership to one thread at a time with NO concurrent
|
||||
// access; we do not (and must not) claim `Sync`.
|
||||
unsafe impl Send for IddPushCapturer {}
|
||||
|
||||
/// Build a `SECURITY_ATTRIBUTES` granting GENERIC_ALL to **SYSTEM** (the host creates+publishes the
|
||||
/// shared event + texture ring) and **LocalService** (the account the pf_vdisplay WUDFHost runs under,
|
||||
/// which consumes them) — `D:(A;;GA;;;SY)(A;;GA;;;LS)`, the same scoping as the gamepad section. The
|
||||
/// old SDDL granted **Everyone** (`WD`), which let any local user open the `Global\pfvd-*` objects and
|
||||
/// read captured screen frames (security-review 2026-06-28 #5). Verified on the RTX box (2026-06-29):
|
||||
/// the WUDFHost token is `S-1-5-19` (LocalService), SYSTEM integrity, zero restricted SIDs — so SY+LS
|
||||
/// suffices for the driver and excludes normal user processes. `psd` must outlive `sa`.
|
||||
/// Build a `SECURITY_ATTRIBUTES` granting GENERIC_ALL to **SYSTEM only** — `D:P(A;;GA;;;SY)`, protected
|
||||
/// (no inherited ACEs), `bInheritHandle: false`. The sealed channel makes this the strictly-minimal
|
||||
/// DACL: the objects are UNNAMED and the driver reaches them via **duplicated handles** (which carry the
|
||||
/// source handle's access — `OpenSharedResourceByName`/`OpenSharedResource1` on a handle does not
|
||||
/// re-check the object DACL against the opener), so the pf_vdisplay WUDFHost (LocalService) no longer
|
||||
/// needs a DACL ACE. Dropping the `LS` ACE removes the last theoretical surface where a leaked handle or
|
||||
/// a name-grown-by-accident could be opened by the (many-service-shared) LocalService SID. Empirically
|
||||
/// confirmed unreachable regardless: a LocalService token is DACL-denied `OpenProcess` on the WUDFHost
|
||||
/// (`PROCESS_DUP_HANDLE`/`VM_READ`/even `QUERY_LIMITED` → ACCESS_DENIED, tested on the RTX box
|
||||
/// 2026-07-03), so it cannot dup the handles out either. History: `Global\`-named + world-openable
|
||||
/// (`WD`, security-review 2026-06-28 #5) → SY+LS-scoped → nameless → now SY-only. `psd` must outlive
|
||||
/// `sa`. See `design/idd-push-security.md`.
|
||||
unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> {
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"),
|
||||
w!("D:P(A;;GA;;;SY)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
@@ -262,20 +482,18 @@ unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTO
|
||||
|
||||
impl IddPushCapturer {
|
||||
/// Create the `RING_LEN` shared keyed-mutex textures for one ring generation, at `format` (matched
|
||||
/// to the display's composition format — FP16 in HDR, BGRA in SDR). Each is shared by the name
|
||||
/// `pfvd-tex-<target>-<generation>-<k>` so the driver opens it; a fresh generation gives fresh names
|
||||
/// (so a recreate never collides with the old ring's not-yet-released handles).
|
||||
/// to the display's composition format — FP16 in HDR, BGRA in SDR). Each is shared through an
|
||||
/// UNNAMED NT handle (nothing to open by name — the sealed channel); the driver reaches it only via
|
||||
/// the duplicate the [`ChannelBroker`] sends after the ring is published.
|
||||
unsafe fn create_ring_slots(
|
||||
device: &ID3D11Device,
|
||||
target_id: u32,
|
||||
generation: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
format: DXGI_FORMAT,
|
||||
) -> Result<Vec<HostSlot>> {
|
||||
let (sa, _psd) = shared_object_sa()?;
|
||||
let mut slots = Vec::new();
|
||||
for k in 0..RING_LEN {
|
||||
for _ in 0..RING_LEN {
|
||||
let desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: w,
|
||||
Height: h,
|
||||
@@ -304,7 +522,7 @@ impl IddPushCapturer {
|
||||
.CreateSharedHandle(
|
||||
Some(&sa as *const SECURITY_ATTRIBUTES),
|
||||
DXGI_SHARED_RESOURCE_RW,
|
||||
&HSTRING::from(texture_name(target_id, generation, k)),
|
||||
PCWSTR::null(), // UNNAMED — reachable only through the broker's duplicate
|
||||
)
|
||||
.context("CreateSharedHandle(IDD-push ring slot)")?;
|
||||
// Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`).
|
||||
@@ -381,22 +599,22 @@ impl IddPushCapturer {
|
||||
// `u32` target id; they read/flip CCD display config and return owned values, borrowing nothing.
|
||||
// - `CreateDXGIFactory1`, `EnumAdapterByLuid`, `make_device`, `shared_object_sa`, `CreateFileMappingW`,
|
||||
// `MapViewOfFile`, `CreateEventW`, and `create_ring_slots` are all `?`-checked, so every returned
|
||||
// interface/handle/view is non-error before use; `&sa`/`&adapter`/`&device`/the `&HSTRING` names
|
||||
// are live borrows that outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid
|
||||
// because its backing `_psd` is held in scope for the whole block.
|
||||
// interface/handle/view is non-error before use; `&sa`/`&adapter`/`&device` are live borrows that
|
||||
// outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid because its backing
|
||||
// `_psd` is held in scope for the whole block.
|
||||
// - The header mapping is created AND viewed at `bytes == size_of::<SharedHeader>().max(64)`; the
|
||||
// view's null is checked (`bail!` on failure, after which the owned `map` closes the mapping). The
|
||||
// OS view base is page-aligned, so `section.ptr::<SharedHeader>()` is suitably aligned for a
|
||||
// `SharedHeader`, and `write_bytes(.., 0, bytes)` plus the `(*header).field = ..` writes all stay
|
||||
// within those `bytes` and write THROUGH the raw pointer without forming any `&mut`. The debug
|
||||
// section is the same pattern at `dbg_bytes == size_of::<DebugBlock>()`, only entered when its
|
||||
// own view is non-null.
|
||||
// within those `bytes` and write THROUGH the raw pointer without forming any `&mut`.
|
||||
// - The `magic` publish stores through `addr_of!((*header).magic) as *const AtomicU32`: `addr_of!`
|
||||
// takes the field address without a reference; the field is a 4-aligned `u32` (valid for
|
||||
// `AtomicU32`), and the `Release` store after the `Release` fence is the cross-process handshake
|
||||
// that orders all preceding writes before the driver may observe `MAGIC`.
|
||||
// - `header`/`dbg_block` point into the OS mappings, NOT into the `MappedSection` structs, so moving
|
||||
// `section`/`dbg_section` into `me` leaves them valid (see the `MappedSection` doc comment).
|
||||
// - `broker.send` requires live `header`/`event` handles of this process: both borrow the just-
|
||||
// created owned section/event for the duration of that synchronous call.
|
||||
// - `header` points into the OS mapping, NOT into the `MappedSection` struct, so moving `section`
|
||||
// into `me` leaves it valid (see the `MappedSection` doc comment).
|
||||
unsafe {
|
||||
// If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and
|
||||
// size the ring FP16 directly — don't race the advanced_color_enabled poll, which may not have
|
||||
@@ -428,14 +646,14 @@ impl IddPushCapturer {
|
||||
let (sa, _psd) = shared_object_sa()?;
|
||||
let bytes = std::mem::size_of::<SharedHeader>().max(64);
|
||||
|
||||
// Header.
|
||||
// Header — UNNAMED (the sealed channel: the driver gets a duplicated handle, not a name).
|
||||
let map = CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
bytes as u32,
|
||||
&HSTRING::from(header_name(target.target_id)),
|
||||
PCWSTR::null(),
|
||||
)
|
||||
.context("CreateFileMapping(IDD-push header)")?;
|
||||
// Own the mapping handle so it (and its view) free via `MappedSection` RAII even on bail.
|
||||
@@ -463,69 +681,45 @@ impl IddPushCapturer {
|
||||
// reads this into its `ring_format` and drops any surface that doesn't match.
|
||||
(*header).dxgi_format = ring_fmt.0 as u32;
|
||||
|
||||
// Frame-ready event (auto-reset).
|
||||
let event = CreateEventW(
|
||||
Some(&sa),
|
||||
false,
|
||||
false,
|
||||
&HSTRING::from(event_name(target.target_id)),
|
||||
)
|
||||
.context("CreateEvent(IDD-push)")?;
|
||||
// Frame-ready event (auto-reset) — UNNAMED, like everything on this channel.
|
||||
let event = CreateEventW(Some(&sa), false, false, PCWSTR::null())
|
||||
.context("CreateEvent(IDD-push)")?;
|
||||
let event = OwnedHandle::from_raw_handle(event.0 as _);
|
||||
|
||||
// Ring of shared keyed-mutex textures, format matched to the display's current mode.
|
||||
let slots =
|
||||
Self::create_ring_slots(&device, target.target_id, generation, w, h, ring_fmt)?;
|
||||
let slots = Self::create_ring_slots(&device, w, h, ring_fmt)?;
|
||||
|
||||
// Bring-up debug block (fixed name) — the driver writes diagnostics here. Best-effort.
|
||||
let dbg_bytes = std::mem::size_of::<DebugBlock>();
|
||||
let (dbg_section, dbg_block) = match CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
dbg_bytes as u32,
|
||||
&HSTRING::from(DBG_NAME),
|
||||
) {
|
||||
Ok(dm) => {
|
||||
// Own the mapping handle so it (and its view) free via `MappedSection` RAII.
|
||||
let dm = OwnedHandle::from_raw_handle(dm.0 as _);
|
||||
let dv = MapViewOfFile(
|
||||
HANDLE(dm.as_raw_handle()),
|
||||
FILE_MAP_ALL_ACCESS,
|
||||
0,
|
||||
0,
|
||||
dbg_bytes,
|
||||
);
|
||||
if dv.Value.is_null() {
|
||||
(None, std::ptr::null_mut()) // `dm` drops → mapping closed
|
||||
} else {
|
||||
let section = MappedSection {
|
||||
handle: dm,
|
||||
view: dv,
|
||||
};
|
||||
let p = section.ptr::<DebugBlock>();
|
||||
std::ptr::write_bytes(p.cast::<u8>(), 0, dbg_bytes);
|
||||
(*p).magic = DBG_MAGIC;
|
||||
(Some(section), p)
|
||||
}
|
||||
}
|
||||
Err(_) => (None, std::ptr::null_mut()),
|
||||
};
|
||||
|
||||
// Publish: magic LAST (Release) — signals the driver the ring is ready to open.
|
||||
// Publish: magic LAST (Release) — the ring must be fully initialized before the driver
|
||||
// (which receives the channel strictly afterwards) can observe MAGIC.
|
||||
std::sync::atomic::fence(Ordering::Release);
|
||||
(*(std::ptr::addr_of!((*header).magic) as *const AtomicU32))
|
||||
.store(MAGIC, Ordering::Release);
|
||||
|
||||
// Deliver the sealed channel: duplicate header + event + every slot texture into the
|
||||
// driver's WUDFHost and hand it the values over the control device. All-or-nothing (the
|
||||
// broker reaps its remote duplicates on failure), and a failure fails the open — without
|
||||
// the delivery the driver can never attach.
|
||||
let broker = ChannelBroker::open(target.wudf_pid)?;
|
||||
broker
|
||||
.send(
|
||||
target.target_id,
|
||||
generation,
|
||||
HANDLE(section.handle.as_raw_handle()),
|
||||
HANDLE(event.as_raw_handle()),
|
||||
&slots,
|
||||
)
|
||||
.context("deliver IDD-push frame channel to the driver")?;
|
||||
|
||||
tracing::info!(
|
||||
target_id = target.target_id,
|
||||
wudf_pid = target.wudf_pid,
|
||||
render_luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
|
||||
mode = format!("{w}x{h}"),
|
||||
display_hdr,
|
||||
client_10bit,
|
||||
ring_fp16 = display_hdr,
|
||||
"IDD push(host): created shared ring; waiting for the driver to attach + publish"
|
||||
"IDD push(host): created sealed ring + delivered the channel; waiting for the driver \
|
||||
to attach + publish"
|
||||
);
|
||||
let me = Self {
|
||||
device,
|
||||
@@ -534,8 +728,7 @@ impl IddPushCapturer {
|
||||
section,
|
||||
header,
|
||||
event,
|
||||
dbg_section,
|
||||
dbg_block,
|
||||
broker,
|
||||
width: w,
|
||||
height: h,
|
||||
slots,
|
||||
@@ -659,34 +852,6 @@ impl IddPushCapturer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Log the driver's bring-up diagnostics (the fixed-name debug block) — independent of the
|
||||
/// per-target header, so it tells us whether the swap-chain processor ran, what target_id it
|
||||
/// resolved, whether the header opened (+ error), and whether frames flowed.
|
||||
fn log_debug_block(&self) {
|
||||
if self.dbg_block.is_null() {
|
||||
tracing::warn!("IDD push DEBUG: no debug block");
|
||||
return;
|
||||
}
|
||||
// SAFETY: `self.dbg_block` was just checked non-null (the early return above); it points into the
|
||||
// owned `dbg_section` mapping sized exactly `size_of::<DebugBlock>()` and page-aligned, so it is
|
||||
// valid + aligned for `DebugBlock`. `d` is a short-lived SHARED reference used only to read the
|
||||
// fields below; we never form `&mut` into this region, and the driver's cross-process writes are
|
||||
// aligned `u32`s that don't tear (best-effort bring-up diagnostics).
|
||||
let d = unsafe { &*self.dbg_block };
|
||||
tracing::error!(
|
||||
run_core_entries = d.run_core_entries,
|
||||
resolved_target_id = d.resolved_target_id,
|
||||
header_open_attempts = d.header_open_attempts,
|
||||
last_open_error = format!("0x{:08x}", d.last_open_error),
|
||||
header_opened = d.header_opened,
|
||||
driver_render_luid = format!("{:08x}:{:08x}", d.render_luid_high, d.render_luid_low),
|
||||
frames_acquired = d.frames_acquired,
|
||||
"IDD push DEBUG: driver-reported diagnostics (run_core_entries=0 ⇒ swap-chain processor \
|
||||
never ran; resolved_target_id≠ours ⇒ name mismatch; last_open_error 0x80070002 ⇒ header \
|
||||
not found; frames_acquired=0 ⇒ idle display)"
|
||||
);
|
||||
}
|
||||
|
||||
/// The output texture format + the [`PixelFormat`] NVENC encodes, driven SOLELY by the DISPLAY's HDR
|
||||
/// state (like the WGC path): HDR → `P010` (BT.2020 PQ 10-bit limited) → NVENC Main10, and the client
|
||||
/// auto-detects PQ from the HEVC VUI; SDR → `Nv12` (BT.709 8-bit limited). Both are native YUV so
|
||||
@@ -712,9 +877,10 @@ impl IddPushCapturer {
|
||||
}
|
||||
|
||||
/// Recreate the ring at the format for `new_display_hdr` (the user flipped "Use HDR"). Bumps the
|
||||
/// generation so the driver re-attaches ([`is_stale`]) to the new-format textures; clears the
|
||||
/// header's `latest` so we don't consume a stale slot from the old ring; drops the conversion
|
||||
/// textures so they rebuild at the new format.
|
||||
/// generation so the driver re-attaches ([`is_stale`]) to the new-format textures and DELIVERS the
|
||||
/// new channel (fresh duplicates of the header + event + the new textures — every delivery is a
|
||||
/// self-contained handle set the driver owns); clears the header's `latest` so we don't consume a
|
||||
/// stale slot from the old ring; drops the conversion textures so they rebuild at the new format.
|
||||
fn recreate_ring(&mut self, new_display_hdr: bool, new_w: u32, new_h: u32) -> Result<()> {
|
||||
self.display_hdr = new_display_hdr;
|
||||
self.width = new_w;
|
||||
@@ -725,16 +891,8 @@ impl IddPushCapturer {
|
||||
// borrow of `self.device` (the capturer's own device, on which the slots are created) plus plain
|
||||
// `u32`/`DXGI_FORMAT` values, and `?` propagates any failure before the slots are used. Every
|
||||
// returned slot's texture + keyed mutex belongs to that same `self.device`.
|
||||
let new_slots = unsafe {
|
||||
Self::create_ring_slots(
|
||||
&self.device,
|
||||
self.target_id,
|
||||
new_gen,
|
||||
self.width,
|
||||
self.height,
|
||||
fmt,
|
||||
)?
|
||||
};
|
||||
let new_slots =
|
||||
unsafe { Self::create_ring_slots(&self.device, self.width, self.height, fmt)? };
|
||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
||||
// `SharedHeader`). The `latest`/`generation` stores go through `addr_of!`-formed field pointers (no
|
||||
// references) of correctly-aligned `u64`/`u32` fields, valid for `AtomicU64`/`AtomicU32`; the
|
||||
@@ -759,6 +917,26 @@ impl IddPushCapturer {
|
||||
}
|
||||
self.slots = new_slots; // drops the old slots → closes their shared handles + SRVs
|
||||
self.generation = new_gen;
|
||||
// Deliver the new generation's channel. The driver's old publisher sees the generation bump
|
||||
// (`is_stale`), drops (closing its old handles), and re-attaches from this delivery. On failure
|
||||
// the broker already reaped its remote duplicates; the recover-or-drop window in `try_consume`
|
||||
// then ends the session cleanly (the driver can never attach to an undelivered ring).
|
||||
// SAFETY: `broker.send` requires live `header`/`event` handles of this process — both borrow the
|
||||
// owned `self.section.handle`/`self.event` for the duration of the synchronous call.
|
||||
if let Err(e) = unsafe {
|
||||
self.broker.send(
|
||||
self.target_id,
|
||||
new_gen,
|
||||
HANDLE(self.section.handle.as_raw_handle()),
|
||||
HANDLE(self.event.as_raw_handle()),
|
||||
&self.slots,
|
||||
)
|
||||
} {
|
||||
tracing::warn!(
|
||||
error = %format!("{e:#}"),
|
||||
"IDD push: frame-channel re-delivery failed after ring recreate"
|
||||
);
|
||||
}
|
||||
self.last_seq = 0;
|
||||
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
|
||||
self.video_conv = None; // converters are sized + HDR-specific → rebuild at the new mode
|
||||
@@ -982,44 +1160,6 @@ impl IddPushCapturer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic observer (O3.1): create the IDD-push ring + debug block as the SYSTEM host (LocalSystem
|
||||
/// — proper privileges, the gamepad pattern) ALONGSIDE the normal WGC path, which provides the
|
||||
/// presentation trigger. Logs whether the driver's `run_core` ran and pushed frames into a
|
||||
/// host-created ring — resolving the `run_core=0` ambiguity (a user-created ring may be unwritable by
|
||||
/// the driver). Gated by `PUNKTFUNK_IDD_PUSH_OBSERVE`; spawns a short-lived sampling thread.
|
||||
pub fn spawn_observer(target: WinCaptureTarget, preferred: Option<(u32, u32, u32)>) {
|
||||
std::thread::spawn(move || {
|
||||
let tid = target.target_id;
|
||||
tracing::info!(
|
||||
target_id = tid,
|
||||
"IDD push OBSERVER: creating host ring (LocalSystem) + debug block alongside WGC"
|
||||
);
|
||||
match IddPushCapturer::open(target, preferred, false, Box::new(())) {
|
||||
Ok(mut cap) => {
|
||||
let mut frames = 0u32;
|
||||
for _ in 0..40 {
|
||||
match cap.try_consume() {
|
||||
Ok(Some(_)) => frames += 1,
|
||||
Ok(None) => {}
|
||||
Err(e) => tracing::warn!("IDD push OBSERVER: consume error: {e:#}"),
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(750));
|
||||
}
|
||||
tracing::info!(
|
||||
target_id = tid,
|
||||
frames_from_ring = frames,
|
||||
"IDD push OBSERVER: sampling done"
|
||||
);
|
||||
cap.log_debug_block();
|
||||
}
|
||||
Err((e, _keep)) => tracing::warn!(
|
||||
target_id = tid,
|
||||
"IDD push OBSERVER: ring open failed: {e:#}"
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The selected render GPU LUID (where the encoder runs), falling back to the monitor's `OsAdapterLuid`.
|
||||
fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID {
|
||||
if let Some(l) = crate::win_adapter::resolve_render_adapter_luid() {
|
||||
@@ -1046,7 +1186,6 @@ impl Capturer for IddPushCapturer {
|
||||
return Ok(f);
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
self.log_debug_block();
|
||||
// SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping — the same
|
||||
// best-effort diagnostic fields as `log_driver_status_once` (aligned word reads can't tear;
|
||||
// no reference into the shared region is formed).
|
||||
@@ -1093,8 +1232,10 @@ impl Capturer for IddPushCapturer {
|
||||
impl Drop for IddPushCapturer {
|
||||
fn drop(&mut self) {
|
||||
self.slots.clear();
|
||||
// The shared header + debug sections (`MappedSection`) and the frame-ready `event`
|
||||
// (`OwnedHandle`) free themselves via RAII (each unmaps its view, then closes its handle).
|
||||
// _keepalive drops after, REMOVEing the virtual display.
|
||||
// The shared header section (`MappedSection`), the frame-ready `event` (`OwnedHandle`) and the
|
||||
// broker's WUDFHost process handle free themselves via RAII (unmap view, then close handle) —
|
||||
// nothing of this session's channel outlives the capturer on the host side; the driver's
|
||||
// duplicates die with its publisher / monitor / WUDFHost (teardown invariant,
|
||||
// `design/idd-push-security.md`). _keepalive drops after, REMOVEing the virtual display.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/dualsense-driver`).
|
||||
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/drivers/pf-dualsense`).
|
||||
//!
|
||||
//! The Windows analogue of the Linux UHID backend ([`super::dualsense`]): same [`DsState`] model and
|
||||
//! the same byte-level report codec ([`super::dualsense_proto`]), but a different transport. Where
|
||||
//! the Linux backend writes report `0x01` to `/dev/uhid` and reads report `0x02` via `UHID_OUTPUT`,
|
||||
//! the Windows backend talks to the UMDF driver over a **named shared-memory section**
|
||||
//! `Global\pfds-shm-<idx>` (256 B: magic `u32@0`, input report `@8`, output seq `u32@72`, output
|
||||
//! report `@76`). The host creates the section (privileged → a permissive SDDL so the WUDFHost can
|
||||
//! open it); the driver maps it from its timer, feeds game `READ_REPORT`s from the input bytes, and
|
||||
//! publishes a game's `0x02` (rumble / lightbar / player-LEDs / adaptive triggers) into the output
|
||||
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
||||
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
||||
//! the Windows backend talks to the UMDF driver over an **unnamed shared DATA section** (256 B `PadShm`:
|
||||
//! magic `u32@0`, input report `@8`, output seq `u32@72`, output report `@76`) reached over the
|
||||
//! **sealed channel** ([`PadChannel`], `design/gamepad-channel-sealing.md`): the host duplicates the
|
||||
//! section handle into the driver's WUDFHost, bootstrapped via the named `Global\pfds-boot-<idx>`
|
||||
//! mailbox. The driver feeds game `READ_REPORT`s from the input bytes and publishes a game's `0x02`
|
||||
//! (rumble / lightbar / player-LEDs / adaptive triggers) into the output bytes. `hidclass` gates the
|
||||
//! device stack, so this user-mode IPC is the only viable channel (a UMDF driver has no control
|
||||
//! device); see `windows-dualsense-scoping.md`.
|
||||
//!
|
||||
//! Device lifecycle: each pad `SwDeviceCreate`s a `pf_pad_<index>` software devnode (hardware id
|
||||
//! `pf_dualsense`, enumerator `punktfunk`) on open and `SwDeviceClose`s it on drop, so the virtual
|
||||
@@ -20,12 +21,13 @@ use super::dualsense_proto::{
|
||||
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||
DS_TOUCH_W,
|
||||
};
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::ffi::c_void;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use windows::core::{w, GUID, HRESULT, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
@@ -49,17 +51,19 @@ pub(super) const OFF_DEVTYPE: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, device_type);
|
||||
pub(super) const OFF_DRIVER_PROTO: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, driver_proto);
|
||||
pub(super) const OFF_PAD_INDEX: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, pad_index);
|
||||
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSHOCK4;
|
||||
|
||||
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
||||
/// loads on it and the HID DualSense appears to games) plus the shared-memory section the driver maps.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
|
||||
struct DsWinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
seq: u8,
|
||||
@@ -184,7 +188,7 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// The pad index, stamped into the device Location — the driver reads it to map `pfds-shm-<index>`
|
||||
// The pad index, stamped into the device Location — the driver reads it to poll `pfds-boot-<index>`
|
||||
// (multi-pad). The buffer outlives the SwDeviceCreate call (we wait on the event before return).
|
||||
let loc: Vec<u16> = format!("{}", p.container_index)
|
||||
.encode_utf16()
|
||||
@@ -266,17 +270,20 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
}
|
||||
|
||||
impl DsWinPad {
|
||||
/// Create + map the section `Global\pfds-shm-<index>`, stamp the magic, then spawn the
|
||||
/// `root\pf_dualsense` devnode (the driver loads on it and maps the section). The devnode lives
|
||||
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
/// Create the sealed channel (unnamed DATA section + `Global\pfds-boot-<index>` mailbox), stamp
|
||||
/// the pad index + neutral report + the magic LAST, then spawn the `pf_pad_<index>` devnode (the
|
||||
/// driver loads on it and receives the DATA handle over the bootstrap). The devnode lives for the
|
||||
/// pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
fn open(index: u8) -> Result<DsWinPad> {
|
||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
|
||||
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// Stamp the pad index (the driver validates it on attach) + the neutral input report, then
|
||||
// the magic LAST (the driver only accepts the section once magic is set). The device-type
|
||||
// stays 0 (DualSense — the section arrives zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX/OFF_INPUT are in range.
|
||||
unsafe {
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS_INPUT_REPORT_LEN], {
|
||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
||||
@@ -286,7 +293,7 @@ impl DsWinPad {
|
||||
}
|
||||
// Spawn the per-session devnode via SwDeviceCreate; `SwDeviceClose` removes it on drop. On the
|
||||
// rare failure we keep the section + data plane and fall back to an out-of-band `pf_dualsense`
|
||||
// devnode (installer / dev-box devgen).
|
||||
// devnode (installer / dev-box devgen) — its persistent driver polls the same mailbox name.
|
||||
let inst = format!("pf_pad_{index}");
|
||||
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
@@ -302,14 +309,17 @@ impl DsWinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery so the driver holds the DATA section before hidclass asks it for
|
||||
// descriptors (the driver reads `device_type` from the section to pick its HID identity).
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(DsWinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_dualsense",
|
||||
"pf_dualsense.inf",
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
seq: 0,
|
||||
@@ -326,30 +336,40 @@ impl DsWinPad {
|
||||
serialize_state(&mut r, st, self.seq, self.ts);
|
||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||
std::ptr::copy_nonoverlapping(
|
||||
r.as_ptr(),
|
||||
self.channel.data_base().add(OFF_INPUT),
|
||||
r.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Poll the section's output slot; parse a new `0x02` report (rumble / LEDs / triggers) into a
|
||||
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything
|
||||
/// new. Also feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps
|
||||
/// `driver_proto` while it has the section mapped).
|
||||
/// new. Also ticks the sealed-channel delivery and feeds the driver-attach health watcher (the
|
||||
/// driver's ~125 Hz timer stamps `driver_proto` while it has the section mapped).
|
||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||
self.channel.pump();
|
||||
let mut fb = DsFeedback::default();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe {
|
||||
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
};
|
||||
self.attach.observe(proto);
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let seq =
|
||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
if seq != self.last_out_seq {
|
||||
self.last_out_seq = seq;
|
||||
let mut out = [0u8; 64];
|
||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.channel.data_base().add(OFF_OUTPUT),
|
||||
out.as_mut_ptr(),
|
||||
64,
|
||||
)
|
||||
};
|
||||
parse_ds_output(pad, &out, &mut fb);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
//! Virtual Sony DualShock 4 on Windows via the UMDF minidriver — the PS4 sibling of
|
||||
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the
|
||||
//! `Global\pfds-shm-<idx>` shared section the driver maps), same controller model ([`DsState`]); only
|
||||
//! the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the report codec
|
||||
//! ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4) into the
|
||||
//! section so the one UMDF driver serves the DS4 descriptor / attributes / features instead of the
|
||||
//! DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar (0xCD `Led`); a DS4
|
||||
//! has no adaptive triggers / player LEDs.
|
||||
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the sealed
|
||||
//! shared-memory channel bootstrapped via `Global\pfds-boot-<idx>`), same controller model
|
||||
//! ([`DsState`]); only the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the
|
||||
//! report codec ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4)
|
||||
//! into the DATA section so the one UMDF driver serves the DS4 descriptor / attributes / features
|
||||
//! instead of the DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar
|
||||
//! (0xCD `Led`); a DS4 has no adaptive triggers / player LEDs.
|
||||
|
||||
use super::dualsense_proto::DsState;
|
||||
use super::dualsense_windows::{
|
||||
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT,
|
||||
OFF_OUTPUT, OFF_OUT_SEQ, SHM_MAGIC, SHM_SIZE,
|
||||
OFF_OUTPUT, OFF_OUT_SEQ, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE,
|
||||
};
|
||||
use super::dualshock4_proto::{
|
||||
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
||||
};
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::Result;
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::HSTRING;
|
||||
|
||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the mapped
|
||||
/// shared section. Dropping it removes the devnode and unmaps + closes the section.
|
||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the sealed
|
||||
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
|
||||
struct Ds4WinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
counter: u8,
|
||||
@@ -36,16 +36,19 @@ struct Ds4WinPad {
|
||||
}
|
||||
|
||||
impl Ds4WinPad {
|
||||
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
|
||||
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
|
||||
/// Create the sealed channel, stamp `device_type = DualShock 4` + the pad index + a neutral
|
||||
/// report + the magic LAST, then spawn the `pf_ds4_<index>` devnode (the driver loads on it and
|
||||
/// receives the DATA handle over the bootstrap).
|
||||
fn open(index: u8) -> Result<Ds4WinPad> {
|
||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// device-type FIRST (so it's visible the moment magic is), pad index, neutral report,
|
||||
// magic LAST.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = DEVTYPE_DUALSHOCK4;
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS4_INPUT_REPORT_LEN], {
|
||||
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
|
||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
||||
@@ -68,14 +71,18 @@ impl Ds4WinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery — for the DS4 this is what closes the identity race: the driver
|
||||
// must read `device_type = 1` from the delivered DATA section before hidclass asks it for
|
||||
// descriptors, or the pad would enumerate with the (default) DualSense identity.
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(Ds4WinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_dualshock4",
|
||||
"pf_dualsense.inf", // one driver package serves both HID identities
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
counter: 0,
|
||||
@@ -92,29 +99,40 @@ impl Ds4WinPad {
|
||||
serialize_state(&mut r, st, self.counter, self.ts);
|
||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||
std::ptr::copy_nonoverlapping(
|
||||
r.as_ptr(),
|
||||
self.channel.data_base().add(OFF_INPUT),
|
||||
r.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Poll the section's output slot; parse a new `0x05` report (rumble / lightbar) into a
|
||||
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new. Also
|
||||
/// feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps `driver_proto`).
|
||||
/// ticks the sealed-channel delivery and feeds the driver-attach health watcher (the driver's
|
||||
/// ~125 Hz timer stamps `driver_proto`).
|
||||
fn service(&mut self) -> Ds4Feedback {
|
||||
self.channel.pump();
|
||||
let mut fb = Ds4Feedback::default();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe {
|
||||
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
};
|
||||
self.attach.observe(proto);
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let seq =
|
||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
if seq != self.last_out_seq {
|
||||
self.last_out_seq = seq;
|
||||
let mut out = [0u8; 64];
|
||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.channel.data_base().add(OFF_OUTPUT),
|
||||
out.as_mut_ptr(),
|
||||
64,
|
||||
)
|
||||
};
|
||||
parse_ds4_output(&out, &mut fb);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
//! Per-pad Windows resource RAII for the gamepad backends (DualSense / DualShock 4 / XUSB).
|
||||
//! Per-pad Windows resource RAII + the **sealed gamepad channel** broker (DualSense / DualShock 4 /
|
||||
//! XUSB backends).
|
||||
//!
|
||||
//! Each virtual pad owns two OS resources: the named shared-memory section (+ its mapped view) the
|
||||
//! `pf_dualsense`/`pf_xusb` driver reads, and the `SwDeviceCreate`'d software devnode the driver loads
|
||||
//! on. Before this module, all three backends hand-rolled the same `CreateFileMappingW` +
|
||||
//! `MapViewOfFile` and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` —
|
||||
//! easy to drift or leak on an error path. [`Shm`] and [`SwDevice`] own those resources with RAII, so a
|
||||
//! backend just holds them and the cleanup (and ordering) happens by construction.
|
||||
//! Each virtual pad owns three OS resources: the **unnamed** DATA section the `pf_dualsense`/`pf_xusb`
|
||||
//! driver works against (`XusbShm`/`PadShm`), the tiny **named** bootstrap mailbox
|
||||
//! (`pf_driver_proto::gamepad::PadBootstrap`) that hands the driver a duplicated handle to it, and the
|
||||
//! `SwDeviceCreate`'d software devnode the driver loads on. [`Shm`] and [`SwDevice`] own the resources
|
||||
//! with RAII; [`PadChannel`] owns the two sections plus the delivery handshake.
|
||||
//!
|
||||
//! **Why the channel is sealed** (`design/gamepad-channel-sealing.md`): the DATA section used to be a
|
||||
//! `Global\pf…-shm-<index>` named section with an SY+LS DACL, which let any *sibling LocalService*
|
||||
//! process open it by name to read the live controller input or inject/forge input and rumble — the
|
||||
//! same name-open vector the frame ring closed (`design/idd-push-security.md`). The DATA section is now
|
||||
//! UNNAMED with a SYSTEM-only DACL and reaches the driver exclusively as a handle this host duplicated
|
||||
//! into its WUDFHost (a duplicated handle carries the source's access, so no LS ACE is needed). The pad
|
||||
//! drivers are UMDF HID minidrivers with **no control device** (hidclass owns the stack), so unlike the
|
||||
//! frame channel there is no IOCTL to deliver the handle or learn the WUDFHost pid — hence the
|
||||
//! late-bound [`PadBootstrap`] mailbox handshake, the one *named* object left. It carries only pids and
|
||||
//! a handle VALUE (meaningless outside the target process), so tampering with it yields at worst a
|
||||
//! gamepad DoS, never a read or an injection; the empirical floor from the frame work holds here too
|
||||
//! (a LocalService token is DACL-denied `OpenProcess` on a UMDF WUDFHost for every access right).
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::os::windows::io::{FromRawHandle, OwnedHandle};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use pf_driver_proto::gamepad::{PadBootstrap, BOOT_MAGIC, GAMEPAD_PROTO_VERSION};
|
||||
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
||||
use std::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::{w, HSTRING, PCWSTR};
|
||||
@@ -17,7 +32,10 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_PROB, CR_SUCCESS, DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED,
|
||||
};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
||||
use windows::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows::Win32::Foundation::{
|
||||
DuplicateHandle, GetLastError, SetLastError, DUPLICATE_HANDLE_OPTIONS, ERROR_ALREADY_EXISTS,
|
||||
HANDLE, INVALID_HANDLE_VALUE, WIN32_ERROR,
|
||||
};
|
||||
use windows::Win32::Security::Authorization::{
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
||||
};
|
||||
@@ -26,54 +44,102 @@ use windows::Win32::System::Memory::{
|
||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
||||
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
|
||||
};
|
||||
use windows::Win32::System::Threading::{
|
||||
GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
/// A named, anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps
|
||||
/// the view, then the [`OwnedHandle`] closes the section handle (in that order). Replaces the three
|
||||
/// backends' hand-duplicated `CreateFileMappingW` + `MapViewOfFile` + manual `Drop`.
|
||||
///
|
||||
/// SDDL `D:(A;;GA;;;SY)(A;;GA;;;LS)`: GENERIC_ALL to **SYSTEM** (the host creates the section and
|
||||
/// writes the live HID input report into it) and **LocalService** (the account the UMDF driver's
|
||||
/// WUDFHost runs under, which reads it). The old SDDL granted **Everyone** (`WD`) — on the (mistaken)
|
||||
/// assumption the driver needed a restricted token's broad access — letting any local user
|
||||
/// `OpenFileMapping` the section to inject controller input or tamper the trusted channel
|
||||
/// (security-review 2026-06-28 #5). Verified on the RTX box (2026-06-29): the WUDFHost token is
|
||||
/// `S-1-5-19` (LocalService), SYSTEM integrity, with **zero restricted SIDs** — so scoping to SY+LS is
|
||||
/// sufficient for the driver and excludes normal (medium-IL, non-service) user processes.
|
||||
/// Least access the pad driver needs on the duplicated DATA section: it only MAPS it read/write, so
|
||||
/// `SECTION_MAP_READ | SECTION_MAP_WRITE` (== the driver's `FILE_MAP_RW`). Granted explicitly in
|
||||
/// [`PadChannel::deliver_to`] instead of `DUPLICATE_SAME_ACCESS` (least privilege for the sealed
|
||||
/// section — the driver's handle then can't take ownership / change security / delete the object).
|
||||
const SECTION_MAP_RW: u32 = 0x0004 | 0x0002;
|
||||
|
||||
/// An anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps the
|
||||
/// view, then the [`OwnedHandle`] closes the section handle (in that order). Created either
|
||||
/// [unnamed](Self::create_unnamed) (the sealed DATA section — reachable only by handle duplication) or
|
||||
/// [named](Self::create_named) (the bootstrap mailbox the driver opens by name).
|
||||
pub(super) struct Shm {
|
||||
/// Owns the section handle (closed on drop). Held only for ownership — never read after construction.
|
||||
_handle: OwnedHandle,
|
||||
/// Owns the section handle (closed on drop). Also the duplication source for the sealed channel —
|
||||
/// see [`Shm::raw_handle`].
|
||||
handle: OwnedHandle,
|
||||
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
||||
}
|
||||
|
||||
/// Build a `SECURITY_ATTRIBUTES` from an SDDL literal (`psd` is OS-allocated and leaked — acceptable
|
||||
/// for the handful of pad channels a host creates; it must outlive the returned `SECURITY_ATTRIBUTES`).
|
||||
fn sddl_sa(sddl: PCWSTR) -> Result<SECURITY_ATTRIBUTES> {
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (leaked — see above).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
sddl,
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
Ok(SECURITY_ATTRIBUTES {
|
||||
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Shm {
|
||||
/// Create + zero a `size`-byte section named `name`, mapped read/write. The section handle is owned
|
||||
/// immediately, so any failure below (or the returned `Shm`'s drop) closes it.
|
||||
pub(super) fn create(name: &HSTRING, size: usize) -> Result<Shm> {
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (freed at process
|
||||
// exit — acceptable for a host-lifetime object).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
/// Create + zero an **unnamed** `size`-byte section, mapped read/write — the sealed DATA section.
|
||||
/// SDDL `D:P(A;;GA;;;SY)` (SYSTEM-only, protected): with no name there is nothing to enumerate,
|
||||
/// open, or squat, and the driver reaches it through a duplicated handle, which carries the
|
||||
/// source's access without re-checking the object DACL (the exact property the frame ring
|
||||
/// validated on-glass — `design/idd-push-security.md`).
|
||||
pub(super) fn create_unnamed(size: usize) -> Result<Shm> {
|
||||
let sa = sddl_sa(w!("D:P(A;;GA;;;SY)"))?;
|
||||
Self::create_inner(&sa, PCWSTR::null(), size).context("create unnamed gamepad DATA section")
|
||||
}
|
||||
|
||||
/// Create + zero a **named** `size`-byte section, mapped read/write — the bootstrap mailbox. SDDL
|
||||
/// `D:(A;;GA;;;SY)(A;;GA;;;LS)`: SYSTEM (this host) + LocalService (the driver's WUDFHost opens it
|
||||
/// by name). Safe to leave name-openable because it carries nothing exploitable (see the module
|
||||
/// docs). **Squat-checked**: `Global\` names are creatable by any service holding
|
||||
/// `SeCreateGlobalPrivilege` (LocalService has it), so if the name already exists —
|
||||
/// `ERROR_ALREADY_EXISTS`, meaning `CreateFileMappingW` silently *opened* a pre-existing object we
|
||||
/// don't control — we close and retry briefly (our own driver holds the name for microseconds per
|
||||
/// poll tick), then fail loudly rather than run the handshake through an attacker-owned (or
|
||||
/// another host instance's) mailbox.
|
||||
pub(super) fn create_named(name: &HSTRING, size: usize) -> Result<Shm> {
|
||||
let sa = sddl_sa(w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"))?;
|
||||
for attempt in 0..5 {
|
||||
if attempt > 0 {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
// SAFETY: clearing the thread error slot so ERROR_ALREADY_EXISTS below is unambiguous.
|
||||
unsafe { SetLastError(WIN32_ERROR(0)) };
|
||||
let shm = Self::create_inner(&sa, PCWSTR(name.as_ptr()), size)
|
||||
.with_context(|| format!("create gamepad bootstrap mailbox {name}"))?;
|
||||
// SAFETY: read immediately after the create; windows-rs only touches the error slot on
|
||||
// failure, so a success here preserves CreateFileMappingW's ALREADY_EXISTS signal.
|
||||
if unsafe { GetLastError() } != ERROR_ALREADY_EXISTS {
|
||||
return Ok(shm);
|
||||
}
|
||||
// `shm` drops here → unmap + close our handle to the foreign object, then retry.
|
||||
}
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the SDDL above.
|
||||
bail!(
|
||||
"bootstrap mailbox {name} already exists and stayed alive across retries — another \
|
||||
punktfunk-host instance is serving this pad index, or a local service is squatting the \
|
||||
name (gamepad DoS attempt?)"
|
||||
);
|
||||
}
|
||||
|
||||
fn create_inner(sa: &SECURITY_ATTRIBUTES, name: PCWSTR, size: usize) -> Result<Shm> {
|
||||
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the caller's SDDL; the
|
||||
// descriptor behind `sa` outlives this call (leaked by `sddl_sa`).
|
||||
let map = unsafe {
|
||||
CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
Some(sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
size as u32,
|
||||
PCWSTR(name.as_ptr()),
|
||||
name,
|
||||
)?
|
||||
};
|
||||
// SAFETY: `map` is a fresh section handle we own; take ownership immediately so that the early
|
||||
@@ -84,14 +150,11 @@ impl Shm {
|
||||
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size) };
|
||||
if view.Value.is_null() {
|
||||
// `handle` drops here → closes the section. No view to unmap.
|
||||
return Err(anyhow!("MapViewOfFile failed for {name}"));
|
||||
return Err(anyhow!("MapViewOfFile failed"));
|
||||
}
|
||||
// SAFETY: `view` points at `size` writable bytes (just mapped).
|
||||
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
|
||||
Ok(Shm {
|
||||
_handle: handle,
|
||||
view,
|
||||
})
|
||||
Ok(Shm { handle, view })
|
||||
}
|
||||
|
||||
/// The mapped section's base pointer. Stable for the `Shm`'s lifetime (moving the `Shm` does not
|
||||
@@ -99,11 +162,16 @@ impl Shm {
|
||||
pub(super) fn base(&self) -> *mut u8 {
|
||||
self.view.Value as *mut u8
|
||||
}
|
||||
|
||||
/// The section handle as a borrowed `HANDLE` (the sealed channel's duplication source).
|
||||
fn raw_handle(&self) -> HANDLE {
|
||||
HANDLE(self.handle.as_raw_handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shm {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `view` came from `MapViewOfFile`; unmap it BEFORE the `_handle` field closes the
|
||||
// SAFETY: `view` came from `MapViewOfFile`; unmap it BEFORE the `handle` field closes the
|
||||
// section (struct fields drop only after this `Drop::drop` returns).
|
||||
unsafe {
|
||||
let _ = UnmapViewOfFile(self.view);
|
||||
@@ -111,6 +179,230 @@ impl Drop for Shm {
|
||||
}
|
||||
}
|
||||
|
||||
// ── The sealed-channel bootstrap broker ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Global delivery sequence for [`PadBootstrap::handle_seq`] — host-wide monotonic and never 0, so two
|
||||
/// consecutive pads on the same index can't hand the (persistent, out-of-band-devnode) driver the same
|
||||
/// seq twice. Starts at 1.
|
||||
static BOOT_SEQ: AtomicU32 = AtomicU32::new(1);
|
||||
|
||||
/// Hard cap on delivery attempts per pad: each attempt duplicates a handle into a WUDFHost, so a
|
||||
/// tampered mailbox flapping `driver_pid` must not mint unbounded remote handles (DoS containment).
|
||||
/// A legitimate pad needs exactly one (a driver restart within one pad lifetime is not a thing —
|
||||
/// the WUDFHost dies with the devnode).
|
||||
const MAX_DELIVERY_ATTEMPTS: u32 = 16;
|
||||
|
||||
/// One pad's sealed host↔driver channel: the unnamed DATA section (the real `XusbShm`/`PadShm`), the
|
||||
/// named bootstrap mailbox, and the delivery state machine ([`Self::pump`]) that hands the driver's
|
||||
/// WUDFHost a duplicated DATA handle once it publishes its pid. Owns both sections (RAII teardown —
|
||||
/// dropping the channel closes the mailbox, whose *name* then disappears, which is how a persistent
|
||||
/// (out-of-band-devnode) driver detects the host is gone).
|
||||
pub(super) struct PadChannel {
|
||||
data: Shm,
|
||||
boot: Shm,
|
||||
boot_name: String,
|
||||
/// Last `driver_pid` acted on (delivered or rejected) — never retry the same value, so a failed
|
||||
/// verify can't be spun into a hot loop by a static mailbox.
|
||||
last_seen_pid: u32,
|
||||
attempts: u32,
|
||||
delivered: bool,
|
||||
warned_proto: bool,
|
||||
warned_cap: bool,
|
||||
}
|
||||
|
||||
impl PadChannel {
|
||||
/// Create the unnamed DATA section (`data_size` bytes, zeroed — the caller stamps its layout and
|
||||
/// magic) plus the named bootstrap mailbox, stamped `host_proto` first and `BOOT_MAGIC` last so a
|
||||
/// driver only trusts a fully-initialized mailbox.
|
||||
pub(super) fn create(boot_name: String, data_size: usize) -> Result<PadChannel> {
|
||||
let data = Shm::create_unnamed(data_size)?;
|
||||
let boot = Shm::create_named(
|
||||
&HSTRING::from(boot_name.as_str()),
|
||||
core::mem::size_of::<PadBootstrap>(),
|
||||
)?;
|
||||
let base = boot.base();
|
||||
// SAFETY: `base` is the live, page-aligned mailbox view (>= size_of::<PadBootstrap>()); the
|
||||
// field offsets are pinned by the proto's asserts and naturally aligned, so the atomic views
|
||||
// are valid. `host_proto` is published BEFORE `magic` (Release) — a driver that observes the
|
||||
// magic (Acquire) sees the version.
|
||||
unsafe {
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, host_proto)) as *const AtomicU32))
|
||||
.store(GAMEPAD_PROTO_VERSION, Ordering::Relaxed);
|
||||
fence(Ordering::Release);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, magic)) as *const AtomicU32))
|
||||
.store(BOOT_MAGIC, Ordering::Release);
|
||||
}
|
||||
Ok(PadChannel {
|
||||
data,
|
||||
boot,
|
||||
boot_name,
|
||||
last_seen_pid: 0,
|
||||
attempts: 0,
|
||||
delivered: false,
|
||||
warned_proto: false,
|
||||
warned_cap: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// The DATA section's mapped base (the host side of `XusbShm`/`PadShm`).
|
||||
pub(super) fn data_base(&self) -> *mut u8 {
|
||||
self.data.base()
|
||||
}
|
||||
|
||||
/// The bootstrap mailbox name (log labelling).
|
||||
pub(super) fn boot_name(&self) -> &str {
|
||||
&self.boot_name
|
||||
}
|
||||
|
||||
/// Atomic `u32` load from a mailbox field.
|
||||
fn boot_load(&self, off: usize) -> u32 {
|
||||
// SAFETY: the mailbox view is live (owned by `self.boot`), page-aligned, and every
|
||||
// `PadBootstrap` u32 field offset is 4-aligned (proto asserts), so the atomic view is valid;
|
||||
// no reference into the shared region outlives the load.
|
||||
unsafe { (*(self.boot.base().add(off) as *const AtomicU32)).load(Ordering::Acquire) }
|
||||
}
|
||||
|
||||
/// One tick of the delivery state machine — called from the pad's regular service pump (≤4 ms
|
||||
/// cadence) and from [`Self::deliver_eager`]. Cheap when idle: two atomic loads.
|
||||
pub(super) fn pump(&mut self) {
|
||||
// Version diagnostics: the driver writes its own proto version even when it refuses to
|
||||
// publish a pid (host/driver mismatch), so the operator sees WHY the pad never attaches.
|
||||
let drv_proto = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_proto));
|
||||
if drv_proto != 0 && drv_proto != GAMEPAD_PROTO_VERSION && !self.warned_proto {
|
||||
self.warned_proto = true;
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
driver_proto = drv_proto,
|
||||
host_proto = GAMEPAD_PROTO_VERSION,
|
||||
"gamepad driver/host protocol mismatch on the bootstrap mailbox — update the \
|
||||
drivers: punktfunk-host.exe driver install --gamepad"
|
||||
);
|
||||
}
|
||||
let pid = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_pid));
|
||||
if pid == 0 || pid == self.last_seen_pid {
|
||||
return;
|
||||
}
|
||||
self.last_seen_pid = pid;
|
||||
if self.attempts >= MAX_DELIVERY_ATTEMPTS {
|
||||
if !self.warned_cap {
|
||||
self.warned_cap = true;
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
attempts = self.attempts,
|
||||
"gamepad channel delivery cap reached — the bootstrap mailbox keeps changing \
|
||||
its driver pid (tampering?); no further handles will be duplicated"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.attempts += 1;
|
||||
match self.deliver_to(pid) {
|
||||
Ok(seq) => {
|
||||
self.delivered = true;
|
||||
tracing::info!(
|
||||
mailbox = %self.boot_name,
|
||||
wudf_pid = pid,
|
||||
seq,
|
||||
"sealed gamepad channel delivered (DATA handle duplicated into the driver's \
|
||||
WUDFHost)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
pid,
|
||||
error = %format!("{e:#}"),
|
||||
"sealed gamepad channel delivery failed — will retry when the mailbox reports \
|
||||
a different driver pid"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate the DATA section into `pid`'s handle table (after verifying it is a genuine
|
||||
/// WUDFHost) and publish the handle value + owning pid, bumping `handle_seq` LAST. The driver
|
||||
/// adopts the handle by consuming the delivery; an unconsumed duplicate dies with the target
|
||||
/// process (nothing to reap — there is no fallible step after the duplication).
|
||||
fn deliver_to(&self, pid: u32) -> Result<u32> {
|
||||
// SAFETY: plain FFI; the handle (checked by `?`) is owned solely here and moved into the
|
||||
// `OwnedHandle` (single owner, closes on drop); `verify_is_wudfhost` borrows it for the
|
||||
// synchronous check and forms no lasting alias.
|
||||
let process = unsafe {
|
||||
let h = OpenProcess(
|
||||
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
false,
|
||||
pid,
|
||||
)
|
||||
.context("OpenProcess(PROCESS_DUP_HANDLE) on the mailbox-reported pid")?;
|
||||
let process = OwnedHandle::from_raw_handle(h.0 as _);
|
||||
crate::capture::idd_push::verify_is_wudfhost(
|
||||
HANDLE(process.as_raw_handle()),
|
||||
pid,
|
||||
"gamepad-channel",
|
||||
)?;
|
||||
process
|
||||
};
|
||||
let mut remote = HANDLE::default();
|
||||
// SAFETY: `self.data.raw_handle()` is the live section handle this channel owns;
|
||||
// `process` is the live PROCESS_DUP_HANDLE target; `&mut remote` is a valid out-param.
|
||||
// Least privilege: the pad driver only MAPS the DATA section read/write (its `FILE_MAP_RW` =
|
||||
// `SECTION_MAP_READ | SECTION_MAP_WRITE`), so grant exactly that instead of copying our
|
||||
// full-access creator handle via `DUPLICATE_SAME_ACCESS` (Chen: don't over-grant unnamed
|
||||
// shared objects — a compromised driver's handle then can't `WRITE_DAC`/`DELETE` the section).
|
||||
unsafe {
|
||||
DuplicateHandle(
|
||||
GetCurrentProcess(),
|
||||
self.data.raw_handle(),
|
||||
HANDLE(process.as_raw_handle()),
|
||||
&mut remote,
|
||||
SECTION_MAP_RW,
|
||||
false,
|
||||
DUPLICATE_HANDLE_OPTIONS(0),
|
||||
)
|
||||
.context("DuplicateHandle(gamepad DATA section) into the driver's WUDFHost")?;
|
||||
}
|
||||
let value = remote.0 as usize as u64;
|
||||
let base = self.boot.base();
|
||||
let seq = BOOT_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
// SAFETY: live, page-aligned mailbox view; `data_handle` is 8-aligned and `handle_pid`/
|
||||
// `handle_seq` 4-aligned (proto asserts). The handle value + owning pid are published BEFORE
|
||||
// the seq (Release) — a driver that observes the new seq (Acquire) sees a complete delivery.
|
||||
unsafe {
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, data_handle)) as *const AtomicU64))
|
||||
.store(value, Ordering::Relaxed);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_pid)) as *const AtomicU32))
|
||||
.store(pid, Ordering::Relaxed);
|
||||
fence(Ordering::Release);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_seq)) as *const AtomicU32))
|
||||
.store(seq, Ordering::Release);
|
||||
}
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
/// Bounded wait at pad-open: pump until the mailbox produces a driver pid we act on (delivered or
|
||||
/// rejected) or `timeout` passes. Closes the identity race for the DualShock 4 (the driver reads
|
||||
/// `device_type` from the DATA section when hidclass asks for descriptors — the channel should be
|
||||
/// attached by then); the regular service pump takes over afterwards either way.
|
||||
pub(super) fn deliver_eager(&mut self, timeout: Duration) {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
self.pump();
|
||||
if self.last_seen_pid != 0 || Instant::now() >= deadline {
|
||||
if !self.delivered {
|
||||
tracing::debug!(
|
||||
mailbox = %self.boot_name,
|
||||
"eager gamepad-channel delivery window passed without an attach — the \
|
||||
service pump keeps polling (driver-attach diagnosis follows if it stays \
|
||||
silent)"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `SwDeviceCreate`'d software devnode; drop removes it via `SwDeviceClose`. Replaces the manual
|
||||
/// `SwDeviceClose` each backend used to call in its `Drop`.
|
||||
pub(super) struct SwDevice(HSWDEVICE);
|
||||
@@ -151,7 +443,7 @@ pub(super) struct DriverAttach {
|
||||
inf: &'static str,
|
||||
/// The driver's own debug log, referenced in the diagnosis line.
|
||||
driver_log: &'static str,
|
||||
/// Section name, for log lines.
|
||||
/// Bootstrap-mailbox name, for log lines (the DATA section is unnamed).
|
||||
shm_name: String,
|
||||
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
|
||||
instance_id: Option<String>,
|
||||
@@ -241,8 +533,8 @@ impl DriverAttach {
|
||||
devnode = %devnode,
|
||||
driver_log = self.driver_log,
|
||||
"gamepad driver has not attached to the shared section — the virtual pad exists but no \
|
||||
driver is serving it (games will not see it); an old (pre-health) driver also reads as \
|
||||
not-attached: update with punktfunk-host.exe driver install --gamepad"
|
||||
driver is serving it (games will not see it); an old (pre-sealed-channel) driver also \
|
||||
reads as not-attached: update with punktfunk-host.exe driver install --gamepad"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
//! Windows virtual Xbox 360 gamepad via the punktfunk **XUSB companion** UMDF driver
|
||||
//! (`packaging/windows/xusb-driver`) — the in-tree replacement for ViGEmBus. One virtual Xbox 360
|
||||
//! (`packaging/windows/drivers/pf-xusb`) — the in-tree replacement for ViGEmBus. One virtual Xbox 360
|
||||
//! controller per client pad index, visible to classic **XInput** (`XInputGetState`) with no kernel
|
||||
//! bus driver: each pad `SwDeviceCreate`s a `pf_xusb_<index>` devnode (the driver loads on it and
|
||||
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into the shared section
|
||||
//! `Global\pfxusb-shm-<index>`. GameStream/Moonlight already speak the XInput conventions (low-16
|
||||
//! button bits, sticks −32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
|
||||
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into an **unnamed** shared
|
||||
//! DATA section the driver reaches over the **sealed channel** ([`PadChannel`] — handle duplicated
|
||||
//! into its WUDFHost, bootstrapped via `Global\pfxusb-boot-<index>`; see
|
||||
//! `design/gamepad-channel-sealing.md`). GameStream/Moonlight already speak the XInput conventions
|
||||
//! (low-16 button bits, sticks −32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
|
||||
//!
|
||||
//! Rumble flows back the other way: a game writes force-feedback via `XInputSetState`, the driver
|
||||
//! parses the `SET_STATE` packet into the shared section, and [`GamepadManager::pump_rumble`] relays
|
||||
//! level changes to the client (the universal 0xCA plane), mirroring the Linux `EV_FF` read path.
|
||||
//!
|
||||
//! NB: the driver currently maps `Global\pfxusb-shm-0` (hardcoded), so a single pad (index 0) is
|
||||
//! fully correct; mixed multi-pad needs the driver to read its own index first (same limitation as
|
||||
//! the DualSense backend).
|
||||
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::ffi::c_void;
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use std::time::Duration;
|
||||
use windows::core::{w, GUID, HRESULT, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
@@ -41,6 +41,7 @@ const OFF_RY: usize = core::mem::offset_of!(XusbShm, thumb_ry);
|
||||
const OFF_RUMBLE_SEQ: usize = core::mem::offset_of!(XusbShm, rumble_seq);
|
||||
const OFF_RUMBLE: usize = core::mem::offset_of!(XusbShm, rumble_large); // large @28, small @29
|
||||
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
|
||||
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
|
||||
|
||||
/// Context for the `SwDeviceCreate` completion callback: an event to signal, the HRESULT it reports,
|
||||
/// and the PnP instance id PnP assigned (captured for devnode health diagnostics).
|
||||
@@ -100,7 +101,7 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// The pad index, stamped into the device Location — the driver reads it to map `pfxusb-shm-<index>`
|
||||
// The pad index, stamped into the device Location — the driver reads it to poll `pfxusb-boot-<index>`
|
||||
// (multi-pad). The buffer must outlive the SwDeviceCreate call (it does; we wait on the event).
|
||||
let loc: Vec<u16> = format!("{index}")
|
||||
.encode_utf16()
|
||||
@@ -171,12 +172,13 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
|
||||
Ok((hsw, ctx.instance_id()))
|
||||
}
|
||||
|
||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
|
||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the sealed shared-memory channel.
|
||||
struct XusbWinPad {
|
||||
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// Owns `Global\pfxusb-shm-<index>` (the section + its mapped view; drop unmaps + closes).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: the unnamed DATA section (the `XusbShm`) + the bootstrap mailbox + the
|
||||
/// handle-delivery state machine (drop closes both sections).
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
packet: u32,
|
||||
@@ -184,17 +186,18 @@ struct XusbWinPad {
|
||||
}
|
||||
|
||||
impl XusbWinPad {
|
||||
/// Create + map `Global\pfxusb-shm-<index>`, stamp the magic, then spawn the devnode.
|
||||
/// Create the sealed channel (unnamed DATA section + `Global\pfxusb-boot-<index>` mailbox), stamp
|
||||
/// the pad index then the magic LAST, spawn the devnode, and eagerly deliver the DATA handle once
|
||||
/// the driver publishes its pid.
|
||||
fn open(index: u8) -> Result<XusbWinPad> {
|
||||
// Permissive-DACL named section the WUDFHost (whatever account) can open; `Shm` owns the
|
||||
// section handle + its mapped view (zero-filled) and unmaps/closes on drop.
|
||||
let shm_name = pf_driver_proto::gamepad::xusb_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// Zero the section then stamp the magic LAST (the driver only accepts it once magic is set).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
let boot_name = pf_driver_proto::gamepad::xusb_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// The section arrives zeroed; stamp the pad index (the driver validates it against its own
|
||||
// devnode index on attach) then the magic LAST (the driver only accepts it once magic is set).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX is in range.
|
||||
unsafe {
|
||||
std::ptr::write_bytes(base, 0, SHM_SIZE);
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
let (hsw, instance_id) = match create_swdevice(index) {
|
||||
@@ -205,14 +208,18 @@ impl XusbWinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery: the driver's EvtDeviceAdd publishes its pid right away; handing it
|
||||
// the DATA handle before we return means the pad is live for the game's first XInput poll.
|
||||
// On a missing/old driver this waits out the window once and the service pump takes over.
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(XusbWinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_xusb",
|
||||
"pf_xusb.inf",
|
||||
"C:\\Users\\Public\\pfxusb-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
packet: 0,
|
||||
@@ -225,7 +232,7 @@ impl XusbWinPad {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
||||
self.packet = self.packet.wrapping_add(1);
|
||||
let base = self.shm.base();
|
||||
let base = self.channel.data_base();
|
||||
// SAFETY: `base` is the start of the mapped section (`SHM_SIZE` bytes, owned by `Shm`); every
|
||||
// `OFF_*` is a fixed in-range offset into it and `write_unaligned` handles the unaligned field
|
||||
// writes. Single owner (`&mut self`), so no concurrent writer races these stores.
|
||||
@@ -242,10 +249,12 @@ impl XusbWinPad {
|
||||
}
|
||||
|
||||
/// Poll the section for a game's rumble (the driver bumps `rumble_seq` on each SET_STATE). Returns
|
||||
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also feeds the driver-attach
|
||||
/// health watcher (the driver stamps `driver_proto` at device add + on every serviced IOCTL).
|
||||
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also ticks the sealed-channel
|
||||
/// delivery (a late-binding driver gets its handle here) and feeds the driver-attach health
|
||||
/// watcher (the driver stamps `driver_proto` once it maps the delivered section + per IOCTL).
|
||||
fn service(&mut self) -> Option<(u8, u8)> {
|
||||
let base = self.shm.base();
|
||||
self.channel.pump();
|
||||
let base = self.channel.data_base();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
|
||||
self.attach.observe(proto);
|
||||
|
||||
@@ -39,11 +39,13 @@ pub(crate) enum MonitorKey {
|
||||
Session(u64),
|
||||
}
|
||||
|
||||
/// What a backend's `add_monitor` returns: the REMOVE key + the OS target id + the render LUID.
|
||||
/// What a backend's `add_monitor` returns: the REMOVE key + the OS target id + the render LUID + the
|
||||
/// driver's WUDFHost pid (the sealed frame channel's handle-duplication target).
|
||||
pub(crate) struct AddedMonitor {
|
||||
pub key: MonitorKey,
|
||||
pub target_id: u32,
|
||||
pub luid: LUID,
|
||||
pub wudf_pid: u32,
|
||||
}
|
||||
|
||||
/// The backend-specific IOCTL surface — the *only* thing that differs between SudoVDA and pf-vdisplay.
|
||||
@@ -91,6 +93,9 @@ struct Monitor {
|
||||
key: MonitorKey,
|
||||
target_id: u32,
|
||||
luid: LUID,
|
||||
/// The driver's WUDFHost pid (from the ADD reply) — carried into [`WinCaptureTarget`] so the
|
||||
/// IDD-push capturer knows where to duplicate the sealed frame channel's handles.
|
||||
wudf_pid: u32,
|
||||
gdi_name: Option<String>,
|
||||
mode: Mode,
|
||||
stop: Arc<AtomicBool>,
|
||||
@@ -109,6 +114,7 @@ impl Monitor {
|
||||
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
|
||||
gdi_name: n,
|
||||
target_id: self.target_id,
|
||||
wudf_pid: self.wudf_pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -166,6 +172,14 @@ pub(crate) fn vdm() -> &'static VirtualDisplayManager {
|
||||
.expect("VirtualDisplayManager used before a backend initialised it")
|
||||
}
|
||||
|
||||
/// The live pf-vdisplay control-device handle, for the IDD-push capturer's sealed-channel delivery
|
||||
/// (`IOCTL_SET_FRAME_CHANNEL`). Safe to hand out as a bare `HANDLE`: the device lives in a `OnceLock`
|
||||
/// that is never cleared or closed for the process lifetime. `None` before the first backend open —
|
||||
/// impossible for a capturer, which only exists on a monitor the manager created.
|
||||
pub(crate) fn control_device_handle() -> Option<HANDLE> {
|
||||
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
||||
}
|
||||
|
||||
impl VirtualDisplayManager {
|
||||
pub(crate) fn backend_name(&self) -> &'static str {
|
||||
self.driver.name()
|
||||
@@ -436,6 +450,7 @@ impl VirtualDisplayManager {
|
||||
key: added.key,
|
||||
target_id: added.target_id,
|
||||
luid: added.luid,
|
||||
wudf_pid: added.wudf_pid,
|
||||
gdi_name,
|
||||
mode,
|
||||
stop,
|
||||
|
||||
@@ -158,6 +158,33 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
|
||||
.context("pf-vdisplay SET_RENDER_ADAPTER")
|
||||
}
|
||||
|
||||
/// Deliver a monitor's sealed frame channel to the driver: the handle values `req` carries were just
|
||||
/// duplicated into the driver's WUDFHost by the IDD-push capturer's broker (`idd_push::ChannelBroker`),
|
||||
/// and on IOCTL success the DRIVER owns them. No output buffer. The caller reaps the remote duplicates
|
||||
/// on failure (the broker's `DUPLICATE_CLOSE_SOURCE` sweep) so no path leaks WUDFHost handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `dev` must be a live pf-vdisplay control handle (see [`super::manager::control_device_handle`]).
|
||||
pub(crate) unsafe fn send_frame_channel(
|
||||
dev: HANDLE,
|
||||
req: &control::SetFrameChannelRequest,
|
||||
) -> Result<()> {
|
||||
let mut none: [u8; 0] = [];
|
||||
// SAFETY: per this fn's contract `dev` is the live control handle. `bytes_of(req)` borrows the
|
||||
// caller's request for the duration of this synchronous call as the input bytes; `none` is empty,
|
||||
// so there is no output buffer.
|
||||
unsafe {
|
||||
ioctl(
|
||||
dev,
|
||||
control::IOCTL_SET_FRAME_CHANNEL,
|
||||
bytemuck::bytes_of(req),
|
||||
&mut none,
|
||||
)
|
||||
}
|
||||
.map(|_| ())
|
||||
.context("pf-vdisplay SET_FRAME_CHANNEL")
|
||||
}
|
||||
|
||||
unsafe fn open_device() -> Result<HANDLE> {
|
||||
let hdev = SetupDiGetClassDevsW(
|
||||
Some(&PF_VDISPLAY_INTERFACE),
|
||||
@@ -354,12 +381,13 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
HighPart: reply.adapter_luid_high,
|
||||
};
|
||||
tracing::info!(
|
||||
"pf-vdisplay created {}x{}@{} (target_id={}, adapter_luid={:#x})",
|
||||
"pf-vdisplay created {}x{}@{} (target_id={}, adapter_luid={:#x}, wudf_pid={})",
|
||||
mode.width,
|
||||
mode.height,
|
||||
mode.refresh_hz,
|
||||
reply.target_id,
|
||||
luid.LowPart
|
||||
luid.LowPart,
|
||||
reply.wudf_pid
|
||||
);
|
||||
// Per-client identity diagnostic: did the driver honor the host's preferred (stable) monitor id?
|
||||
// A pre-Phase-2 driver leaves resolved_monitor_id=0 (it ignored the field); a current driver echoes
|
||||
@@ -395,6 +423,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
key: MonitorKey::Session(session_id),
|
||||
target_id: reply.target_id,
|
||||
luid,
|
||||
wudf_pid: reply.wudf_pid,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -162,9 +162,28 @@ fn install_gamepad(dir: &Path) -> Result<()> {
|
||||
eprintln!("warning: pnputil /add-driver {} failed", inf.display());
|
||||
}
|
||||
}
|
||||
// Sweep pad devnodes, INCLUDING phantoms a host crash / service stop left behind: a re-created
|
||||
// SwDevice with a known instance id REVIVES the existing devnode with its previously-bound
|
||||
// driver — it never re-ranks against the store — so after an upgrade the old driver keeps
|
||||
// serving (or, across the v1→v2 sealed-channel fence, fails closed and the pad plays dead).
|
||||
// Proven in the field on the RTX box: a v1 phantom pinned the old package through a v2
|
||||
// install. The devnodes are per-session objects the host recreates on demand, so removing
|
||||
// them at driver-install time is always safe; the next pad binds the fresh package.
|
||||
remove_pad_devnodes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `pnputil /remove-device` every punktfunk virtual-pad devnode (live or phantom).
|
||||
fn remove_pad_devnodes() {
|
||||
for id in pad_instance_ids() {
|
||||
if run_quiet("pnputil", &["/remove-device", &id]) {
|
||||
println!("removed stale pad devnode {id}");
|
||||
} else {
|
||||
eprintln!("warning: pnputil /remove-device {id} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── `driver uninstall [--gamepad]` ──────────────────────────────────────────────────────────────
|
||||
// The uninstaller's cleanup counterpart (Inno [UninstallRun]) — the field report was that our
|
||||
// virtual-device drivers survived an uninstall. Removes the pf-vdisplay device node(s) + driver
|
||||
@@ -204,6 +223,9 @@ fn uninstall_pf_vdisplay() -> Result<()> {
|
||||
}
|
||||
|
||||
fn uninstall_gamepad() -> Result<()> {
|
||||
// Devnodes first (incl. phantoms — the same ghost-device complaint the vdisplay uninstall
|
||||
// fixed), then the store packages.
|
||||
remove_pad_devnodes();
|
||||
delete_store_drivers(&["pf_dualsense", "pf_dualshock4", "pf_xusb"]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -235,6 +257,28 @@ fn pf_vdisplay_instance_ids() -> Vec<String> {
|
||||
ids
|
||||
}
|
||||
|
||||
/// Instance IDs of punktfunk virtual-pad devnodes (`SWD\PUNKTFUNK\…`), INCLUDING phantoms left by
|
||||
/// a host crash / service stop (`pnputil /enum-devices` lists disconnected devnodes too). Same
|
||||
/// un-localized VALUE-side parsing as [`pf_vdisplay_instance_ids`]; matched on the instance-id
|
||||
/// prefix itself — the pads span two device classes (HIDClass + System), so no `/class` filter.
|
||||
fn pad_instance_ids() -> Vec<String> {
|
||||
let out = run_capture("pnputil", &["/enum-devices"]);
|
||||
let mut ids = Vec::new();
|
||||
for block in out.split("\r\n\r\n").flat_map(|b| b.split("\n\n")) {
|
||||
let Some(first) = block.lines().find(|l| !l.trim().is_empty()) else {
|
||||
continue;
|
||||
};
|
||||
let Some((_, value)) = first.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
let id = value.trim();
|
||||
if id.to_ascii_uppercase().starts_with("SWD\\PUNKTFUNK\\") && !id.contains(' ') {
|
||||
ids.push(id.to_string());
|
||||
}
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// Delete every driver-store package (`%WINDIR%\INF\oem*.inf`) whose INF text mentions one of
|
||||
/// `needles` — our driver names are unique enough that a content match identifies the package
|
||||
/// without parsing `pnputil /enum-drivers`' localized output. `/uninstall /force` also unbinds it
|
||||
|
||||
Reference in New Issue
Block a user