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:
@@ -131,11 +131,21 @@ jobs:
|
|||||||
# dispatched provisioning workflow landing on a different one. Path is relative to the job
|
# dispatched provisioning workflow landing on a different one. Path is relative to the job
|
||||||
# working-directory (packaging/windows/drivers). Near-noop once the toolchain is present.
|
# working-directory (packaging/windows/drivers). Near-noop once the toolchain is present.
|
||||||
run: ../../../scripts/ci/ensure-windows-toolchain.ps1
|
run: ../../../scripts/ci/ensure-windows-toolchain.ps1
|
||||||
- name: cargo build the driver workspace (wdk-probe + wdk-iddcx + pf-vdisplay)
|
- name: cargo build the driver workspace (wdk-probe + wdk-iddcx + pf-vdisplay + gamepad drivers)
|
||||||
# Whole workspace: wdk-probe (toolchain/surface-assert probe) + wdk-iddcx (DDI wrappers) +
|
# Whole workspace: wdk-probe (toolchain/surface-assert probe) + wdk-iddcx (DDI wrappers) +
|
||||||
# pf-vdisplay (the real IddCx driver). pf-vdisplay linking proves the IddCx call sites resolve
|
# pf-vdisplay (the real IddCx driver) + pf-umdf-util (the safe UMDF primitive layer) + the two
|
||||||
# against IddCxStub end-to-end (M1 step 2 gate).
|
# gamepad drivers. pf-vdisplay linking proves the IddCx call sites resolve against IddCxStub
|
||||||
|
# end-to-end (M1 step 2 gate); the gamepad drivers prove pf-umdf-util's WDF dispatch links.
|
||||||
run: cargo build -v
|
run: cargo build -v
|
||||||
|
- name: cargo clippy the shipped drivers (-D warnings — enforces the unsafe-audit gates)
|
||||||
|
# The gamepad drivers' business logic is 100% safe (it moved onto pf-umdf-util, the audited
|
||||||
|
# unsafe layer); pf-vdisplay + wdk-iddcx are inherently FFI-bound but every `unsafe {}` carries a
|
||||||
|
# `// SAFETY:` proof. Both invariants are lint-gated (`unsafe_op_in_unsafe_fn` +
|
||||||
|
# `undocumented_unsafe_blocks`); this step keeps them from regressing. (wdk-probe is a
|
||||||
|
# toolchain-only probe crate and is excluded.)
|
||||||
|
run: cargo clippy -p pf-umdf-util -p pf-xusb -p pf-dualsense -p wdk-iddcx -p pf-vdisplay --all-targets -- -D warnings
|
||||||
|
- name: cargo fmt --check the safe-layer + gamepad drivers
|
||||||
|
run: cargo fmt -p pf-umdf-util -p pf-xusb -p pf-dualsense --check
|
||||||
- name: Inspect /INTEGRITYCHECK (before) — expect FORCE_INTEGRITY set by wdk-build
|
- name: Inspect /INTEGRITYCHECK (before) — expect FORCE_INTEGRITY set by wdk-build
|
||||||
run: |
|
run: |
|
||||||
# explicit --target (.cargo/config.toml) -> output under the triple subdir.
|
# explicit --target (.cargo/config.toml) -> output under the triple subdir.
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
//!
|
//!
|
||||||
//! Two planes:
|
//! Two planes:
|
||||||
//! * [`control`] — the low-frequency `DeviceIoControl` plane (add/remove a virtual monitor, pin the
|
//! * [`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.
|
//! render adapter, keepalive, info, clear-all, deliver the frame channel). Owned, clean, versioned —
|
||||||
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of shared keyed-mutex textures
|
//! NOT the SudoVDA ABI.
|
||||||
//! (+ a header + a frame-ready event) and the driver opens them and publishes composited frames into
|
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of **unnamed** shared
|
||||||
//! them. This crate owns the [`frame::SharedHeader`] layout, the [`frame::FrameToken`] packing, the
|
//! keyed-mutex textures (+ a header + a frame-ready event), duplicates their handles into the
|
||||||
//! `Global\` object-name scheme, and the driver-status codes.
|
//! 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`
|
//! 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
|
//! 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
|
/// 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.
|
/// 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)`.
|
/// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`.
|
||||||
pub const fn ctl_code(func: u32) -> u32 {
|
pub const fn ctl_code(func: u32) -> u32 {
|
||||||
(0x22u32 << 16) | (func << 2)
|
(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 {
|
pub mod control {
|
||||||
use super::ctl_code;
|
use super::ctl_code;
|
||||||
|
use super::frame::RING_LEN;
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
// Contiguous op space at 0x900 — distinct from SudoVDA's gappy 0x800/0x888/0x8FF numbering.
|
// 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
|
/// Tear down every virtual monitor (host-startup orphan reap). No payload. First-class op — NOT the
|
||||||
/// SudoVDA "send-and-hope-it's-ignored" hack.
|
/// SudoVDA "send-and-hope-it's-ignored" hack.
|
||||||
pub const IOCTL_CLEAR_ALL: u32 = ctl_code(0x905);
|
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
|
/// `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
|
/// 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
|
/// `_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.
|
/// preference was ignored (stale driver) and log it instead of silently losing per-client config.
|
||||||
pub resolved_monitor_id: u32,
|
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.
|
/// `IOCTL_REMOVE` input.
|
||||||
@@ -129,6 +150,39 @@ pub mod control {
|
|||||||
pub watchdog_timeout_s: u32,
|
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
|
// 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!`
|
// 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.
|
// 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, refresh_hz) == 16);
|
||||||
assert!(offset_of!(AddRequest, preferred_monitor_id) == 20);
|
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_low) == 0);
|
||||||
assert!(offset_of!(AddReply, adapter_luid_high) == 4);
|
assert!(offset_of!(AddReply, adapter_luid_high) == 4);
|
||||||
assert!(offset_of!(AddReply, target_id) == 8);
|
assert!(offset_of!(AddReply, target_id) == 8);
|
||||||
assert!(offset_of!(AddReply, resolved_monitor_id) == 12);
|
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!(size_of::<RemoveRequest>() == 8);
|
||||||
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
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 IDD-push frame transport: the host-created shared ring header, the publish token, and the
|
||||||
/// the driver-status codes. The texture ring itself is host-created D3D11 keyed-mutex textures (opened
|
/// driver-status codes. The texture ring itself is host-created **unnamed** D3D11 keyed-mutex textures;
|
||||||
/// by name on the driver side); only the *layout/contract* lives here.
|
/// 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 {
|
pub mod frame {
|
||||||
use alloc::string::String;
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
/// Header magic (`"PFVD"` LE). The host stamps it LAST (after the ring textures exist) so the driver
|
/// 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 struct SharedHeader {
|
||||||
pub magic: u32,
|
pub magic: u32,
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format/names). The driver
|
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format + a fresh
|
||||||
/// re-attaches when it changes; a publish carries it so the host rejects a stale-ring publish.
|
/// [`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 generation: u32,
|
||||||
pub ring_len: u32,
|
pub ring_len: u32,
|
||||||
pub width: 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
|
// 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
|
// 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.
|
// `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!`
|
/// (`design/windows-host-rewrite.md` §2.7). Owning them here with `Pod` derives + `offset_of!`
|
||||||
/// asserts makes a one-sided edit a compile error.
|
/// asserts makes a one-sided edit a compile error.
|
||||||
///
|
///
|
||||||
/// The host creates the section (privileged, permissive DACL so the restricted WUDFHost token can
|
/// Since v2 the channel is **sealed** (`design/gamepad-channel-sealing.md`, mirroring the frame
|
||||||
/// open it) and the driver maps it. Layout only; the section itself is host-created shared memory.
|
/// 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 {
|
pub mod gamepad {
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use bytemuck::{Pod, Zeroable};
|
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
|
/// 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
|
/// 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.
|
/// (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.
|
/// Bootstrap-mailbox magic (`"PFBT"` LE) — the host stamps it LAST (after `host_proto`), so a
|
||||||
pub fn xusb_shm_name(index: u8) -> String {
|
/// driver only trusts a fully-initialized mailbox.
|
||||||
alloc::format!("Global\\pfxusb-shm-{index}")
|
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.
|
/// `Global\pfds-boot-<index>` — the DualSense / DualShock 4 pad's bootstrap mailbox
|
||||||
pub fn pad_shm_name(index: u8) -> String {
|
/// ([`PadBootstrap`]).
|
||||||
alloc::format!("Global\\pfds-shm-{index}")
|
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
|
/// 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
|
/// 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).
|
/// only advances while something polls the slot, so a static value is not an error).
|
||||||
pub driver_heartbeat: u32,
|
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
|
/// 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
|
/// 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).
|
/// XUSB one, this advances whenever the driver is loaded, game or not).
|
||||||
pub driver_heartbeat: u32,
|
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
|
// 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, rumble_small) == 29);
|
||||||
assert!(offset_of!(XusbShm, driver_proto) == 32);
|
assert!(offset_of!(XusbShm, driver_proto) == 32);
|
||||||
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
|
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
|
||||||
|
assert!(offset_of!(XusbShm, pad_index) == 40);
|
||||||
|
|
||||||
assert!(size_of::<PadShm>() == 256);
|
assert!(size_of::<PadShm>() == 256);
|
||||||
assert!(offset_of!(PadShm, magic) == 0);
|
assert!(offset_of!(PadShm, magic) == 0);
|
||||||
@@ -417,6 +532,16 @@ pub mod gamepad {
|
|||||||
assert!(offset_of!(PadShm, device_type) == 140);
|
assert!(offset_of!(PadShm, device_type) == 140);
|
||||||
assert!(offset_of!(PadShm, driver_proto) == 144);
|
assert!(offset_of!(PadShm, driver_proto) == 144);
|
||||||
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
|
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,
|
adapter_luid_high: -2,
|
||||||
target_id: 262,
|
target_id: 262,
|
||||||
resolved_monitor_id: 7,
|
resolved_monitor_id: 7,
|
||||||
|
wudf_pid: 4242,
|
||||||
};
|
};
|
||||||
let rbytes = bytemuck::bytes_of(&reply);
|
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);
|
assert_eq!(*bytemuck::from_bytes::<control::AddReply>(rbytes), reply);
|
||||||
// resolved_monitor_id occupies the old `_reserved` slot at offset 12 — byte-compatible.
|
// resolved_monitor_id occupies the old `_reserved` slot at offset 12 — byte-compatible.
|
||||||
assert_eq!(rbytes[12..16], 7u32.to_le_bytes());
|
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]
|
#[test]
|
||||||
fn names_are_stable() {
|
fn frame_channel_request_roundtrips_through_bytes() {
|
||||||
assert_eq!(frame::header_name(10), "Global\\pfvd-hdr-10");
|
let mut req = control::SetFrameChannelRequest {
|
||||||
assert_eq!(frame::event_name(10), "Global\\pfvd-evt-10");
|
target_id: 262,
|
||||||
assert_eq!(frame::texture_name(10, 3, 5), "Global\\pfvd-tex-10-3-5");
|
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]
|
#[test]
|
||||||
fn gamepad_names_and_magics_are_stable() {
|
fn gamepad_names_and_magics_are_stable() {
|
||||||
assert_eq!(gamepad::xusb_shm_name(0), "Global\\pfxusb-shm-0");
|
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
||||||
assert_eq!(gamepad::pad_shm_name(2), "Global\\pfds-shm-2");
|
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).
|
// 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::XUSB_MAGIC, 0x5558_4650);
|
||||||
assert_eq!(gamepad::PAD_MAGIC, 0x5046_4453);
|
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]
|
#[test]
|
||||||
@@ -521,6 +689,7 @@ mod tests {
|
|||||||
control::IOCTL_PING,
|
control::IOCTL_PING,
|
||||||
control::IOCTL_GET_INFO,
|
control::IOCTL_GET_INFO,
|
||||||
control::IOCTL_CLEAR_ALL,
|
control::IOCTL_CLEAR_ALL,
|
||||||
|
control::IOCTL_SET_FRAME_CHANNEL,
|
||||||
];
|
];
|
||||||
for (i, a) in all.iter().enumerate() {
|
for (i, a) in all.iter().enumerate() {
|
||||||
for b in &all[i + 1..] {
|
for b in &all[i + 1..] {
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ pub struct WinCaptureTarget {
|
|||||||
pub gdi_name: String,
|
pub gdi_name: String,
|
||||||
/// Stable SudoVDA target id — re-resolved to the current GDI name on every recovery.
|
/// Stable SudoVDA target id — re-resolved to the current GDI name on every recovery.
|
||||||
pub target_id: u32,
|
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).
|
/// 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
|
//! P2 direct frame push (kill DDA) — HOST side, over the **sealed channel**
|
||||||
//! kernel objects, so — exactly like the gamepad UMDF drivers (`inject/dualsense_windows.rs`) — the
|
//! (`design/idd-push-security.md`). The frame channel carries whole-desktop pixels, so its protection
|
||||||
//! HOST (privileged) CREATES the shared header + frame-ready event + ring of keyed-mutex textures
|
//! must match DDA's (where capturer and consumer are one process and there is no openable channel at
|
||||||
//! (`Global\` names, scoped `D:(A;;GA;;;SY)(A;;GA;;;LS)` to SYSTEM + the driver's LocalService host —
|
//! all): the HOST (SYSTEM) creates the shared header + frame-ready event + ring of keyed-mutex textures
|
||||||
//! see `shared_object_sa`) on the discrete render GPU, and the driver only OPENS them and copies frames in. We then consume the ring
|
//! **UNNAMED** on the discrete render GPU — nothing to enumerate, open by name, or pre-create
|
||||||
//! straight into the zero-copy NVENC path — no DXGI Desktop Duplication, no `win32u` hook. Gated by
|
//! ("squat") — then DUPLICATES the handles into the pf-vdisplay driver's WUDFHost process
|
||||||
//! `PUNKTFUNK_IDD_PUSH`. Driver counterpart: `packaging/windows/drivers/pf-vdisplay/src/
|
//! ([`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
|
//! 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
|
//! `DRV_STATUS_*` codes, the channel-delivery struct and the publish token all come from
|
||||||
//! [`pf_driver_proto::frame`] (which OWNS the contract, with `const` size asserts) — both sides
|
//! [`pf_driver_proto`] (which OWNS the contract, with `const` size asserts) — both sides `use` it, so
|
||||||
//! `use` it, so drift is a compile error rather than a "must match" comment.
|
//! 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).
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
@@ -16,12 +22,15 @@
|
|||||||
use super::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget};
|
use super::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget};
|
||||||
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
||||||
use anyhow::{bail, Context, Result};
|
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::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
||||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use windows::core::{w, Interface, HSTRING};
|
use windows::core::{w, Interface, PCWSTR, PWSTR};
|
||||||
use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, LUID};
|
use windows::Win32::Foundation::{
|
||||||
|
DuplicateHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_HANDLE_OPTIONS, DUPLICATE_SAME_ACCESS,
|
||||||
|
HANDLE, INVALID_HANDLE_VALUE, LUID,
|
||||||
|
};
|
||||||
use windows::Win32::Graphics::Direct3D11::{
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
|
ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
|
||||||
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX,
|
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,
|
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
||||||
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
|
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
|
// 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
|
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
|
||||||
// `use frame::*`, so a layout/name/code drift is a compile error (the proto has `const` size asserts).
|
// `use` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
|
||||||
use frame::{
|
use frame::{
|
||||||
event_name, header_name, texture_name, SharedHeader, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
|
SharedHeader, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN,
|
||||||
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` for `CreateSharedHandle`/`OpenSharedResourceByName`. Local (not
|
/// `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).
|
/// 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;
|
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
|
/// 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
|
/// 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.
|
/// pipeline depth of 2 with one slot of margin.
|
||||||
const OUT_RING: usize = 3;
|
const OUT_RING: usize = 3;
|
||||||
|
|
||||||
/// Bring-up debug block (fixed name) — the host creates it; the driver writes diagnostics into it
|
/// Monotonic per-process generation stamped into the header + every publish token, so the host rejects
|
||||||
/// independent of the per-target header. NOT part of `pf_driver_proto` (a host-side bring-up channel,
|
/// a stale-ring publish and the driver detects a recreate. (With unnamed textures there is no name
|
||||||
/// not the data path); the matching `DebugBlock` lives in the OLD oracle driver's `frame_transport.rs`.
|
/// collision to avoid — the generation's remaining job is the recreate/stale-publish handshake.)
|
||||||
#[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.
|
|
||||||
static IDD_GENERATION: AtomicU32 = AtomicU32::new(1);
|
static IDD_GENERATION: AtomicU32 = AtomicU32::new(1);
|
||||||
|
|
||||||
fn now_ns() -> u64 {
|
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,
|
/// 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).
|
/// 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
|
/// 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).
|
/// OS mapping, so the borrowed pointer stays valid).
|
||||||
struct MappedSection {
|
struct MappedSection {
|
||||||
@@ -122,10 +127,9 @@ impl Drop for MappedSection {
|
|||||||
struct HostSlot {
|
struct HostSlot {
|
||||||
tex: ID3D11Texture2D,
|
tex: ID3D11Texture2D,
|
||||||
mutex: IDXGIKeyedMutex,
|
mutex: IDXGIKeyedMutex,
|
||||||
/// The named shared-resource handle, held only to keep the resource alive (the driver opens it by
|
/// The UNNAMED shared-resource NT handle: keeps the resource alive for the session AND is the
|
||||||
/// NAME). An [`OwnedHandle`] so it closes on drop (was a manual `CloseHandle` in a `Drop` impl);
|
/// source the [`ChannelBroker`] duplicates into the driver's WUDFHost (the ONLY way the driver can
|
||||||
/// never read directly — its sole purpose is the RAII close.
|
/// reach this texture — there is no name to open). An [`OwnedHandle`] so it closes on drop.
|
||||||
#[allow(dead_code)]
|
|
||||||
shared: OwnedHandle,
|
shared: OwnedHandle,
|
||||||
/// SRV on the slot texture so the HDR path samples the FP16 slot DIRECTLY (no slot→scratch copy);
|
/// 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
|
/// 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`].
|
/// Creates + owns the shared ring; yields the driver's frames as [`FramePayload::D3d11`].
|
||||||
pub struct IddPushCapturer {
|
pub struct IddPushCapturer {
|
||||||
device: ID3D11Device,
|
device: ID3D11Device,
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
target_id: u32,
|
target_id: u32,
|
||||||
/// Owns the shared-header file mapping + its mapped view (RAII unmap-then-close). Declared BEFORE
|
/// 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
|
/// `header`, which is a raw pointer borrowed into this view via [`MappedSection::ptr`]. Also the
|
||||||
/// directly (the `header` pointer is) — held purely so the mapping outlives the capturer.
|
/// duplication source for the driver's header handle on every [`ChannelBroker::send`].
|
||||||
#[allow(dead_code)]
|
|
||||||
section: MappedSection,
|
section: MappedSection,
|
||||||
header: *mut SharedHeader,
|
header: *mut SharedHeader,
|
||||||
event: OwnedHandle,
|
event: OwnedHandle,
|
||||||
/// Owns the bring-up debug section (mapping + view), or `None` when the debug block wasn't created.
|
/// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open
|
||||||
/// Never read directly (the `dbg_block` pointer is) — held purely for the RAII unmap/close.
|
/// and again on every ring recreate to deliver fresh duplicates.
|
||||||
#[allow(dead_code)]
|
broker: ChannelBroker,
|
||||||
dbg_section: Option<MappedSection>,
|
|
||||||
dbg_block: *mut DebugBlock,
|
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
slots: Vec<HostSlot>,
|
slots: Vec<HostSlot>,
|
||||||
/// The ring/texture generation, bumped every time the ring is recreated at a new format (the
|
/// 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,
|
generation: u32,
|
||||||
/// The CLIENT's advertised 10-bit capability (= negotiated `bit_depth >= 10`). Only used at `open`
|
/// 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
|
/// 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,
|
status_logged: bool,
|
||||||
_keepalive: Box<dyn Send>,
|
_keepalive: Box<dyn Send>,
|
||||||
}
|
}
|
||||||
// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader`/`*mut DebugBlock` raw
|
// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader` raw pointer (and the
|
||||||
// pointers (and the COM interfaces). It is created, used, and dropped by a SINGLE thread — the owning
|
// COM interfaces / the broker's bare control `HANDLE`, which is process-global and never closed). It is
|
||||||
// capture/encode thread — never shared: the `ID3D11DeviceContext` is the device's IMMEDIATE context
|
// created, used, and dropped by a SINGLE thread — the owning capture/encode thread — never shared: the
|
||||||
// (single-threaded by D3D11 contract) and is only ever touched from that thread, and the header/
|
// `ID3D11DeviceContext` is the device's IMMEDIATE context (single-threaded by D3D11 contract) and is
|
||||||
// dbg_block pointers (into mappings this struct owns) are only dereferenced there. `Send` transfers
|
// only ever touched from that thread, and the header pointer (into the mapping this struct owns) is
|
||||||
// ownership to one thread at a time with NO concurrent access; we do not (and must not) claim `Sync`.
|
// 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 {}
|
unsafe impl Send for IddPushCapturer {}
|
||||||
|
|
||||||
/// Build a `SECURITY_ATTRIBUTES` granting GENERIC_ALL to **SYSTEM** (the host creates+publishes the
|
/// Build a `SECURITY_ATTRIBUTES` granting GENERIC_ALL to **SYSTEM only** — `D:P(A;;GA;;;SY)`, protected
|
||||||
/// shared event + texture ring) and **LocalService** (the account the pf_vdisplay WUDFHost runs under,
|
/// (no inherited ACEs), `bInheritHandle: false`. The sealed channel makes this the strictly-minimal
|
||||||
/// which consumes them) — `D:(A;;GA;;;SY)(A;;GA;;;LS)`, the same scoping as the gamepad section. The
|
/// DACL: the objects are UNNAMED and the driver reaches them via **duplicated handles** (which carry the
|
||||||
/// old SDDL granted **Everyone** (`WD`), which let any local user open the `Global\pfvd-*` objects and
|
/// source handle's access — `OpenSharedResourceByName`/`OpenSharedResource1` on a handle does not
|
||||||
/// read captured screen frames (security-review 2026-06-28 #5). Verified on the RTX box (2026-06-29):
|
/// re-check the object DACL against the opener), so the pf_vdisplay WUDFHost (LocalService) no longer
|
||||||
/// the WUDFHost token is `S-1-5-19` (LocalService), SYSTEM integrity, zero restricted SIDs — so SY+LS
|
/// needs a DACL ACE. Dropping the `LS` ACE removes the last theoretical surface where a leaked handle or
|
||||||
/// suffices for the driver and excludes normal user processes. `psd` must outlive `sa`.
|
/// 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)> {
|
unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> {
|
||||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||||
w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"),
|
w!("D:P(A;;GA;;;SY)"),
|
||||||
SDDL_REVISION_1,
|
SDDL_REVISION_1,
|
||||||
&mut psd,
|
&mut psd,
|
||||||
None,
|
None,
|
||||||
@@ -262,20 +482,18 @@ unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTO
|
|||||||
|
|
||||||
impl IddPushCapturer {
|
impl IddPushCapturer {
|
||||||
/// Create the `RING_LEN` shared keyed-mutex textures for one ring generation, at `format` (matched
|
/// 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
|
/// to the display's composition format — FP16 in HDR, BGRA in SDR). Each is shared through an
|
||||||
/// `pfvd-tex-<target>-<generation>-<k>` so the driver opens it; a fresh generation gives fresh names
|
/// UNNAMED NT handle (nothing to open by name — the sealed channel); the driver reaches it only via
|
||||||
/// (so a recreate never collides with the old ring's not-yet-released handles).
|
/// the duplicate the [`ChannelBroker`] sends after the ring is published.
|
||||||
unsafe fn create_ring_slots(
|
unsafe fn create_ring_slots(
|
||||||
device: &ID3D11Device,
|
device: &ID3D11Device,
|
||||||
target_id: u32,
|
|
||||||
generation: u32,
|
|
||||||
w: u32,
|
w: u32,
|
||||||
h: u32,
|
h: u32,
|
||||||
format: DXGI_FORMAT,
|
format: DXGI_FORMAT,
|
||||||
) -> Result<Vec<HostSlot>> {
|
) -> Result<Vec<HostSlot>> {
|
||||||
let (sa, _psd) = shared_object_sa()?;
|
let (sa, _psd) = shared_object_sa()?;
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
for k in 0..RING_LEN {
|
for _ in 0..RING_LEN {
|
||||||
let desc = D3D11_TEXTURE2D_DESC {
|
let desc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: w,
|
Width: w,
|
||||||
Height: h,
|
Height: h,
|
||||||
@@ -304,7 +522,7 @@ impl IddPushCapturer {
|
|||||||
.CreateSharedHandle(
|
.CreateSharedHandle(
|
||||||
Some(&sa as *const SECURITY_ATTRIBUTES),
|
Some(&sa as *const SECURITY_ATTRIBUTES),
|
||||||
DXGI_SHARED_RESOURCE_RW,
|
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)")?;
|
.context("CreateSharedHandle(IDD-push ring slot)")?;
|
||||||
// Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`).
|
// 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.
|
// `u32` target id; they read/flip CCD display config and return owned values, borrowing nothing.
|
||||||
// - `CreateDXGIFactory1`, `EnumAdapterByLuid`, `make_device`, `shared_object_sa`, `CreateFileMappingW`,
|
// - `CreateDXGIFactory1`, `EnumAdapterByLuid`, `make_device`, `shared_object_sa`, `CreateFileMappingW`,
|
||||||
// `MapViewOfFile`, `CreateEventW`, and `create_ring_slots` are all `?`-checked, so every returned
|
// `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
|
// interface/handle/view is non-error before use; `&sa`/`&adapter`/`&device` are live borrows that
|
||||||
// are live borrows that outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid
|
// outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid because its backing
|
||||||
// because its backing `_psd` is held in scope for the whole block.
|
// `_psd` is held in scope for the whole block.
|
||||||
// - The header mapping is created AND viewed at `bytes == size_of::<SharedHeader>().max(64)`; the
|
// - 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
|
// 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
|
// 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
|
// `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
|
// within those `bytes` and write THROUGH the raw pointer without forming any `&mut`.
|
||||||
// section is the same pattern at `dbg_bytes == size_of::<DebugBlock>()`, only entered when its
|
|
||||||
// own view is non-null.
|
|
||||||
// - The `magic` publish stores through `addr_of!((*header).magic) as *const AtomicU32`: `addr_of!`
|
// - 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
|
// 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
|
// `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`.
|
// 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
|
// - `broker.send` requires live `header`/`event` handles of this process: both borrow the just-
|
||||||
// `section`/`dbg_section` into `me` leaves them valid (see the `MappedSection` doc comment).
|
// 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 {
|
unsafe {
|
||||||
// If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and
|
// 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
|
// 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 (sa, _psd) = shared_object_sa()?;
|
||||||
let bytes = std::mem::size_of::<SharedHeader>().max(64);
|
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(
|
let map = CreateFileMappingW(
|
||||||
INVALID_HANDLE_VALUE,
|
INVALID_HANDLE_VALUE,
|
||||||
Some(&sa),
|
Some(&sa),
|
||||||
PAGE_READWRITE,
|
PAGE_READWRITE,
|
||||||
0,
|
0,
|
||||||
bytes as u32,
|
bytes as u32,
|
||||||
&HSTRING::from(header_name(target.target_id)),
|
PCWSTR::null(),
|
||||||
)
|
)
|
||||||
.context("CreateFileMapping(IDD-push header)")?;
|
.context("CreateFileMapping(IDD-push header)")?;
|
||||||
// Own the mapping handle so it (and its view) free via `MappedSection` RAII even on bail.
|
// 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.
|
// reads this into its `ring_format` and drops any surface that doesn't match.
|
||||||
(*header).dxgi_format = ring_fmt.0 as u32;
|
(*header).dxgi_format = ring_fmt.0 as u32;
|
||||||
|
|
||||||
// Frame-ready event (auto-reset).
|
// Frame-ready event (auto-reset) — UNNAMED, like everything on this channel.
|
||||||
let event = CreateEventW(
|
let event = CreateEventW(Some(&sa), false, false, PCWSTR::null())
|
||||||
Some(&sa),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
&HSTRING::from(event_name(target.target_id)),
|
|
||||||
)
|
|
||||||
.context("CreateEvent(IDD-push)")?;
|
.context("CreateEvent(IDD-push)")?;
|
||||||
let event = OwnedHandle::from_raw_handle(event.0 as _);
|
let event = OwnedHandle::from_raw_handle(event.0 as _);
|
||||||
|
|
||||||
// Ring of shared keyed-mutex textures, format matched to the display's current mode.
|
// Ring of shared keyed-mutex textures, format matched to the display's current mode.
|
||||||
let slots =
|
let slots = Self::create_ring_slots(&device, w, h, ring_fmt)?;
|
||||||
Self::create_ring_slots(&device, target.target_id, generation, w, h, ring_fmt)?;
|
|
||||||
|
|
||||||
// Bring-up debug block (fixed name) — the driver writes diagnostics here. Best-effort.
|
// Publish: magic LAST (Release) — the ring must be fully initialized before the driver
|
||||||
let dbg_bytes = std::mem::size_of::<DebugBlock>();
|
// (which receives the channel strictly afterwards) can observe MAGIC.
|
||||||
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.
|
|
||||||
std::sync::atomic::fence(Ordering::Release);
|
std::sync::atomic::fence(Ordering::Release);
|
||||||
(*(std::ptr::addr_of!((*header).magic) as *const AtomicU32))
|
(*(std::ptr::addr_of!((*header).magic) as *const AtomicU32))
|
||||||
.store(MAGIC, Ordering::Release);
|
.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!(
|
tracing::info!(
|
||||||
target_id = target.target_id,
|
target_id = target.target_id,
|
||||||
|
wudf_pid = target.wudf_pid,
|
||||||
render_luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
|
render_luid = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
|
||||||
mode = format!("{w}x{h}"),
|
mode = format!("{w}x{h}"),
|
||||||
display_hdr,
|
display_hdr,
|
||||||
client_10bit,
|
client_10bit,
|
||||||
ring_fp16 = display_hdr,
|
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 {
|
let me = Self {
|
||||||
device,
|
device,
|
||||||
@@ -534,8 +728,7 @@ impl IddPushCapturer {
|
|||||||
section,
|
section,
|
||||||
header,
|
header,
|
||||||
event,
|
event,
|
||||||
dbg_section,
|
broker,
|
||||||
dbg_block,
|
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
slots,
|
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
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// generation so the driver re-attaches ([`is_stale`]) to the new-format textures and DELIVERS the
|
||||||
/// header's `latest` so we don't consume a stale slot from the old ring; drops the conversion
|
/// new channel (fresh duplicates of the header + event + the new textures — every delivery is a
|
||||||
/// textures so they rebuild at the new format.
|
/// 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<()> {
|
fn recreate_ring(&mut self, new_display_hdr: bool, new_w: u32, new_h: u32) -> Result<()> {
|
||||||
self.display_hdr = new_display_hdr;
|
self.display_hdr = new_display_hdr;
|
||||||
self.width = new_w;
|
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
|
// 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
|
// `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`.
|
// returned slot's texture + keyed mutex belongs to that same `self.device`.
|
||||||
let new_slots = unsafe {
|
let new_slots =
|
||||||
Self::create_ring_slots(
|
unsafe { Self::create_ring_slots(&self.device, self.width, self.height, fmt)? };
|
||||||
&self.device,
|
|
||||||
self.target_id,
|
|
||||||
new_gen,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
fmt,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
// 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
|
// `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
|
// 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.slots = new_slots; // drops the old slots → closes their shared handles + SRVs
|
||||||
self.generation = new_gen;
|
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.last_seq = 0;
|
||||||
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
|
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
|
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`.
|
/// 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 {
|
fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID {
|
||||||
if let Some(l) = crate::win_adapter::resolve_render_adapter_luid() {
|
if let Some(l) = crate::win_adapter::resolve_render_adapter_luid() {
|
||||||
@@ -1046,7 +1186,6 @@ impl Capturer for IddPushCapturer {
|
|||||||
return Ok(f);
|
return Ok(f);
|
||||||
}
|
}
|
||||||
if Instant::now() > deadline {
|
if Instant::now() > deadline {
|
||||||
self.log_debug_block();
|
|
||||||
// SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping — the same
|
// 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;
|
// best-effort diagnostic fields as `log_driver_status_once` (aligned word reads can't tear;
|
||||||
// no reference into the shared region is formed).
|
// no reference into the shared region is formed).
|
||||||
@@ -1093,8 +1232,10 @@ impl Capturer for IddPushCapturer {
|
|||||||
impl Drop for IddPushCapturer {
|
impl Drop for IddPushCapturer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.slots.clear();
|
self.slots.clear();
|
||||||
// The shared header + debug sections (`MappedSection`) and the frame-ready `event`
|
// The shared header section (`MappedSection`), the frame-ready `event` (`OwnedHandle`) and the
|
||||||
// (`OwnedHandle`) free themselves via RAII (each unmaps its view, then closes its handle).
|
// broker's WUDFHost process handle free themselves via RAII (unmap view, then close handle) —
|
||||||
// _keepalive drops after, REMOVEing the virtual display.
|
// 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 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 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 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**
|
//! the Windows backend talks to the UMDF driver over an **unnamed shared DATA section** (256 B `PadShm`:
|
||||||
//! `Global\pfds-shm-<idx>` (256 B: magic `u32@0`, input report `@8`, output seq `u32@72`, output
|
//! magic `u32@0`, input report `@8`, output seq `u32@72`, output report `@76`) reached over the
|
||||||
//! report `@76`). The host creates the section (privileged → a permissive SDDL so the WUDFHost can
|
//! **sealed channel** ([`PadChannel`], `design/gamepad-channel-sealing.md`): the host duplicates the
|
||||||
//! open it); the driver maps it from its timer, feeds game `READ_REPORT`s from the input bytes, and
|
//! section handle into the driver's WUDFHost, bootstrapped via the named `Global\pfds-boot-<idx>`
|
||||||
//! publishes a game's `0x02` (rumble / lightbar / player-LEDs / adaptive triggers) into the output
|
//! mailbox. The driver feeds game `READ_REPORT`s from the input bytes and publishes a game's `0x02`
|
||||||
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
//! (rumble / lightbar / player-LEDs / adaptive triggers) into the output bytes. `hidclass` gates the
|
||||||
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
//! 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
|
//! 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
|
//! `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,
|
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||||
DS_TOUCH_W,
|
DS_TOUCH_W,
|
||||||
};
|
};
|
||||||
|
use super::gamepad_raii::PadChannel;
|
||||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::time::{Duration, Instant};
|
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::{
|
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
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);
|
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, device_type);
|
||||||
pub(super) const OFF_DRIVER_PROTO: usize =
|
pub(super) const OFF_DRIVER_PROTO: usize =
|
||||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, driver_proto);
|
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;
|
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
|
/// 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.
|
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
||||||
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
|
||||||
struct DsWinPad {
|
struct DsWinPad {
|
||||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
/// 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).
|
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
||||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||||
shm: super::gamepad_raii::Shm,
|
channel: PadChannel,
|
||||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||||
attach: super::gamepad_raii::DriverAttach,
|
attach: super::gamepad_raii::DriverAttach,
|
||||||
seq: u8,
|
seq: u8,
|
||||||
@@ -184,7 +188,7 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
|||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
.chain(std::iter::once(0))
|
.chain(std::iter::once(0))
|
||||||
.collect();
|
.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).
|
// (multi-pad). The buffer outlives the SwDeviceCreate call (we wait on the event before return).
|
||||||
let loc: Vec<u16> = format!("{}", p.container_index)
|
let loc: Vec<u16> = format!("{}", p.container_index)
|
||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
@@ -266,17 +270,20 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DsWinPad {
|
impl DsWinPad {
|
||||||
/// Create + map the section `Global\pfds-shm-<index>`, stamp the magic, then spawn the
|
/// Create the sealed channel (unnamed DATA section + `Global\pfds-boot-<index>` mailbox), stamp
|
||||||
/// `root\pf_dualsense` devnode (the driver loads on it and maps the section). The devnode lives
|
/// the pad index + neutral report + the magic LAST, then spawn the `pf_pad_<index>` devnode (the
|
||||||
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
/// 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> {
|
fn open(index: u8) -> Result<DsWinPad> {
|
||||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||||
let base = shm.base();
|
let base = channel.data_base();
|
||||||
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
|
// Stamp the pad index (the driver validates it on attach) + the neutral input report, then
|
||||||
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
|
// the magic LAST (the driver only accepts the section once magic is set). The device-type
|
||||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
// stays 0 (DualSense — the section arrives zeroed).
|
||||||
|
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX/OFF_INPUT are in range.
|
||||||
unsafe {
|
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], {
|
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS_INPUT_REPORT_LEN], {
|
||||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
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
|
// 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`
|
// 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 inst = format!("pf_pad_{index}");
|
||||||
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
||||||
instance: &inst,
|
instance: &inst,
|
||||||
@@ -302,14 +309,17 @@ impl DsWinPad {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
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 {
|
Ok(DsWinPad {
|
||||||
_sw,
|
_sw,
|
||||||
shm,
|
channel,
|
||||||
attach: super::gamepad_raii::DriverAttach::new(
|
attach: super::gamepad_raii::DriverAttach::new(
|
||||||
"pf_dualsense",
|
"pf_dualsense",
|
||||||
"pf_dualsense.inf",
|
"pf_dualsense.inf",
|
||||||
"C:\\Users\\Public\\pfds-driver.log",
|
"C:\\Users\\Public\\pfds-driver.log",
|
||||||
shm_name,
|
boot_name,
|
||||||
instance_id,
|
instance_id,
|
||||||
),
|
),
|
||||||
seq: 0,
|
seq: 0,
|
||||||
@@ -326,30 +336,40 @@ impl DsWinPad {
|
|||||||
serialize_state(&mut r, st, self.seq, self.ts);
|
serialize_state(&mut r, st, self.seq, self.ts);
|
||||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||||
unsafe {
|
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
|
/// 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
|
/// [`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
|
/// new. Also ticks the sealed-channel delivery and feeds the driver-attach health watcher (the
|
||||||
/// `driver_proto` while it has the section mapped).
|
/// driver's ~125 Hz timer stamps `driver_proto` while it has the section mapped).
|
||||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||||
|
self.channel.pump();
|
||||||
let mut fb = DsFeedback::default();
|
let mut fb = DsFeedback::default();
|
||||||
// SAFETY: base points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let proto = unsafe {
|
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);
|
self.attach.observe(proto);
|
||||||
// SAFETY: base points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let seq =
|
let seq = unsafe {
|
||||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||||
|
};
|
||||||
if seq != self.last_out_seq {
|
if seq != self.last_out_seq {
|
||||||
self.last_out_seq = seq;
|
self.last_out_seq = seq;
|
||||||
let mut out = [0u8; 64];
|
let mut out = [0u8; 64];
|
||||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||||
unsafe {
|
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);
|
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
|
//! 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
|
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the sealed
|
||||||
//! `Global\pfds-shm-<idx>` shared section the driver maps), same controller model ([`DsState`]); only
|
//! shared-memory channel bootstrapped via `Global\pfds-boot-<idx>`), same controller model
|
||||||
//! the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the report codec
|
//! ([`DsState`]); only the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the
|
||||||
//! ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4) into the
|
//! report codec ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4)
|
||||||
//! section so the one UMDF driver serves the DS4 descriptor / attributes / features instead of the
|
//! into the DATA section so the one UMDF driver serves the DS4 descriptor / attributes / features
|
||||||
//! DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar (0xCD `Led`); a DS4
|
//! instead of the DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar
|
||||||
//! has no adaptive triggers / player LEDs.
|
//! (0xCD `Led`); a DS4 has no adaptive triggers / player LEDs.
|
||||||
|
|
||||||
use super::dualsense_proto::DsState;
|
use super::dualsense_proto::DsState;
|
||||||
use super::dualsense_windows::{
|
use super::dualsense_windows::{
|
||||||
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT,
|
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::{
|
use super::dualshock4_proto::{
|
||||||
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
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 crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use windows::core::HSTRING;
|
|
||||||
|
|
||||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the mapped
|
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the sealed
|
||||||
/// shared section. Dropping it removes the devnode and unmaps + closes the section.
|
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
|
||||||
struct Ds4WinPad {
|
struct Ds4WinPad {
|
||||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||||
shm: super::gamepad_raii::Shm,
|
channel: PadChannel,
|
||||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||||
attach: super::gamepad_raii::DriverAttach,
|
attach: super::gamepad_raii::DriverAttach,
|
||||||
counter: u8,
|
counter: u8,
|
||||||
@@ -36,16 +36,19 @@ struct Ds4WinPad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Ds4WinPad {
|
impl Ds4WinPad {
|
||||||
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
|
/// Create the sealed channel, stamp `device_type = DualShock 4` + the pad index + a neutral
|
||||||
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
|
/// 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> {
|
fn open(index: u8) -> Result<Ds4WinPad> {
|
||||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||||
let base = shm.base();
|
let base = channel.data_base();
|
||||||
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
|
// device-type FIRST (so it's visible the moment magic is), pad index, neutral report,
|
||||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
|
// magic LAST.
|
||||||
|
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
|
||||||
unsafe {
|
unsafe {
|
||||||
*base.add(OFF_DEVTYPE) = DEVTYPE_DUALSHOCK4;
|
*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], {
|
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS4_INPUT_REPORT_LEN], {
|
||||||
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
|
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
|
||||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
||||||
@@ -68,14 +71,18 @@ impl Ds4WinPad {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
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 {
|
Ok(Ds4WinPad {
|
||||||
_sw,
|
_sw,
|
||||||
shm,
|
channel,
|
||||||
attach: super::gamepad_raii::DriverAttach::new(
|
attach: super::gamepad_raii::DriverAttach::new(
|
||||||
"pf_dualshock4",
|
"pf_dualshock4",
|
||||||
"pf_dualsense.inf", // one driver package serves both HID identities
|
"pf_dualsense.inf", // one driver package serves both HID identities
|
||||||
"C:\\Users\\Public\\pfds-driver.log",
|
"C:\\Users\\Public\\pfds-driver.log",
|
||||||
shm_name,
|
boot_name,
|
||||||
instance_id,
|
instance_id,
|
||||||
),
|
),
|
||||||
counter: 0,
|
counter: 0,
|
||||||
@@ -92,29 +99,40 @@ impl Ds4WinPad {
|
|||||||
serialize_state(&mut r, st, self.counter, self.ts);
|
serialize_state(&mut r, st, self.counter, self.ts);
|
||||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||||
unsafe {
|
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
|
/// 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
|
/// [`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 {
|
fn service(&mut self) -> Ds4Feedback {
|
||||||
|
self.channel.pump();
|
||||||
let mut fb = Ds4Feedback::default();
|
let mut fb = Ds4Feedback::default();
|
||||||
// SAFETY: base points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let proto = unsafe {
|
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);
|
self.attach.observe(proto);
|
||||||
// SAFETY: base points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let seq =
|
let seq = unsafe {
|
||||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||||
|
};
|
||||||
if seq != self.last_out_seq {
|
if seq != self.last_out_seq {
|
||||||
self.last_out_seq = seq;
|
self.last_out_seq = seq;
|
||||||
let mut out = [0u8; 64];
|
let mut out = [0u8; 64];
|
||||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||||
unsafe {
|
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);
|
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
|
//! Each virtual pad owns three OS resources: the **unnamed** DATA section the `pf_dualsense`/`pf_xusb`
|
||||||
//! `pf_dualsense`/`pf_xusb` driver reads, and the `SwDeviceCreate`'d software devnode the driver loads
|
//! driver works against (`XusbShm`/`PadShm`), the tiny **named** bootstrap mailbox
|
||||||
//! on. Before this module, all three backends hand-rolled the same `CreateFileMappingW` +
|
//! (`pf_driver_proto::gamepad::PadBootstrap`) that hands the driver a duplicated handle to it, and the
|
||||||
//! `MapViewOfFile` and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` —
|
//! `SwDeviceCreate`'d software devnode the driver loads on. [`Shm`] and [`SwDevice`] own the resources
|
||||||
//! easy to drift or leak on an error path. [`Shm`] and [`SwDevice`] own those resources with RAII, so a
|
//! with RAII; [`PadChannel`] owns the two sections plus the delivery handshake.
|
||||||
//! backend just holds them and the cleanup (and ordering) happens by construction.
|
//!
|
||||||
|
//! **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 anyhow::{anyhow, bail, Context, Result};
|
||||||
use std::os::windows::io::{FromRawHandle, OwnedHandle};
|
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::sync::OnceLock;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use windows::core::{w, HSTRING, PCWSTR};
|
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,
|
CM_PROB, CR_SUCCESS, DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED,
|
||||||
};
|
};
|
||||||
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
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::{
|
use windows::Win32::Security::Authorization::{
|
||||||
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
||||||
};
|
};
|
||||||
@@ -26,54 +44,102 @@ use windows::Win32::System::Memory::{
|
|||||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
||||||
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
|
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
|
/// Least access the pad driver needs on the duplicated DATA section: it only MAPS it read/write, so
|
||||||
/// the view, then the [`OwnedHandle`] closes the section handle (in that order). Replaces the three
|
/// `SECTION_MAP_READ | SECTION_MAP_WRITE` (== the driver's `FILE_MAP_RW`). Granted explicitly in
|
||||||
/// backends' hand-duplicated `CreateFileMappingW` + `MapViewOfFile` + manual `Drop`.
|
/// [`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).
|
||||||
/// SDDL `D:(A;;GA;;;SY)(A;;GA;;;LS)`: GENERIC_ALL to **SYSTEM** (the host creates the section and
|
const SECTION_MAP_RW: u32 = 0x0004 | 0x0002;
|
||||||
/// 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)
|
/// An anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps the
|
||||||
/// assumption the driver needed a restricted token's broad access — letting any local user
|
/// view, then the [`OwnedHandle`] closes the section handle (in that order). Created either
|
||||||
/// `OpenFileMapping` the section to inject controller input or tamper the trusted channel
|
/// [unnamed](Self::create_unnamed) (the sealed DATA section — reachable only by handle duplication) or
|
||||||
/// (security-review 2026-06-28 #5). Verified on the RTX box (2026-06-29): the WUDFHost token is
|
/// [named](Self::create_named) (the bootstrap mailbox the driver opens by name).
|
||||||
/// `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.
|
|
||||||
pub(super) struct Shm {
|
pub(super) struct Shm {
|
||||||
/// Owns the section handle (closed on drop). Held only for ownership — never read after construction.
|
/// Owns the section handle (closed on drop). Also the duplication source for the sealed channel —
|
||||||
_handle: OwnedHandle,
|
/// see [`Shm::raw_handle`].
|
||||||
|
handle: OwnedHandle,
|
||||||
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shm {
|
/// Build a `SECURITY_ATTRIBUTES` from an SDDL literal (`psd` is OS-allocated and leaked — acceptable
|
||||||
/// Create + zero a `size`-byte section named `name`, mapped read/write. The section handle is owned
|
/// for the handful of pad channels a host creates; it must outlive the returned `SECURITY_ATTRIBUTES`).
|
||||||
/// immediately, so any failure below (or the returned `Shm`'s drop) closes it.
|
fn sddl_sa(sddl: PCWSTR) -> Result<SECURITY_ATTRIBUTES> {
|
||||||
pub(super) fn create(name: &HSTRING, size: usize) -> Result<Shm> {
|
|
||||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||||
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (freed at process
|
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (leaked — see above).
|
||||||
// exit — acceptable for a host-lifetime object).
|
|
||||||
unsafe {
|
unsafe {
|
||||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||||
w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"),
|
sddl,
|
||||||
SDDL_REVISION_1,
|
SDDL_REVISION_1,
|
||||||
&mut psd,
|
&mut psd,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
let sa = SECURITY_ATTRIBUTES {
|
Ok(SECURITY_ATTRIBUTES {
|
||||||
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||||
lpSecurityDescriptor: psd.0,
|
lpSecurityDescriptor: psd.0,
|
||||||
bInheritHandle: false.into(),
|
bInheritHandle: false.into(),
|
||||||
};
|
})
|
||||||
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the SDDL above.
|
}
|
||||||
|
|
||||||
|
impl Shm {
|
||||||
|
/// 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.
|
||||||
|
}
|
||||||
|
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 {
|
let map = unsafe {
|
||||||
CreateFileMappingW(
|
CreateFileMappingW(
|
||||||
INVALID_HANDLE_VALUE,
|
INVALID_HANDLE_VALUE,
|
||||||
Some(&sa),
|
Some(sa),
|
||||||
PAGE_READWRITE,
|
PAGE_READWRITE,
|
||||||
0,
|
0,
|
||||||
size as u32,
|
size as u32,
|
||||||
PCWSTR(name.as_ptr()),
|
name,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
// SAFETY: `map` is a fresh section handle we own; take ownership immediately so that the early
|
// 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) };
|
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size) };
|
||||||
if view.Value.is_null() {
|
if view.Value.is_null() {
|
||||||
// `handle` drops here → closes the section. No view to unmap.
|
// `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).
|
// SAFETY: `view` points at `size` writable bytes (just mapped).
|
||||||
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
|
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
|
||||||
Ok(Shm {
|
Ok(Shm { handle, view })
|
||||||
_handle: handle,
|
|
||||||
view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapped section's base pointer. Stable for the `Shm`'s lifetime (moving the `Shm` does not
|
/// 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 {
|
pub(super) fn base(&self) -> *mut u8 {
|
||||||
self.view.Value as *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 {
|
impl Drop for Shm {
|
||||||
fn drop(&mut self) {
|
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).
|
// section (struct fields drop only after this `Drop::drop` returns).
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = UnmapViewOfFile(self.view);
|
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
|
/// A `SwDeviceCreate`'d software devnode; drop removes it via `SwDeviceClose`. Replaces the manual
|
||||||
/// `SwDeviceClose` each backend used to call in its `Drop`.
|
/// `SwDeviceClose` each backend used to call in its `Drop`.
|
||||||
pub(super) struct SwDevice(HSWDEVICE);
|
pub(super) struct SwDevice(HSWDEVICE);
|
||||||
@@ -151,7 +443,7 @@ pub(super) struct DriverAttach {
|
|||||||
inf: &'static str,
|
inf: &'static str,
|
||||||
/// The driver's own debug log, referenced in the diagnosis line.
|
/// The driver's own debug log, referenced in the diagnosis line.
|
||||||
driver_log: &'static str,
|
driver_log: &'static str,
|
||||||
/// Section name, for log lines.
|
/// Bootstrap-mailbox name, for log lines (the DATA section is unnamed).
|
||||||
shm_name: String,
|
shm_name: String,
|
||||||
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
|
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
|
||||||
instance_id: Option<String>,
|
instance_id: Option<String>,
|
||||||
@@ -241,8 +533,8 @@ impl DriverAttach {
|
|||||||
devnode = %devnode,
|
devnode = %devnode,
|
||||||
driver_log = self.driver_log,
|
driver_log = self.driver_log,
|
||||||
"gamepad driver has not attached to the shared section — the virtual pad exists but no \
|
"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 \
|
driver is serving it (games will not see it); an old (pre-sealed-channel) driver also \
|
||||||
not-attached: update with punktfunk-host.exe driver install --gamepad"
|
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
|
//! 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
|
//! 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
|
//! 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
|
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into an **unnamed** shared
|
||||||
//! `Global\pfxusb-shm-<index>`. GameStream/Moonlight already speak the XInput conventions (low-16
|
//! DATA section the driver reaches over the **sealed channel** ([`PadChannel`] — handle duplicated
|
||||||
//! button bits, sticks −32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
|
//! 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
|
//! 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
|
//! 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.
|
//! 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 crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::ffi::c_void;
|
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::{
|
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
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_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_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_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,
|
/// 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).
|
/// 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()
|
.encode_utf16()
|
||||||
.chain(std::iter::once(0))
|
.chain(std::iter::once(0))
|
||||||
.collect();
|
.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).
|
// (multi-pad). The buffer must outlive the SwDeviceCreate call (it does; we wait on the event).
|
||||||
let loc: Vec<u16> = format!("{index}")
|
let loc: Vec<u16> = format!("{index}")
|
||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
@@ -171,12 +172,13 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
|
|||||||
Ok((hsw, ctx.instance_id()))
|
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 {
|
struct XusbWinPad {
|
||||||
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
|
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
|
||||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
/// Owns `Global\pfxusb-shm-<index>` (the section + its mapped view; drop unmaps + closes).
|
/// The sealed channel: the unnamed DATA section (the `XusbShm`) + the bootstrap mailbox + the
|
||||||
shm: super::gamepad_raii::Shm,
|
/// handle-delivery state machine (drop closes both sections).
|
||||||
|
channel: PadChannel,
|
||||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||||
attach: super::gamepad_raii::DriverAttach,
|
attach: super::gamepad_raii::DriverAttach,
|
||||||
packet: u32,
|
packet: u32,
|
||||||
@@ -184,17 +186,18 @@ struct XusbWinPad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
fn open(index: u8) -> Result<XusbWinPad> {
|
||||||
// Permissive-DACL named section the WUDFHost (whatever account) can open; `Shm` owns the
|
let boot_name = pf_driver_proto::gamepad::xusb_boot_name(index);
|
||||||
// section handle + its mapped view (zero-filled) and unmaps/closes on drop.
|
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||||
let shm_name = pf_driver_proto::gamepad::xusb_shm_name(index);
|
let base = channel.data_base();
|
||||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
// The section arrives zeroed; stamp the pad index (the driver validates it against its own
|
||||||
let base = shm.base();
|
// devnode index on attach) then the magic LAST (the driver only accepts it once magic is set).
|
||||||
// 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; OFF_PAD_INDEX is in range.
|
||||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
|
||||||
unsafe {
|
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);
|
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||||
}
|
}
|
||||||
let (hsw, instance_id) = match create_swdevice(index) {
|
let (hsw, instance_id) = match create_swdevice(index) {
|
||||||
@@ -205,14 +208,18 @@ impl XusbWinPad {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
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 {
|
Ok(XusbWinPad {
|
||||||
_sw,
|
_sw,
|
||||||
shm,
|
channel,
|
||||||
attach: super::gamepad_raii::DriverAttach::new(
|
attach: super::gamepad_raii::DriverAttach::new(
|
||||||
"pf_xusb",
|
"pf_xusb",
|
||||||
"pf_xusb.inf",
|
"pf_xusb.inf",
|
||||||
"C:\\Users\\Public\\pfxusb-driver.log",
|
"C:\\Users\\Public\\pfxusb-driver.log",
|
||||||
shm_name,
|
boot_name,
|
||||||
instance_id,
|
instance_id,
|
||||||
),
|
),
|
||||||
packet: 0,
|
packet: 0,
|
||||||
@@ -225,7 +232,7 @@ impl XusbWinPad {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
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);
|
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
|
// 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
|
// `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.
|
// 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
|
/// 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
|
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also ticks the sealed-channel
|
||||||
/// health watcher (the driver stamps `driver_proto` at device add + on every serviced IOCTL).
|
/// 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)> {
|
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.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
|
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
|
||||||
self.attach.observe(proto);
|
self.attach.observe(proto);
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ pub(crate) enum MonitorKey {
|
|||||||
Session(u64),
|
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(crate) struct AddedMonitor {
|
||||||
pub key: MonitorKey,
|
pub key: MonitorKey,
|
||||||
pub target_id: u32,
|
pub target_id: u32,
|
||||||
pub luid: LUID,
|
pub luid: LUID,
|
||||||
|
pub wudf_pid: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The backend-specific IOCTL surface — the *only* thing that differs between SudoVDA and pf-vdisplay.
|
/// The backend-specific IOCTL surface — the *only* thing that differs between SudoVDA and pf-vdisplay.
|
||||||
@@ -91,6 +93,9 @@ struct Monitor {
|
|||||||
key: MonitorKey,
|
key: MonitorKey,
|
||||||
target_id: u32,
|
target_id: u32,
|
||||||
luid: LUID,
|
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>,
|
gdi_name: Option<String>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
stop: Arc<AtomicBool>,
|
stop: Arc<AtomicBool>,
|
||||||
@@ -109,6 +114,7 @@ impl Monitor {
|
|||||||
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
|
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
|
||||||
gdi_name: n,
|
gdi_name: n,
|
||||||
target_id: self.target_id,
|
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")
|
.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 {
|
impl VirtualDisplayManager {
|
||||||
pub(crate) fn backend_name(&self) -> &'static str {
|
pub(crate) fn backend_name(&self) -> &'static str {
|
||||||
self.driver.name()
|
self.driver.name()
|
||||||
@@ -436,6 +450,7 @@ impl VirtualDisplayManager {
|
|||||||
key: added.key,
|
key: added.key,
|
||||||
target_id: added.target_id,
|
target_id: added.target_id,
|
||||||
luid: added.luid,
|
luid: added.luid,
|
||||||
|
wudf_pid: added.wudf_pid,
|
||||||
gdi_name,
|
gdi_name,
|
||||||
mode,
|
mode,
|
||||||
stop,
|
stop,
|
||||||
|
|||||||
@@ -158,6 +158,33 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
|
|||||||
.context("pf-vdisplay SET_RENDER_ADAPTER")
|
.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> {
|
unsafe fn open_device() -> Result<HANDLE> {
|
||||||
let hdev = SetupDiGetClassDevsW(
|
let hdev = SetupDiGetClassDevsW(
|
||||||
Some(&PF_VDISPLAY_INTERFACE),
|
Some(&PF_VDISPLAY_INTERFACE),
|
||||||
@@ -354,12 +381,13 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
HighPart: reply.adapter_luid_high,
|
HighPart: reply.adapter_luid_high,
|
||||||
};
|
};
|
||||||
tracing::info!(
|
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.width,
|
||||||
mode.height,
|
mode.height,
|
||||||
mode.refresh_hz,
|
mode.refresh_hz,
|
||||||
reply.target_id,
|
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?
|
// 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
|
// 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),
|
key: MonitorKey::Session(session_id),
|
||||||
target_id: reply.target_id,
|
target_id: reply.target_id,
|
||||||
luid,
|
luid,
|
||||||
|
wudf_pid: reply.wudf_pid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,9 +162,28 @@ fn install_gamepad(dir: &Path) -> Result<()> {
|
|||||||
eprintln!("warning: pnputil /add-driver {} failed", inf.display());
|
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(())
|
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]` ──────────────────────────────────────────────────────────────
|
// ── `driver uninstall [--gamepad]` ──────────────────────────────────────────────────────────────
|
||||||
// The uninstaller's cleanup counterpart (Inno [UninstallRun]) — the field report was that our
|
// 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
|
// 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<()> {
|
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"]);
|
delete_store_drivers(&["pf_dualsense", "pf_dualshock4", "pf_xusb"]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -235,6 +257,28 @@ fn pf_vdisplay_instance_ids() -> Vec<String> {
|
|||||||
ids
|
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
|
/// 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
|
/// `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
|
/// without parsing `pnputil /enum-drivers`' localized output. `/uninstall /force` also unbinds it
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Handoff — sealing the gamepad SHM channels
|
||||||
|
|
||||||
|
Status: **implemented (Option A), 2026-07-03 — Windows CI + on-glass validation pending.** The design
|
||||||
|
below was implemented as proposed; the "Implementation notes" section records what was actually built
|
||||||
|
and the deltas. Remaining: build + sign + redeploy both pad drivers, then the hardware validation plan
|
||||||
|
(§Validation) — it needs a physical controller on the box.
|
||||||
|
|
||||||
|
This closes the one open residual left by the IDD-push sealed-channel work
|
||||||
|
(`design/idd-push-security.md`): frames were sealed; the gamepad input/output channel was not.
|
||||||
|
|
||||||
|
## Unsafe hygiene (2026-07-03 follow-up — the drivers' `unsafe` was confined)
|
||||||
|
|
||||||
|
After the seal landed, the pad drivers' `unsafe` footprint (raw `OpenFileMapping`/`MapViewOfFile`,
|
||||||
|
`read_unaligned`, the whole bootstrap state machine as bare-pointer arithmetic) was pulled into a new
|
||||||
|
audited crate **`pf-umdf-util`** (`packaging/windows/drivers/pf-umdf-util/`), so the drivers benefit
|
||||||
|
from Rust instead of being C-in-Rust:
|
||||||
|
|
||||||
|
- `section::MappedView` — a mapped section wrapped as bounds- + alignment-checked accessors
|
||||||
|
(`load_u32`/`store_u32`/`read_bytes`/…). Callers never see the base pointer; an out-of-range offset
|
||||||
|
asserts instead of corrupting. `ViewCell` holds the adopted view as a leaked `&'static` (the
|
||||||
|
re-delivery-must-not-unmap rule, now type-enforced).
|
||||||
|
- `channel::ChannelClient` — the ENTIRE sealed-channel driver side (publish pid → adopt handle →
|
||||||
|
validate magic+`pad_index`), as a **`#![forbid(unsafe_code)]`** module over `MappedView`. One
|
||||||
|
implementation both pad drivers share (was hand-duplicated).
|
||||||
|
- `wdf::{Request, query_location_index, retrieve_next_request}` — the WDF request/memory/property FFI
|
||||||
|
behind safe methods; a callback turns its raw `WDFREQUEST` into a `Request` token once (the only
|
||||||
|
`unsafe` at the driver boundary), and completion consumes the token.
|
||||||
|
|
||||||
|
Result: `pf-xusb`/`pf-dualsense` business logic is **100 % safe Rust**; the only remaining `unsafe` in
|
||||||
|
them is the unavoidable WDF *setup* FFI in `DriverEntry`/`EvtDeviceAdd`/the timer, each with a
|
||||||
|
`// SAFETY:` proof. The display driver `pf-vdisplay` is inherently FFI-bound (D3D11 / IddCx DDIs /
|
||||||
|
cross-process textures) so it can't be unsafe-*free*, but it's now unsafe-*audited*: every `unsafe {}`
|
||||||
|
carries a proof. Both invariants are lint-gated across the whole drivers workspace
|
||||||
|
(`#![deny(unsafe_op_in_unsafe_fn)]` + `#![deny(clippy::undocumented_unsafe_blocks)]`) and enforced by
|
||||||
|
a new `cargo clippy -D warnings` step in `windows-drivers.yml`. Verified on the RTX box (.173): the
|
||||||
|
whole workspace builds + clippies + fmt-checks clean; both gamepad DLLs still produce.
|
||||||
|
|
||||||
|
## Implementation notes (what was built, 2026-07-03)
|
||||||
|
|
||||||
|
- **Contract** (`pf_driver_proto::gamepad`, `GAMEPAD_PROTO_VERSION = 2`): `PadBootstrap` (32 B —
|
||||||
|
`magic "PFBT"`, `host_proto`, `driver_pid`, `driver_proto`, `data_handle: u64`, `handle_pid`,
|
||||||
|
`handle_seq`) with `Pod` + `offset_of!` asserts; `xusb_boot_name`/`pad_boot_name`
|
||||||
|
(`Global\pf…-boot-<index>`) REPLACE the old `*_shm_name` fns (the DATA-section name is gone);
|
||||||
|
`XusbShm`/`PadShm` gained `pad_index` (carved from reserved space) so the DRIVER validates a
|
||||||
|
delivery resolves to *its own* pad — the authentic-side answer to the "redirect the dup into a
|
||||||
|
different pad's WUDFHost" hardening note (the section content is host-written and unreachable by a
|
||||||
|
sibling LS, so the check can't be spoofed). Both pad drivers now path-dep `pf-driver-proto` (as
|
||||||
|
pf-vdisplay does) instead of hand-synced literals.
|
||||||
|
- **Host** (`inject/windows/gamepad_raii.rs`): `Shm::create_unnamed` (DATA, `D:P(A;;GA;;;SY)`) +
|
||||||
|
`Shm::create_named` (mailbox, SY+LS, **squat-checked** — `ERROR_ALREADY_EXISTS` on create is
|
||||||
|
close+retry×5 then a hard error, so the handshake never runs through a pre-created object; this also
|
||||||
|
turns the previously-silent two-hosts-same-index cross-wire into a loud failure). `PadChannel` owns
|
||||||
|
both + the delivery state machine: poll `driver_pid` → `OpenProcess` →
|
||||||
|
`verify_is_wudfhost` (now shared with the frame broker in `capture/windows/idd_push.rs`) →
|
||||||
|
`DuplicateHandle` → publish `data_handle`/`handle_pid`, bump `handle_seq` last (Release). Pumped
|
||||||
|
from each backend's existing service tick (≤4 ms) + a bounded **eager delivery** (1.5 s) at pad-open
|
||||||
|
so the DS4's `device_type` is readable before hidclass asks for descriptors. Delivery attempts are
|
||||||
|
**capped at 16 per pad** so a tampered flapping mailbox can't mint unbounded remote handles. Same
|
||||||
|
pid never retried (failed verify can't be spun into a hot loop).
|
||||||
|
- **Drivers** (`pf-xusb`, `pf-dualsense`): per-tick `pump_bootstrap()` (the DS timer / every XUSB
|
||||||
|
IOCTL + a bounded EvtDeviceAdd worker thread for XUSB's no-game-running case) opens the mailbox *by
|
||||||
|
name each time* — the name existing doubles as host-liveness, replacing the old per-access section
|
||||||
|
open; mailbox gone → detach (DS additionally resets the pended-read report to neutral instead of
|
||||||
|
the old frozen-last-state behavior). The driver writes `driver_proto` always but publishes its pid
|
||||||
|
**only when `host_proto` matches** (fail closed both ways: v1 host never creates a mailbox a v2
|
||||||
|
driver polls; a v1 driver opens a name that no longer exists). A delivery is adopted once
|
||||||
|
(CAS on `handle_seq`, reset when the mailbox disappears so a new host session's counter can't
|
||||||
|
collide), mapped, and validated: `magic` AND `pad_index == SHM_INDEX` — else unmapped + ignored
|
||||||
|
(the handle is deliberately NOT closed on validation failure: a tampered value could name an
|
||||||
|
unrelated handle in the driver's own table). The adopted view is cached and never unmapped
|
||||||
|
(re-delivery swaps + leaks the old 64/256 B mapping on purpose — a concurrent reader may hold it).
|
||||||
|
Driver log line for validation step 3: `sealed pad channel mapped (index …)`.
|
||||||
|
- **Not built:** Option B (devnode custom properties). The residual named mailbox is documented and
|
||||||
|
DoS-bounded; migrate later if it's ever deemed worth removing.
|
||||||
|
|
||||||
|
## The problem (why this exists)
|
||||||
|
|
||||||
|
Each virtual pad's host↔driver channel is a **named** shared-memory section:
|
||||||
|
|
||||||
|
- `Global\pfxusb-shm-<index>` (64 B, [`pf_driver_proto::gamepad::XusbShm`]) — virtual Xbox 360 / XInput.
|
||||||
|
- `Global\pfds-shm-<index>` (256 B, [`pf_driver_proto::gamepad::PadShm`]) — virtual DualSense / DualShock 4.
|
||||||
|
|
||||||
|
Both are created by the SYSTEM host with DACL `D:(A;;GA;;;SY)(A;;GA;;;LS)` (`inject/windows/gamepad_raii.rs`
|
||||||
|
`Shm::create`) so the driver's WUDFHost (LocalService) can open them by name. That means **a sibling
|
||||||
|
LocalService process can `OpenFileMapping` the section by name** and:
|
||||||
|
|
||||||
|
- **read** the victim's live controller input (buttons/sticks/gyro/touchpad — host→driver `input` region), and
|
||||||
|
- **inject/forge** gamepad input or rumble (write the `input` region → the driver feeds it to whatever game
|
||||||
|
has focus; write the `output` region + bump `out_seq` → forge rumble/LED back to the client).
|
||||||
|
|
||||||
|
This is the *same* name-open vector we closed for frames, one module over. Severity is lower than desktop
|
||||||
|
capture (it's game-controller I/O, scoped to the focused app, and requires the attacker to already have
|
||||||
|
LocalService code execution), but it is real and it is inconsistent to leave named next to a sealed frame ring.
|
||||||
|
|
||||||
|
**Not a stopgap:** randomizing the section name is inadequate — the object namespace is enumerable with
|
||||||
|
`NtQueryDirectoryObject`, so a random name is discoverable. (Same reason it was rejected for frames.) The fix
|
||||||
|
is to remove the name.
|
||||||
|
|
||||||
|
## Why it isn't already sealed the frame way
|
||||||
|
|
||||||
|
The frame channel seals cleanly because pf-vdisplay has a **control device** (the IddCx device interface):
|
||||||
|
the host duplicates the unnamed handles into the driver's WUDFHost and delivers the values over
|
||||||
|
`IOCTL_SET_FRAME_CHANNEL`, and the driver reports its own pid in the `IOCTL_ADD` reply.
|
||||||
|
|
||||||
|
The pad drivers (`pf-dualsense`, `pf-xusb`) are **UMDF HID minidrivers with no control device** — hidclass
|
||||||
|
owns the device stack and blocks a freely-openable control interface. That is *why* they use a named section
|
||||||
|
in the first place. So there is no IOCTL to (a) hand the driver a duplicated handle or (b) learn the driver's
|
||||||
|
WUDFHost pid. Compounding it: `pszDeviceLocation` (the existing host→driver property) is fixed at
|
||||||
|
`SwDeviceCreate` time — **before** the WUDFHost process exists — so the host can't duplicate a handle into a
|
||||||
|
not-yet-created process and stamp its value there. A bidirectional, late-bound handshake is required.
|
||||||
|
|
||||||
|
## Current architecture (what to modify)
|
||||||
|
|
||||||
|
Host (`crates/punktfunk-host/src/inject/windows/`):
|
||||||
|
- `gamepad_raii.rs` — `Shm::create(name, size)` creates the **named** section (SY+LS SDDL) + maps it;
|
||||||
|
`SwDevice` wraps the `SwDeviceCreate` devnode.
|
||||||
|
- `gamepad_windows.rs` (XUSB), `dualsense_windows.rs` (DualSense/DS4), `dualshock4_windows.rs` — each creates
|
||||||
|
its `Shm`, then `create_swdevice(index)` / `create_swdevice(profile)` which stamps the pad **index** into
|
||||||
|
`info.pszDeviceLocation` (a UTF-16 decimal string) and creates `pf_xusb_<index>` / `pf_pad_<index>`.
|
||||||
|
|
||||||
|
Driver (`packaging/windows/drivers/pf-{xusb,dualsense}/src/lib.rs`):
|
||||||
|
- `query_shm_index(device)` — `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` → parses the
|
||||||
|
decimal → `SHM_INDEX` static.
|
||||||
|
- On first control activity it builds `format!("Global\\pf…-shm-{}", SHM_INDEX)`, `OpenFileMappingW` +
|
||||||
|
`MapViewOfFile`. The dualsense driver also runs a ~125 Hz timer (writes `driver_heartbeat`) — an existing
|
||||||
|
poll loop to piggyback a bootstrap-wait on.
|
||||||
|
|
||||||
|
Contract (`crates/pf-driver-proto/src/lib.rs` `mod gamepad`): owns `XusbShm`/`PadShm` layouts, the magics,
|
||||||
|
`xusb_shm_name`/`pad_shm_name`, `device_type`, `GAMEPAD_PROTO_VERSION`, and the driver_proto/heartbeat fields.
|
||||||
|
|
||||||
|
## Proposed design — a late-bound bootstrap handshake
|
||||||
|
|
||||||
|
Split each pad's channel into **(1) an unnamed DATA section** (the real `XusbShm`/`PadShm`, host↔driver) and
|
||||||
|
**(2) a tiny bootstrap mailbox** that carries only a magic + the driver's pid + a handle value. The handshake:
|
||||||
|
|
||||||
|
1. **Host**, per pad: create the DATA section **unnamed** (`CreateFileMappingW` with `PCWSTR::null()`, DACL
|
||||||
|
`D:P(A;;GA;;;SY)` — SYSTEM-only, exactly as the sealed frame ring now uses; the driver reaches it by
|
||||||
|
duplicated handle, which carries access, so no LS ACE is needed). Then create the devnode via
|
||||||
|
`SwDeviceCreate`, stamping the pad index into `pszDeviceLocation` **as today** (the index still identifies
|
||||||
|
*which* pad's bootstrap the driver should use).
|
||||||
|
2. **Driver** `EvtDeviceAdd`: read the index (unchanged `query_shm_index`). Write `std::process::id()` where
|
||||||
|
the host can read it, then **poll** (piggyback the existing timer) for a delivered handle value; map the
|
||||||
|
DATA section from it once non-zero.
|
||||||
|
3. **Host**: learn the driver's pid, `OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION)`,
|
||||||
|
**verify it is the WUDFHost servicing this pad's devnode** (see hardening note), `DuplicateHandle` the
|
||||||
|
DATA section into the WUDFHost, and deliver the resulting handle value back to the driver.
|
||||||
|
|
||||||
|
Two viable transports for steps 2–3's pid-out / handle-in (pick one):
|
||||||
|
|
||||||
|
- **Option A — named bootstrap mailbox** (`Global\pf…-boot-<index>`, ~32 B, SY+LS): host creates it; driver
|
||||||
|
opens it by name (index from location), writes `driver_pid`, spins on `data_handle` != 0; host polls
|
||||||
|
`driver_pid`, dups the DATA section in, writes `data_handle` + a ready seq. **Safe to leave named + SY+LS**
|
||||||
|
because it carries *only* a pid (not sensitive) and a handle value (meaningless outside the target WUDFHost)
|
||||||
|
— identical to the frame channel's "the bootstrap ACL is not load-bearing" argument. A sibling LS that reads
|
||||||
|
it learns nothing exploitable; one that tampers it can at worst feed a bogus pid/handle → the driver maps a
|
||||||
|
value that doesn't resolve in its own table → **DoS, not a breach** (the attacker cannot place a valid
|
||||||
|
section handle in the WUDFHost, so it cannot make the driver map an attacker-controlled section). *Fastest to
|
||||||
|
build — reuses the existing named-section + poll machinery.*
|
||||||
|
- **Option B — devnode custom properties** (no `Global\` object at all): driver writes its pid via
|
||||||
|
`WdfDeviceAssignProperty(DEVPROPKEY_pf_pad_pid)`; host reads it via `CM_Get_DevNode_PropertyW` /
|
||||||
|
`SetupDiGetDevicePropertyW`, dups in, writes a `DEVPROPKEY_pf_pad_handle` property; driver re-queries it in
|
||||||
|
its timer. Tighter (property store isn't world-readable like the Global namespace) but more moving parts and
|
||||||
|
UMDF-property-write ergonomics to prove out. *Cleaner end-state.*
|
||||||
|
|
||||||
|
Recommendation: **build Option A first** (small, mirrors the frame channel, gets the DATA section unnamed —
|
||||||
|
which is the actual isolation win, proven by #3 below), then optionally migrate the bootstrap to Option B if
|
||||||
|
the residual named mailbox is deemed worth removing.
|
||||||
|
|
||||||
|
## Reuse the frame-channel precedent
|
||||||
|
|
||||||
|
- **Ownership/adopt-on-success** discipline from `capture/windows/idd_push.rs` `ChannelBroker` — exactly one
|
||||||
|
side ever closes a duplicated handle value; reap remote duplicates (`DUPLICATE_CLOSE_SOURCE`) on any failure.
|
||||||
|
- **`verify_is_wudfhost`** (`idd_push.rs`) — before duplicating into the driver-reported pid, confirm it's
|
||||||
|
`%SystemRoot%\System32\WUDFHost.exe`. **Strengthen it here**: also confirm the pid is the host *servicing
|
||||||
|
this pad's devnode* (walk devnode → process, e.g. via the driver writing a per-pad nonce it echoes, or a
|
||||||
|
devnode/PID association) so a tampered bootstrap can't redirect the dup into a *different* pad's WUDFHost.
|
||||||
|
- **Contract in `pf_driver_proto::gamepad`** — add the bootstrap layout (`PadBootstrap { magic, driver_pid,
|
||||||
|
data_handle: u64, seq }`) with `Pod` + `offset_of!` asserts, bump `GAMEPAD_PROTO_VERSION`, and (Option A)
|
||||||
|
keep `pad_shm_name`/`xusb_shm_name` only for the bootstrap mailbox, dropping the data-section name.
|
||||||
|
- **SDDL** on the DATA section: `D:P(A;;GA;;;SY)` (SYSTEM-only) — validated safe for a duplicated-handle
|
||||||
|
consumer on the frame ring (the driver's `OpenSharedResource`/`MapViewOfFile` on a handle does not re-check
|
||||||
|
the object DACL).
|
||||||
|
|
||||||
|
## Security properties after the change
|
||||||
|
|
||||||
|
- The **DATA section is unnamed** and only ever handle-duplicated into the pad WUDFHost. Empirically
|
||||||
|
(`design/idd-push-security.md`, RTX box 2026-07-03) a **LocalService token is DACL-denied `OpenProcess` on a
|
||||||
|
UMDF WUDFHost for every access right incl. `QUERY_LIMITED`** — so a sibling LS cannot dup the handle out or
|
||||||
|
read the WUDFHost's memory. Unnamed + unopenable-host ⇒ no sibling-LS path to the input/output data. This is
|
||||||
|
the same guarantee the frame channel now has, and it rests on the same verified property.
|
||||||
|
- **Residual (Option A):** the bootstrap mailbox stays named + SY+LS, but carries only a pid + handle value →
|
||||||
|
worst case a sibling LS causes a **gamepad DoS**, never a read or injection. Option B removes even that.
|
||||||
|
- **Unchanged inherent limits:** admin/SYSTEM = total; the game reading the pad sees the input by design.
|
||||||
|
|
||||||
|
## Validation plan (needs hardware)
|
||||||
|
|
||||||
|
The blocker for calling this done is that it **requires a physical controller on the box** — the memory notes
|
||||||
|
repeatedly flag the gamepad path as "needs a physical pad to live-verify," and neither the probe nor a
|
||||||
|
synthetic client exercises a real game reading the virtual pad.
|
||||||
|
|
||||||
|
1. Build + sign + redeploy `pf-dualsense` and `pf-xusb` (same loop as pf-vdisplay:
|
||||||
|
`packaging/windows/drivers/deploy-dev.ps1` per driver, or `redeploy-*`; DriverVer must strictly increase).
|
||||||
|
Bump `GAMEPAD_PROTO_VERSION` — a v_new host against a v_old pad driver (or vice-versa) must fail closed, so
|
||||||
|
deploy host + both pad drivers together.
|
||||||
|
2. Connect a real client with a physical controller; confirm in a game that input works and rumble/LED return.
|
||||||
|
3. Driver log (`C:\Users\Public\pfds-driver.log` / `pfxusb-driver.log` in debug builds): confirm the driver
|
||||||
|
reports its pid, receives a handle, and maps the DATA section (add a `dbglog!` "sealed pad channel mapped").
|
||||||
|
4. Re-run the **sibling-LS `OpenFileMapping` test**: from a LocalService scheduled task, attempt to open the
|
||||||
|
old `Global\pf…-shm-<index>` name — it must now **fail (name gone)**, and attempting to open the bootstrap
|
||||||
|
(Option A) must yield only pid+handle bytes. (Reuse the scheduled-task P/Invoke harness from the #3 frame
|
||||||
|
test — see the session that produced `design/idd-push-security.md`.)
|
||||||
|
5. Multi-pad: two controllers → two devnodes, two unnamed DATA sections, two bootstraps by index; confirm no
|
||||||
|
cross-talk and clean teardown (`SwDeviceClose` + host handle close; the WUDFHost dies with its devnode).
|
||||||
|
|
||||||
|
## Risks / gotchas
|
||||||
|
|
||||||
|
- **Regression risk to a working feature.** Gamepad input currently works on glass; this reroutes its
|
||||||
|
bootstrap. Keep the change behind the `GAMEPAD_PROTO_VERSION` bump and be ready to revert both drivers.
|
||||||
|
- **Chicken-and-egg timing.** The driver loads and wants the handle before the host has dup'd it — the poll
|
||||||
|
loop must tolerate a bounded wait (mirror the frame path's `wait_for_attach`, ~4 s) and the driver must not
|
||||||
|
block `EvtDeviceAdd` on it (spin in the timer, not the add callback).
|
||||||
|
- **Handle value in shared memory is a `u64`.** A WUDFHost handle value is process-local; writing it to the
|
||||||
|
bootstrap is safe (meaningless elsewhere), but the driver must treat it as untrusted (validate the mapped
|
||||||
|
DATA section's magic before use — the existing `XusbShm`/`PadShm` magic already gives this).
|
||||||
|
- **Two drivers, one contract.** DualSense and DualShock 4 share `pf-dualsense`/`PadShm`; XUSB is separate.
|
||||||
|
Factor the bootstrap into `pf_driver_proto::gamepad` so both drivers + the host use one definition (as the
|
||||||
|
frame channel does).
|
||||||
|
|
||||||
|
## Effort
|
||||||
|
|
||||||
|
Medium — comparable to the frame sealed-channel change but across **two** drivers plus the host inject code,
|
||||||
|
and gated on **physical-controller validation** that can't be driven over SSH. Files: `pf_driver_proto`
|
||||||
|
(gamepad module), `inject/windows/{gamepad_raii,gamepad_windows,dualsense_windows,dualshock4_windows}.rs`,
|
||||||
|
`packaging/windows/drivers/pf-{xusb,dualsense}/src/lib.rs`. Reference implementation: the frame sealed channel
|
||||||
|
(`capture/windows/idd_push.rs` + `packaging/windows/drivers/pf-vdisplay/src/{control,monitor,frame_transport}.rs`
|
||||||
|
+ `pf_driver_proto` `control`/`frame`).
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# IDD-push frame channel — security model (the sealed channel)
|
||||||
|
|
||||||
|
Status: **implemented** (host `capture/windows/idd_push.rs` + driver
|
||||||
|
`packaging/windows/drivers/pf-vdisplay/src/{control,monitor,frame_transport}.rs`, contract
|
||||||
|
`crates/pf-driver-proto` v2). Windows CI-validated; on-glass validation pending.
|
||||||
|
|
||||||
|
## What is being protected
|
||||||
|
|
||||||
|
The IDD-push path moves **whole-desktop frames** — including the secure desktop (UAC prompts, the
|
||||||
|
lock screen) — from the pf-vdisplay driver (UMDF, running in a `WUDFHost.exe` under LocalService)
|
||||||
|
into the SYSTEM host for encoding. That data is SYSTEM-tier-sensitive, and because we bypass the OS
|
||||||
|
capture APIs (Desktop Duplication / WGC), **we own the isolation those APIs would have provided.**
|
||||||
|
|
||||||
|
DDA's isolation property is that capturer and consumer are the same process: there is no openable
|
||||||
|
channel at all — to reach the frames you must own the capturing process. The sealed channel
|
||||||
|
reproduces exactly that property for our two-process design.
|
||||||
|
|
||||||
|
## The design
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐ control device (SY+BA only) ┌───────────────────────────┐
|
||||||
|
│ Host (SYSTEM service) │ ── IOCTL_SET_FRAME_CHANNEL: handle ────▶ │ pf-vdisplay driver │
|
||||||
|
│ creates header/event/ │ VALUES only (integers) │ (WUDFHost, LocalService) │
|
||||||
|
│ ring textures UNNAMED, │ │ maps/opens the duplicated │
|
||||||
|
│ DuplicateHandle()s them │ ◀── frames via keyed-mutex textures ──── │ handles; publishes frames │
|
||||||
|
│ INTO WUDFHost, encodes │ (no names anywhere) │ │
|
||||||
|
└──────────────────────────┘ └───────────────────────────┘
|
||||||
|
trust boundary: only these two processes ever hold a handle to any frame object
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Every frame object is unnamed** (header section, frame-ready event, all ring textures —
|
||||||
|
`CreateFileMappingW`/`CreateEventW`/`CreateSharedHandle` with a null name). An unnamed object is
|
||||||
|
in no namespace: it cannot be enumerated (`NtQueryDirectoryObject` can't see it), cannot be
|
||||||
|
opened by name, and cannot be pre-created ("squatted"). It can be shared **only** by handle
|
||||||
|
duplication.
|
||||||
|
2. **The host is the broker.** SYSTEM opens the driver's WUDFHost with `PROCESS_DUP_HANDLE` (the pid
|
||||||
|
comes from the `IOCTL_ADD` reply, per-monitor, so a WUDFHost restart can't leave us duplicating
|
||||||
|
into a dead process) and `DuplicateHandle`s each object in. The reverse direction — LocalService
|
||||||
|
injecting into SYSTEM — is correctly denied by the OS, which is why the broker must be the host.
|
||||||
|
3. **The bootstrap carries only integers.** `IOCTL_SET_FRAME_CHANNEL` delivers the duplicated handle
|
||||||
|
*values*. A handle value is only meaningful inside the target process's handle table: a third
|
||||||
|
party that read (or even forged) the message would learn nothing openable and could at most feed
|
||||||
|
values that don't resolve — a DoS of its own session, not a read. The bootstrap's ACL is therefore
|
||||||
|
**not load-bearing**; we still restrict the control device to `D:P(A;;GA;;;SY)(A;;GA;;;BA)`
|
||||||
|
(INF `Security`), because ADD/REMOVE/CLEAR_ALL shouldn't be world-callable either.
|
||||||
|
|
||||||
|
Net result: the only way to reach the frames is to already run code as SYSTEM (the host) or inside
|
||||||
|
that specific WUDFHost (the driver) — DDA's property, achieved in user mode.
|
||||||
|
|
||||||
|
## Why user-mode, not a kernel driver
|
||||||
|
|
||||||
|
Ring level does not govern cross-process memory visibility — the handle/VAD access checks do; a user
|
||||||
|
process cannot `ReadProcessMemory` a LocalService process regardless of rings. What kernel-mode
|
||||||
|
*would* change is the blast radius of a driver bug: UMDF caps a pf-vdisplay compromise at the
|
||||||
|
LocalService token, a KMDF display driver would make it ring-0 full-system. Least-blast-radius is
|
||||||
|
the reason punktfunk ships **zero** kernel drivers (the gamepad stack dropped ViGEmBus for UMDF for
|
||||||
|
the same reason). The correct control for "SYSTEM-tier data in the channel" is sealing the channel —
|
||||||
|
done above — not raising the ring.
|
||||||
|
|
||||||
|
## Handle-lifetime invariants (the auditable list)
|
||||||
|
|
||||||
|
1. Frame objects unnamed; bootstrap carries only handle values. ✔ by construction
|
||||||
|
2. `bInheritHandle: false` on every object — no child inherits a handle. ✔
|
||||||
|
3. Zero-init header + atomic `magic`-last publish (the driver never acts on a half-initialized
|
||||||
|
ring); generation-tagged publish tokens reject stale-ring frames. ✔
|
||||||
|
4. Attacker-influenced header fields are bounds-checked before use (generation/seq/slot unpacking;
|
||||||
|
`ring_len` clamped; the driver validates `IOCTL_SET_FRAME_CHANNEL` before adopting anything). ✔
|
||||||
|
5. **Adopt-on-success-only:** the driver owns (and eventually closes) the delivered handles iff the
|
||||||
|
IOCTL completed successfully; on ANY error completion it leaves them untouched and the host reaps
|
||||||
|
its remote duplicates (`DUPLICATE_CLOSE_SOURCE`). Exactly one side closes each value — no
|
||||||
|
double-close of possibly-reused handle values, no leak on a half-delivered channel. ✔
|
||||||
|
6. Single ownership inside the driver: each delivery lives in exactly one place (monitor stash →
|
||||||
|
publisher), and whichever owner dies — replaced stash, dropped publisher, removed monitor, reaped
|
||||||
|
watchdog, departed device — closes the handles (`FrameChannel`/publisher `Drop`). Host-side
|
||||||
|
objects are RAII (`MappedSection`, `OwnedHandle`); nothing survives the capturer. ✔
|
||||||
|
7. The object DACL is `D:P(A;;GA;;;SY)` — **SYSTEM only, protected**. Since the driver reaches the
|
||||||
|
objects via duplicated handles (which carry their own access; `OpenSharedResource1` on a handle does
|
||||||
|
not re-check the object DACL), the LocalService ACE was dropped — the minimal DACL. ✔ *(on-glass
|
||||||
|
confirmed 2026-07-03: the driver still attaches + delivers frames with SYSTEM-only objects.)*
|
||||||
|
8. **The duplication target is verified.** Before duplicating frame handles into `AddReply.wudf_pid`,
|
||||||
|
the host confirms that pid is `%SystemRoot%\System32\WUDFHost.exe` (`verify_is_wudfhost`). A spoofed
|
||||||
|
devnode advertising our interface GUID cannot redirect frames to an arbitrary process. ✔
|
||||||
|
9. **Handles are duplicated with least privilege, not `DUPLICATE_SAME_ACCESS`.** The driver's copy of
|
||||||
|
the header section is `SECTION_MAP_READ|WRITE` (matched by the driver mapping `FILE_MAP_READ|WRITE`,
|
||||||
|
not `FILE_MAP_ALL_ACCESS`), the frame-ready event is `EVENT_MODIFY_STATE` (the driver only signals
|
||||||
|
it), and the ring textures keep their already-scoped `CreateSharedHandle` access
|
||||||
|
(`DXGI_SHARED_RESOURCE_READ|WRITE`). So a compromised driver's handles can map/signal but cannot
|
||||||
|
`WRITE_DAC`/`WRITE_OWNER`/`DELETE` the objects — the "give unnamed shared objects proper (minimal)
|
||||||
|
security attributes, because `DuplicateHandle` can still reach them" discipline (Raymond Chen,
|
||||||
|
*devblogs 2015-06-04*). Marginal here (the driver is already a trusted frame endpoint) but correct
|
||||||
|
hygiene, and it applies identically to the gamepad DATA section. ✔ *(on-glass confirmed 2026-07-03:
|
||||||
|
the driver attaches + streams `frames=7035` with the least-access header handle.)*
|
||||||
|
|
||||||
|
Ring recreation (mid-session HDR flip) and host build-retries re-deliver a complete fresh handle set;
|
||||||
|
the driver treats a pending delivery as newest-wins (a retry's ring is a *different* header mapping,
|
||||||
|
whose generation bump an old publisher can never observe).
|
||||||
|
|
||||||
|
## Empirical verification (2026-07-03, RTX box)
|
||||||
|
|
||||||
|
The headline claim — "reaching a frame requires already being one of the two endpoint processes" —
|
||||||
|
was tested, not just argued. A **LocalService-token** process (scheduled task, the sibling-service
|
||||||
|
stand-in) attempting `OpenProcess` on the pf_vdisplay WUDFHost was **denied every access right**:
|
||||||
|
`PROCESS_DUP_HANDLE`, `PROCESS_VM_READ`, `PROCESS_QUERY_INFORMATION`, and even
|
||||||
|
`PROCESS_QUERY_LIMITED_INFORMATION` → `ERROR_ACCESS_DENIED`. The `QUERY_LIMITED` denial is decisive:
|
||||||
|
it is a read-class right MIC permits across integrity levels, so its denial is a **DACL exclusion of
|
||||||
|
the LocalService SID**, not an integrity ceiling — meaning even a higher-integrity LocalService
|
||||||
|
*service* is denied (LocalService lacks `SeDebugPrivilege`, so it cannot bypass the DACL). Combined
|
||||||
|
with the objects being unnamed, a sibling LocalService has **no reachable path to a frame**: no
|
||||||
|
name to open, no way to dup the handles out of WUDFHost, no way to read WUDFHost's memory. The
|
||||||
|
baseline (an elevated admin, holding `SeDebugPrivilege`) opened WUDFHost freely — expected, and the
|
||||||
|
reason "admin/SYSTEM = total" stays on the residual list below.
|
||||||
|
|
||||||
|
## Residual limits — the honest floor
|
||||||
|
|
||||||
|
* **The virtual display is a real monitor.** Any process in the interactive session can capture it
|
||||||
|
through the ordinary OS APIs (DDA/WGC/BitBlt), exactly as it can capture any physical monitor.
|
||||||
|
That floor is identical for every virtual-display streaming stack (Sunshine + VDD, Apollo/SudoVDA);
|
||||||
|
the sealed channel keeps *our* transport above that floor rather than below it. **This is the single
|
||||||
|
most realistic way for unprivileged session code to see the streamed pixels, and it is outside our
|
||||||
|
channel entirely.**
|
||||||
|
* **The gamepad channels are now sealed too** (2026-07-03, `design/gamepad-channel-sealing.md`,
|
||||||
|
gamepad proto v2 — on-glass validation pending): the pad DATA sections (`XusbShm`/`PadShm`) are
|
||||||
|
UNNAMED with `D:P(A;;GA;;;SY)`, handle-duplicated into the pad's WUDFHost by the host broker
|
||||||
|
(`inject/windows/gamepad_raii.rs` `PadChannel`, reusing this design's `verify_is_wudfhost` +
|
||||||
|
adopt-on-success discipline), and the driver validates the mapped section's magic + `pad_index`
|
||||||
|
before use. The pad drivers have no control device (hidclass), so the handshake runs over a tiny
|
||||||
|
**named bootstrap mailbox** (`Global\pf…-boot-<index>`, SY+LS, `PadBootstrap`) that carries only
|
||||||
|
pids and a handle value — nothing exploitable; the *residual* is that a sibling LocalService can
|
||||||
|
tamper the mailbox for a **gamepad DoS** (never a read or an injection; deliveries are capped, and
|
||||||
|
the mailbox is squat-checked at create). The old sibling-LS read/inject vector on
|
||||||
|
`Global\pf…-shm-*` is gone — the names no longer exist.
|
||||||
|
* **Admin / SYSTEM = total.** The control device is `D:P(A;;GA;;;SY)(A;;GA;;;BA)`, so an admin can drive
|
||||||
|
`IOCTL_SET_FRAME_CHANNEL` (DoS a live session) and, with `SeDebugPrivilege`, dup a section into
|
||||||
|
WUDFHost to exfiltrate; and an admin can plant a fake devnode with our interface GUID to impersonate
|
||||||
|
the driver. All admin-gated (no non-privileged escalation), but the control plane is explicitly not a
|
||||||
|
boundary against admin. The host↔driver channel has no mutual authentication beyond the `GET_INFO`
|
||||||
|
version handshake + the `verify_is_wudfhost` image check.
|
||||||
|
* **`WDA_EXCLUDEFROMCAPTURE` windows are visible.** IDD-push taps the *present* side, not the
|
||||||
|
*capture* side, so windows that exclude themselves from capture still appear in the stream — true
|
||||||
|
of every virtual-display streaming stack. Untested on our lab box; treat as expected behavior.
|
||||||
|
* **DRM/HDCP:** protected content is blanked by DWM at composition, and HDCP is a monitor↔GPU
|
||||||
|
handshake an indirect display cannot satisfy — neither is bypassed by this path.
|
||||||
|
* IDD-push is currently the **sole Windows capture path** (DDA and the WGC relay were removed). An
|
||||||
|
OS-mediated-capture-only mode would trade away secure-desktop capture and latency; if a deployment
|
||||||
|
requires it, that's a feature request, not a toggle that exists today.
|
||||||
Generated
+12
@@ -405,11 +405,21 @@ dependencies = [
|
|||||||
name = "pf-dualsense"
|
name = "pf-dualsense"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"pf-driver-proto",
|
||||||
|
"pf-umdf-util",
|
||||||
"wdk",
|
"wdk",
|
||||||
"wdk-build",
|
"wdk-build",
|
||||||
"wdk-sys",
|
"wdk-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-umdf-util"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"pf-driver-proto",
|
||||||
|
"wdk-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-vdisplay"
|
name = "pf-vdisplay"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -427,6 +437,8 @@ dependencies = [
|
|||||||
name = "pf-xusb"
|
name = "pf-xusb"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"pf-driver-proto",
|
||||||
|
"pf-umdf-util",
|
||||||
"wdk",
|
"wdk",
|
||||||
"wdk-build",
|
"wdk-build",
|
||||||
"wdk-sys",
|
"wdk-sys",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# crates/pf-driver-proto from the main tree.
|
# crates/pf-driver-proto from the main tree.
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["wdk-probe", "wdk-iddcx", "pf-vdisplay", "pf-dualsense", "pf-xusb"]
|
members = ["wdk-probe", "wdk-iddcx", "pf-umdf-util", "pf-vdisplay", "pf-dualsense", "pf-xusb"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@@ -20,6 +20,7 @@ wdk = "0.4.1"
|
|||||||
wdk-sys = "0.5.1"
|
wdk-sys = "0.5.1"
|
||||||
wdk-build = "0.5.1"
|
wdk-build = "0.5.1"
|
||||||
wdk-iddcx = { path = "wdk-iddcx" }
|
wdk-iddcx = { path = "wdk-iddcx" }
|
||||||
|
pf-umdf-util = { path = "pf-umdf-util" }
|
||||||
pf-driver-proto = { path = "../../../crates/pf-driver-proto" }
|
pf-driver-proto = { path = "../../../crates/pf-driver-proto" }
|
||||||
|
|
||||||
# Vendored windows-drivers-rs 0.5.1 (the published, self-contained crates) + an added `iddcx`
|
# Vendored windows-drivers-rs 0.5.1 (the published, self-contained crates) + an added `iddcx`
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ wdk-build.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
wdk.workspace = true
|
wdk.workspace = true
|
||||||
wdk-sys.workspace = true
|
wdk-sys.workspace = true
|
||||||
|
pf-driver-proto.workspace = true
|
||||||
|
pf-umdf-util.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["hid"]
|
default = ["hid"]
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ silently breaks them:
|
|||||||
|
|
||||||
- **Multi-pad** works via `UmdfHostProcessSharing=ProcessSharingDisabled` — each pad gets its own
|
- **Multi-pad** works via `UmdfHostProcessSharing=ProcessSharingDisabled` — each pad gets its own
|
||||||
WUDFHost (so the per-instance statics don't collide), and the driver reads its pad index from the
|
WUDFHost (so the per-instance statics don't collide), and the driver reads its pad index from the
|
||||||
device Location (`WdfDeviceAllocAndQueryProperty`) to map its own `*-shm-<index>` channel.
|
device Location (`WdfDeviceAllocAndQueryProperty`) to poll its own `*-boot-<index>` bootstrap
|
||||||
|
mailbox (the DATA section itself is unnamed — the sealed pad channel,
|
||||||
|
`design/gamepad-channel-sealing.md` — and its `pad_index` is validated against this index on
|
||||||
|
attach).
|
||||||
- Port of the WDK `vhidmini2` UMDF2 sample; the DualSense identity + 273-byte descriptor + feature
|
- Port of the WDK `vhidmini2` UMDF2 sample; the DualSense identity + 273-byte descriptor + feature
|
||||||
blobs `0x05`/`0x09`/`0x20` come from `crates/punktfunk-host/src/inject/dualsense.rs`.
|
blobs `0x05`/`0x09`/`0x20` come from `crates/punktfunk-host/src/inject/dualsense.rs`.
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
// punktfunk virtual DualSense — UMDF2 HID minidriver (M0 spike).
|
// punktfunk virtual DualSense / DualShock 4 — UMDF2 HID minidriver.
|
||||||
//
|
//
|
||||||
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
|
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
|
||||||
// (VID 054C / PID 0CE6) using the inputtino report descriptor + feature blobs punktfunk already
|
// (VID 054C / PID 0CE6) or DualShock 4 (device_type=1) using the inputtino report descriptor +
|
||||||
// ships in `inject/dualsense.rs`. Its purpose for M0(b) is to (1) enumerate as a genuine DualSense
|
// feature blobs punktfunk already ships in `inject/{dualsense,dualshock4}.rs`. Games see a genuine
|
||||||
// and (2) LOG every output report the game writes — the adaptive-trigger `0x02` gate.
|
// HID PS controller; the host streams input in / reads output (rumble/lightbar/triggers) back.
|
||||||
//
|
//
|
||||||
// No WDF object contexts: this is a singleton virtual device, so per-device state lives in statics.
|
// No WDF object contexts: this is a singleton virtual device, so per-device state lives in statics.
|
||||||
// All WDF calls go through `call_unsafe_wdf_function_binding!`; HID/WDF structs are hand-built.
|
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
|
||||||
|
// whole handshake + all shared-memory access lives in `pf_umdf_util` (the audited unsafe layer), so
|
||||||
|
// this crate's channel/HID/IOCTL logic is 100% SAFE Rust. The only `unsafe` here is the unavoidable
|
||||||
|
// WDF setup FFI in DriverEntry/EvtDeviceAdd/the timer, each with a `// SAFETY:` proof.
|
||||||
|
|
||||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||||
|
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
|
||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
use core::ffi::c_void;
|
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
|
||||||
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
|
||||||
|
|
||||||
|
use pf_driver_proto::gamepad::PadShm;
|
||||||
|
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
|
||||||
|
use pf_umdf_util::wdf::{self, Request};
|
||||||
use wdk_sys::{
|
use wdk_sys::{
|
||||||
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
||||||
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
|
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
|
||||||
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
|
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
|
||||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- NTSTATUS values ----
|
// ---- NTSTATUS values ----
|
||||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||||
const STATUS_UNSUCCESSFUL: NTSTATUS = 0xC000_0001u32 as NTSTATUS;
|
|
||||||
const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
||||||
const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
|
const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
|
||||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
|
||||||
|
|
||||||
#[inline]
|
use pf_umdf_util::nt_success;
|
||||||
fn nt_success(s: NTSTATUS) -> bool {
|
|
||||||
s >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- HID minidriver IOCTLs: CTL_CODE(FILE_DEVICE_KEYBOARD=0x0b, id, METHOD_NEITHER=3, ANY) ----
|
// ---- HID minidriver IOCTLs: CTL_CODE(FILE_DEVICE_KEYBOARD=0x0b, id, METHOD_NEITHER=3, ANY) ----
|
||||||
const fn hid_ctl(id: u32) -> u32 {
|
const fn hid_ctl(id: u32) -> u32 {
|
||||||
@@ -225,26 +228,45 @@ static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut(
|
|||||||
/// to pended game READ_REPORTs. Defaults to neutral until the host connects.
|
/// to pended game READ_REPORTs. Defaults to neutral until the host connects.
|
||||||
static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
|
static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
|
||||||
|
|
||||||
// ---- user-mode shared-memory IPC with the punktfunk host ----
|
// ---- the sealed pad channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
|
||||||
// UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
|
// UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
|
||||||
// (custom interface CreateFile → err 31; custom IOCTL on the HID handle → err 1) and UMDF has no
|
// (custom interface CreateFile → err 31; custom IOCTL on the HID handle → err 1) and UMDF has no
|
||||||
// control device, so the host channel is a named section the (privileged) host CREATES and the driver
|
// control device. So the DATA section (`PadShm`, 256 B — input report @8, output seq @72, output
|
||||||
// OPENS. Layout (256 B, must match pf_driver_proto::gamepad::PadShm): magic u32 @0 ("PFDS"),
|
// report @76, device_type @140, health marks @144/@148, pad_index @152) is UNNAMED and reached only
|
||||||
// input_seq u32 @4, input_report[64] @8, output_seq u32 @72, output_report[64] @76,
|
// through a handle the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named mailbox
|
||||||
// device_type u8 @140, driver_proto u32 @144 (we stamp GAMEPAD_PROTO_VERSION = the host's
|
// `Global\pfds-boot-<index>`. The handshake + all shared-memory access live in `pf_umdf_util`.
|
||||||
// driver-attach health signal), driver_heartbeat u32 @148 (we bump per timer tick = liveness).
|
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS"
|
||||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
|
const SHM_SIZE: usize = core::mem::size_of::<PadShm>();
|
||||||
const SHM_MAGIC: u32 = 0x5046_4453; // "PFDS" little-endian
|
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
|
||||||
const SHM_SIZE: usize = 256;
|
|
||||||
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
|
|
||||||
static LOGGED_SHM: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
|
|
||||||
|
|
||||||
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
|
// PadShm field offsets (the driver reads input + device_type, writes output + health marks).
|
||||||
unsafe extern "system" {
|
const OFF_INPUT: usize = core::mem::offset_of!(PadShm, input);
|
||||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
const OFF_OUT_SEQ: usize = core::mem::offset_of!(PadShm, out_seq);
|
||||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
const OFF_OUTPUT: usize = core::mem::offset_of!(PadShm, output);
|
||||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
const OFF_DEVICE_TYPE: usize = core::mem::offset_of!(PadShm, device_type);
|
||||||
fn CloseHandle(h: *mut c_void) -> i32;
|
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(PadShm, driver_proto);
|
||||||
|
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(PadShm, driver_heartbeat);
|
||||||
|
const OFF_PAD_INDEX: usize = core::mem::offset_of!(PadShm, pad_index);
|
||||||
|
|
||||||
|
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
|
||||||
|
/// this static is per-pad). The handshake/adoption/validation state machine lives in `pf_umdf_util`.
|
||||||
|
static CHANNEL: ChannelClient = ChannelClient::new();
|
||||||
|
/// The last observed `device_type` (0 = DualSense, 1 = DualShock 4) — the neutral-report shape when
|
||||||
|
/// the channel detaches, and the fallback identity while unattached.
|
||||||
|
static LAST_DEVTYPE: AtomicU32 = AtomicU32::new(0);
|
||||||
|
/// device_type()'s bounded first-read wait fires at most once (see its docs).
|
||||||
|
static DEVTYPE_WAITED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// This pad's channel config (magic/size/pad_index offset + our logger).
|
||||||
|
fn channel_cfg() -> ChannelConfig {
|
||||||
|
ChannelConfig {
|
||||||
|
tag: "pf-ds",
|
||||||
|
boot_name_prefix: "Global\\pfds-boot-",
|
||||||
|
data_magic: SHM_MAGIC,
|
||||||
|
data_size: SHM_SIZE,
|
||||||
|
pad_index_off: OFF_PAD_INDEX,
|
||||||
|
log,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(s: &str) {
|
fn log(s: &str) {
|
||||||
@@ -289,59 +311,6 @@ pub unsafe extern "system" fn driver_entry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pad index this device serves (which `pfds-shm-<index>` section to map). The host stamps it into
|
|
||||||
/// the device Location (`pszDeviceLocation`); the driver reads it in EvtDeviceAdd. With
|
|
||||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) each pad gets its own WUDFHost, so this
|
|
||||||
/// static is per-pad — the basis for multi-pad.
|
|
||||||
static SHM_INDEX: AtomicU32 = AtomicU32::new(0);
|
|
||||||
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (not re-exported at the wdk_sys root).
|
|
||||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
|
||||||
|
|
||||||
/// Read the pad index the host stamped into the device Location (a NUL-terminated UTF-16 decimal
|
|
||||||
/// string). Defaults to 0 (single-pad) if absent.
|
|
||||||
fn query_shm_index(device: WDFDEVICE) -> u32 {
|
|
||||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
|
||||||
// SAFETY: device valid; property = LocationInformation; pool ignored in UMDF; mem receives the handle.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(
|
|
||||||
WdfDeviceAllocAndQueryProperty,
|
|
||||||
device,
|
|
||||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
|
||||||
0,
|
|
||||||
WDF_NO_OBJECT_ATTRIBUTES,
|
|
||||||
&mut mem
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if !nt_success(st) || mem.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let mut len: usize = 0;
|
|
||||||
// SAFETY: mem valid.
|
|
||||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
|
||||||
as *const u16;
|
|
||||||
if buf.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let mut idx: u32 = 0;
|
|
||||||
let mut any = false;
|
|
||||||
for i in 0..(len / 2).min(8) {
|
|
||||||
// SAFETY: buf valid for len bytes; i < len/2.
|
|
||||||
let c = unsafe { *buf.add(i) };
|
|
||||||
if c == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (0x30..=0x39).contains(&c) {
|
|
||||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
|
||||||
any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if any {
|
|
||||||
idx
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
||||||
log("[pf-ds] EvtDeviceAdd");
|
log("[pf-ds] EvtDeviceAdd");
|
||||||
|
|
||||||
@@ -364,8 +333,9 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
let shm_idx = query_shm_index(device);
|
// SAFETY: `device` is the live device just created — the exact contract this fn requires.
|
||||||
SHM_INDEX.store(shm_idx, Ordering::Relaxed);
|
let shm_idx = unsafe { wdf::query_location_index(device) };
|
||||||
|
CHANNEL.set_index(shm_idx);
|
||||||
dbglog!("[pf-ds] shm index = {shm_idx}");
|
dbglog!("[pf-ds] shm index = {shm_idx}");
|
||||||
|
|
||||||
// Default parallel queue handling all IOCTLs.
|
// Default parallel queue handling all IOCTLs.
|
||||||
@@ -428,6 +398,8 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
tcfg.EvtTimerFunc = Some(evt_timer);
|
tcfg.EvtTimerFunc = Some(evt_timer);
|
||||||
tcfg.Period = 8; // ms
|
tcfg.Period = 8; // ms
|
||||||
tcfg.AutomaticSerialization = 1; // TRUE — UMDF requires a serialized timer (vhidmini2 pattern)
|
tcfg.AutomaticSerialization = 1; // TRUE — UMDF requires a serialized timer (vhidmini2 pattern)
|
||||||
|
// SAFETY: a zeroed WDF_OBJECT_ATTRIBUTES is a valid all-null attributes struct; we set Size + the
|
||||||
|
// fields we use below.
|
||||||
let mut tattr: WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
|
let mut tattr: WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
|
||||||
tattr.Size = core::mem::size_of::<WDF_OBJECT_ATTRIBUTES>() as ULONG;
|
tattr.Size = core::mem::size_of::<WDF_OBJECT_ATTRIBUTES>() as ULONG;
|
||||||
tattr.ParentObject = manual_queue.cast();
|
tattr.ParentObject = manual_queue.cast();
|
||||||
@@ -458,141 +430,73 @@ extern "C" fn evt_io_device_control(
|
|||||||
_input_len: usize,
|
_input_len: usize,
|
||||||
ioctl: ULONG,
|
ioctl: ULONG,
|
||||||
) {
|
) {
|
||||||
let mut complete = true;
|
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
|
||||||
|
// contract `Request::new` requires. Everything after is safe (the token owns completion).
|
||||||
|
let request = unsafe { Request::new(request) };
|
||||||
|
|
||||||
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test;
|
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test;
|
||||||
// the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
|
// the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
|
||||||
if ioctl != IOCTL_HID_READ_REPORT {
|
if ioctl != IOCTL_HID_READ_REPORT {
|
||||||
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
|
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
|
||||||
}
|
}
|
||||||
let status: NTSTATUS = match ioctl {
|
|
||||||
IOCTL_HID_GET_DEVICE_DESCRIPTOR => {
|
// READ_REPORT forwards to the manual queue (the timer completes it) — this CONSUMES the request
|
||||||
copy_to_output(request, if device_type() == 1 { &DS4_HID_DESC } else { &HID_DESC })
|
// token, so it's handled apart from the status-and-complete paths below.
|
||||||
|
if ioctl == IOCTL_HID_READ_REPORT {
|
||||||
|
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
|
||||||
|
// SAFETY: `mq` is the manual queue created in EvtDeviceAdd (a live WDFQUEUE of this device).
|
||||||
|
match unsafe { request.forward_to_queue(mq) } {
|
||||||
|
Ok(()) => {} // framework owns it now (completed by the timer)
|
||||||
|
Err((req, st)) => req.complete(st), // forward failed → complete with the error
|
||||||
}
|
}
|
||||||
IOCTL_HID_GET_DEVICE_ATTRIBUTES => copy_to_output(request, &hid_attrs(device_type() == 1)),
|
return;
|
||||||
IOCTL_HID_GET_REPORT_DESCRIPTOR => copy_to_output(
|
}
|
||||||
request,
|
|
||||||
if device_type() == 1 {
|
let status: NTSTATUS = match ioctl {
|
||||||
|
IOCTL_HID_GET_DEVICE_DESCRIPTOR => request.copy_to_output(if device_type() == 1 {
|
||||||
|
&DS4_HID_DESC
|
||||||
|
} else {
|
||||||
|
&HID_DESC
|
||||||
|
}),
|
||||||
|
IOCTL_HID_GET_DEVICE_ATTRIBUTES => request.copy_to_output(&hid_attrs(device_type() == 1)),
|
||||||
|
IOCTL_HID_GET_REPORT_DESCRIPTOR => request.copy_to_output(if device_type() == 1 {
|
||||||
&DS4_RDESC[..]
|
&DS4_RDESC[..]
|
||||||
} else {
|
} else {
|
||||||
&DUALSENSE_RDESC[..]
|
&DUALSENSE_RDESC[..]
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
IOCTL_HID_READ_REPORT => {
|
|
||||||
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
|
|
||||||
// SAFETY: request valid; mq is the manual queue created in EvtDeviceAdd.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, request, mq)
|
|
||||||
};
|
|
||||||
if nt_success(st) {
|
|
||||||
complete = false;
|
|
||||||
STATUS_SUCCESS
|
|
||||||
} else {
|
|
||||||
st
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
|
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
|
||||||
on_output_report(request, ioctl)
|
on_output_report(&request, ioctl)
|
||||||
}
|
}
|
||||||
IOCTL_UMDF_HID_SET_FEATURE => {
|
IOCTL_UMDF_HID_SET_FEATURE => {
|
||||||
log("[pf-ds] SET_FEATURE (stub ok)");
|
log("[pf-ds] SET_FEATURE (stub ok)");
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request),
|
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(&request),
|
||||||
IOCTL_UMDF_HID_GET_INPUT_REPORT => {
|
IOCTL_UMDF_HID_GET_INPUT_REPORT => {
|
||||||
copy_to_output(request, &neutral_report(device_type() == 1))
|
request.copy_to_output(&neutral_report(device_type() == 1))
|
||||||
}
|
}
|
||||||
IOCTL_HID_GET_STRING => on_get_string(request),
|
IOCTL_HID_GET_STRING => on_get_string(&request),
|
||||||
_ => STATUS_NOT_IMPLEMENTED,
|
_ => STATUS_NOT_IMPLEMENTED,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ioctl != IOCTL_HID_READ_REPORT {
|
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x}", status as u32);
|
||||||
dbglog!(
|
request.complete(status);
|
||||||
"[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}",
|
|
||||||
status as u32
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if complete {
|
|
||||||
// SAFETY: request valid and not forwarded.
|
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy `src` into the request's output memory and set the completed byte count.
|
|
||||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
|
||||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
|
||||||
// SAFETY: request valid; mem receives the memory handle.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
|
||||||
};
|
|
||||||
if !nt_success(st) {
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
let mut outlen: usize = 0;
|
|
||||||
// SAFETY: mem valid; outlen receives the buffer size.
|
|
||||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
|
||||||
if outlen < src.len() {
|
|
||||||
return STATUS_INVALID_BUFFER_SIZE;
|
|
||||||
}
|
|
||||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(
|
|
||||||
WdfMemoryCopyFromBuffer,
|
|
||||||
mem,
|
|
||||||
0usize,
|
|
||||||
src.as_ptr() as *mut c_void,
|
|
||||||
src.len()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if !nt_success(st) {
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
// SAFETY: request valid.
|
|
||||||
unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
|
||||||
};
|
|
||||||
STATUS_SUCCESS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 0x02 gate: a game writing an output report (rumble / lightbar / ADAPTIVE TRIGGERS). Per the
|
// The 0x02 gate: a game writing an output report (rumble / lightbar / ADAPTIVE TRIGGERS). Per the
|
||||||
// UMDF marshalling convention the report data is the *input* buffer and the report id is carried in
|
// UMDF marshalling convention the report data is the *input* buffer and the report id is carried in
|
||||||
// the *output* buffer length. We log it.
|
// the *output* buffer length. We log it, then publish it to the DATA section for the host.
|
||||||
fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
|
fn on_output_report(request: &Request, ioctl: ULONG) -> NTSTATUS {
|
||||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
let (bytes, inlen) = match request.input_bytes(64) {
|
||||||
// SAFETY: request valid.
|
Ok(v) => v,
|
||||||
let st = unsafe {
|
Err(st) => return st,
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
|
||||||
};
|
};
|
||||||
if !nt_success(st) {
|
let report_id = request.output_buffer_len() as u32; // report id, UMDF convention
|
||||||
return st;
|
|
||||||
}
|
|
||||||
let mut inlen: usize = 0;
|
|
||||||
// SAFETY: inmem valid.
|
|
||||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
|
||||||
as *const u8;
|
|
||||||
|
|
||||||
// report id from output-buffer length (UMDF convention).
|
|
||||||
let mut report_id: u32 = 0;
|
|
||||||
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
|
||||||
// SAFETY: request valid; output memory is optional here.
|
|
||||||
if nt_success(unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut outmem)
|
|
||||||
}) {
|
|
||||||
let mut outlen: usize = 0;
|
|
||||||
// SAFETY: outmem valid.
|
|
||||||
let _ =
|
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
|
||||||
report_id = outlen as u32;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n = inlen.min(48);
|
|
||||||
let mut hex = String::new();
|
let mut hex = String::new();
|
||||||
if !inbuf.is_null() {
|
for b in bytes.iter().take(48) {
|
||||||
// SAFETY: inbuf valid for inlen bytes; we read at most n.
|
|
||||||
let bytes = unsafe { core::slice::from_raw_parts(inbuf, n) };
|
|
||||||
for b in bytes {
|
|
||||||
hex.push_str(&format!("{b:02x} "));
|
hex.push_str(&format!("{b:02x} "));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let kind = if ioctl == IOCTL_HID_WRITE_REPORT {
|
let kind = if ioctl == IOCTL_HID_WRITE_REPORT {
|
||||||
"WRITE_REPORT"
|
"WRITE_REPORT"
|
||||||
} else {
|
} else {
|
||||||
@@ -600,45 +504,29 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
|
|||||||
};
|
};
|
||||||
dbglog!("[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
|
dbglog!("[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
|
||||||
|
|
||||||
// Publish the game's 0x02 output report to shared memory for the host (rumble / lightbar /
|
// Publish the game's 0x02 output report to the sealed DATA section for the host (rumble /
|
||||||
// player-LEDs / adaptive triggers). output_report @76, output_seq @72.
|
// lightbar / player-LEDs / adaptive triggers), then bump the host-polled output seq.
|
||||||
if !inbuf.is_null() && inlen > 0 {
|
if !bytes.is_empty()
|
||||||
let n = inlen.min(64);
|
&& let Some(view) = CHANNEL.data()
|
||||||
with_shm(|view| {
|
{
|
||||||
// SAFETY: view is a mapped 256-byte section; write the report then bump the host-polled seq.
|
view.write_bytes(OFF_OUTPUT, &bytes);
|
||||||
unsafe {
|
let seq = view.read_u32(OFF_OUT_SEQ).wrapping_add(1);
|
||||||
core::ptr::copy_nonoverlapping(inbuf, view.add(76), n);
|
view.write_u32(OFF_OUT_SEQ, seq);
|
||||||
let seqp = view.add(72) as *mut u32;
|
|
||||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
|
||||||
core::ptr::write_unaligned(seqp, seq);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: request valid.
|
request.set_information(inlen as u64);
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, inlen as u64) };
|
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense feature blob.
|
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense/DualShock 4 blob.
|
||||||
fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
|
fn on_get_feature(request: &Request) -> NTSTATUS {
|
||||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
let (bytes, _) = match request.input_bytes(1) {
|
||||||
// SAFETY: request valid.
|
Ok(v) => v,
|
||||||
let st = unsafe {
|
Err(st) => return st,
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
|
||||||
};
|
};
|
||||||
if !nt_success(st) {
|
let Some(&report_id) = bytes.first() else {
|
||||||
return st;
|
|
||||||
}
|
|
||||||
let mut inlen: usize = 0;
|
|
||||||
// SAFETY: inmem valid.
|
|
||||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
|
||||||
as *const u8;
|
|
||||||
if inbuf.is_null() || inlen < 1 {
|
|
||||||
return STATUS_INVALID_PARAMETER;
|
return STATUS_INVALID_PARAMETER;
|
||||||
}
|
};
|
||||||
// SAFETY: inbuf valid for >=1 byte.
|
|
||||||
let report_id = unsafe { *inbuf };
|
|
||||||
// DualSense uses feature ids 0x05/0x09/0x20; DualShock 4 uses 0x02/0x12/0xa3.
|
// DualSense uses feature ids 0x05/0x09/0x20; DualShock 4 uses 0x02/0x12/0xa3.
|
||||||
let blob: &[u8] = match (device_type() == 1, report_id) {
|
let blob: &[u8] = match (device_type() == 1, report_id) {
|
||||||
(false, 0x05) => &DS_FEATURE_CALIBRATION,
|
(false, 0x05) => &DS_FEATURE_CALIBRATION,
|
||||||
@@ -652,31 +540,21 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
|
|||||||
return STATUS_INVALID_PARAMETER;
|
return STATUS_INVALID_PARAMETER;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
copy_to_output(request, blob)
|
request.copy_to_output(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IOCTL_HID_GET_STRING: the input is a ULONG whose low word is the string id and whose high word is
|
// IOCTL_HID_GET_STRING: the input is a ULONG whose low word is the string id and whose high word is
|
||||||
// the language id. Reply with the requested device string as a NUL-terminated UTF-16 buffer. Native
|
// the language id. Reply with the requested device string as a NUL-terminated UTF-16 buffer. Native
|
||||||
// PS5 / Steam code reads these (HidD_GetProductString / HidD_GetSerialNumberString — the serial is one
|
// PS5 / Steam code reads these (HidD_GetProductString / HidD_GetSerialNumberString — the serial is one
|
||||||
// way they tell USB from BT); the old default returned STATUS_NOT_IMPLEMENTED, leaving them blank.
|
// way they tell USB from BT). Observed live: Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409)
|
||||||
// Observed live on this device, Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409) cyclically — the
|
// cyclically — the manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; both.
|
||||||
// manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; we map both forms.
|
fn on_get_string(request: &Request) -> NTSTATUS {
|
||||||
fn on_get_string(request: WDFREQUEST) -> NTSTATUS {
|
let (bytes, _) = match request.input_bytes(4) {
|
||||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
Ok(v) => v,
|
||||||
// SAFETY: request valid.
|
Err(st) => return st,
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
|
||||||
};
|
};
|
||||||
if !nt_success(st) {
|
let id_val: u32 = if bytes.len() >= 4 {
|
||||||
return st;
|
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
|
||||||
}
|
|
||||||
let mut inlen: usize = 0;
|
|
||||||
// SAFETY: inmem valid.
|
|
||||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
|
||||||
as *const u8;
|
|
||||||
// SAFETY: inbuf is valid for inlen bytes; read the 4-byte id value when present.
|
|
||||||
let id_val: u32 = if !inbuf.is_null() && inlen >= 4 {
|
|
||||||
unsafe { core::ptr::read_unaligned(inbuf as *const u32) }
|
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
@@ -706,96 +584,81 @@ fn on_get_string(request: WDFREQUEST) -> NTSTATUS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut wide: Vec<u16> = s.encode_utf16().collect();
|
let mut wide: Vec<u8> = Vec::with_capacity(s.len() * 2 + 2);
|
||||||
wide.push(0); // NUL terminator
|
for u in s.encode_utf16() {
|
||||||
// SAFETY: reinterpret the UTF-16 buffer as bytes for the byte-oriented copy_to_output.
|
wide.extend_from_slice(&u.to_le_bytes());
|
||||||
let bytes = unsafe { core::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2) };
|
}
|
||||||
copy_to_output(request, bytes)
|
wide.extend_from_slice(&[0, 0]); // NUL terminator (UTF-16)
|
||||||
|
request.copy_to_output(&wide)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open + map the host's shared-memory section (Global\pfds-shm-0) and run `f` against the mapped base
|
/// The host's device-type selector from the sealed DATA section (`device_type` @140): 0 = DualSense
|
||||||
// if it exists with a valid magic, then unmap. NOT cached: re-mapped per access so the driver always
|
/// (default), 1 = DualShock 4. Read fresh on each enumeration query — cheap. If the channel hasn't
|
||||||
// sees the current section (UMDF groups all devices in one WUDFHost, and the host may recreate the
|
/// attached when hidclass first asks (the host stamps the section + eager-delivers before
|
||||||
// section across restarts — a cached view would go stale). ~125 maps/s from the timer = negligible.
|
/// `SwDeviceCreate` returns, but the handshake can be a few ms behind), pump the channel briefly —
|
||||||
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
|
/// ONCE — for the delivery: a DS4 pad must not enumerate with the default DualSense identity because
|
||||||
let name: Vec<u16> = format!("Global\\pfds-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
/// of a lost race. After that one bounded wait, fall back to the last observed type.
|
||||||
.encode_utf16()
|
fn device_type() -> u8 {
|
||||||
.chain(std::iter::once(0))
|
if let Some(view) = CHANNEL.data() {
|
||||||
.collect();
|
let t = view.read_u8(OFF_DEVICE_TYPE);
|
||||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
|
||||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
return t;
|
||||||
if h.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// SAFETY: h is a valid mapping handle; map the whole section. The view keeps the section alive,
|
if !DEVTYPE_WAITED.swap(true, Ordering::SeqCst) {
|
||||||
// so the handle can be closed right away.
|
let cfg = channel_cfg();
|
||||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
for _ in 0..100 {
|
||||||
unsafe { CloseHandle(h) };
|
if let Some(view) = CHANNEL.pump(&cfg) {
|
||||||
if view.is_null() {
|
let t = view.read_u8(OFF_DEVICE_TYPE);
|
||||||
return;
|
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
// SAFETY: view points at >= 4 mapped bytes.
|
|
||||||
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
|
|
||||||
if magic == SHM_MAGIC {
|
|
||||||
if !LOGGED_SHM.swap(true, Ordering::Relaxed) {
|
|
||||||
dbglog!(
|
dbglog!(
|
||||||
"[pf-ds] control: shared memory mapped (Global\\pfds-shm-{})",
|
"[pf-ds] device_type: sealed channel not attached within 1s — defaulting to the last observed identity"
|
||||||
SHM_INDEX.load(Ordering::Relaxed)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
f(view);
|
LAST_DEVTYPE.load(Ordering::Relaxed) as u8
|
||||||
}
|
|
||||||
// SAFETY: view came from MapViewOfFile.
|
|
||||||
unsafe { UnmapViewOfFile(view as *const c_void) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The host's device-type selector from shared memory (`device_type` byte @140): 0 = DualSense
|
|
||||||
/// (default), 1 = DualShock 4. Read fresh on each enumeration query — cheap, and the host stamps the
|
|
||||||
/// section before `SwDeviceCreate`, so it's set by the time hidclass asks for the descriptor /
|
|
||||||
/// attributes. Defaults to DualSense if the section isn't mapped yet (magic absent).
|
|
||||||
fn device_type() -> u8 {
|
|
||||||
let mut t = 0u8;
|
|
||||||
with_shm(|view| {
|
|
||||||
// SAFETY: view points at a mapped 256-byte section; the device-type byte is at offset 140.
|
|
||||||
t = unsafe { *view.add(140) };
|
|
||||||
});
|
|
||||||
t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn evt_timer(timer: WDFTIMER) {
|
extern "C" fn evt_timer(timer: WDFTIMER) {
|
||||||
// Pull the latest host input report from shared memory (if the host has connected).
|
// One sealed-channel tick: publish our pid / adopt a delivery / detect host-gone, then pull the
|
||||||
with_shm(|view| {
|
// latest host input report from the attached DATA section (all safe, via pf_umdf_util).
|
||||||
|
match CHANNEL.pump(&channel_cfg()) {
|
||||||
|
Some(view) => {
|
||||||
let mut buf = [0u8; 64];
|
let mut buf = [0u8; 64];
|
||||||
// SAFETY: view points at a mapped 256-byte section; input lives at offset 8..72.
|
view.read_bytes(OFF_INPUT, &mut buf);
|
||||||
unsafe { core::ptr::copy_nonoverlapping(view.add(8), buf.as_mut_ptr(), 64) };
|
if buf[0] == 0x01
|
||||||
if buf[0] == 0x01 {
|
&& let Ok(mut g) = INPUT_REPORT.lock()
|
||||||
if let Ok(mut g) = INPUT_REPORT.lock() {
|
{
|
||||||
*g = buf;
|
*g = buf;
|
||||||
}
|
}
|
||||||
}
|
// Health marks the host watches: driver_proto (attach signal, idempotent) and
|
||||||
// Health marks the host watches: driver_proto @144 (attach signal, idempotent) and
|
// driver_heartbeat (+1 per ~8 ms tick = liveness). Lets the host tell "driver bound
|
||||||
// driver_heartbeat @148 (+1 per ~8 ms tick = liveness). Lets the host tell "driver bound
|
|
||||||
// and alive" apart from "driver package missing/failed to bind".
|
// and alive" apart from "driver package missing/failed to bind".
|
||||||
// SAFETY: view points at a mapped 256-byte section; proto @144, heartbeat @148.
|
view.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
|
||||||
unsafe {
|
let hb = view.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
|
||||||
core::ptr::write_unaligned(view.add(144) as *mut u32, GAMEPAD_PROTO_VERSION);
|
view.write_u32(OFF_DRIVER_HEARTBEAT, hb);
|
||||||
let hb = view.add(148) as *mut u32;
|
|
||||||
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
|
|
||||||
}
|
}
|
||||||
});
|
None => {
|
||||||
// SAFETY: timer valid; parent is the manual queue.
|
// Host gone (mailbox name vanished) or channel not attached yet: feed games the neutral
|
||||||
|
// report instead of a frozen last state (matters for the persistent out-of-band devnode,
|
||||||
|
// which outlives host sessions).
|
||||||
|
if let Ok(mut g) = INPUT_REPORT.lock() {
|
||||||
|
*g = neutral_report(LAST_DEVTYPE.load(Ordering::Relaxed) == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the next pended READ_REPORT with the current input report (safe queue/request API).
|
||||||
|
// SAFETY: the timer's parent object is the manual queue (set in EvtDeviceAdd); the framework
|
||||||
|
// guarantees a live handle here.
|
||||||
let queue =
|
let queue =
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfTimerGetParentObject, timer) } as WDFQUEUE;
|
unsafe { call_unsafe_wdf_function_binding!(WdfTimerGetParentObject, timer) } as WDFQUEUE;
|
||||||
let mut request: WDFREQUEST = core::ptr::null_mut();
|
// SAFETY: `queue` is that live manual queue — the exact contract `retrieve_next_request` needs.
|
||||||
// SAFETY: queue valid; request receives the next pended request if any.
|
if let Some(request) = unsafe { wdf::retrieve_next_request(queue) } {
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
|
||||||
};
|
|
||||||
if nt_success(st) {
|
|
||||||
let report = INPUT_REPORT.lock().map(|g| *g).unwrap_or(NEUTRAL_REPORT);
|
let report = INPUT_REPORT.lock().map(|g| *g).unwrap_or(NEUTRAL_REPORT);
|
||||||
let s = copy_to_output(request, &report);
|
let st = request.copy_to_output(&report);
|
||||||
// SAFETY: request valid and dequeued.
|
request.complete(st);
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, s) };
|
|
||||||
}
|
}
|
||||||
let _ = STATUS_UNSUCCESSFUL; // keep the const referenced
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# pf-umdf-util - the audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers.
|
||||||
|
# Everything a pad driver does with raw pointers or Win32/WDF FFI lives HERE, behind small safe
|
||||||
|
# (or explicitly-contracted unsafe) APIs, so the driver crates' business logic is 100% safe Rust:
|
||||||
|
# section - MappedView: bounds+alignment-checked shared-memory access (atomics for sync fields)
|
||||||
|
# channel - ChannelClient: the sealed pad channel's driver-side state machine (a SAFE module)
|
||||||
|
# wdf - Request/queue/device-property helpers over call_unsafe_wdf_function_binding
|
||||||
|
[package]
|
||||||
|
name = "pf-umdf-util"
|
||||||
|
edition.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
description = "punktfunk UMDF driver util: safe shared-memory + sealed-channel + WDF request primitives"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wdk-sys.workspace = true
|
||||||
|
pf-driver-proto.workspace = true
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
//! The sealed pad channel, driver side (`design/gamepad-channel-sealing.md`, gamepad proto v2):
|
||||||
|
//! poll the named bootstrap mailbox by index, publish our pid (iff the host's proto version
|
||||||
|
//! matches), adopt the host-delivered DATA-section handle, and validate the mapped section's magic
|
||||||
|
//! and `pad_index` before use. One implementation shared by `pf-xusb` and `pf-dualsense` (they used
|
||||||
|
//! to hand-duplicate it), parameterized by [`ChannelConfig`].
|
||||||
|
//!
|
||||||
|
//! This module **forbids `unsafe`**: the entire state machine is safe Rust over
|
||||||
|
//! [`section`](crate::section)'s checked accessors — the memory-safety surface of the sealed
|
||||||
|
//! channel lives in that module alone.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use crate::section::{MappedView, ViewCell, close_handle_value};
|
||||||
|
use core::mem::offset_of;
|
||||||
|
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
|
use pf_driver_proto::gamepad::{BOOT_MAGIC, GAMEPAD_PROTO_VERSION, PadBootstrap};
|
||||||
|
|
||||||
|
// PadBootstrap field offsets (the mailbox handshake; pinned by pf_driver_proto's asserts).
|
||||||
|
const BOOT_OFF_MAGIC: usize = offset_of!(PadBootstrap, magic);
|
||||||
|
const BOOT_OFF_HOST_PROTO: usize = offset_of!(PadBootstrap, host_proto);
|
||||||
|
const BOOT_OFF_DRIVER_PID: usize = offset_of!(PadBootstrap, driver_pid);
|
||||||
|
const BOOT_OFF_DRIVER_PROTO: usize = offset_of!(PadBootstrap, driver_proto);
|
||||||
|
const BOOT_OFF_DATA_HANDLE: usize = offset_of!(PadBootstrap, data_handle);
|
||||||
|
const BOOT_OFF_HANDLE_PID: usize = offset_of!(PadBootstrap, handle_pid);
|
||||||
|
const BOOT_OFF_HANDLE_SEQ: usize = offset_of!(PadBootstrap, handle_seq);
|
||||||
|
const BOOT_SIZE: usize = core::mem::size_of::<PadBootstrap>();
|
||||||
|
|
||||||
|
/// What varies between the two pad drivers.
|
||||||
|
pub struct ChannelConfig {
|
||||||
|
/// Log-line prefix (`"pf-xusb"` / `"pf-ds"`).
|
||||||
|
pub tag: &'static str,
|
||||||
|
/// Mailbox name prefix, completed with the pad index (`"Global\\pfxusb-boot-"` / `"Global\\pfds-boot-"`).
|
||||||
|
pub boot_name_prefix: &'static str,
|
||||||
|
/// The DATA section's magic (`XUSB_MAGIC` / `PAD_MAGIC`).
|
||||||
|
pub data_magic: u32,
|
||||||
|
/// The DATA section's size (`size_of::<XusbShm>()` / `size_of::<PadShm>()`).
|
||||||
|
pub data_size: usize,
|
||||||
|
/// `offset_of!(…Shm, pad_index)` in the DATA section.
|
||||||
|
pub pad_index_off: usize,
|
||||||
|
/// The driver's logger (each driver tees to its own debug file).
|
||||||
|
pub log: fn(&str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-pad channel state (a `static` in each driver — per-pad because
|
||||||
|
/// `UmdfHostProcessSharing=ProcessSharingDisabled` gives each pad its own WUDFHost).
|
||||||
|
pub struct ChannelClient {
|
||||||
|
/// The pad index from the devnode Location (which mailbox to poll + the `pad_index` the
|
||||||
|
/// delivered DATA section must carry).
|
||||||
|
index: AtomicU32,
|
||||||
|
/// The adopted DATA view; leaked-on-publish (see [`ViewCell`]) so a re-delivery can never
|
||||||
|
/// unmap a view a concurrent callback still reads through.
|
||||||
|
data: ViewCell,
|
||||||
|
/// The last `handle_seq` consumed (CAS-guarded so concurrent pumps adopt a delivery exactly
|
||||||
|
/// once). Reset to 0 when the mailbox disappears, so a NEW host session's delivery is always
|
||||||
|
/// fresh even if its (per-host-process) seq counter collides with the previous session's.
|
||||||
|
consumed_seq: AtomicU32,
|
||||||
|
logged_proto_mismatch: AtomicBool,
|
||||||
|
logged_pid: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChannelClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelClient {
|
||||||
|
pub const fn new() -> ChannelClient {
|
||||||
|
ChannelClient {
|
||||||
|
index: AtomicU32::new(0),
|
||||||
|
data: ViewCell::new(),
|
||||||
|
consumed_seq: AtomicU32::new(0),
|
||||||
|
logged_proto_mismatch: AtomicBool::new(false),
|
||||||
|
logged_pid: AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the pad index (from the devnode Location, in `EvtDeviceAdd`).
|
||||||
|
pub fn set_index(&self, idx: u32) {
|
||||||
|
self.index.store(idx, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(&self) -> u32 {
|
||||||
|
self.index.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The adopted DATA view regardless of mailbox liveness — for write paths where acting on a
|
||||||
|
/// stale section is harmless (the pump owns the detach semantics).
|
||||||
|
pub fn data(&self) -> Option<&'static MappedView> {
|
||||||
|
self.data.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One tick of the sealed-channel state machine: publish our pid (+ proto version) in the
|
||||||
|
/// mailbox, adopt a delivered DATA handle, and return the attached DATA view — `None` while
|
||||||
|
/// unattached, on a host/driver version mismatch (fail closed), or when the mailbox is gone
|
||||||
|
/// (host gone). The mailbox is re-opened by name on every call: the name existing doubles as
|
||||||
|
/// host-liveness (the host closes it when the pad is torn down).
|
||||||
|
pub fn pump(&self, cfg: &ChannelConfig) -> Option<&'static MappedView> {
|
||||||
|
let name = format!("{}{}", cfg.boot_name_prefix, self.index());
|
||||||
|
let boot = match MappedView::open_named(&name, BOOT_SIZE) {
|
||||||
|
Some(b) => b,
|
||||||
|
None => {
|
||||||
|
// Mailbox gone → the host (or this pad) is gone. Forget the consumed seq so the
|
||||||
|
// NEXT host session's first delivery always reads as fresh.
|
||||||
|
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Acquire pairs with the host's Release magic store, so a valid magic implies `host_proto`
|
||||||
|
// is visible. A missing/garbled magic reads as "no usable mailbox" (same as absent).
|
||||||
|
if boot.load_u32(BOOT_OFF_MAGIC, Ordering::Acquire) != BOOT_MAGIC {
|
||||||
|
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Publish our proto version first (idempotent) — the host logs a mismatch even when we
|
||||||
|
// refuse to publish a pid below.
|
||||||
|
boot.store_u32(
|
||||||
|
BOOT_OFF_DRIVER_PROTO,
|
||||||
|
GAMEPAD_PROTO_VERSION,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
);
|
||||||
|
let host_proto = boot.load_u32(BOOT_OFF_HOST_PROTO, Ordering::Relaxed);
|
||||||
|
if host_proto != GAMEPAD_PROTO_VERSION {
|
||||||
|
if !self.logged_proto_mismatch.swap(true, Ordering::Relaxed) {
|
||||||
|
(cfg.log)(&format!(
|
||||||
|
"[{}] host proto {host_proto} != driver proto {GAMEPAD_PROTO_VERSION} — \
|
||||||
|
refusing the handshake (update host + drivers together)",
|
||||||
|
cfg.tag
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return None; // version mismatch — fail closed
|
||||||
|
}
|
||||||
|
let mypid = std::process::id();
|
||||||
|
if boot.load_u32(BOOT_OFF_DRIVER_PID, Ordering::Relaxed) != mypid {
|
||||||
|
boot.store_u32(BOOT_OFF_DRIVER_PID, mypid, Ordering::Release);
|
||||||
|
if !self.logged_pid.swap(true, Ordering::Relaxed) {
|
||||||
|
(cfg.log)(&format!("[{}] bootstrap: published pid {mypid}", cfg.tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A delivery addressed to us we haven't consumed? CAS so concurrent pumps (worker thread /
|
||||||
|
// timer + IOCTL paths) adopt exactly once.
|
||||||
|
let seq = boot.load_u32(BOOT_OFF_HANDLE_SEQ, Ordering::Acquire);
|
||||||
|
let cur = self.consumed_seq.load(Ordering::Relaxed);
|
||||||
|
if seq != 0
|
||||||
|
&& seq != cur
|
||||||
|
&& boot.load_u32(BOOT_OFF_HANDLE_PID, Ordering::Relaxed) == mypid
|
||||||
|
&& self
|
||||||
|
.consumed_seq
|
||||||
|
.compare_exchange(cur, seq, Ordering::SeqCst, Ordering::SeqCst)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
self.adopt(cfg, boot.load_u64(BOOT_OFF_DATA_HANDLE, Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
self.data()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map + validate a delivered DATA-section handle VALUE (untrusted until the mapped section
|
||||||
|
/// carries our magic AND our pad index). On success we own the handle (adopt-on-success) and
|
||||||
|
/// close it — the view keeps the section alive. On validation failure the handle is
|
||||||
|
/// deliberately NOT closed: a tampered value could name an unrelated handle in our own table.
|
||||||
|
fn adopt(&self, cfg: &ChannelConfig, value: u64) {
|
||||||
|
let Some(view) = MappedView::from_handle_value(value, cfg.data_size) else {
|
||||||
|
if value != 0 {
|
||||||
|
(cfg.log)(&format!(
|
||||||
|
"[{}] delivered DATA handle 0x{value:x} did not map — ignoring",
|
||||||
|
cfg.tag
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let magic = view.load_u32(0, Ordering::Relaxed);
|
||||||
|
let idx = view.load_u32(cfg.pad_index_off, Ordering::Relaxed);
|
||||||
|
let want = self.index();
|
||||||
|
if magic != cfg.data_magic || idx != want {
|
||||||
|
(cfg.log)(&format!(
|
||||||
|
"[{}] delivered DATA section failed validation (magic 0x{magic:08x}, pad_index \
|
||||||
|
{idx}, want {want}) — ignoring",
|
||||||
|
cfg.tag
|
||||||
|
));
|
||||||
|
// `view` drops here → unmapped; the handle stays open (see above).
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The value resolved to OUR pad's section, so it is the handle the host duplicated for us —
|
||||||
|
// we own it; the (about-to-be-leaked) view keeps the section alive after the close.
|
||||||
|
close_handle_value(value);
|
||||||
|
self.data.set(view);
|
||||||
|
(cfg.log)(&format!(
|
||||||
|
"[{}] sealed pad channel mapped (index {want})",
|
||||||
|
cfg.tag
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//! The audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers (`pf-xusb`,
|
||||||
|
//! `pf-dualsense`).
|
||||||
|
//!
|
||||||
|
//! A UMDF driver cannot be literally free of `unsafe` — WDF dispatch, Win32 section mapping and
|
||||||
|
//! cross-process shared memory are FFI by nature. What Rust *can* buy is confining every raw
|
||||||
|
//! operation to one small, reviewed layer with explicit contracts, so the drivers' business logic
|
||||||
|
//! (the sealed-channel state machine, report plumbing, IOCTL policy) is **100 % safe code** and a
|
||||||
|
//! memory-safety bug can only live in this crate. Three modules:
|
||||||
|
//!
|
||||||
|
//! * [`section`] — [`section::MappedView`]: bounds- and alignment-checked access to a mapped shared
|
||||||
|
//! section (atomics for the cross-process sync fields), plus the leaked-view [`section::ViewCell`].
|
||||||
|
//! * [`channel`] — [`channel::ChannelClient`]: the sealed pad channel's driver side
|
||||||
|
//! (`design/gamepad-channel-sealing.md`), a **`#[forbid(unsafe_code)]` module** — the entire
|
||||||
|
//! handshake/validation/adoption state machine is safe Rust over [`section`]'s API.
|
||||||
|
//! * [`wdf`] — [`wdf::Request`] + queue/device-property helpers: each framework callback converts
|
||||||
|
//! its raw `WDFREQUEST` into a token exactly once (`unsafe`, with the framework's validity as the
|
||||||
|
//! contract); everything after that is safe.
|
||||||
|
//!
|
||||||
|
//! Lint gates (mirrored in every driver crate, enforced by the drivers CI clippy step):
|
||||||
|
//! `unsafe_op_in_unsafe_fn` + `clippy::undocumented_unsafe_blocks` — every remaining `unsafe {}`
|
||||||
|
//! must carry a `// SAFETY:` proof.
|
||||||
|
|
||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
pub mod channel;
|
||||||
|
pub mod section;
|
||||||
|
pub mod wdf;
|
||||||
|
|
||||||
|
/// `NT_SUCCESS` — an NTSTATUS is an error iff negative.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn nt_success(status: wdk_sys::NTSTATUS) -> bool {
|
||||||
|
status >= 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
//! Safe access to Win32 shared-memory sections: [`MappedView`] wraps a mapped view of a known
|
||||||
|
//! length and exposes bounds- and alignment-checked accessors, so callers never touch the raw base
|
||||||
|
//! pointer. Cross-process sync fields (seqs, pids, handle values) go through real atomics; bulk
|
||||||
|
//! report regions use plain unaligned copies, guarded by the channel protocol's seq fields — the
|
||||||
|
//! same access discipline the host side uses (`inject/windows/gamepad_raii.rs`).
|
||||||
|
|
||||||
|
use core::ffi::c_void;
|
||||||
|
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering};
|
||||||
|
|
||||||
|
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
|
||||||
|
|
||||||
|
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
|
||||||
|
unsafe extern "system" {
|
||||||
|
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||||
|
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||||
|
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||||
|
fn CloseHandle(h: *mut c_void) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read/write view over a mapped shared section of exactly `len` bytes. Every accessor
|
||||||
|
/// bounds-checks (and, for the atomic ones, alignment-checks) its offset, so no caller can read or
|
||||||
|
/// write outside the mapping — the offsets are `offset_of!` constants from `pf_driver_proto`, making
|
||||||
|
/// a failed check a compile-shaped logic bug (it aborts the WUDFHost rather than corrupting).
|
||||||
|
///
|
||||||
|
/// Concurrency: the peer process writes the section concurrently. Fields used for cross-process
|
||||||
|
/// synchronization must be accessed through the `load_*`/`store_*` atomic accessors; the bulk
|
||||||
|
/// byte/scalar accessors are plain unaligned accesses whose consistency is guarded by the channel
|
||||||
|
/// protocol (seq-fenced publishes), exactly as on the host side.
|
||||||
|
pub struct MappedView {
|
||||||
|
base: *mut u8,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: `MappedView` is a pointer + length over an OS mapping that stays valid until
|
||||||
|
// `UnmapViewOfFile` in `Drop` (or forever, once leaked into a `ViewCell`). All access goes through
|
||||||
|
// the checked accessors — atomics for shared sync fields, unaligned reads/writes for bulk data —
|
||||||
|
// none of which require a single-thread owner, so sharing/sending the view across the driver's
|
||||||
|
// callback threads is sound.
|
||||||
|
unsafe impl Send for MappedView {}
|
||||||
|
// SAFETY: as above — `&MappedView` only exposes accessors that are safe under concurrent use.
|
||||||
|
unsafe impl Sync for MappedView {}
|
||||||
|
|
||||||
|
impl MappedView {
|
||||||
|
/// Open the named section `name` and map its first `len` bytes read/write. `None` if the name
|
||||||
|
/// does not exist (e.g. the host is gone) or the mapping fails. The section handle is closed
|
||||||
|
/// immediately — the view keeps the section alive.
|
||||||
|
pub fn open_named(name: &str, len: usize) -> Option<MappedView> {
|
||||||
|
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
// SAFETY: `wide` is a valid NUL-terminated UTF-16 string for the duration of the call.
|
||||||
|
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, wide.as_ptr()) };
|
||||||
|
if h.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: `h` is the valid mapping handle just opened; map `len` bytes read/write. The view
|
||||||
|
// keeps the section alive, so the handle can be closed right away.
|
||||||
|
let base = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, len) } as *mut u8;
|
||||||
|
// SAFETY: `h` is the valid handle from `OpenFileMappingW`, owned solely by this function.
|
||||||
|
unsafe { CloseHandle(h) };
|
||||||
|
if base.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(MappedView { base, len })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map `len` bytes of a section from a raw handle VALUE (the sealed channel's delivery — a
|
||||||
|
/// handle the host duplicated into this process). `None` if the value does not resolve to a
|
||||||
|
/// mappable section. The handle itself is NOT consumed — the caller decides after validating
|
||||||
|
/// the mapped content (see [`close_handle_value`]).
|
||||||
|
pub fn from_handle_value(value: u64, len: usize) -> Option<MappedView> {
|
||||||
|
if value == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: `MapViewOfFile` on an arbitrary handle value is safe — it fails (returns null)
|
||||||
|
// unless the value resolves to a section handle in this process's table with RW access.
|
||||||
|
let base = unsafe { MapViewOfFile(value as usize as *mut c_void, FILE_MAP_RW, 0, 0, len) }
|
||||||
|
as *mut u8;
|
||||||
|
if base.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(MappedView { base, len })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert `off..off+n` is inside the view and, for atomics, `align`-aligned. The view base is
|
||||||
|
/// page-aligned (`MapViewOfFile`), so field alignment reduces to offset alignment.
|
||||||
|
#[inline]
|
||||||
|
fn check(&self, off: usize, n: usize, align: usize) {
|
||||||
|
assert!(
|
||||||
|
off.is_multiple_of(align) && off.checked_add(n).is_some_and(|end| end <= self.len),
|
||||||
|
"MappedView access out of bounds/alignment (off={off}, n={n}, len={})",
|
||||||
|
self.len
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomic `u32` load at `off` (must be 4-aligned) — the cross-process sync accessor.
|
||||||
|
#[inline]
|
||||||
|
pub fn load_u32(&self, off: usize, order: Ordering) -> u32 {
|
||||||
|
self.check(off, 4, 4);
|
||||||
|
// SAFETY: `off` is in-bounds + 4-aligned per `check`, and the page-aligned mapping stays
|
||||||
|
// valid while `&self` lives; an `AtomicU32` view over shared memory is the defined way to
|
||||||
|
// race the peer process.
|
||||||
|
unsafe { (*(self.base.add(off) as *const AtomicU32)).load(order) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomic `u32` store at `off` (must be 4-aligned).
|
||||||
|
#[inline]
|
||||||
|
pub fn store_u32(&self, off: usize, v: u32, order: Ordering) {
|
||||||
|
self.check(off, 4, 4);
|
||||||
|
// SAFETY: as `load_u32` — in-bounds, aligned, valid for `&self`'s lifetime.
|
||||||
|
unsafe { (*(self.base.add(off) as *const AtomicU32)).store(v, order) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomic `u64` load at `off` (must be 8-aligned).
|
||||||
|
#[inline]
|
||||||
|
pub fn load_u64(&self, off: usize, order: Ordering) -> u64 {
|
||||||
|
self.check(off, 8, 8);
|
||||||
|
// SAFETY: as `load_u32`, with 8-byte size/alignment checked.
|
||||||
|
unsafe { (*(self.base.add(off) as *const AtomicU64)).load(order) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain byte read at `off` (bulk-region accessor — protocol-guarded, see the type docs).
|
||||||
|
#[inline]
|
||||||
|
pub fn read_u8(&self, off: usize) -> u8 {
|
||||||
|
self.check(off, 1, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; a one-byte read cannot tear.
|
||||||
|
unsafe { *self.base.add(off) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain byte write at `off`.
|
||||||
|
#[inline]
|
||||||
|
pub fn write_u8(&self, off: usize, v: u8) {
|
||||||
|
self.check(off, 1, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; a one-byte write cannot tear.
|
||||||
|
unsafe { *self.base.add(off) = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain (unaligned) `u16` read at `off`.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_u16(&self, off: usize) -> u16 {
|
||||||
|
self.check(off, 2, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||||
|
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u16) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain (unaligned) `u32` read at `off` — the bulk-region accessor for a DATA-section scalar
|
||||||
|
/// (host-written state / a driver-written publish counter; consistency comes from the channel
|
||||||
|
/// protocol's seq fences, not from this access, exactly as on the host side).
|
||||||
|
#[inline]
|
||||||
|
pub fn read_u32(&self, off: usize) -> u32 {
|
||||||
|
self.check(off, 4, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||||
|
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u32) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain (unaligned) `u32` write at `off` (bulk-region accessor).
|
||||||
|
#[inline]
|
||||||
|
pub fn write_u32(&self, off: usize, v: u32) {
|
||||||
|
self.check(off, 4, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; `write_unaligned` has no alignment requirement.
|
||||||
|
unsafe { core::ptr::write_unaligned(self.base.add(off) as *mut u32, v) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain (unaligned) `i16` read at `off`.
|
||||||
|
#[inline]
|
||||||
|
pub fn read_i16(&self, off: usize) -> i16 {
|
||||||
|
self.check(off, 2, 1);
|
||||||
|
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||||
|
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const i16) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy `dst.len()` bytes out of the view starting at `off`.
|
||||||
|
pub fn read_bytes(&self, off: usize, dst: &mut [u8]) {
|
||||||
|
self.check(off, dst.len(), 1);
|
||||||
|
// SAFETY: the source range is in-bounds per `check`; `dst` is a live exclusive borrow of
|
||||||
|
// `dst.len()` writable bytes and cannot overlap the foreign mapping.
|
||||||
|
unsafe { core::ptr::copy_nonoverlapping(self.base.add(off), dst.as_mut_ptr(), dst.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy `src` into the view starting at `off`.
|
||||||
|
pub fn write_bytes(&self, off: usize, src: &[u8]) {
|
||||||
|
self.check(off, src.len(), 1);
|
||||||
|
// SAFETY: the destination range is in-bounds per `check`; `src` is a live borrow that
|
||||||
|
// cannot overlap the foreign mapping.
|
||||||
|
unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), self.base.add(off), src.len()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MappedView {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: `base` is the live view from `MapViewOfFile`, unmapped exactly once (here).
|
||||||
|
unsafe {
|
||||||
|
UnmapViewOfFile(self.base as *const c_void);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a raw handle VALUE owned by this process — the sealed channel's adopt-on-success step
|
||||||
|
/// (the mapped view keeps the section alive after the close). Closing a value that is not a live
|
||||||
|
/// handle of this process is a logic error the OS rejects (returns FALSE); it is not memory-unsafe.
|
||||||
|
pub fn close_handle_value(value: u64) {
|
||||||
|
if value == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: `CloseHandle` validates the value against this process's handle table; no memory is
|
||||||
|
// dereferenced through it.
|
||||||
|
unsafe { CloseHandle(value as usize as *mut c_void) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A lock-free cell holding the driver's adopted DATA view as a **leaked** `&'static MappedView`.
|
||||||
|
/// [`set`](Self::set) leaks the new view (and abandons the old one) instead of ever unmapping:
|
||||||
|
/// a concurrent framework callback may still be reading through a previously-returned reference, so
|
||||||
|
/// the mapping must never be torn down — a deliberate, bounded leak (one small view per delivery,
|
||||||
|
/// at most a handful per pad lifetime).
|
||||||
|
pub struct ViewCell(AtomicPtr<MappedView>);
|
||||||
|
|
||||||
|
impl Default for ViewCell {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewCell {
|
||||||
|
pub const fn new() -> ViewCell {
|
||||||
|
ViewCell(AtomicPtr::new(core::ptr::null_mut()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current view, if one was published. The `'static` lifetime is real: published views are
|
||||||
|
/// leaked and never unmapped.
|
||||||
|
pub fn get(&self) -> Option<&'static MappedView> {
|
||||||
|
let p = self.0.load(Ordering::Acquire);
|
||||||
|
// SAFETY: `p` is either null or a `Box::leak`ed `MappedView` published by `set`, which is
|
||||||
|
// never dropped or unmapped — so the reference is valid for the process lifetime.
|
||||||
|
(!p.is_null()).then(|| unsafe { &*p })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish `view`, leaking it (and abandoning — NOT freeing — any previous view; see the type
|
||||||
|
/// docs for why the old mapping must stay alive).
|
||||||
|
pub fn set(&self, view: MappedView) {
|
||||||
|
let leaked: &'static mut MappedView = Box::leak(Box::new(view));
|
||||||
|
self.0.swap(leaked, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
//! Safe(ly-contracted) helpers over the WDF request/memory/property DDIs the pad drivers use. The
|
||||||
|
//! pattern: a framework callback converts its raw `WDFREQUEST` into a [`Request`] token **once**
|
||||||
|
//! (`unsafe`, the framework's validity guarantee is the contract); every operation after that is a
|
||||||
|
//! safe method, and completion consumes the token so a request cannot be completed twice or used
|
||||||
|
//! after completion from safe code.
|
||||||
|
|
||||||
|
use wdk_sys::{
|
||||||
|
NTSTATUS, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
||||||
|
call_unsafe_wdf_function_binding,
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||||
|
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
||||||
|
/// wdk_sys root; the value is stable WDM).
|
||||||
|
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn nt_success(s: NTSTATUS) -> bool {
|
||||||
|
s >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A validity token for one framework-delivered `WDFREQUEST`. Not `Copy`/`Clone`: completing or
|
||||||
|
/// forwarding consumes it, so safe code cannot touch a request the framework already owns again.
|
||||||
|
pub struct Request(WDFREQUEST);
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Wrap the raw request handed to the current framework callback.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `raw` must be the live, framework-provided `WDFREQUEST` of the callback invocation this is
|
||||||
|
/// called from (WDF owns handle validity; a forged/dangling handle is framework UB).
|
||||||
|
pub unsafe fn new(raw: WDFREQUEST) -> Request {
|
||||||
|
Request(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete the request with `status` (consumes the token — the framework owns it afterwards).
|
||||||
|
pub fn complete(self, status: NTSTATUS) {
|
||||||
|
// SAFETY: `self.0` is the live callback request per `Request::new`'s contract, not yet
|
||||||
|
// completed or forwarded (both consume the token).
|
||||||
|
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, self.0, status) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
||||||
|
/// Returns the status to complete with (`STATUS_INVALID_BUFFER_SIZE` if the buffer is short).
|
||||||
|
pub fn copy_to_output(&self, src: &[u8]) -> NTSTATUS {
|
||||||
|
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||||
|
// SAFETY: `self.0` is the live callback request; `mem` receives the memory handle.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut mem)
|
||||||
|
};
|
||||||
|
if !nt_success(st) {
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
let mut outlen: usize = 0;
|
||||||
|
// SAFETY: `mem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||||
|
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||||
|
if outlen < src.len() {
|
||||||
|
return STATUS_INVALID_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
// SAFETY: `mem` is valid and at least `src.len()` bytes; `src` is a live borrow.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(
|
||||||
|
WdfMemoryCopyFromBuffer,
|
||||||
|
mem,
|
||||||
|
0usize,
|
||||||
|
src.as_ptr() as *mut core::ffi::c_void,
|
||||||
|
src.len()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if !nt_success(st) {
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
// SAFETY: `self.0` is the live callback request.
|
||||||
|
unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, src.len() as u64)
|
||||||
|
};
|
||||||
|
0 // STATUS_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The request's input buffer: up to `cap` bytes copied out, plus the buffer's TRUE length.
|
||||||
|
/// `Err(status)` if the input memory can't be retrieved (propagate as the completion status).
|
||||||
|
pub fn input_bytes(&self, cap: usize) -> Result<(Vec<u8>, usize), NTSTATUS> {
|
||||||
|
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||||
|
// SAFETY: `self.0` is the live callback request; `inmem` receives the memory handle.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, self.0, &mut inmem)
|
||||||
|
};
|
||||||
|
if !nt_success(st) {
|
||||||
|
return Err(st);
|
||||||
|
}
|
||||||
|
let mut len: usize = 0;
|
||||||
|
// SAFETY: `inmem` is the valid memory object just retrieved; `len` receives its size.
|
||||||
|
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
||||||
|
as *const u8;
|
||||||
|
if p.is_null() {
|
||||||
|
return Ok((Vec::new(), 0));
|
||||||
|
}
|
||||||
|
let n = len.min(cap);
|
||||||
|
// SAFETY: `p` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `n <= len`.
|
||||||
|
let bytes = unsafe { core::slice::from_raw_parts(p, n) }.to_vec();
|
||||||
|
Ok((bytes, len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The request's output-buffer LENGTH (0 if unavailable) — UMDF HID marshalling carries the
|
||||||
|
/// output-report id in it.
|
||||||
|
pub fn output_buffer_len(&self) -> usize {
|
||||||
|
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
||||||
|
// SAFETY: `self.0` is the live callback request; output memory is optional here.
|
||||||
|
if !nt_success(unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut outmem)
|
||||||
|
}) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut outlen: usize = 0;
|
||||||
|
// SAFETY: `outmem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||||
|
let _ =
|
||||||
|
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
||||||
|
outlen
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the completed-bytes information field (for paths that complete with a length but no
|
||||||
|
/// output copy, e.g. echoing an output report's length).
|
||||||
|
pub fn set_information(&self, info: u64) {
|
||||||
|
// SAFETY: `self.0` is the live callback request.
|
||||||
|
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, info) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forward the request to a manual queue. On success the framework owns it (the token is
|
||||||
|
/// consumed by value — the caller cannot touch the request again); on failure the token is
|
||||||
|
/// handed back with the status so the caller completes it. (`Request` has no `Drop`, so the
|
||||||
|
/// consumed-on-success token simply falls out of scope — nothing to run.)
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `queue` must be a live manual `WDFQUEUE` of the same device (e.g. the one created in
|
||||||
|
/// `EvtDeviceAdd` and stashed in a static).
|
||||||
|
pub unsafe fn forward_to_queue(self, queue: WDFQUEUE) -> Result<(), (Request, NTSTATUS)> {
|
||||||
|
// SAFETY: `self.0` is the live callback request; `queue` is live per this fn's contract.
|
||||||
|
let st =
|
||||||
|
unsafe { call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, self.0, queue) };
|
||||||
|
if nt_success(st) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err((self, st))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the next pended request off a manual queue (`None` when empty).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `queue` must be a live manual `WDFQUEUE` (e.g. the timer's parent object).
|
||||||
|
pub unsafe fn retrieve_next_request(queue: WDFQUEUE) -> Option<Request> {
|
||||||
|
let mut request: WDFREQUEST = core::ptr::null_mut();
|
||||||
|
// SAFETY: `queue` is live per this fn's contract; `request` receives the next pended request.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
||||||
|
};
|
||||||
|
// SAFETY: on success `request` is a live framework request this caller now services — the
|
||||||
|
// exact contract `Request::new` requires.
|
||||||
|
nt_success(st).then(|| unsafe { Request::new(request) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), a
|
||||||
|
/// NUL-terminated UTF-16 decimal string. Defaults to 0 (single-pad) if absent. (The WDFMEMORY is
|
||||||
|
/// device-parented and freed by the framework at device teardown — one small alloc per device add.)
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `device` must be the live `WDFDEVICE` created in the current `EvtDeviceAdd`.
|
||||||
|
pub unsafe fn query_location_index(device: WDFDEVICE) -> u32 {
|
||||||
|
let mut mem: wdk_sys::WDFMEMORY = core::ptr::null_mut();
|
||||||
|
// SAFETY: `device` is live per this fn's contract; property = LocationInformation; pool ignored
|
||||||
|
// in UMDF; `mem` receives the handle.
|
||||||
|
let st = unsafe {
|
||||||
|
call_unsafe_wdf_function_binding!(
|
||||||
|
WdfDeviceAllocAndQueryProperty,
|
||||||
|
device,
|
||||||
|
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
||||||
|
0,
|
||||||
|
WDF_NO_OBJECT_ATTRIBUTES,
|
||||||
|
&mut mem
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if !nt_success(st) || mem.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut len: usize = 0;
|
||||||
|
// SAFETY: `mem` is the valid memory object just allocated; `len` receives its size.
|
||||||
|
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
||||||
|
as *const u16;
|
||||||
|
if buf.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let units = (len / 2).min(8);
|
||||||
|
// SAFETY: `buf` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `units * 2 <= len`.
|
||||||
|
let chars = unsafe { core::slice::from_raw_parts(buf, units) };
|
||||||
|
let mut idx: u32 = 0;
|
||||||
|
let mut any = false;
|
||||||
|
for &c in chars {
|
||||||
|
if c == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (0x30..=0x39).contains(&c) {
|
||||||
|
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any { idx } else { 0 }
|
||||||
|
}
|
||||||
@@ -42,8 +42,10 @@ AddReg=pf_vdisplay_HardwareDeviceSettings
|
|||||||
[pf_vdisplay_HardwareDeviceSettings]
|
[pf_vdisplay_HardwareDeviceSettings]
|
||||||
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
|
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
|
||||||
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
|
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
|
||||||
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
|
; Only the host (LocalSystem service) + admins may open the control device. Deliberately NO Everyone
|
||||||
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
|
; ACE (SudoVDA ships one for its user-mode host): the control plane creates/removes monitors and
|
||||||
|
; bootstraps the sealed frame channel (IOCTL_SET_FRAME_CHANNEL), so it is not for unprivileged callers.
|
||||||
|
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)"
|
||||||
|
|
||||||
[pf_vdisplay_Install.NT.Services]
|
[pf_vdisplay_Install.NT.Services]
|
||||||
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
|
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ struct SendAdapter(iddcx::IDDCX_ADAPTER);
|
|||||||
// SAFETY: an opaque IddCx handle, used only as an argument to IddCx DDIs (themselves the synchronisation
|
// SAFETY: an opaque IddCx handle, used only as an argument to IddCx DDIs (themselves the synchronisation
|
||||||
// point) — never dereferenced in Rust. Storing it across threads in a OnceLock is sound.
|
// point) — never dereferenced in Rust. Storing it across threads in a OnceLock is sound.
|
||||||
unsafe impl Send for SendAdapter {}
|
unsafe impl Send for SendAdapter {}
|
||||||
|
// SAFETY: as above — the handle is only ever passed by value to IddCx DDIs, never dereferenced, so
|
||||||
|
// shared `&SendAdapter` access across threads is sound.
|
||||||
unsafe impl Sync for SendAdapter {}
|
unsafe impl Sync for SendAdapter {}
|
||||||
|
|
||||||
static ADAPTER: OnceLock<SendAdapter> = OnceLock::new();
|
static ADAPTER: OnceLock<SendAdapter> = OnceLock::new();
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ pub unsafe extern "C" fn parse_monitor_description(
|
|||||||
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
||||||
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
// SAFETY: framework-provided in/out args, valid for the call.
|
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||||
let in_args = unsafe { &*p_in };
|
let in_args = unsafe { &*p_in };
|
||||||
|
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||||
let out_args = unsafe { &mut *p_out };
|
let out_args = unsafe { &mut *p_out };
|
||||||
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
||||||
let edid = unsafe {
|
let edid = unsafe {
|
||||||
@@ -100,8 +101,9 @@ pub unsafe extern "C" fn parse_monitor_description2(
|
|||||||
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2,
|
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2,
|
||||||
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
// SAFETY: framework-provided in/out args, valid for the call.
|
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||||
let in_args = unsafe { &*p_in };
|
let in_args = unsafe { &*p_in };
|
||||||
|
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||||
let out_args = unsafe { &mut *p_out };
|
let out_args = unsafe { &mut *p_out };
|
||||||
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
||||||
let edid = unsafe {
|
let edid = unsafe {
|
||||||
@@ -156,8 +158,9 @@ pub unsafe extern "C" fn monitor_query_modes(
|
|||||||
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
|
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
|
||||||
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
// SAFETY: framework-provided in/out args, valid for the call.
|
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||||
let in_args = unsafe { &*p_in };
|
let in_args = unsafe { &*p_in };
|
||||||
|
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||||
let out_args = unsafe { &mut *p_out };
|
let out_args = unsafe { &mut *p_out };
|
||||||
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
||||||
return STATUS_NOT_FOUND;
|
return STATUS_NOT_FOUND;
|
||||||
@@ -183,8 +186,9 @@ pub unsafe extern "C" fn monitor_query_modes2(
|
|||||||
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2,
|
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2,
|
||||||
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
// SAFETY: framework-provided in/out args, valid for the call.
|
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||||
let in_args = unsafe { &*p_in };
|
let in_args = unsafe { &*p_in };
|
||||||
|
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||||
let out_args = unsafe { &mut *p_out };
|
let out_args = unsafe { &mut *p_out };
|
||||||
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
||||||
return STATUS_NOT_FOUND;
|
return STATUS_NOT_FOUND;
|
||||||
@@ -279,7 +283,8 @@ pub unsafe extern "C" fn assign_swap_chain(
|
|||||||
drop(crate::monitor::take_swap_chain_processor(monitor));
|
drop(crate::monitor::take_swap_chain_processor(monitor));
|
||||||
|
|
||||||
// The OS target id (stamped on the monitor at creation, after IddCxMonitorArrival) keys the
|
// The OS target id (stamped on the monitor at creation, after IddCxMonitorArrival) keys the
|
||||||
// per-monitor objects STEP 6's host opens. 0 (default) if the monitor isn't found.
|
// frame-channel stash STEP 6's worker attaches from (the host addresses its IOCTL_SET_FRAME_CHANNEL
|
||||||
|
// delivery by this id). 0 (default) if the monitor isn't found — the worker then never attaches.
|
||||||
let target_id = crate::monitor::target_id_for_object(monitor).unwrap_or(0);
|
let target_id = crate::monitor::target_id_for_object(monitor).unwrap_or(0);
|
||||||
|
|
||||||
if let Some(device) = crate::direct_3d_device::pooled_device(luid) {
|
if let Some(device) = crate::direct_3d_device::pooled_device(luid) {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
|||||||
}
|
}
|
||||||
// SAFETY: `request` is the framework WDFREQUEST.
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
|
control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
|
||||||
_ => complete(request, STATUS_NOT_FOUND),
|
_ => complete(request, STATUS_NOT_FOUND),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,11 +150,49 @@ unsafe fn add(request: WDFREQUEST) {
|
|||||||
adapter_luid_high: luid_high,
|
adapter_luid_high: luid_high,
|
||||||
target_id,
|
target_id,
|
||||||
resolved_monitor_id: monitor_id,
|
resolved_monitor_id: monitor_id,
|
||||||
|
// This WUDFHost's pid — where the host duplicates the sealed frame channel's handles INTO
|
||||||
|
// (`ProcessSharingDisabled`: this process is exclusively ours and dies with the device).
|
||||||
|
wudf_pid: std::process::id(),
|
||||||
};
|
};
|
||||||
// SAFETY: `request` is the framework WDFREQUEST.
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
unsafe { write_output_complete(request, &reply) };
|
unsafe { write_output_complete(request, &reply) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `IOCTL_SET_FRAME_CHANNEL`: adopt the handle values the host duplicated into this process and stash
|
||||||
|
/// them on the target monitor for the swap-chain worker to attach with. The ownership contract with
|
||||||
|
/// the host is **adopt-on-success only**: this driver owns (and eventually closes) the handles iff the
|
||||||
|
/// IOCTL completes successfully; on ANY error completion it leaves them untouched, because the host
|
||||||
|
/// reaps its remote duplicates whenever the IOCTL fails — a close on both sides would double-close
|
||||||
|
/// values the OS may already have reused for unrelated handles.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn set_frame_channel(request: WDFREQUEST) {
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// A malformed request adopts nothing (no FrameChannel is built, so no Drop can close anything).
|
||||||
|
let Some(ch) = crate::frame_transport::FrameChannel::from_request(&req) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match crate::monitor::set_frame_channel(req.target_id, ch) {
|
||||||
|
Ok(()) => complete(request, STATUS_SUCCESS),
|
||||||
|
Err(ch) => {
|
||||||
|
dbglog!(
|
||||||
|
"[pf-vd] SET_FRAME_CHANNEL: no monitor with target_id {} — rejecting (host reaps the handles)",
|
||||||
|
req.target_id
|
||||||
|
);
|
||||||
|
// NOT adopted: disarm the channel so its Drop does NOT close the handles (see the contract
|
||||||
|
// above — the host's error path reaps them remotely).
|
||||||
|
ch.into_unowned();
|
||||||
|
complete(request, STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
|
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
|||||||
@@ -123,11 +123,11 @@ static DEVICE_POOL: Mutex<Option<(i64, Arc<Direct3DDevice>)>> = Mutex::new(None)
|
|||||||
pub fn pooled_device(luid: LUID) -> Option<Arc<Direct3DDevice>> {
|
pub fn pooled_device(luid: LUID) -> Option<Arc<Direct3DDevice>> {
|
||||||
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart);
|
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart);
|
||||||
let mut pool = DEVICE_POOL.lock().ok()?;
|
let mut pool = DEVICE_POOL.lock().ok()?;
|
||||||
if let Some((k, dev)) = pool.as_ref() {
|
if let Some((k, dev)) = pool.as_ref()
|
||||||
if *k == key {
|
&& *k == key
|
||||||
|
{
|
||||||
return Some(dev.clone());
|
return Some(dev.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
match Direct3DDevice::init(luid) {
|
match Direct3DDevice::init(luid) {
|
||||||
Ok(d) => {
|
Ok(d) => {
|
||||||
let a = Arc::new(d);
|
let a = Arc::new(d);
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
//! STEP 6 — IDD-push frame publisher (DRIVER side).
|
//! STEP 6 — IDD-push frame publisher (DRIVER side), attached over the **sealed channel**.
|
||||||
//!
|
//!
|
||||||
//! The restricted WUDFHost token canNOT create named kernel objects (proven on the RTX box: it can't
|
//! The restricted WUDFHost token canNOT create named kernel objects — and since the frame channel
|
||||||
//! even write a world-writable file), so — exactly like the gamepad UMDF drivers
|
//! carries whole-desktop pixels, the objects are not merely host-created but **unnamed**: nothing to
|
||||||
//! (`crates/punktfunk-host/src/inject/dualsense_windows.rs`: *"the host creates the section, privileged,
|
//! enumerate, open by name, or pre-create ("squat"). The **host** creates the shared header +
|
||||||
//! with a permissive SDDL so the WUDFHost can open it; the driver maps it"*) — the **host** creates the
|
//! frame-ready event + ring of keyed-mutex textures with no names, duplicates the handles INTO this
|
||||||
//! shared header + frame-ready event + ring of keyed-mutex textures, and the driver only **OPENS** them.
|
//! WUDFHost process (`DuplicateHandle` — SYSTEM can, we can't reciprocate, which is why the host is the
|
||||||
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header (our
|
//! broker), and delivers the handle VALUES over `IOCTL_SET_FRAME_CHANNEL` ([`crate::control`] stashes
|
||||||
//! only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write files),
|
//! them per monitor as a [`FrameChannel`]). The swap-chain worker picks the stash up and attaches with
|
||||||
//! then copies each acquired swap-chain surface into the next ring slot and signals the host.
|
//! [`FramePublisher::from_channel`]. Only the two endpoint processes ever hold a handle to any frame
|
||||||
|
//! object — see `design/idd-push-security.md`.
|
||||||
//!
|
//!
|
||||||
//! Host counterpart: `crates/punktfunk-host/src/capture/idd_push.rs`. The shared `SharedHeader` layout,
|
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header
|
||||||
//! the [`FrameToken`] packing, the `Global\` object-name scheme, the `MAGIC`/`RING_LEN` and the
|
//! (our only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write
|
||||||
//! `DRV_STATUS_*` codes are NOT hand-duplicated here: both sides `use pf_driver_proto::frame::*`, which
|
//! files), then copies each acquired swap-chain surface into the next ring slot and signals the host.
|
||||||
//! OWNS the contract (with `const` size asserts so any drift is a compile error).
|
|
||||||
//!
|
//!
|
||||||
//! Ported from the proven oracle (`packaging/windows/vdisplay-driver/pf-vdisplay/src/frame_transport.rs`).
|
//! Host counterpart: `crates/punktfunk-host/src/capture/windows/idd_push.rs`. The shared `SharedHeader`
|
||||||
//! Differences from the oracle:
|
//! layout, the [`FrameToken`] packing, the `MAGIC`/`RING_LEN`, the `DRV_STATUS_*` codes and the
|
||||||
//! * the layout/consts/names/token come from `pf_driver_proto::frame` instead of being re-declared;
|
//! channel-delivery struct are NOT hand-duplicated here: both sides `use pf_driver_proto::{control,
|
||||||
//! * `dbglog!` replaces `log::info!`;
|
//! frame}`, which OWNS the contract (with `const` size asserts so any drift is a compile error).
|
||||||
//! * the optional fixed-name `Global\pfvd-dbg` `DebugBlock` bring-up channel is SKIPPED (not on the data
|
|
||||||
//! path). FOLLOW-UP: if the host bring-up diagnostics are needed again, port the oracle's `DebugBlock`
|
|
||||||
//! here too (it is owned by `idd_push.rs`, not the proto).
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use pf_driver_proto::control::SetFrameChannelRequest;
|
||||||
use pf_driver_proto::frame::{
|
use pf_driver_proto::frame::{
|
||||||
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, FrameToken, MAGIC, RING_LEN,
|
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, FrameToken, MAGIC, RING_LEN,
|
||||||
SharedHeader, event_name, header_name, texture_name,
|
SharedHeader,
|
||||||
};
|
};
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
use windows::Win32::Graphics::Direct3D11::{
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
@@ -34,28 +32,95 @@ use windows::Win32::Graphics::Direct3D11::{
|
|||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
|
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
|
||||||
use windows::Win32::System::Memory::{
|
use windows::Win32::System::Memory::{
|
||||||
FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW,
|
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
|
||||||
UnmapViewOfFile,
|
|
||||||
};
|
};
|
||||||
use windows::Win32::System::Threading::{OpenEventW, SYNCHRONIZATION_ACCESS_RIGHTS, SetEvent};
|
use windows::Win32::System::Threading::SetEvent;
|
||||||
use windows::core::{HSTRING, Interface};
|
use windows::core::Interface;
|
||||||
|
|
||||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` — passed to `OpenSharedResourceByName` (matches the host's
|
|
||||||
/// `CreateSharedHandle` access). Kept local: it is a `OpenSharedResourceByName` arg, not part of the
|
|
||||||
/// proto contract. (Same value the host uses in `idd_push.rs`.)
|
|
||||||
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
|
|
||||||
/// SYNCHRONIZE | EVENT_MODIFY_STATE — the driver does not wait on the event, only SIGNALS it.
|
|
||||||
const EVENT_ACCESS: u32 = 0x0010_0000 | 0x0002;
|
|
||||||
/// `WAIT_TIMEOUT` as an HRESULT — `AcquireSync` returns this when the slot is held by the consumer.
|
/// `WAIT_TIMEOUT` as an HRESULT — `AcquireSync` returns this when the slot is held by the consumer.
|
||||||
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
|
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
|
||||||
|
|
||||||
|
/// One monitor's sealed-channel bootstrap: the handle VALUES the host duplicated into THIS process
|
||||||
|
/// (`IOCTL_SET_FRAME_CHANNEL`). Owning a `FrameChannel` means owning those handles — exactly one of
|
||||||
|
/// {the monitor stash ([`crate::monitor`]), a [`FramePublisher`] under construction} holds it at any
|
||||||
|
/// time, and `Drop` closes every entry not consumed, so a replaced/unmatched/failed delivery can never
|
||||||
|
/// leak entries in the WUDFHost handle table. A `0` field means "taken" (or never valid) and is skipped.
|
||||||
|
pub struct FrameChannel {
|
||||||
|
/// The ring generation these textures belong to (checked against the header at attach).
|
||||||
|
generation: u32,
|
||||||
|
ring_len: u32,
|
||||||
|
header: u64,
|
||||||
|
event: u64,
|
||||||
|
textures: [u64; RING_LEN as usize],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameChannel {
|
||||||
|
/// Validate + adopt the handle values from the host's IOCTL. `None` on a malformed request (bad
|
||||||
|
/// `ring_len`, zero handles) — the caller completes with `STATUS_INVALID_PARAMETER` and nothing is
|
||||||
|
/// adopted (a zero value is never treated as a handle).
|
||||||
|
pub fn from_request(req: &SetFrameChannelRequest) -> Option<Self> {
|
||||||
|
if req.ring_len == 0 || req.ring_len > RING_LEN {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if req.header_handle == 0
|
||||||
|
|| req.event_handle == 0
|
||||||
|
|| req.texture_handles[..req.ring_len as usize].contains(&0)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self {
|
||||||
|
generation: req.generation,
|
||||||
|
ring_len: req.ring_len,
|
||||||
|
header: req.header_handle,
|
||||||
|
event: req.event_handle,
|
||||||
|
textures: req.texture_handles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move a handle value out of the channel: the caller now owns it; `Drop` skips the zeroed slot.
|
||||||
|
fn take(v: &mut u64) -> HANDLE {
|
||||||
|
HANDLE(core::mem::take(v) as usize as *mut core::ffi::c_void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disarm without closing anything — for the adopt-on-success-only contract: a delivery rejected
|
||||||
|
/// with an error completion was never adopted, and the HOST reaps its remote duplicates on that
|
||||||
|
/// error, so closing here too would double-close (see `crate::control::set_frame_channel`).
|
||||||
|
pub fn into_unowned(mut self) {
|
||||||
|
self.header = 0;
|
||||||
|
self.event = 0;
|
||||||
|
self.textures = [0; RING_LEN as usize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for FrameChannel {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for v in [&mut self.header, &mut self.event]
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.textures.iter_mut())
|
||||||
|
{
|
||||||
|
if *v != 0 {
|
||||||
|
let h = Self::take(v);
|
||||||
|
// SAFETY: `h` is a live handle the host duplicated into this process for us to own; it
|
||||||
|
// was not consumed (non-zero), so this is its sole close.
|
||||||
|
unsafe {
|
||||||
|
let _ = CloseHandle(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: `FrameChannel` is plain integers, so it is auto-`Send` — it crosses from the control-plane
|
||||||
|
// dispatch thread (stash) to the swap-chain worker (attach) with `MONITOR_MODES` serializing the
|
||||||
|
// hand-off; no manual impl needed (handle values are process-global tokens, not thread-affine).
|
||||||
|
|
||||||
struct Slot {
|
struct Slot {
|
||||||
tex: ID3D11Texture2D,
|
tex: ID3D11Texture2D,
|
||||||
mutex: IDXGIKeyedMutex,
|
mutex: IDXGIKeyedMutex,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain processor
|
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain processor
|
||||||
/// thread; attached lazily once the host has created the shared objects.
|
/// thread; attached lazily once the host's channel delivery lands in the monitor stash.
|
||||||
pub struct FramePublisher {
|
pub struct FramePublisher {
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
map: HANDLE,
|
map: HANDLE,
|
||||||
@@ -70,7 +135,8 @@ pub struct FramePublisher {
|
|||||||
ring_format: u32,
|
ring_format: u32,
|
||||||
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
|
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
|
||||||
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
|
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
|
||||||
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
|
/// detects that so `run_core` re-attaches to the new ring (whose channel the host re-delivers)
|
||||||
|
/// instead of dropping every frame.
|
||||||
generation: u32,
|
generation: u32,
|
||||||
/// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a
|
/// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a
|
||||||
/// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1).
|
/// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1).
|
||||||
@@ -81,102 +147,99 @@ pub struct FramePublisher {
|
|||||||
unsafe impl Send for FramePublisher {}
|
unsafe impl Send for FramePublisher {}
|
||||||
|
|
||||||
impl FramePublisher {
|
impl FramePublisher {
|
||||||
/// Try ONCE to attach to the host-created shared objects. Returns `Err` cheaply if the host hasn't
|
/// Attach to the host ring from a delivered [`FrameChannel`]. Consumes the channel: on ANY failure
|
||||||
/// created/published them yet — the drain loop retries periodically, so a non-IDD-push session just
|
/// every handle is closed (taken ones explicitly, the rest by the channel's `Drop`) and the host
|
||||||
/// keeps draining with no stall. All early-return paths clean up the handles/mapping they opened
|
/// re-delivers on the next recreate — there is nothing to poll, so failure is terminal for THIS
|
||||||
/// explicitly (raw-handle style, no RAII — matches the rest of this driver).
|
/// delivery (the host's `wait_for_attach` sees the status code and fails the session open). All
|
||||||
pub fn try_open(
|
/// early-return paths clean up explicitly (raw-handle style, no RAII — matches the rest of this
|
||||||
target_id: u32,
|
/// driver).
|
||||||
|
pub fn from_channel(
|
||||||
|
mut channel: FrameChannel,
|
||||||
render_luid_low: u32,
|
render_luid_low: u32,
|
||||||
render_luid_high: i32,
|
render_luid_high: i32,
|
||||||
device: &ID3D11Device,
|
device: &ID3D11Device,
|
||||||
context: &ID3D11DeviceContext,
|
context: &ID3D11DeviceContext,
|
||||||
) -> windows::core::Result<Self> {
|
) -> windows::core::Result<Self> {
|
||||||
// 1. Open the host-created header (RW). Err if the host hasn't created it yet.
|
let ring_len = channel.ring_len;
|
||||||
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call (`?` returns on failure).
|
|
||||||
let map = unsafe {
|
// 1. Map the header from the duplicated section handle (ours from here on).
|
||||||
OpenFileMappingW(
|
let map = FrameChannel::take(&mut channel.header);
|
||||||
FILE_MAP_ALL_ACCESS.0,
|
// SAFETY: `map` is the live section handle the host duplicated into this process; mapping
|
||||||
false,
|
// size_of::<SharedHeader>() bytes of it (the host created the mapping at >= that size). The null
|
||||||
&HSTRING::from(header_name(target_id)),
|
// `view.Value` is checked below.
|
||||||
)?
|
|
||||||
};
|
|
||||||
// SAFETY: `map` is the just-opened file mapping; mapping size_of::<SharedHeader>() bytes of it
|
|
||||||
// (the host created the mapping at >= that size). The null `view.Value` is checked below.
|
|
||||||
let view = unsafe {
|
let view = unsafe {
|
||||||
|
// Read/write only — the host now duplicates the header handle with least access
|
||||||
|
// (`SECTION_MAP_READ | SECTION_MAP_WRITE`), so `FILE_MAP_ALL_ACCESS` would exceed the
|
||||||
|
// granted rights and fail. We read the layout + write status/publish-token fields; RW covers it.
|
||||||
MapViewOfFile(
|
MapViewOfFile(
|
||||||
map,
|
map,
|
||||||
FILE_MAP_ALL_ACCESS,
|
FILE_MAP_READ | FILE_MAP_WRITE,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
core::mem::size_of::<SharedHeader>(),
|
core::mem::size_of::<SharedHeader>(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if view.Value.is_null() {
|
if view.Value.is_null() {
|
||||||
// SAFETY: `map` is the just-opened mapping handle, closed once here on the error path.
|
let err = windows::core::Error::from_win32();
|
||||||
|
// SAFETY: `map` is the taken section handle, closed once here on the error path (the rest of
|
||||||
|
// `channel` closes via its Drop).
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = CloseHandle(map);
|
let _ = CloseHandle(map);
|
||||||
}
|
}
|
||||||
return Err(windows::core::Error::from_win32());
|
return Err(err);
|
||||||
}
|
}
|
||||||
let header = view.Value.cast::<SharedHeader>();
|
let header = view.Value.cast::<SharedHeader>();
|
||||||
|
|
||||||
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
|
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
|
||||||
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>() bytes);
|
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>()
|
||||||
// these scalar writes are within it. The host opened the section with a permissive SDDL for us.
|
// bytes); these scalar writes are within it.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*header).driver_render_luid_low = render_luid_low;
|
(*header).driver_render_luid_low = render_luid_low;
|
||||||
(*header).driver_render_luid_high = render_luid_high;
|
(*header).driver_render_luid_high = render_luid_high;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. The host sets magic==MAGIC only once the ring textures exist. Not ready → retry later.
|
// 3. The host stamps magic==MAGIC BEFORE delivering the channel, and this channel's generation
|
||||||
// SAFETY: `header` is the mapped host header; `magic` lives within it and is read atomically
|
// must match the header's CURRENT generation — a mismatch means the host recreated the ring
|
||||||
// (Acquire) to pair with the host's Release store once the ring textures are published.
|
// again before we attached (a fresh delivery is on its way); drop this stale one.
|
||||||
let magic = unsafe {
|
// SAFETY: `header` is the mapped host header; `magic`/`generation` live within it and are read
|
||||||
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32)).load(Ordering::Acquire)
|
// atomically (Acquire) to pair with the host's Release publishes.
|
||||||
};
|
let (magic, header_gen) = unsafe {
|
||||||
if magic != MAGIC {
|
(
|
||||||
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once on this path.
|
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32))
|
||||||
unsafe {
|
.load(Ordering::Acquire),
|
||||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
(*(core::ptr::addr_of!((*header).generation) as *const AtomicU32))
|
||||||
Value: header.cast(),
|
.load(Ordering::Acquire),
|
||||||
});
|
|
||||||
let _ = CloseHandle(map);
|
|
||||||
}
|
|
||||||
return Err(windows::core::Error::from_win32());
|
|
||||||
}
|
|
||||||
// SAFETY: `header` is the mapped host header; these scalar fields live within it.
|
|
||||||
let (generation, ring_len) =
|
|
||||||
unsafe { ((*header).generation, (*header).ring_len.min(RING_LEN)) };
|
|
||||||
|
|
||||||
// 4. Open the event (SYNCHRONIZE | EVENT_MODIFY_STATE so we can SetEvent).
|
|
||||||
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call.
|
|
||||||
let event = match unsafe {
|
|
||||||
OpenEventW(
|
|
||||||
SYNCHRONIZATION_ACCESS_RIGHTS(EVENT_ACCESS),
|
|
||||||
false,
|
|
||||||
&HSTRING::from(event_name(target_id)),
|
|
||||||
)
|
)
|
||||||
} {
|
};
|
||||||
Ok(e) => e,
|
if magic != MAGIC || header_gen != channel.generation {
|
||||||
Err(e) => {
|
dbglog!(
|
||||||
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once here.
|
"[pf-vd] frame-push(driver): dropping channel delivery (magic ok: {}, channel gen {} vs header gen {header_gen})",
|
||||||
|
magic == MAGIC,
|
||||||
|
channel.generation
|
||||||
|
);
|
||||||
|
// SAFETY: `header`/`map` are the live mapped view + taken handle; unmapped + closed once on
|
||||||
|
// this path.
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||||
Value: header.cast(),
|
Value: header.cast(),
|
||||||
});
|
});
|
||||||
let _ = CloseHandle(map);
|
let _ = CloseHandle(map);
|
||||||
}
|
}
|
||||||
return Err(e);
|
// E_BOUNDS — stand-in for "stale delivery"; the caller only drops the attempt.
|
||||||
|
return Err(windows::core::HRESULT(0x8000_000Bu32 as i32).into());
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 5. Open device1 + the ring textures the host created (same render adapter required).
|
// 4. The frame-ready event (duplicated with the host handle's full access, so SetEvent works).
|
||||||
|
let event = FrameChannel::take(&mut channel.event);
|
||||||
|
|
||||||
|
// 5. Open device1 + the ring textures from their duplicated shared handles (same render adapter
|
||||||
|
// required). Each NT handle is closed right after the open — the COM object holds its own
|
||||||
|
// reference, and the HOST keeps the resource alive with its own handle.
|
||||||
let device1: ID3D11Device1 = match device.cast() {
|
let device1: ID3D11Device1 = match device.cast() {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are the
|
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are
|
||||||
// live handles, all released once on this error path.
|
// the taken live handles, all released once on this error path.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
|
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
|
||||||
let _ = CloseHandle(event);
|
let _ = CloseHandle(event);
|
||||||
@@ -189,34 +252,35 @@ impl FramePublisher {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
for k in 0..ring_len {
|
// Take each texture handle one at a time (NOT the whole array up front), so an error return
|
||||||
let name = HSTRING::from(texture_name(target_id, generation, k));
|
// mid-loop still lets `channel`'s Drop close every not-yet-taken handle.
|
||||||
// SAFETY: `device1` is a live ID3D11Device1; the name HSTRING is valid for the call.
|
for value in channel.textures.iter_mut().take(ring_len as usize) {
|
||||||
|
let tex_handle = FrameChannel::take(value);
|
||||||
|
// SAFETY: `device1` is a live ID3D11Device1; `tex_handle` is the duplicated shared NT handle
|
||||||
|
// for this ring texture.
|
||||||
let opened: windows::core::Result<ID3D11Texture2D> =
|
let opened: windows::core::Result<ID3D11Texture2D> =
|
||||||
unsafe { device1.OpenSharedResourceByName(&name, DXGI_SHARED_RESOURCE_RW) };
|
unsafe { device1.OpenSharedResource1(tex_handle) };
|
||||||
match opened {
|
// SAFETY: `tex_handle` is ours (taken above) and no longer needed whether the open succeeded
|
||||||
|
// (the COM object holds the resource) or failed — close it exactly once here.
|
||||||
|
unsafe {
|
||||||
|
let _ = CloseHandle(tex_handle);
|
||||||
|
}
|
||||||
|
let failed = match opened {
|
||||||
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
|
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
|
||||||
Ok(mutex) => slots.push(Slot { tex, mutex }),
|
Ok(mutex) => {
|
||||||
Err(e) => {
|
slots.push(Slot { tex, mutex });
|
||||||
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
None
|
||||||
// are the live handles, all released once on this error path.
|
|
||||||
unsafe {
|
|
||||||
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
|
||||||
(*header).driver_status_detail = e.code().0 as u32;
|
|
||||||
let _ = CloseHandle(event);
|
|
||||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
|
||||||
Value: header.cast(),
|
|
||||||
});
|
|
||||||
let _ = CloseHandle(map);
|
|
||||||
}
|
|
||||||
return Err(e);
|
|
||||||
}
|
}
|
||||||
|
Err(e) => Some(e),
|
||||||
},
|
},
|
||||||
Err(e) => {
|
// Most likely a render-adapter mismatch (the host made the textures on a different GPU
|
||||||
// Most likely a render-adapter mismatch (the host made the textures on a different
|
// than the swap-chain renders on). Tell the host so it can report it.
|
||||||
// GPU than the swap-chain renders on). Tell the host so it can report it.
|
Err(e) => Some(e),
|
||||||
|
};
|
||||||
|
if let Some(e) = failed {
|
||||||
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
||||||
// are the live handles, all released once on this error path.
|
// are the taken live handles, all released once on this error path (the not-yet-taken
|
||||||
|
// texture handles close via `channel`'s Drop).
|
||||||
unsafe {
|
unsafe {
|
||||||
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
||||||
(*header).driver_status_detail = e.code().0 as u32;
|
(*header).driver_status_detail = e.code().0 as u32;
|
||||||
@@ -229,14 +293,13 @@ impl FramePublisher {
|
|||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: `header` is the mapped host header; the status field lives within it.
|
// SAFETY: `header` is the mapped host header; the status field lives within it.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*header).driver_status = DRV_STATUS_OPENED;
|
(*header).driver_status = DRV_STATUS_OPENED;
|
||||||
}
|
}
|
||||||
dbglog!(
|
dbglog!(
|
||||||
"[pf-vd] frame-push(driver): attached to host ring gen {generation} ({ring_len} slots)"
|
"[pf-vd] frame-push(driver): attached to host ring gen {header_gen} ({ring_len} slots, sealed channel)"
|
||||||
);
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
context: context.clone(),
|
context: context.clone(),
|
||||||
@@ -248,7 +311,7 @@ impl FramePublisher {
|
|||||||
seq: 0,
|
seq: 0,
|
||||||
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
|
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
|
||||||
ring_format: unsafe { (*header).dxgi_format },
|
ring_format: unsafe { (*header).dxgi_format },
|
||||||
generation,
|
generation: header_gen,
|
||||||
mismatch_logged: false,
|
mismatch_logged: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -261,8 +324,8 @@ impl FramePublisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's HDR
|
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's HDR
|
||||||
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and the texture names now carry a new
|
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and a fresh channel delivery is coming.
|
||||||
/// generation. `run_core` drops the publisher on this so it re-attaches to the new ring.
|
/// `run_core` drops the publisher on this so it re-attaches to the new ring.
|
||||||
pub fn is_stale(&self) -> bool {
|
pub fn is_stale(&self) -> bool {
|
||||||
// SAFETY: `self.header` stays mapped for the publisher's lifetime; `generation` lives within it and
|
// SAFETY: `self.header` stays mapped for the publisher's lifetime; `generation` lives within it and
|
||||||
// is read atomically (Acquire) to pair with the host's Release bump on a mid-session ring recreate.
|
// is read atomically (Acquire) to pair with the host's Release bump on a mid-session ring recreate.
|
||||||
@@ -338,8 +401,8 @@ impl FramePublisher {
|
|||||||
}
|
}
|
||||||
.pack();
|
.pack();
|
||||||
self.latest_cell().store(latest, Ordering::Release);
|
self.latest_cell().store(latest, Ordering::Release);
|
||||||
// SAFETY: `self.event` is the live host-created frame-ready event we opened with
|
// SAFETY: `self.event` is the live host-created frame-ready event, duplicated into
|
||||||
// EVENT_MODIFY_STATE; signalling it wakes the host consumer.
|
// this process with the creator's access; signalling it wakes the host consumer.
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = SetEvent(self.event);
|
let _ = SetEvent(self.event);
|
||||||
}
|
}
|
||||||
@@ -357,10 +420,11 @@ impl FramePublisher {
|
|||||||
impl Drop for FramePublisher {
|
impl Drop for FramePublisher {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Slots FIRST (release the shared textures + keyed mutexes), THEN unmap the header, THEN the
|
// Slots FIRST (release the shared textures + keyed mutexes), THEN unmap the header, THEN the
|
||||||
// handles.
|
// handles — nothing of the channel outlives the publisher (teardown invariant,
|
||||||
|
// `design/idd-push-security.md`).
|
||||||
self.slots.clear();
|
self.slots.clear();
|
||||||
// SAFETY: drop runs once; `self.header` (if non-null) is the live mapped view and `self.event`/
|
// SAFETY: drop runs once; `self.header` (if non-null) is the live mapped view and `self.event`/
|
||||||
// `self.map` are the live handles this publisher opened — each unmapped/closed exactly once here.
|
// `self.map` are the live handles this publisher owns — each unmapped/closed exactly once here.
|
||||||
unsafe {
|
unsafe {
|
||||||
if !self.header.is_null() {
|
if !self.header.is_null() {
|
||||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
|
|
||||||
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
||||||
// P0 lint (audit §8): an unsafe op inside an `unsafe fn` must be in an explicit `unsafe {}` block, so the
|
// P0 lint (audit §8): an unsafe op inside an `unsafe fn` must be in an explicit `unsafe {}` block, so the
|
||||||
// fn-level `unsafe` never silently blesses the whole body. (The per-site `// SAFETY:` discipline already
|
// fn-level `unsafe` never silently blesses the whole body, AND every `unsafe {}` must carry a `// SAFETY:`
|
||||||
// landed in STEP 8.)
|
// proof. An IddCx display driver is inherently FFI-bound (D3D11 / IddCx DDIs / cross-process shared
|
||||||
|
// textures), so it can't be unsafe-FREE the way the gamepad drivers now are (their logic moved onto the
|
||||||
|
// safe `pf_umdf_util` layer); these gates make it unsafe-AUDITED instead, and stop it regressing.
|
||||||
#![deny(unsafe_op_in_unsafe_fn)]
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod log;
|
mod log;
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ pub fn log(s: &str) {
|
|||||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||||
}
|
}
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if let Some(m) = file_appender() {
|
if let Some(m) = file_appender()
|
||||||
if let Ok(mut f) = m.lock() {
|
&& let Ok(mut f) = m.lock()
|
||||||
|
{
|
||||||
let _ = writeln!(f, "{s}");
|
let _ = writeln!(f, "{s}");
|
||||||
let _ = f.flush();
|
let _ = f.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! dbglog {
|
macro_rules! dbglog {
|
||||||
($($a:tt)*) => { $crate::log::log(&::std::format!($($a)*)) };
|
($($a:tt)*) => { $crate::log::log(&::std::format!($($a)*)) };
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ pub struct MonitorObject {
|
|||||||
/// The live swap-chain drain worker, set by `assign_swap_chain` and dropped (RAII-joins the worker
|
/// The live swap-chain drain worker, set by `assign_swap_chain` and dropped (RAII-joins the worker
|
||||||
/// thread) by `unassign_swap_chain` / departure (STEP 5).
|
/// thread) by `unassign_swap_chain` / departure (STEP 5).
|
||||||
pub swap_chain_processor: Option<crate::swap_chain_processor::SwapChainProcessor>,
|
pub swap_chain_processor: Option<crate::swap_chain_processor::SwapChainProcessor>,
|
||||||
|
/// The host's sealed-channel delivery (`IOCTL_SET_FRAME_CHANNEL`) awaiting pickup by the swap-chain
|
||||||
|
/// worker ([`take_frame_channel`]). Exactly one owner per delivery: replacing or dropping the entry
|
||||||
|
/// closes an unconsumed channel's handles via [`FrameChannel`]'s `Drop`, so no delivery can leak
|
||||||
|
/// handles in the WUDFHost table whatever the monitor's fate.
|
||||||
|
pub frame_channel: Option<crate::frame_transport::FrameChannel>,
|
||||||
/// When the entry was created — the watchdog skips still-initializing monitors.
|
/// When the entry was created — the watchdog skips still-initializing monitors.
|
||||||
pub created_at: Instant,
|
pub created_at: Instant,
|
||||||
}
|
}
|
||||||
@@ -256,8 +261,8 @@ pub fn modes_for_object(object: iddcx::IDDCX_MONITOR) -> Option<Vec<Mode>> {
|
|||||||
.map(|m| m.modes.clone())
|
.map(|m| m.modes.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to name the
|
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to key the
|
||||||
/// shared-ring objects). `None` if the monitor isn't found.
|
/// frame-channel stash for its worker). `None` if the monitor isn't found.
|
||||||
pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
|
pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
|
||||||
MONITOR_MODES
|
MONITOR_MODES
|
||||||
.lock()
|
.lock()
|
||||||
@@ -267,6 +272,52 @@ pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
|
|||||||
.map(|m| m.target_id)
|
.map(|m| m.target_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stash a host frame-channel delivery on the monitor with `target_id` (an ARRIVED monitor — a pending
|
||||||
|
/// entry's `target_id` is still 0, which the host can never send since OS target ids are non-zero).
|
||||||
|
/// Replacing an unconsumed delivery drops it → its handles close (it WAS adopted by a prior success).
|
||||||
|
/// `Err(ch)` if no such monitor exists — the caller must NOT close those handles (the host only sees
|
||||||
|
/// the error status and reaps its remote duplicates itself; closing here too would double-close values
|
||||||
|
/// the OS may have reused).
|
||||||
|
pub fn set_frame_channel(
|
||||||
|
target_id: u32,
|
||||||
|
ch: crate::frame_transport::FrameChannel,
|
||||||
|
) -> Result<(), crate::frame_transport::FrameChannel> {
|
||||||
|
if target_id == 0 {
|
||||||
|
return Err(ch);
|
||||||
|
}
|
||||||
|
let mut lock = lock_monitors();
|
||||||
|
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
|
||||||
|
m.frame_channel = Some(ch);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take (remove) the pending frame-channel delivery for `target_id`, transferring handle ownership to
|
||||||
|
/// the caller (the swap-chain worker's attach). `None` until the host delivers one.
|
||||||
|
pub fn take_frame_channel(target_id: u32) -> Option<crate::frame_transport::FrameChannel> {
|
||||||
|
if target_id == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
lock_monitors()
|
||||||
|
.iter_mut()
|
||||||
|
.find(|m| m.target_id == target_id)?
|
||||||
|
.frame_channel
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is a frame-channel delivery pending for `target_id`? The swap-chain worker treats a pending
|
||||||
|
/// delivery as NEWEST-WINS: it supersedes an attached publisher, because the host only re-delivers
|
||||||
|
/// after (re)creating the ring — and a retry-created ring is a DIFFERENT header mapping, whose
|
||||||
|
/// generation bump an old publisher (mapped to the previous header) can never observe.
|
||||||
|
pub fn has_frame_channel(target_id: u32) -> bool {
|
||||||
|
target_id != 0
|
||||||
|
&& lock_monitors()
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
|
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
|
||||||
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
|
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
|
||||||
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
|
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
|
||||||
@@ -351,6 +402,7 @@ pub fn create_monitor(
|
|||||||
adapter_luid_low: 0,
|
adapter_luid_low: 0,
|
||||||
adapter_luid_high: 0,
|
adapter_luid_high: 0,
|
||||||
swap_chain_processor: None,
|
swap_chain_processor: None,
|
||||||
|
frame_channel: None,
|
||||||
created_at: Instant::now(),
|
created_at: Instant::now(),
|
||||||
});
|
});
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ pub struct SwapChainProcessor {
|
|||||||
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
|
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
|
||||||
// terminate flag.
|
// terminate flag.
|
||||||
unsafe impl Send for SwapChainProcessor {}
|
unsafe impl Send for SwapChainProcessor {}
|
||||||
|
// SAFETY: as above — the raw pointer is only touched by the serialised worker, so a shared
|
||||||
|
// `&SwapChainProcessor` reference exposes no unsynchronised access.
|
||||||
unsafe impl Sync for SwapChainProcessor {}
|
unsafe impl Sync for SwapChainProcessor {}
|
||||||
|
|
||||||
impl SwapChainProcessor {
|
impl SwapChainProcessor {
|
||||||
@@ -223,10 +225,11 @@ impl SwapChainProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF token can't
|
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring over the SEALED channel. The
|
||||||
// create named objects, so the host creates the header + event + textures and we only OPEN them
|
// frame objects are unnamed — the host duplicates their handles into this process and delivers
|
||||||
// once they appear (`try_open`). Until then we just drain — exactly the STEP-5 behaviour — so a
|
// the values via IOCTL_SET_FRAME_CHANNEL, which the control plane stashes on our monitor
|
||||||
// non-IDD-push session never stalls. Retried every ~30 loop iterations.
|
// (`monitor::take_frame_channel`). Until a delivery lands we just drain — exactly the STEP-5
|
||||||
|
// behaviour — so a non-IDD-push session never stalls. The stash is polled every ~30 iterations.
|
||||||
let mut publisher: Option<FramePublisher> = None;
|
let mut publisher: Option<FramePublisher> = None;
|
||||||
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first loop iteration
|
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first loop iteration
|
||||||
|
|
||||||
@@ -243,25 +246,34 @@ impl SwapChainProcessor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
|
// Re-attach triggers, either of:
|
||||||
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach to
|
// * `is_stale` — the host recreated the ring mid-session (HDR flip): it bumps OUR header's
|
||||||
// the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring, whose
|
// generation and re-delivers; without dropping here we'd keep CopyResource'ing into the
|
||||||
// format now mismatches the surface → the publish() format-guard drops every frame and the
|
// stale ring, whose format now mismatches the surface → the publish() format-guard drops
|
||||||
// stream freezes until the next swap-chain recreate.
|
// every frame and the stream freezes until the next swap-chain recreate.
|
||||||
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
|
// * a PENDING delivery (newest-wins) — a host build-retry creates a whole NEW ring with a
|
||||||
|
// DIFFERENT header mapping; the old publisher's header never changes, so `is_stale` can't
|
||||||
|
// fire. The host only delivers after fully (re)creating a ring, so a pending delivery
|
||||||
|
// always supersedes whatever we're attached to.
|
||||||
|
if publisher.as_ref().is_some_and(FramePublisher::is_stale)
|
||||||
|
|| (publisher.is_some() && crate::monitor::has_frame_channel(target_id))
|
||||||
|
{
|
||||||
publisher = None;
|
publisher = None;
|
||||||
frames_since_try = u32::MAX; // re-attach immediately
|
frames_since_try = u32::MAX; // re-attach immediately
|
||||||
}
|
}
|
||||||
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is idle
|
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is idle
|
||||||
// (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open` is a
|
// (E_PENDING / no frames presented yet), not only when a frame is acquired. Checking the
|
||||||
// cheap OpenFileMapping that fails fast until the host has created the ring.
|
// stash is a cheap mutex peek that stays empty until the host's channel delivery lands; a
|
||||||
|
// taken delivery is consumed whether the attach succeeds or not (on failure its handles are
|
||||||
|
// closed, the host's wait-for-attach reads the status code, and any retry is a NEW delivery).
|
||||||
if publisher.is_none() {
|
if publisher.is_none() {
|
||||||
if frames_since_try >= 30 {
|
if frames_since_try >= 30 {
|
||||||
frames_since_try = 0;
|
frames_since_try = 0;
|
||||||
|
if let Some(channel) = crate::monitor::take_frame_channel(target_id) {
|
||||||
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
|
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
|
||||||
// happy under `-D warnings`; semantics are identical — attach on success, retry on Err.
|
// happy under `-D warnings`; attach on success, drop the delivery on Err.
|
||||||
if let Ok(p) = FramePublisher::try_open(
|
if let Ok(p) = FramePublisher::from_channel(
|
||||||
target_id,
|
channel,
|
||||||
render_luid_low,
|
render_luid_low,
|
||||||
render_luid_high,
|
render_luid_high,
|
||||||
&device.device,
|
&device.device,
|
||||||
@@ -269,6 +281,7 @@ impl SwapChainProcessor {
|
|||||||
) {
|
) {
|
||||||
publisher = Some(p);
|
publisher = Some(p);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
frames_since_try += 1;
|
frames_since_try += 1;
|
||||||
}
|
}
|
||||||
@@ -337,13 +350,13 @@ impl SwapChainProcessor {
|
|||||||
if !raw.is_null() {
|
if !raw.is_null() {
|
||||||
// SAFETY: `raw` is IddCx's live surface pointer (valid until the next
|
// SAFETY: `raw` is IddCx's live surface pointer (valid until the next
|
||||||
// ReleaseAndAcquire); `from_raw_borrowed` does not consume the refcount.
|
// ReleaseAndAcquire); `from_raw_borrowed` does not consume the refcount.
|
||||||
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
|
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) }
|
||||||
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
|
&& let Ok(tex) = res.cast::<ID3D11Texture2D>()
|
||||||
|
{
|
||||||
p.publish(&tex);
|
p.publish(&tex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: driver is loaded; `swap_chain` is valid.
|
// SAFETY: driver is loaded; `swap_chain` is valid.
|
||||||
let hr = unsafe { wdk_iddcx::IddCxSwapChainFinishedProcessingFrame(swap_chain) };
|
let hr = unsafe { wdk_iddcx::IddCxSwapChainFinishedProcessingFrame(swap_chain) };
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ wdk-build.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
wdk.workspace = true
|
wdk.workspace = true
|
||||||
wdk-sys.workspace = true
|
wdk-sys.workspace = true
|
||||||
|
pf-driver-proto.workspace = true
|
||||||
|
pf-umdf-util.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ instance (= player slot 0–3) with `CreateFile`, and polls it with buffered IOC
|
|||||||
**System** setup class;
|
**System** setup class;
|
||||||
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
|
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
|
||||||
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
|
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
|
||||||
controller state the host publishes into a shared section `Global\pfxusb-shm-0`; a game's rumble
|
controller state the host publishes into an **unnamed** shared DATA section reached over the
|
||||||
(`SET_STATE`) is published back for the host to forward to the client.
|
**sealed pad channel** (`design/gamepad-channel-sealing.md`): the host duplicates the section
|
||||||
|
handle into this driver's WUDFHost, bootstrapped via the named `Global\pfxusb-boot-<index>`
|
||||||
|
mailbox (`pf_driver_proto::gamepad::PadBootstrap`); a game's rumble (`SET_STATE`) is published
|
||||||
|
back for the host to forward to the client.
|
||||||
|
|
||||||
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
|
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
|
||||||
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
|
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
|
||||||
@@ -37,11 +40,13 @@ GameInput admission additionally needs a `xinputhid` `UpperFilters` registry tri
|
|||||||
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
|
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
|
||||||
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
|
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
|
||||||
|
|
||||||
## Shared-memory layout `Global\pfxusb-shm-0` (64 B) — host writes state, driver writes rumble
|
## Shared-memory layout (unnamed DATA section, 64 B) — host writes state, driver writes rumble
|
||||||
|
|
||||||
|
`pf_driver_proto::gamepad::XusbShm` (the crate owns the offsets; both sides compile against it):
|
||||||
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
|
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
|
||||||
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
||||||
`large @28` · `small @29`.
|
`large @28` · `small @29` · health marks `@32/@36` · `pad_index u32 @40` (validated against the
|
||||||
|
devnode's Location index when the delivered handle is mapped).
|
||||||
|
|
||||||
## Validated live (2026-06-22, maintainer's RTX test box)
|
## Validated live (2026-06-22, maintainer's RTX test box)
|
||||||
|
|
||||||
@@ -66,7 +71,8 @@ the whole build/sign/stage flow in CI. The manual steps:
|
|||||||
## Host integration (done)
|
## Host integration (done)
|
||||||
|
|
||||||
`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by
|
`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by
|
||||||
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, writes
|
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, delivers the unnamed DATA
|
||||||
|
section over the sealed channel (`PadChannel`), writes
|
||||||
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
||||||
is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI
|
is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI
|
||||||
(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via
|
(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via
|
||||||
@@ -75,8 +81,8 @@ is **no ViGEmBus dependency** anymore. The driver is built + signed from source
|
|||||||
## Multi-pad
|
## Multi-pad
|
||||||
|
|
||||||
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
|
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
|
||||||
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and maps its own
|
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and polls its own
|
||||||
`pfxusb-shm-<index>`. `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
`pfxusb-boot-<index>` bootstrap mailbox (the delivered DATA section's `pad_index` is validated against it). `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
||||||
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
|
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
|
||||||
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
|
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
|
||||||
index — which only routes shared memory.)
|
index — which only routes shared memory.)
|
||||||
|
|||||||
@@ -3,42 +3,39 @@
|
|||||||
//
|
//
|
||||||
// xinput1_4.dll enumerates GUID_DEVINTERFACE_XUSB, opens the Nth instance (= player slot), and polls
|
// xinput1_4.dll enumerates GUID_DEVINTERFACE_XUSB, opens the Nth instance (= player slot), and polls
|
||||||
// it with buffered IOCTLs. We register the interface and answer those IOCTLs from controller state the
|
// it with buffered IOCTLs. We register the interface and answer those IOCTLs from controller state the
|
||||||
// host publishes into a shared-memory section (`Global\pfxusb-shm-0`); a game's rumble (SET_STATE) is
|
// host publishes into a shared DATA section; a game's rumble (SET_STATE) is published back for the
|
||||||
// published back for the host to forward. Byte formats are the source-verified xusb22 wire layout
|
// host to forward. Byte formats are the source-verified xusb22 wire layout (HIDMaestro
|
||||||
// (HIDMaestro driver/companion.c + nefarius/XInputHooker XUSB.h + ViGEm XUSB_REPORT).
|
// driver/companion.c + nefarius/XInputHooker XUSB.h + ViGEm XUSB_REPORT).
|
||||||
|
//
|
||||||
|
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
|
||||||
|
// DATA section (`pf_driver_proto::gamepad::XusbShm`) is UNNAMED — we reach it only through a handle
|
||||||
|
// the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named `Global\pfxusb-boot-<i>`
|
||||||
|
// mailbox. The whole handshake + all shared-memory access lives in `pf_umdf_util` (audited unsafe
|
||||||
|
// layer): this crate's channel/IOCTL/state logic is 100% SAFE Rust. The only `unsafe` here is the
|
||||||
|
// unavoidable WDF setup FFI in DriverEntry/EvtDeviceAdd, each with a `// SAFETY:` proof.
|
||||||
//
|
//
|
||||||
// We answer the WAIT_* IOCTLs with STATUS_INVALID_DEVICE_REQUEST, which makes xinput1_4 fall back to
|
// We answer the WAIT_* IOCTLs with STATUS_INVALID_DEVICE_REQUEST, which makes xinput1_4 fall back to
|
||||||
// synchronous GET_STATE polling — so no manual queue / timer is needed for classic XInput.
|
// synchronous GET_STATE polling — so no manual queue / timer is needed for classic XInput.
|
||||||
|
|
||||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||||
|
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
|
||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
use core::ffi::c_void;
|
use pf_driver_proto::gamepad::XusbShm;
|
||||||
use core::sync::atomic::{AtomicU32, Ordering};
|
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
|
||||||
|
use pf_umdf_util::nt_success;
|
||||||
|
use pf_umdf_util::section::MappedView;
|
||||||
|
use pf_umdf_util::wdf::{self, Request};
|
||||||
use wdk_sys::{
|
use wdk_sys::{
|
||||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA, GUID, NTSTATUS, PCUNICODE_STRING,
|
GUID, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
||||||
PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFDRIVER, WDFQUEUE,
|
||||||
WDF_DRIVER_CONFIG, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES,
|
WDFREQUEST, call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
||||||
};
|
};
|
||||||
|
|
||||||
// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
|
||||||
// wdk_sys root; the value is stable WDM).
|
|
||||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
|
||||||
|
|
||||||
/// The pad index this device serves (which `pfxusb-shm-<index>` section to map). The host stamps it
|
|
||||||
/// into the device Location (`pszDeviceLocation`); the driver reads it in EvtDeviceAdd. With
|
|
||||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) each pad gets its own WUDFHost, so this
|
|
||||||
/// static is per-pad — the basis for multi-pad.
|
|
||||||
static SHM_INDEX: AtomicU32 = AtomicU32::new(0);
|
|
||||||
|
|
||||||
// ---- NTSTATUS ----
|
// ---- NTSTATUS ----
|
||||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||||
const STATUS_INVALID_DEVICE_REQUEST: NTSTATUS = 0xC000_0010u32 as NTSTATUS;
|
const STATUS_INVALID_DEVICE_REQUEST: NTSTATUS = 0xC000_0010u32 as NTSTATUS;
|
||||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn nt_success(s: NTSTATUS) -> bool {
|
|
||||||
s >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GUID_DEVINTERFACE_XUSB {EC87F1E3-C13B-4100-B5F7-8B84D54260CB} — what xinput1_4 enumerates + opens.
|
// GUID_DEVINTERFACE_XUSB {EC87F1E3-C13B-4100-B5F7-8B84D54260CB} — what xinput1_4 enumerates + opens.
|
||||||
const GUID_DEVINTERFACE_XUSB: GUID = GUID {
|
const GUID_DEVINTERFACE_XUSB: GUID = GUID {
|
||||||
@@ -70,27 +67,46 @@ const XUSB_VERSION: u16 = 0x0103;
|
|||||||
const WdfIoQueueDispatchParallel: i32 = 2;
|
const WdfIoQueueDispatchParallel: i32 = 2;
|
||||||
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
|
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
|
||||||
|
|
||||||
// ---- shared-memory layout (host ↔ driver), must match pf_driver_proto::gamepad::XusbShm ----
|
// ---- the sealed host channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
|
||||||
// magic u32 @0 ("PFXU"); packet u32 @4 (host bumps on state change → dwPacketNumber); the XUSB_REPORT
|
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::XUSB_MAGIC; // "PFXU"
|
||||||
// payload @8: wButtons u16 @8, bLeftTrigger @10, bRightTrigger @11, sThumbLX i16 @12, LY @14, RX @16,
|
const SHM_SIZE: usize = core::mem::size_of::<XusbShm>();
|
||||||
// RY @18; rumble_seq u32 @24 (driver bumps on SET_STATE); rumble large @28, small @29;
|
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
|
||||||
// driver_proto u32 @32 (we stamp GAMEPAD_PROTO_VERSION = attach signal for the host's health check);
|
|
||||||
// driver_heartbeat u32 @36 (we bump per serviced IOCTL = the game-visible polling path moves).
|
|
||||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004;
|
|
||||||
const SHM_MAGIC: u32 = 0x5558_4650; // "PFXU" little-endian
|
|
||||||
const SHM_SIZE: usize = 64;
|
|
||||||
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
|
|
||||||
|
|
||||||
unsafe extern "system" {
|
// XusbShm field offsets (host writes state, we answer XInput; we write rumble + health marks).
|
||||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
const OFF_PACKET: usize = core::mem::offset_of!(XusbShm, packet);
|
||||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
const OFF_BUTTONS: usize = core::mem::offset_of!(XusbShm, buttons);
|
||||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
const OFF_LT: usize = core::mem::offset_of!(XusbShm, left_trigger);
|
||||||
fn CloseHandle(h: *mut c_void) -> i32;
|
const OFF_RT: usize = core::mem::offset_of!(XusbShm, right_trigger);
|
||||||
|
const OFF_LX: usize = core::mem::offset_of!(XusbShm, thumb_lx);
|
||||||
|
const OFF_LY: usize = core::mem::offset_of!(XusbShm, thumb_ly);
|
||||||
|
const OFF_RX: usize = core::mem::offset_of!(XusbShm, thumb_rx);
|
||||||
|
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_LARGE: usize = core::mem::offset_of!(XusbShm, rumble_large);
|
||||||
|
const OFF_RUMBLE_SMALL: usize = core::mem::offset_of!(XusbShm, rumble_small);
|
||||||
|
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
|
||||||
|
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(XusbShm, driver_heartbeat);
|
||||||
|
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
|
||||||
|
|
||||||
|
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
|
||||||
|
/// this static is per-pad). All shared-memory access + the bootstrap handshake live in `pf_umdf_util`.
|
||||||
|
static CHANNEL: ChannelClient = ChannelClient::new();
|
||||||
|
|
||||||
|
/// This pad's channel config (magic/size/pad_index offset + our logger).
|
||||||
|
fn channel_cfg() -> ChannelConfig {
|
||||||
|
ChannelConfig {
|
||||||
|
tag: "pf-xusb",
|
||||||
|
boot_name_prefix: "Global\\pfxusb-boot-",
|
||||||
|
data_magic: SHM_MAGIC,
|
||||||
|
data_size: SHM_SIZE,
|
||||||
|
pad_index_off: OFF_PAD_INDEX,
|
||||||
|
log,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(s: &str) {
|
fn log(s: &str) {
|
||||||
if let Ok(c) = std::ffi::CString::new(s) {
|
if let Ok(c) = std::ffi::CString::new(s) {
|
||||||
// SAFETY: c is a valid null-terminated string for the duration of the call.
|
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
||||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||||
}
|
}
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -110,11 +126,11 @@ pub unsafe extern "system" fn driver_entry(
|
|||||||
registry_path: PCUNICODE_STRING,
|
registry_path: PCUNICODE_STRING,
|
||||||
) -> NTSTATUS {
|
) -> NTSTATUS {
|
||||||
log("[pf-xusb] DriverEntry");
|
log("[pf-xusb] DriverEntry");
|
||||||
// SAFETY: zeroed config then Size + callback set.
|
// SAFETY: a zeroed WDF_DRIVER_CONFIG is a valid all-null config; we then set Size + the callback.
|
||||||
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
|
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
|
||||||
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
|
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
|
||||||
config.EvtDriverDeviceAdd = Some(evt_device_add);
|
config.EvtDriverDeviceAdd = Some(evt_device_add);
|
||||||
// SAFETY: all pointers valid; provided by the loader.
|
// SAFETY: `driver`/`registry_path` are the loader-provided pointers; the config is valid.
|
||||||
unsafe {
|
unsafe {
|
||||||
call_unsafe_wdf_function_binding!(
|
call_unsafe_wdf_function_binding!(
|
||||||
WdfDriverCreate,
|
WdfDriverCreate,
|
||||||
@@ -127,56 +143,11 @@ pub unsafe extern "system" fn driver_entry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), a NUL-terminated
|
|
||||||
/// UTF-16 decimal string. Defaults to 0 (single-pad) if absent.
|
|
||||||
fn query_shm_index(device: WDFDEVICE) -> u32 {
|
|
||||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
|
||||||
// SAFETY: device valid; property = LocationInformation; pool ignored in UMDF; mem receives the handle.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(
|
|
||||||
WdfDeviceAllocAndQueryProperty,
|
|
||||||
device,
|
|
||||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
|
||||||
0,
|
|
||||||
WDF_NO_OBJECT_ATTRIBUTES,
|
|
||||||
&mut mem
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if !nt_success(st) || mem.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let mut len: usize = 0;
|
|
||||||
// SAFETY: mem valid.
|
|
||||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
|
||||||
as *const u16;
|
|
||||||
if buf.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let mut idx: u32 = 0;
|
|
||||||
let mut any = false;
|
|
||||||
for i in 0..(len / 2).min(8) {
|
|
||||||
// SAFETY: buf valid for len bytes; i < len/2.
|
|
||||||
let c = unsafe { *buf.add(i) };
|
|
||||||
if c == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (0x30..=0x39).contains(&c) {
|
|
||||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
|
||||||
any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if any {
|
|
||||||
idx
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
||||||
log("[pf-xusb] EvtDeviceAdd");
|
log("[pf-xusb] EvtDeviceAdd");
|
||||||
|
|
||||||
let mut device: WDFDEVICE = core::ptr::null_mut();
|
let mut device: WDFDEVICE = core::ptr::null_mut();
|
||||||
// SAFETY: device_init valid; attributes null; device receives the handle.
|
// SAFETY: `device_init` is the framework-provided init; attributes null; `device` receives it.
|
||||||
let st = unsafe {
|
let st = unsafe {
|
||||||
call_unsafe_wdf_function_binding!(
|
call_unsafe_wdf_function_binding!(
|
||||||
WdfDeviceCreate,
|
WdfDeviceCreate,
|
||||||
@@ -190,12 +161,14 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
let idx = query_shm_index(device);
|
// SAFETY: `device` is the live device just created — the exact contract `query_location_index`
|
||||||
SHM_INDEX.store(idx, Ordering::Relaxed);
|
// requires.
|
||||||
|
let idx = unsafe { wdf::query_location_index(device) };
|
||||||
|
CHANNEL.set_index(idx);
|
||||||
dbglog!("[pf-xusb] shm index = {idx}");
|
dbglog!("[pf-xusb] shm index = {idx}");
|
||||||
|
|
||||||
// Register the XUSB device interface (no reference string) — what xinput1_4 enumerates + opens.
|
// Register the XUSB device interface (no reference string) — what xinput1_4 enumerates + opens.
|
||||||
// SAFETY: device valid; GUID static; null reference string.
|
// SAFETY: `device` is live; the GUID is a static; null reference string.
|
||||||
let st = unsafe {
|
let st = unsafe {
|
||||||
call_unsafe_wdf_function_binding!(
|
call_unsafe_wdf_function_binding!(
|
||||||
WdfDeviceCreateDeviceInterface,
|
WdfDeviceCreateDeviceInterface,
|
||||||
@@ -213,7 +186,7 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default parallel queue: all the XUSB IOCTLs land here.
|
// Default parallel queue: all the XUSB IOCTLs land here.
|
||||||
// SAFETY: zeroed config then fields set; Size matches the struct.
|
// SAFETY: a zeroed WDF_IO_QUEUE_CONFIG is valid; we then set Size + the fields we use.
|
||||||
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
|
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
|
||||||
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
||||||
qcfg.DispatchType = WdfIoQueueDispatchParallel;
|
qcfg.DispatchType = WdfIoQueueDispatchParallel;
|
||||||
@@ -222,7 +195,7 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
|
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
|
||||||
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
|
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
|
||||||
let mut queue: WDFQUEUE = core::ptr::null_mut();
|
let mut queue: WDFQUEUE = core::ptr::null_mut();
|
||||||
// SAFETY: device + config valid; attributes null; queue receives the handle.
|
// SAFETY: `device` + `qcfg` are valid; attributes null; `queue` receives the handle.
|
||||||
let st = unsafe {
|
let st = unsafe {
|
||||||
call_unsafe_wdf_function_binding!(
|
call_unsafe_wdf_function_binding!(
|
||||||
WdfIoQueueCreate,
|
WdfIoQueueCreate,
|
||||||
@@ -237,93 +210,69 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
|||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the host we're alive on the section (its driver-attach health check keys off this).
|
// Run the sealed-channel handshake on a worker (must NOT block EvtDeviceAdd): publish our pid in
|
||||||
touch_driver_marks();
|
// the bootstrap mailbox and poll for the host's delivered DATA handle, so the pad attaches (and
|
||||||
|
// the host's driver-attach health check goes green) even before any game polls XInput. Bounded;
|
||||||
|
// a later host (or a re-delivery) is still picked up by the per-IOCTL pump. This closure is 100%
|
||||||
|
// safe — the whole channel state machine lives in pf_umdf_util.
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
let cfg = channel_cfg();
|
||||||
|
for _ in 0..500 {
|
||||||
|
if let Some(v) = CHANNEL.pump(&cfg) {
|
||||||
|
touch_driver_marks(v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
log(
|
||||||
|
"[pf-xusb] no sealed-channel delivery within 10s (host absent, or host/driver version mismatch — see above)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
log("[pf-xusb] device ready (XUSB interface registered)");
|
log("[pf-xusb] device ready (XUSB interface registered)");
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open + map the host's shared section and run `f` against the mapped base if magic is valid, then
|
/// The current controller state from the attached DATA section (zeros / neutral when unattached).
|
||||||
// unmap. Re-mapped per access (the host may recreate the section across restarts).
|
|
||||||
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
|
|
||||||
let name: Vec<u16> = format!("Global\\pfxusb-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
|
||||||
.encode_utf16()
|
|
||||||
.chain(std::iter::once(0))
|
|
||||||
.collect();
|
|
||||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
|
||||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
|
||||||
if h.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// SAFETY: h is a valid mapping handle; map the whole section; the view keeps it alive.
|
|
||||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
|
||||||
unsafe { CloseHandle(h) };
|
|
||||||
if view.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// SAFETY: view points at >= 4 mapped bytes.
|
|
||||||
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
|
|
||||||
if magic == SHM_MAGIC {
|
|
||||||
f(view);
|
|
||||||
}
|
|
||||||
// SAFETY: view came from MapViewOfFile.
|
|
||||||
unsafe { UnmapViewOfFile(view as *const c_void) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current controller state from shared memory (zeros / neutral if the host hasn't connected).
|
|
||||||
/// Returns `(dwPacketNumber, wButtons, lt, rt, lx, ly, rx, ry)`.
|
/// Returns `(dwPacketNumber, wButtons, lt, rt, lx, ly, rx, ry)`.
|
||||||
fn read_state() -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
fn read_state(data: Option<&MappedView>) -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
||||||
let mut out = (0u32, 0u16, 0u8, 0u8, 0i16, 0i16, 0i16, 0i16);
|
match data {
|
||||||
with_shm(|v| {
|
Some(v) => (
|
||||||
// SAFETY: v points at a mapped SHM_SIZE section with valid magic.
|
v.read_u32(OFF_PACKET),
|
||||||
unsafe {
|
v.read_u16(OFF_BUTTONS),
|
||||||
out.0 = core::ptr::read_unaligned(v.add(4) as *const u32);
|
v.read_u8(OFF_LT),
|
||||||
out.1 = core::ptr::read_unaligned(v.add(8) as *const u16);
|
v.read_u8(OFF_RT),
|
||||||
out.2 = *v.add(10);
|
v.read_i16(OFF_LX),
|
||||||
out.3 = *v.add(11);
|
v.read_i16(OFF_LY),
|
||||||
out.4 = core::ptr::read_unaligned(v.add(12) as *const i16);
|
v.read_i16(OFF_RX),
|
||||||
out.5 = core::ptr::read_unaligned(v.add(14) as *const i16);
|
v.read_i16(OFF_RY),
|
||||||
out.6 = core::ptr::read_unaligned(v.add(16) as *const i16);
|
),
|
||||||
out.7 = core::ptr::read_unaligned(v.add(18) as *const i16);
|
None => (0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
}
|
}
|
||||||
});
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamp the driver health marks the host watches: `driver_proto` @32 (the attach signal,
|
/// Stamp the driver health marks the host watches: `driver_proto` (the attach signal, idempotent)
|
||||||
/// idempotent) and `driver_heartbeat` @36 (+1). Called at device add and on every serviced IOCTL,
|
/// and `driver_heartbeat` (+1). Called once the channel attaches and on every serviced IOCTL, so the
|
||||||
/// so the host can tell "driver bound and alive" apart from "driver package missing/failed to
|
/// host can tell "driver bound and alive" apart from "driver package missing/failed to bind" and see
|
||||||
/// bind" and see the game-visible polling path advance. No-op until the host's section exists
|
/// the game-visible polling path advance.
|
||||||
/// (with_shm re-opens per access, so a section created after we started still gets marked).
|
fn touch_driver_marks(data: &MappedView) {
|
||||||
fn touch_driver_marks() {
|
data.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
|
||||||
with_shm(|v| {
|
let hb = data.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
|
||||||
// SAFETY: v points at a mapped SHM_SIZE section with valid magic; proto @32, heartbeat @36.
|
data.write_u32(OFF_DRIVER_HEARTBEAT, hb);
|
||||||
unsafe {
|
|
||||||
core::ptr::write_unaligned(v.add(32) as *mut u32, GAMEPAD_PROTO_VERSION);
|
|
||||||
let hb = v.add(36) as *mut u32;
|
|
||||||
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish a game's rumble (from SET_STATE) into shared memory for the host to forward.
|
/// Publish a game's rumble (from SET_STATE) into the DATA section for the host to forward.
|
||||||
fn publish_rumble(large: u8, small: u8) {
|
fn publish_rumble(data: Option<&MappedView>, large: u8, small: u8) {
|
||||||
with_shm(|v| {
|
let Some(v) = data else { return };
|
||||||
// SAFETY: v points at a mapped SHM_SIZE section; rumble_seq @24, large @28, small @29.
|
v.write_u8(OFF_RUMBLE_LARGE, large);
|
||||||
unsafe {
|
v.write_u8(OFF_RUMBLE_SMALL, small);
|
||||||
*v.add(28) = large;
|
let seq = v.read_u32(OFF_RUMBLE_SEQ).wrapping_add(1);
|
||||||
*v.add(29) = small;
|
v.write_u32(OFF_RUMBLE_SEQ, seq);
|
||||||
let seqp = v.add(24) as *mut u32;
|
|
||||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
|
||||||
core::ptr::write_unaligned(seqp, seq);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the 29-byte GET_STATE buffer (the layout xinput1_4 parses).
|
// Build the 29-byte GET_STATE buffer (the layout xinput1_4 parses).
|
||||||
fn build_get_state() -> [u8; 29] {
|
fn build_get_state(data: Option<&MappedView>) -> [u8; 29] {
|
||||||
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state();
|
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state(data);
|
||||||
let mut s = [0u8; 29];
|
let mut s = [0u8; 29];
|
||||||
s[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
s[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||||
s[2] = 0x01; // device count
|
s[2] = 0x01; // device count
|
||||||
@@ -374,11 +323,20 @@ extern "C" fn evt_io_device_control(
|
|||||||
input_len: usize,
|
input_len: usize,
|
||||||
ioctl: ULONG,
|
ioctl: ULONG,
|
||||||
) {
|
) {
|
||||||
// Health marks first: attach signal + heartbeat (also covers a section the host created after
|
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
|
||||||
// this device started — the marks land on the next XInput poll).
|
// contract `Request::new` requires. From here everything is safe (the token owns completion).
|
||||||
touch_driver_marks();
|
let request = unsafe { Request::new(request) };
|
||||||
|
|
||||||
|
// Sealed-channel pump + health marks first: adopt a (late) delivery, detach when the host's
|
||||||
|
// mailbox is gone, and stamp the attach/heartbeat marks the host watches (also covers a host
|
||||||
|
// started after this device — the pump attaches on the next XInput poll).
|
||||||
|
let data = CHANNEL.pump(&channel_cfg());
|
||||||
|
if let Some(v) = data {
|
||||||
|
touch_driver_marks(v);
|
||||||
|
}
|
||||||
|
|
||||||
let status: NTSTATUS = match ioctl {
|
let status: NTSTATUS = match ioctl {
|
||||||
IOCTL_XUSB_GET_INFORMATION => copy_to_output(request, &build_information()),
|
IOCTL_XUSB_GET_INFORMATION => request.copy_to_output(&build_information()),
|
||||||
IOCTL_XUSB_GET_INFORMATION_EX => {
|
IOCTL_XUSB_GET_INFORMATION_EX => {
|
||||||
let mut ex = [0u8; 64];
|
let mut ex = [0u8; 64];
|
||||||
ex[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
ex[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||||
@@ -387,21 +345,19 @@ extern "C" fn evt_io_device_control(
|
|||||||
ex[8..10].copy_from_slice(&XUSB_VID.to_le_bytes());
|
ex[8..10].copy_from_slice(&XUSB_VID.to_le_bytes());
|
||||||
ex[10..12].copy_from_slice(&XUSB_PID.to_le_bytes());
|
ex[10..12].copy_from_slice(&XUSB_PID.to_le_bytes());
|
||||||
let n = output_len.min(64);
|
let n = output_len.min(64);
|
||||||
copy_to_output(request, &ex[..n])
|
request.copy_to_output(&ex[..n])
|
||||||
}
|
}
|
||||||
IOCTL_XUSB_GET_CAPABILITIES => {
|
IOCTL_XUSB_GET_CAPABILITIES => {
|
||||||
if output_len >= 36 {
|
if output_len >= 36 {
|
||||||
copy_to_output(request, &build_caps_v2())
|
request.copy_to_output(&build_caps_v2())
|
||||||
} else {
|
} else {
|
||||||
copy_to_output(request, &CAPS_V1)
|
request.copy_to_output(&CAPS_V1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IOCTL_XUSB_GET_STATE => copy_to_output(request, &build_get_state()),
|
IOCTL_XUSB_GET_STATE => request.copy_to_output(&build_get_state(data)),
|
||||||
IOCTL_XUSB_GET_LED_STATE => copy_to_output(request, &[0x00, 0x00, 0x06]),
|
IOCTL_XUSB_GET_LED_STATE => request.copy_to_output(&[0x00, 0x00, 0x06]),
|
||||||
IOCTL_XUSB_GET_BATTERY_INFORMATION => {
|
IOCTL_XUSB_GET_BATTERY_INFORMATION => request.copy_to_output(&[0x00, 0x01, 0x03, 0x00]),
|
||||||
copy_to_output(request, &[0x00, 0x01, 0x03, 0x00])
|
IOCTL_XUSB_SET_STATE => on_set_state(&request, data),
|
||||||
}
|
|
||||||
IOCTL_XUSB_SET_STATE => on_set_state(request),
|
|
||||||
IOCTL_XUSB_POWER_DOWN | IOCTL_XUSB_GET_XINPUT_MANAGEMENT_DRIVER => STATUS_SUCCESS,
|
IOCTL_XUSB_POWER_DOWN | IOCTL_XUSB_GET_XINPUT_MANAGEMENT_DRIVER => STATUS_SUCCESS,
|
||||||
// Decline the async waits → xinput1_4 falls back to synchronous GET_STATE polling.
|
// Decline the async waits → xinput1_4 falls back to synchronous GET_STATE polling.
|
||||||
IOCTL_XUSB_WAIT_GUIDE_BUTTON | IOCTL_XUSB_WAIT_FOR_INPUT => STATUS_INVALID_DEVICE_REQUEST,
|
IOCTL_XUSB_WAIT_GUIDE_BUTTON | IOCTL_XUSB_WAIT_FOR_INPUT => STATUS_INVALID_DEVICE_REQUEST,
|
||||||
@@ -410,30 +366,18 @@ extern "C" fn evt_io_device_control(
|
|||||||
STATUS_INVALID_DEVICE_REQUEST
|
STATUS_INVALID_DEVICE_REQUEST
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// SAFETY: request valid and not forwarded.
|
request.complete(status);
|
||||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SET_STATE: the rumble packet. Classic xusb22 layout is small; the motor bytes sit near the end.
|
// SET_STATE: the rumble packet. Classic xusb22 layout is small; the motor bytes sit near the end.
|
||||||
// We publish a best-effort (large = byte 3, small = byte 4 for the 5-byte form) and log the raw bytes
|
// We publish a best-effort (large = byte 2, small = byte 3 for the 5-byte form) and log the raw bytes
|
||||||
// so the exact offsets can be confirmed against a real pad.
|
// so the exact offsets can be confirmed against a real pad.
|
||||||
fn on_set_state(request: WDFREQUEST) -> NTSTATUS {
|
fn on_set_state(request: &Request, data: Option<&MappedView>) -> NTSTATUS {
|
||||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
if let Ok((bytes, len)) = request.input_bytes(8)
|
||||||
// SAFETY: request valid.
|
&& len >= 2
|
||||||
let st = unsafe {
|
{
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
|
||||||
};
|
|
||||||
if nt_success(st) {
|
|
||||||
let mut len: usize = 0;
|
|
||||||
// SAFETY: inmem valid.
|
|
||||||
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
|
||||||
as *const u8;
|
|
||||||
if !p.is_null() && len >= 2 {
|
|
||||||
let n = len.min(8);
|
|
||||||
// SAFETY: p valid for len bytes; read at most n.
|
|
||||||
let bytes = unsafe { core::slice::from_raw_parts(p, n) };
|
|
||||||
let mut hex = String::new();
|
let mut hex = String::new();
|
||||||
for b in bytes {
|
for b in &bytes {
|
||||||
hex.push_str(&format!("{b:02x} "));
|
hex.push_str(&format!("{b:02x} "));
|
||||||
}
|
}
|
||||||
dbglog!("[pf-xusb] SET_STATE len={len} data: {hex}");
|
dbglog!("[pf-xusb] SET_STATE len={len} data: {hex}");
|
||||||
@@ -441,47 +385,10 @@ fn on_set_state(request: WDFREQUEST) -> NTSTATUS {
|
|||||||
// (large/low-freq at [2], small/high-freq at [3]); 0x01 = player-LED set (ignored).
|
// (large/low-freq at [2], small/high-freq at [3]); 0x01 = player-LED set (ignored).
|
||||||
// 4-byte = raw XINPUT_VIBRATION → the two motor hi bytes.
|
// 4-byte = raw XINPUT_VIBRATION → the two motor hi bytes.
|
||||||
if len >= 5 && bytes[4] == 0x02 {
|
if len >= 5 && bytes[4] == 0x02 {
|
||||||
publish_rumble(bytes[2], bytes[3]);
|
publish_rumble(data, bytes[2], bytes[3]);
|
||||||
} else if len == 4 {
|
} else if len == 4 {
|
||||||
publish_rumble(bytes[1], bytes[3]);
|
publish_rumble(data, bytes[1], bytes[3]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
STATUS_SUCCESS
|
STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
|
||||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
|
||||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
|
||||||
// SAFETY: request valid; mem receives the memory handle.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
|
||||||
};
|
|
||||||
if !nt_success(st) {
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
let mut outlen: usize = 0;
|
|
||||||
// SAFETY: mem valid; outlen receives the buffer size.
|
|
||||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
|
||||||
if outlen < src.len() {
|
|
||||||
return STATUS_INVALID_BUFFER_SIZE;
|
|
||||||
}
|
|
||||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
|
||||||
let st = unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(
|
|
||||||
WdfMemoryCopyFromBuffer,
|
|
||||||
mem,
|
|
||||||
0usize,
|
|
||||||
src.as_ptr() as *mut c_void,
|
|
||||||
src.len()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if !nt_success(st) {
|
|
||||||
return st;
|
|
||||||
}
|
|
||||||
// SAFETY: request valid.
|
|
||||||
unsafe {
|
|
||||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
|
||||||
};
|
|
||||||
STATUS_SUCCESS
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
//! code — handled at the call site in STEP 5).
|
//! code — handled at the call site in STEP 5).
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
||||||
// P0 lint (audit §8): require explicit `unsafe {}` blocks inside `unsafe fn`s.
|
// P0 lint (audit §8): require explicit `unsafe {}` blocks inside `unsafe fn`s + a `// SAFETY:` proof on
|
||||||
|
// each (this crate is the IddCx DDI dispatch layer — inherently unsafe, so audited, not unsafe-free).
|
||||||
#![deny(unsafe_op_in_unsafe_fn)]
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
pub use wdk_sys::iddcx;
|
pub use wdk_sys::iddcx;
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ unsafe fn ddi<T: Copy>(index: i32) -> T {
|
|||||||
let table = (&raw const iddcx::IddFunctions).cast::<iddcx::PFN_IDD_CX>();
|
let table = (&raw const iddcx::IddFunctions).cast::<iddcx::PFN_IDD_CX>();
|
||||||
// SAFETY: `index` is a valid IddCx table slot; the slot holds a `PFN_*` whose layout is `T`.
|
// SAFETY: `index` is a valid IddCx table slot; the slot holds a `PFN_*` whose layout is `T`.
|
||||||
let slot = unsafe { table.add(index as usize) };
|
let slot = unsafe { table.add(index as usize) };
|
||||||
|
// SAFETY: `slot` points at the `index`th (in-bounds) populated table entry, a `PFN_*` of layout `T`.
|
||||||
unsafe { slot.cast::<T>().read() }
|
unsafe { slot.cast::<T>().read() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,10 @@ macro_rules! iddcx_ddi {
|
|||||||
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub unsafe fn $name( $( $arg: $aty ),* ) -> NTSTATUS {
|
pub unsafe fn $name( $( $arg: $aty ),* ) -> NTSTATUS {
|
||||||
|
// SAFETY: `$idx`/`$pfn` are the matched IddCx table index + PFN type (pinned by this macro
|
||||||
|
// invocation), and the table is populated once the driver is loaded (this fn's contract).
|
||||||
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
||||||
|
// SAFETY: only reads the stub-provided globals pointer; valid post-load per the contract.
|
||||||
let g = unsafe { globals() };
|
let g = unsafe { globals() };
|
||||||
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
||||||
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
||||||
@@ -79,7 +85,10 @@ macro_rules! iddcx_ddi {
|
|||||||
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub unsafe fn $name( $( $arg: $aty ),* ) {
|
pub unsafe fn $name( $( $arg: $aty ),* ) {
|
||||||
|
// SAFETY: `$idx`/`$pfn` are the matched IddCx table index + PFN type (pinned by this macro
|
||||||
|
// invocation), and the table is populated once the driver is loaded (this fn's contract).
|
||||||
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
||||||
|
// SAFETY: only reads the stub-provided globals pointer; valid post-load per the contract.
|
||||||
let g = unsafe { globals() };
|
let g = unsafe { globals() };
|
||||||
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
||||||
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
||||||
|
|||||||
Reference in New Issue
Block a user