89455032a0
Batch A of the audit's medium tier (M1+M2+M3): - M1 driver-death detection: a dead WUDFHost stops publishing, which at the ring is indistinguishable from an idle desktop — SDR sessions streamed a frozen frame forever (next_frame's 20 s bail is unreachable once anything presented). The ChannelBroker's process handle now doubles as a liveness probe (SYNCHRONIZE at OpenProcess); while no fresh frame arrives, try_consume polls it (rate-limited) and fails the capturer, landing in the session's bounded in-place rebuild. - M2 reopenable control device: the manager's OnceLock-cached handle is now a retire/reopen DeviceSlot — a gone-classified IOCTL failure (driver upgrade / WUDFHost restart; pinger, create, or REMOVE) retires the handle and the next use reopens + re-handshakes. Retired handles are deliberately kept alive forever: bare-HANDLE holders (pinger, ChannelBroker) rely on never-closed, and a retired handle only fails IOCTLs. CLEAR_ALL runs on the FIRST open only (a reopen races live-ish sessions); acquire retries the monitor create once after a reopen. The JOIN path now probes the active monitor's WUDFHost pid and preempts a DEAD monitor instead of handing the rebuilding session its stale target — without this the whole recovery chain starved to the rebuild budget. - M3 interface discovery: enumerate ALL interface instances with an SPINT_ACTIVE filter (a Code-10 devnode at index 0 no longer shadows the live interface), HDEVINFO behind RAII (error paths leaked one per probe), the raw device handle wrapped before GET_INFO (leaked on handshake failure), and the detail-sizing result guarded before the cbSize write. - pf-driver-proto: SetFrameChannelRequest doc now states the real adopt-on-success contract (the old wording invited a driver-side close-on-error — a cross-process double-close against the host's reap). - install: pf_vdisplay_present() passes /connected so a phantom devnode can't suppress creating a live ROOT node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
711 lines
36 KiB
Rust
711 lines
36 KiB
Rust
//! Shared binary contract between the punktfunk host and the `pf-vdisplay` IddCx driver.
|
|
//!
|
|
//! Two planes:
|
|
//! * [`control`] — the low-frequency `DeviceIoControl` plane (add/remove a virtual monitor, pin the
|
|
//! render adapter, keepalive, info, clear-all, deliver the frame channel). Owned, clean, versioned —
|
|
//! NOT the SudoVDA ABI.
|
|
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of **unnamed** shared
|
|
//! keyed-mutex textures (+ a header + a frame-ready event), duplicates their handles into the
|
|
//! driver's WUDFHost process and delivers the handle VALUES over
|
|
//! [`control::IOCTL_SET_FRAME_CHANNEL`]; the driver publishes composited frames into them. There is
|
|
//! deliberately no object-name scheme: an unnamed object cannot be enumerated, opened by name, or
|
|
//! pre-created ("squatted") — only the two endpoint processes ever hold a handle to any frame object
|
|
//! (the sealed channel, `design/idd-push-security.md`). This crate owns the [`frame::SharedHeader`]
|
|
//! layout, the [`frame::FrameToken`] packing, the channel-delivery struct, and the driver-status
|
|
//! codes.
|
|
//!
|
|
//! Both planes were previously hand-duplicated, byte-for-byte, across `idd_push.rs`/`frame_transport.rs`
|
|
//! and `vdisplay/sudovda.rs`/`control.rs` with only "must match" comments guarding them. Defining them
|
|
//! once here — with bytemuck `Pod` derives and `const` size asserts — makes any drift a compile error.
|
|
//!
|
|
//! The GUID and LUID are carried as plain integers; the host converts to `windows::core::GUID` /
|
|
//! `windows::Win32::Foundation::LUID` and the driver to its own bindgen types via the same constants.
|
|
|
|
#![cfg_attr(not(test), no_std)]
|
|
|
|
extern crate alloc;
|
|
|
|
/// Freshly-minted pf-vdisplay device-interface GUID — `{70667664-7044-5350-a1b2-c3d4e5f60001}`.
|
|
/// Deliberately NOT SudoVDA's `{e5bcc234-…}`: we own the driver, so a private interface GUID signals
|
|
/// it and removes any accidental coexistence with a real SudoVDA install. Construct on each side via
|
|
/// `GUID::from_u128(PF_VDISPLAY_INTERFACE_GUID_U128)`.
|
|
pub const PF_VDISPLAY_INTERFACE_GUID_U128: u128 = 0x7066_7664_7044_5350_a1b2_c3d4_e5f6_0001;
|
|
|
|
/// The interface GUID split into Windows `GUID` fields — `(Data1, Data2, Data3, Data4)` — so the driver
|
|
/// (and host) can build a `windows`/`wdk_sys` `GUID` without re-deriving the byte layout. Standard GUID
|
|
/// layout from the u128: `Data1` = high 32 bits, `Data2`/`Data3` = next two 16-bit groups, `Data4` =
|
|
/// the low 64 bits big-endian. (This crate is `no_std` + provider-agnostic, so it returns the fields
|
|
/// rather than depend on a `GUID` type.)
|
|
#[must_use]
|
|
pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
|
|
let g = PF_VDISPLAY_INTERFACE_GUID_U128;
|
|
(
|
|
(g >> 96) as u32,
|
|
(g >> 80) as u16,
|
|
(g >> 64) as u16,
|
|
(g as u64).to_be_bytes(),
|
|
)
|
|
}
|
|
|
|
/// 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.
|
|
/// v2: the sealed frame channel — the frame objects are unnamed and delivered by handle duplication
|
|
/// ([`control::IOCTL_SET_FRAME_CHANNEL`]), and [`control::AddReply`] grew `wudf_pid` (the duplication
|
|
/// target). A v1 driver has no channel-delivery IOCTL and expects named objects, so the pairing is
|
|
/// incompatible by design.
|
|
pub const PROTOCOL_VERSION: u32 = 2;
|
|
|
|
/// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`.
|
|
pub const fn ctl_code(func: u32) -> u32 {
|
|
(0x22u32 << 16) | (func << 2)
|
|
}
|
|
|
|
/// The control (`DeviceIoControl`) plane: add/remove a virtual monitor + adapter pin + keepalive +
|
|
/// frame-channel delivery.
|
|
pub mod control {
|
|
use super::ctl_code;
|
|
use super::frame::RING_LEN;
|
|
use bytemuck::{Pod, Zeroable};
|
|
|
|
// Contiguous op space at 0x900 — distinct from SudoVDA's gappy 0x800/0x888/0x8FF numbering.
|
|
/// Add a virtual monitor at a mode → [`AddReply`]. Input [`AddRequest`].
|
|
pub const IOCTL_ADD: u32 = ctl_code(0x900);
|
|
/// Remove a virtual monitor by session id. Input [`RemoveRequest`].
|
|
pub const IOCTL_REMOVE: u32 = ctl_code(0x901);
|
|
/// Pin the IddCx render adapter (hybrid-GPU IDD-push). Input [`SetRenderAdapterRequest`].
|
|
pub const IOCTL_SET_RENDER_ADAPTER: u32 = ctl_code(0x902);
|
|
/// Keepalive (resets the driver watchdog). No payload.
|
|
pub const IOCTL_PING: u32 = ctl_code(0x903);
|
|
/// Version + watchdog handshake → [`InfoReply`]. No input.
|
|
pub const IOCTL_GET_INFO: u32 = ctl_code(0x904);
|
|
/// Tear down every virtual monitor (host-startup orphan reap). No payload. First-class op — NOT the
|
|
/// SudoVDA "send-and-hope-it's-ignored" hack.
|
|
pub const IOCTL_CLEAR_ALL: u32 = ctl_code(0x905);
|
|
/// Deliver a monitor's IDD-push frame channel: the handle VALUES of the unnamed shared objects the
|
|
/// host duplicated into the driver's WUDFHost process. Input [`SetFrameChannelRequest`]. Sent once
|
|
/// after the ring is created and again on every mid-session ring recreate (HDR-mode flip).
|
|
pub const IOCTL_SET_FRAME_CHANNEL: u32 = ctl_code(0x906);
|
|
|
|
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
|
|
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
|
|
/// mode as preferred; the host still CCD-forces the active mode (the OS activates IDDs at a default).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct AddRequest {
|
|
pub session_id: u64,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub refresh_hz: u32,
|
|
/// Host-preferred per-client monitor id (`1..=15`) — the EDID serial / IddCx `ConnectorIndex` /
|
|
/// `ContainerId` the driver names this monitor by. A given client (keyed by its cert fingerprint)
|
|
/// gets a STABLE id across reconnects, so the OS device path + EDID stay identical and Windows
|
|
/// reapplies that client's saved per-monitor config (DPI scaling). `0` = AUTO: the driver
|
|
/// allocates the lowest-free id (the original slot-based behavior — used for anonymous/TOFU and
|
|
/// GameStream sessions). Byte-compatible with the old `_reserved` (offset 20): an un-upgraded
|
|
/// driver ignores it (→ auto), which the host detects via [`AddReply::resolved_monitor_id`].
|
|
pub preferred_monitor_id: u32,
|
|
}
|
|
|
|
/// `IOCTL_ADD` reply: the OS target id + the adapter LUID the IDD landed on (split low/high to
|
|
/// match `windows` `LUID { LowPart: u32, HighPart: i32 }`).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct AddReply {
|
|
pub adapter_luid_low: u32,
|
|
pub adapter_luid_high: i32,
|
|
pub target_id: u32,
|
|
/// The monitor id the driver ACTUALLY used — echoes [`AddRequest::preferred_monitor_id`] when the
|
|
/// preference was honored, or the auto-allocated id otherwise. Byte-compatible with the old
|
|
/// `_reserved` (offset 12): an un-upgraded driver leaves it `0`, so the host can tell its
|
|
/// preference was ignored (stale driver) and log it instead of silently losing per-client config.
|
|
pub resolved_monitor_id: u32,
|
|
/// The driver's own process id (the WUDFHost hosting `pf_vdisplay`) — the target the host
|
|
/// duplicates the unnamed frame-object handles INTO (`OpenProcess(PROCESS_DUP_HANDLE)` +
|
|
/// `DuplicateHandle`, then [`IOCTL_SET_FRAME_CHANNEL`]). Reported per-ADD, not per-open, so a
|
|
/// WUDFHost restart between sessions can never leave the host duplicating into a dead process.
|
|
pub wudf_pid: u32,
|
|
}
|
|
|
|
/// `IOCTL_REMOVE` input.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct RemoveRequest {
|
|
pub session_id: u64,
|
|
}
|
|
|
|
/// `IOCTL_SET_RENDER_ADAPTER` input (the GPU the IddCx swap-chain should render on).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct SetRenderAdapterRequest {
|
|
pub luid_low: u32,
|
|
pub luid_high: i32,
|
|
}
|
|
|
|
/// `IOCTL_GET_INFO` reply: the protocol version (asserted against [`super::PROTOCOL_VERSION`]) and
|
|
/// the watchdog timeout the host must ping within.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct InfoReply {
|
|
pub protocol_version: 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. Ownership is
|
|
/// **adopt-on-success-only** (`design/idd-push-security.md` invariant 5): the driver owns (and
|
|
/// eventually closes) the handles IFF it completes the IOCTL successfully — a replaced or
|
|
/// later-unconsumed delivery is then the driver's to close. On ANY error completion (malformed
|
|
/// request, unknown `target_id`) the driver must NOT close them: the HOST reaps its remote
|
|
/// duplicates (`DUPLICATE_CLOSE_SOURCE`). Exactly one side closes each value; a driver that closed
|
|
/// on error would double-close possibly-reused handle values against the host's reap.
|
|
///
|
|
/// Handle values are only meaningful inside the target process's handle table, so this struct is
|
|
/// harmless to any third party: reading it leaks nothing openable, and spoofing it (were the control
|
|
/// device reachable — it is ACL'd to SYSTEM + admins) could at worst feed the driver values that
|
|
/// don't resolve, a DoS of the attacker's own session. The frame objects themselves are unnamed and
|
|
/// therefore unreachable by any process that isn't one of the two endpoints.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct SetFrameChannelRequest {
|
|
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
|
|
pub target_id: u32,
|
|
/// The ring generation these textures belong to (must match the shared header's generation at
|
|
/// attach time; a stale delivery is dropped by the driver — a fresh one follows every recreate).
|
|
pub generation: u32,
|
|
/// How many leading entries of `texture_handles` are valid (`1..=`[`RING_LEN`]).
|
|
pub ring_len: u32,
|
|
pub _pad: u32,
|
|
/// The shared-header file-mapping handle (the driver maps it and writes status/publish tokens).
|
|
pub header_handle: u64,
|
|
/// The frame-ready auto-reset event handle (the driver signals it after each publish).
|
|
pub event_handle: u64,
|
|
/// The ring textures' shared NT handles (opened via `ID3D11Device1::OpenSharedResource1`).
|
|
pub texture_handles: [u64; RING_LEN_USIZE],
|
|
}
|
|
|
|
/// [`RING_LEN`] as a usize for the `texture_handles` array length (the wire struct sizes the array
|
|
/// at the compile-time maximum; `ring_len` says how many entries are live).
|
|
pub const RING_LEN_USIZE: usize = RING_LEN as usize;
|
|
|
|
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
|
|
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
|
|
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
|
|
const _: () = {
|
|
use core::mem::{offset_of, size_of};
|
|
|
|
assert!(size_of::<AddRequest>() == 24);
|
|
assert!(offset_of!(AddRequest, session_id) == 0);
|
|
assert!(offset_of!(AddRequest, width) == 8);
|
|
assert!(offset_of!(AddRequest, height) == 12);
|
|
assert!(offset_of!(AddRequest, refresh_hz) == 16);
|
|
assert!(offset_of!(AddRequest, preferred_monitor_id) == 20);
|
|
|
|
assert!(size_of::<AddReply>() == 20);
|
|
assert!(offset_of!(AddReply, adapter_luid_low) == 0);
|
|
assert!(offset_of!(AddReply, adapter_luid_high) == 4);
|
|
assert!(offset_of!(AddReply, target_id) == 8);
|
|
assert!(offset_of!(AddReply, resolved_monitor_id) == 12);
|
|
assert!(offset_of!(AddReply, wudf_pid) == 16);
|
|
|
|
assert!(size_of::<SetFrameChannelRequest>() == 32 + 8 * RING_LEN_USIZE);
|
|
assert!(offset_of!(SetFrameChannelRequest, target_id) == 0);
|
|
assert!(offset_of!(SetFrameChannelRequest, generation) == 4);
|
|
assert!(offset_of!(SetFrameChannelRequest, ring_len) == 8);
|
|
assert!(offset_of!(SetFrameChannelRequest, header_handle) == 16);
|
|
assert!(offset_of!(SetFrameChannelRequest, event_handle) == 24);
|
|
assert!(offset_of!(SetFrameChannelRequest, texture_handles) == 32);
|
|
|
|
assert!(size_of::<RemoveRequest>() == 8);
|
|
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
|
|
|
assert!(size_of::<SetRenderAdapterRequest>() == 8);
|
|
assert!(offset_of!(SetRenderAdapterRequest, luid_low) == 0);
|
|
assert!(offset_of!(SetRenderAdapterRequest, luid_high) == 4);
|
|
|
|
assert!(size_of::<InfoReply>() == 8);
|
|
assert!(offset_of!(InfoReply, protocol_version) == 0);
|
|
assert!(offset_of!(InfoReply, watchdog_timeout_s) == 4);
|
|
};
|
|
}
|
|
|
|
/// The IDD-push frame transport: the host-created shared ring header, the publish token, and the
|
|
/// driver-status codes. The texture ring itself is host-created **unnamed** D3D11 keyed-mutex textures;
|
|
/// the driver reaches them (and the header + event) only through handles the host duplicated into its
|
|
/// process and delivered via [`crate::control::IOCTL_SET_FRAME_CHANNEL`] — the sealed channel. Only the
|
|
/// *layout/contract* lives here.
|
|
pub mod frame {
|
|
use bytemuck::{Pod, Zeroable};
|
|
|
|
/// Header magic (`"PFVD"` LE). The host stamps it LAST (after the ring textures exist) so the driver
|
|
/// only attaches to a fully-published ring.
|
|
pub const MAGIC: u32 = 0x4456_4650;
|
|
/// Frame-plane version (independent bump of the header layout).
|
|
pub const VERSION: u32 = 1;
|
|
/// Ring slots. Headroom so the driver's 0 ms-timeout publish always finds a free slot while the host
|
|
/// holds one across the convert/copy + the pipelined encode. MUST be identical on both sides — it is,
|
|
/// because both read this one constant.
|
|
pub const RING_LEN: u32 = 6;
|
|
|
|
/// `driver_status` values the driver writes into the host header (the host logs them on a timeout).
|
|
pub const DRV_STATUS_NONE: u32 = 0;
|
|
/// Driver attached to the ring and is publishing.
|
|
pub const DRV_STATUS_OPENED: u32 = 1;
|
|
/// Driver could not open the host's textures — render-adapter mismatch (it renders on a different GPU
|
|
/// than where the host created the ring). `driver_status_detail` carries the HRESULT.
|
|
pub const DRV_STATUS_TEX_FAIL: u32 = 2;
|
|
/// Driver has no `ID3D11Device1` to open shared resources.
|
|
pub const DRV_STATUS_NO_DEVICE1: u32 = 3;
|
|
|
|
/// The shared metadata header (host-created, mapped by both sides). Atomic fields (`magic`, `latest`,
|
|
/// `generation`) are accessed via each side's own atomic view over the mapping; this is the layout.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
|
pub struct SharedHeader {
|
|
pub magic: u32,
|
|
pub version: u32,
|
|
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format + a fresh
|
|
/// [`control::IOCTL_SET_FRAME_CHANNEL`](crate::control::IOCTL_SET_FRAME_CHANNEL) delivery). The
|
|
/// driver re-attaches when it changes; a publish carries it so the host rejects a stale-ring
|
|
/// publish.
|
|
pub generation: u32,
|
|
pub ring_len: u32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub dxgi_format: u32,
|
|
pub _pad: u32,
|
|
/// Driver-written after each copy; host loads `Acquire`. See [`FrameToken`].
|
|
pub latest: u64,
|
|
pub qpc_pts: u64,
|
|
/// Driver-written: the adapter the swap-chain actually renders on (mismatch detection).
|
|
pub driver_render_luid_low: u32,
|
|
pub driver_render_luid_high: i32,
|
|
/// Driver-written status (visibility channel — UMDF hides OutputDebugString + the restricted
|
|
/// token blocks file writes, so this header is how the driver reports state).
|
|
pub driver_status: u32,
|
|
pub driver_status_detail: u32,
|
|
}
|
|
|
|
/// The `SharedHeader.latest` publish token: `(generation << 40) | (seq << 8) | slot`.
|
|
/// `generation` is 24-bit, `seq` 32-bit, `slot` 8-bit. The generation tag lets the host REJECT a
|
|
/// publish from a stale ring (an old-generation publisher racing a mid-session recreate) so it never
|
|
/// consumes an unwritten new-ring slot — eliminating the toggle-time garbage frame.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct FrameToken {
|
|
pub generation: u32,
|
|
pub seq: u32,
|
|
pub slot: u8,
|
|
}
|
|
|
|
impl FrameToken {
|
|
/// Low 24 bits of `generation` are significant (see the field docs).
|
|
pub const GENERATION_MASK: u32 = 0x00FF_FFFF;
|
|
|
|
pub const fn pack(self) -> u64 {
|
|
(((self.generation & Self::GENERATION_MASK) as u64) << 40)
|
|
| (((self.seq as u64) & 0xFFFF_FFFF) << 8)
|
|
| (self.slot as u64)
|
|
}
|
|
|
|
pub const fn unpack(v: u64) -> Self {
|
|
Self {
|
|
generation: ((v >> 40) as u32) & Self::GENERATION_MASK,
|
|
seq: ((v >> 8) & 0xFFFF_FFFF) as u32,
|
|
slot: (v & 0xFF) as u8,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Size + per-field offsets are load-bearing: both sides access these via raw atomic views over the
|
|
// mapping, so a same-size field reorder would silently corrupt. Pin every offset. The `_pad` after
|
|
// `dxgi_format` is what 8-aligns the `u64 latest` at offset 32 — assert that too.
|
|
const _: () = {
|
|
use core::mem::{offset_of, size_of};
|
|
|
|
assert!(size_of::<SharedHeader>() == 64);
|
|
assert!(offset_of!(SharedHeader, magic) == 0);
|
|
assert!(offset_of!(SharedHeader, version) == 4);
|
|
assert!(offset_of!(SharedHeader, generation) == 8);
|
|
assert!(offset_of!(SharedHeader, ring_len) == 12);
|
|
assert!(offset_of!(SharedHeader, width) == 16);
|
|
assert!(offset_of!(SharedHeader, height) == 20);
|
|
assert!(offset_of!(SharedHeader, dxgi_format) == 24);
|
|
assert!(offset_of!(SharedHeader, _pad) == 28);
|
|
assert!(offset_of!(SharedHeader, latest) == 32);
|
|
assert!(offset_of!(SharedHeader, qpc_pts) == 40);
|
|
assert!(offset_of!(SharedHeader, driver_render_luid_low) == 48);
|
|
assert!(offset_of!(SharedHeader, driver_render_luid_high) == 52);
|
|
assert!(offset_of!(SharedHeader, driver_status) == 56);
|
|
assert!(offset_of!(SharedHeader, driver_status_detail) == 60);
|
|
};
|
|
}
|
|
|
|
/// Gamepad shared-memory layouts (host ↔ the UMDF gamepad drivers `pf_xusb` / `pf_dualsense`).
|
|
///
|
|
/// These were hand-duplicated as `OFF_*`/`SHM_*` constants in `inject/{gamepad,dualsense}_windows.rs`
|
|
/// and (as bare literals — `*view.add(140)`) in the standalone `xusb-driver`/`dualsense-driver`
|
|
/// workspaces, guarded only by "must match" comments — the top ABI-drift hazard the audit flagged
|
|
/// (`design/windows-host-rewrite.md` §2.7). Owning them here with `Pod` derives + `offset_of!`
|
|
/// asserts makes a one-sided edit a compile error.
|
|
///
|
|
/// Since v2 the channel is **sealed** (`design/gamepad-channel-sealing.md`, mirroring the frame
|
|
/// channel): the host creates the DATA section ([`XusbShm`]/[`PadShm`]) UNNAMED (SYSTEM-only DACL)
|
|
/// and duplicates its handle into the driver's WUDFHost; only the tiny [`PadBootstrap`] mailbox
|
|
/// stays named (it carries nothing exploitable). Layout only; the sections are host-created.
|
|
pub mod gamepad {
|
|
use alloc::string::String;
|
|
use bytemuck::{Pod, Zeroable};
|
|
|
|
/// XUSB section magic — the exact u32 the shipped host + `pf_xusb` driver compare (loosely "PFXU").
|
|
pub const XUSB_MAGIC: u32 = 0x5558_4650;
|
|
/// Pad section magic — the exact u32 the shipped host + `pf_dualsense` driver compare (loosely
|
|
/// "PFDS"). (Note: the two magics happen to use opposite byte-order mnemonics in the legacy code;
|
|
/// only the u32 value is the contract.)
|
|
pub const PAD_MAGIC: u32 = 0x5046_4453;
|
|
|
|
/// `device_type` selector the `pf_dualsense` driver reads to pick its HID identity. The section is
|
|
/// zeroed, so `0` = DualSense is the default; one driver serves either identity.
|
|
pub const DEVTYPE_DUALSENSE: u8 = 0;
|
|
/// `device_type` = DualShock 4 (`VID_054C&PID_09CC` HID identity).
|
|
pub const DEVTYPE_DUALSHOCK4: u8 = 1;
|
|
|
|
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
|
|
/// the host's positive "driver is alive on this section" signal (health check + version audit).
|
|
/// The section starts zeroed, so `0` always means "no driver has attached (yet)"; a pre-health
|
|
/// driver never writes the field and reads as not-attached, which the host log line calls out
|
|
/// (the remedy is the same: reinstall the drivers). Bump on a gamepad-layout change.
|
|
///
|
|
/// 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;
|
|
|
|
/// Bootstrap-mailbox magic (`"PFBT"` LE) — the host stamps it LAST (after `host_proto`), so a
|
|
/// driver only trusts a fully-initialized mailbox.
|
|
pub const BOOT_MAGIC: u32 = 0x5442_4650;
|
|
|
|
/// `Global\pfxusb-boot-<index>` — the virtual Xbox 360 pad's bootstrap mailbox ([`PadBootstrap`]).
|
|
pub fn xusb_boot_name(index: u8) -> String {
|
|
alloc::format!("Global\\pfxusb-boot-{index}")
|
|
}
|
|
/// `Global\pfds-boot-<index>` — the DualSense / DualShock 4 pad's bootstrap mailbox
|
|
/// ([`PadBootstrap`]).
|
|
pub fn pad_boot_name(index: u8) -> String {
|
|
alloc::format!("Global\\pfds-boot-{index}")
|
|
}
|
|
|
|
/// The per-pad bootstrap mailbox (32 B, named `Global\pf…-boot-<index>`, SY+LS DACL) — the ONLY
|
|
/// named object left on the gamepad channel. It exists because the pad drivers are UMDF HID
|
|
/// minidrivers with no control device (hidclass owns the stack), so there is no IOCTL to hand the
|
|
/// driver a duplicated handle or learn its WUDFHost pid; this mailbox is the late-bound handshake:
|
|
///
|
|
/// 1. host creates it (zeroed), stamps `host_proto` then `magic` (in that order);
|
|
/// 2. driver opens it by name (pad index from `pszDeviceLocation`), writes `driver_proto`, and —
|
|
/// iff `host_proto` matches its own version — publishes `driver_pid`;
|
|
/// 3. host polls `driver_pid`, verifies the pid is a genuine WUDFHost, duplicates the unnamed DATA
|
|
/// section into it, then writes `data_handle` + `handle_pid` and bumps `handle_seq` LAST;
|
|
/// 4. driver sees a fresh `handle_seq` addressed to its own pid, maps `data_handle`, and validates
|
|
/// the mapped section's magic + `pad_index` before use.
|
|
///
|
|
/// Deliberately safe to leave named + LS-openable: it carries only pids (not sensitive) and a
|
|
/// handle VALUE (meaningless outside the target WUDFHost's handle table). A sibling LocalService
|
|
/// that tampers with it can at worst mis-route a delivery — a gamepad DoS, never a read or an
|
|
/// injection (it cannot place a valid section handle in the WUDFHost, and the driver's
|
|
/// magic+`pad_index` validation rejects any handle that doesn't resolve to this pad's section).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
|
pub struct PadBootstrap {
|
|
/// [`BOOT_MAGIC`], host-stamped last at creation.
|
|
pub magic: u32,
|
|
/// The host's [`GAMEPAD_PROTO_VERSION`]. A driver whose own version differs must NOT publish
|
|
/// its pid (fail closed) — it still writes `driver_proto` so the host can log the mismatch.
|
|
pub host_proto: u32,
|
|
/// The driver's WUDFHost process id (driver-written; `0` = no driver yet). The duplication
|
|
/// target the host verifies (`verify_is_wudfhost`) before duplicating the DATA section into it.
|
|
pub driver_pid: u32,
|
|
/// The driver's [`GAMEPAD_PROTO_VERSION`] (driver-written; diagnostics only).
|
|
pub driver_proto: u32,
|
|
/// The DATA-section handle VALUE the host duplicated into `handle_pid`'s handle table
|
|
/// (host-written; valid only inside that process).
|
|
pub data_handle: u64,
|
|
/// The pid `data_handle` was duplicated for — a driver whose pid differs ignores the delivery.
|
|
pub handle_pid: u32,
|
|
/// Bumped by the host (host-global monotonic, never 0) AFTER `data_handle`/`handle_pid` are in
|
|
/// place — the driver's new-delivery trigger.
|
|
pub handle_seq: u32,
|
|
}
|
|
|
|
/// Virtual Xbox 360 (XInput) shared section (64 B). The host writes the XInput state (a bumped
|
|
/// `packet` number + buttons/triggers/sticks in XInput conventions); the driver answers
|
|
/// `XInputGetState`. The driver writes force-feedback (`XInputSetState`) into `rumble_*`, bumping
|
|
/// `rumble_seq`, which the host relays to the client.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
|
pub struct XusbShm {
|
|
pub magic: u32,
|
|
/// XInput `dwPacketNumber` — bumped by the host on every state change.
|
|
pub packet: u32,
|
|
pub buttons: u16,
|
|
pub left_trigger: u8,
|
|
pub right_trigger: u8,
|
|
pub thumb_lx: i16,
|
|
pub thumb_ly: i16,
|
|
pub thumb_rx: i16,
|
|
pub thumb_ry: i16,
|
|
pub _reserved0: u32,
|
|
/// Bumped by the driver on a new force-feedback packet.
|
|
pub rumble_seq: u32,
|
|
pub rumble_large: u8,
|
|
pub rumble_small: u8,
|
|
pub _pad0: [u8; 2],
|
|
/// Written by the driver when it binds (device add) and on every serviced IOCTL:
|
|
/// [`GAMEPAD_PROTO_VERSION`]. `0` = no driver attached — the host health check keys off it.
|
|
pub driver_proto: u32,
|
|
/// Bumped by the driver on every serviced XInput IOCTL — proves the game-visible path (it
|
|
/// only advances while something polls the slot, so a static value is not an error).
|
|
pub driver_heartbeat: u32,
|
|
/// 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
|
|
/// input report into `input`; the driver feeds it to game `READ_REPORT`s and publishes a game's
|
|
/// `0x02` output (rumble / lightbar / player-LEDs / adaptive triggers) into `output`, bumping
|
|
/// `out_seq`. `device_type` selects the HID identity ([`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`]).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
|
pub struct PadShm {
|
|
pub magic: u32,
|
|
pub _reserved0: u32,
|
|
/// Input report region (host-written; the codec's report is <= 64 B — see
|
|
/// `inject::dualsense_proto::DS_INPUT_REPORT_LEN`). The region spans `magic`+pad .. `out_seq`.
|
|
pub input: [u8; 64],
|
|
/// Bumped by the driver when it publishes a new `output` report.
|
|
pub out_seq: u32,
|
|
/// Output report region (driver-written): rumble / lightbar / player-LEDs / adaptive triggers.
|
|
pub output: [u8; 64],
|
|
/// HID identity selector — see [`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`].
|
|
pub device_type: u8,
|
|
pub _pad0: [u8; 3],
|
|
/// Written by the driver's timer while it has the section mapped: [`GAMEPAD_PROTO_VERSION`].
|
|
/// `0` = no driver attached — the host health check keys off it.
|
|
pub driver_proto: u32,
|
|
/// Bumped by the driver's ~125 Hz timer each tick — a true liveness heartbeat (unlike the
|
|
/// XUSB one, this advances whenever the driver is loaded, game or not).
|
|
pub driver_heartbeat: u32,
|
|
/// 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
|
|
// assert here means the struct no longer matches the historical `OFF_*` layout (host) / `view.add(N)`
|
|
// literal (driver) and must be fixed before either side switches to the type.
|
|
const _: () = {
|
|
use core::mem::{offset_of, size_of};
|
|
|
|
assert!(size_of::<XusbShm>() == 64);
|
|
assert!(offset_of!(XusbShm, magic) == 0);
|
|
assert!(offset_of!(XusbShm, packet) == 4);
|
|
assert!(offset_of!(XusbShm, buttons) == 8);
|
|
assert!(offset_of!(XusbShm, left_trigger) == 10);
|
|
assert!(offset_of!(XusbShm, right_trigger) == 11);
|
|
assert!(offset_of!(XusbShm, thumb_lx) == 12);
|
|
assert!(offset_of!(XusbShm, thumb_ly) == 14);
|
|
assert!(offset_of!(XusbShm, thumb_rx) == 16);
|
|
assert!(offset_of!(XusbShm, thumb_ry) == 18);
|
|
assert!(offset_of!(XusbShm, rumble_seq) == 24);
|
|
assert!(offset_of!(XusbShm, rumble_large) == 28);
|
|
assert!(offset_of!(XusbShm, rumble_small) == 29);
|
|
assert!(offset_of!(XusbShm, driver_proto) == 32);
|
|
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
|
|
assert!(offset_of!(XusbShm, pad_index) == 40);
|
|
|
|
assert!(size_of::<PadShm>() == 256);
|
|
assert!(offset_of!(PadShm, magic) == 0);
|
|
assert!(offset_of!(PadShm, input) == 8);
|
|
assert!(offset_of!(PadShm, out_seq) == 72);
|
|
assert!(offset_of!(PadShm, output) == 76);
|
|
assert!(offset_of!(PadShm, device_type) == 140);
|
|
assert!(offset_of!(PadShm, driver_proto) == 144);
|
|
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
|
|
assert!(offset_of!(PadShm, pad_index) == 152);
|
|
|
|
assert!(size_of::<PadBootstrap>() == 32);
|
|
assert!(offset_of!(PadBootstrap, magic) == 0);
|
|
assert!(offset_of!(PadBootstrap, host_proto) == 4);
|
|
assert!(offset_of!(PadBootstrap, driver_pid) == 8);
|
|
assert!(offset_of!(PadBootstrap, driver_proto) == 12);
|
|
assert!(offset_of!(PadBootstrap, data_handle) == 16);
|
|
assert!(offset_of!(PadBootstrap, handle_pid) == 24);
|
|
assert!(offset_of!(PadBootstrap, handle_seq) == 28);
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use bytemuck::Zeroable;
|
|
|
|
#[test]
|
|
fn frame_token_roundtrips() {
|
|
for (g, s, slot) in [
|
|
(1u32, 0u32, 0u8),
|
|
(5, 12_345, 3),
|
|
(frame::FrameToken::GENERATION_MASK, 0xFFFF_FFFF, 5),
|
|
(0, 1, 255),
|
|
] {
|
|
let t = frame::FrameToken {
|
|
generation: g,
|
|
seq: s,
|
|
slot,
|
|
};
|
|
assert_eq!(frame::FrameToken::unpack(t.pack()), t);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn frame_token_packing_matches_legacy_layout() {
|
|
// The legacy code packed (gen<<40)|(seq<<8)|slot by hand; lock the bit positions.
|
|
let t = frame::FrameToken {
|
|
generation: 7,
|
|
seq: 42,
|
|
slot: 3,
|
|
};
|
|
assert_eq!(t.pack(), (7u64 << 40) | (42u64 << 8) | 3u64);
|
|
}
|
|
|
|
#[test]
|
|
fn shared_header_is_pod_and_64_bytes() {
|
|
let mut h = frame::SharedHeader::zeroed();
|
|
h.magic = frame::MAGIC;
|
|
h.width = 5120;
|
|
h.height = 1440;
|
|
let bytes = bytemuck::bytes_of(&h);
|
|
assert_eq!(bytes.len(), 64);
|
|
let back: frame::SharedHeader = *bytemuck::from_bytes(bytes);
|
|
assert_eq!(back.magic, frame::MAGIC);
|
|
assert_eq!(back.width, 5120);
|
|
assert_eq!(back.height, 1440);
|
|
}
|
|
|
|
#[test]
|
|
fn control_structs_roundtrip_through_bytes() {
|
|
let req = control::AddRequest {
|
|
session_id: 0xDEAD_BEEF_CAFE_F00D,
|
|
width: 3840,
|
|
height: 2160,
|
|
refresh_hz: 120,
|
|
preferred_monitor_id: 7,
|
|
};
|
|
let bytes = bytemuck::bytes_of(&req);
|
|
assert_eq!(bytes.len(), 24);
|
|
assert_eq!(*bytemuck::from_bytes::<control::AddRequest>(bytes), req);
|
|
// preferred_monitor_id occupies the old `_reserved` slot at offset 20 — byte-compatible.
|
|
assert_eq!(bytes[20..24], 7u32.to_le_bytes());
|
|
|
|
let reply = control::AddReply {
|
|
adapter_luid_low: 0x1234_5678,
|
|
adapter_luid_high: -2,
|
|
target_id: 262,
|
|
resolved_monitor_id: 7,
|
|
wudf_pid: 4242,
|
|
};
|
|
let rbytes = bytemuck::bytes_of(&reply);
|
|
assert_eq!(rbytes.len(), 20);
|
|
assert_eq!(*bytemuck::from_bytes::<control::AddReply>(rbytes), reply);
|
|
// resolved_monitor_id occupies the old `_reserved` slot at offset 12 — byte-compatible.
|
|
assert_eq!(rbytes[12..16], 7u32.to_le_bytes());
|
|
// The v2 duplication-target pid trails at offset 16.
|
|
assert_eq!(rbytes[16..20], 4242u32.to_le_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn frame_channel_request_roundtrips_through_bytes() {
|
|
let mut req = control::SetFrameChannelRequest {
|
|
target_id: 262,
|
|
generation: 3,
|
|
ring_len: frame::RING_LEN,
|
|
_pad: 0,
|
|
header_handle: 0x0000_0000_0000_1a2c,
|
|
event_handle: 0x0000_0000_0000_1b30,
|
|
texture_handles: [0; control::RING_LEN_USIZE],
|
|
};
|
|
for (k, t) in req.texture_handles.iter_mut().enumerate() {
|
|
*t = 0x2000 + k as u64 * 4;
|
|
}
|
|
let bytes = bytemuck::bytes_of(&req);
|
|
assert_eq!(bytes.len(), 32 + 8 * control::RING_LEN_USIZE);
|
|
assert_eq!(
|
|
*bytemuck::from_bytes::<control::SetFrameChannelRequest>(bytes),
|
|
req
|
|
);
|
|
// The handle values ride at 8-byte alignment from offset 16 (header, event, then the ring).
|
|
assert_eq!(bytes[16..24], 0x1a2cu64.to_le_bytes());
|
|
assert_eq!(bytes[24..32], 0x1b30u64.to_le_bytes());
|
|
assert_eq!(bytes[32..40], 0x2000u64.to_le_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn gamepad_names_and_magics_are_stable() {
|
|
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
|
assert_eq!(gamepad::pad_boot_name(2), "Global\\pfds-boot-2");
|
|
// Lock the exact u32 magics the shipped host/drivers use (inject/{gamepad,dualsense}_windows.rs).
|
|
assert_eq!(gamepad::XUSB_MAGIC, 0x5558_4650);
|
|
assert_eq!(gamepad::PAD_MAGIC, 0x5046_4453);
|
|
// "PFBT" little-endian.
|
|
assert_eq!(gamepad::BOOT_MAGIC.to_le_bytes(), *b"PFBT");
|
|
}
|
|
|
|
#[test]
|
|
fn pad_bootstrap_roundtrips_through_bytes() {
|
|
let b = gamepad::PadBootstrap {
|
|
magic: gamepad::BOOT_MAGIC,
|
|
host_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
|
driver_pid: 1234,
|
|
driver_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
|
data_handle: 0x0000_0000_0000_2a4c,
|
|
handle_pid: 1234,
|
|
handle_seq: 7,
|
|
};
|
|
let bytes = bytemuck::bytes_of(&b);
|
|
assert_eq!(bytes.len(), 32);
|
|
assert_eq!(*bytemuck::from_bytes::<gamepad::PadBootstrap>(bytes), b);
|
|
// The handle value rides 8-aligned at offset 16; the seq trails at 28 (written LAST by the host).
|
|
assert_eq!(bytes[16..24], 0x2a4cu64.to_le_bytes());
|
|
assert_eq!(bytes[28..32], 7u32.to_le_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn ctl_codes_are_contiguous_and_distinct() {
|
|
assert_eq!(control::IOCTL_ADD, ctl_code(0x900));
|
|
let all = [
|
|
control::IOCTL_ADD,
|
|
control::IOCTL_REMOVE,
|
|
control::IOCTL_SET_RENDER_ADAPTER,
|
|
control::IOCTL_PING,
|
|
control::IOCTL_GET_INFO,
|
|
control::IOCTL_CLEAR_ALL,
|
|
control::IOCTL_SET_FRAME_CHANNEL,
|
|
];
|
|
for (i, a) in all.iter().enumerate() {
|
|
for b in &all[i + 1..] {
|
|
assert_ne!(a, b);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn guid_is_not_sudovda() {
|
|
const SUDOVDA: u128 = 0xE5BC_C234_1E0C_418A_A0D4_EF8B_7501_414D;
|
|
assert_ne!(PF_VDISPLAY_INTERFACE_GUID_U128, SUDOVDA);
|
|
}
|
|
}
|