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:
@@ -1,15 +1,16 @@
|
||||
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/dualsense-driver`).
|
||||
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/drivers/pf-dualsense`).
|
||||
//!
|
||||
//! The Windows analogue of the Linux UHID backend ([`super::dualsense`]): same [`DsState`] model and
|
||||
//! the same byte-level report codec ([`super::dualsense_proto`]), but a different transport. Where
|
||||
//! the Linux backend writes report `0x01` to `/dev/uhid` and reads report `0x02` via `UHID_OUTPUT`,
|
||||
//! the Windows backend talks to the UMDF driver over a **named shared-memory section**
|
||||
//! `Global\pfds-shm-<idx>` (256 B: magic `u32@0`, input report `@8`, output seq `u32@72`, output
|
||||
//! report `@76`). The host creates the section (privileged → a permissive SDDL so the WUDFHost can
|
||||
//! open it); the driver maps it from its timer, feeds game `READ_REPORT`s from the input bytes, and
|
||||
//! publishes a game's `0x02` (rumble / lightbar / player-LEDs / adaptive triggers) into the output
|
||||
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
||||
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
||||
//! the Windows backend talks to the UMDF driver over an **unnamed shared DATA section** (256 B `PadShm`:
|
||||
//! magic `u32@0`, input report `@8`, output seq `u32@72`, output report `@76`) reached over the
|
||||
//! **sealed channel** ([`PadChannel`], `design/gamepad-channel-sealing.md`): the host duplicates the
|
||||
//! section handle into the driver's WUDFHost, bootstrapped via the named `Global\pfds-boot-<idx>`
|
||||
//! mailbox. The driver feeds game `READ_REPORT`s from the input bytes and publishes a game's `0x02`
|
||||
//! (rumble / lightbar / player-LEDs / adaptive triggers) into the output bytes. `hidclass` gates the
|
||||
//! device stack, so this user-mode IPC is the only viable channel (a UMDF driver has no control
|
||||
//! device); see `windows-dualsense-scoping.md`.
|
||||
//!
|
||||
//! Device lifecycle: each pad `SwDeviceCreate`s a `pf_pad_<index>` software devnode (hardware id
|
||||
//! `pf_dualsense`, enumerator `punktfunk`) on open and `SwDeviceClose`s it on drop, so the virtual
|
||||
@@ -20,12 +21,13 @@ use super::dualsense_proto::{
|
||||
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||
DS_TOUCH_W,
|
||||
};
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::ffi::c_void;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use windows::core::{w, GUID, HRESULT, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
@@ -49,17 +51,19 @@ pub(super) const OFF_DEVTYPE: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, device_type);
|
||||
pub(super) const OFF_DRIVER_PROTO: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, driver_proto);
|
||||
pub(super) const OFF_PAD_INDEX: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, pad_index);
|
||||
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSHOCK4;
|
||||
|
||||
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
||||
/// loads on it and the HID DualSense appears to games) plus the shared-memory section the driver maps.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
|
||||
struct DsWinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
seq: u8,
|
||||
@@ -184,7 +188,7 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// The pad index, stamped into the device Location — the driver reads it to map `pfds-shm-<index>`
|
||||
// The pad index, stamped into the device Location — the driver reads it to poll `pfds-boot-<index>`
|
||||
// (multi-pad). The buffer outlives the SwDeviceCreate call (we wait on the event before return).
|
||||
let loc: Vec<u16> = format!("{}", p.container_index)
|
||||
.encode_utf16()
|
||||
@@ -266,17 +270,20 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
}
|
||||
|
||||
impl DsWinPad {
|
||||
/// Create + map the section `Global\pfds-shm-<index>`, stamp the magic, then spawn the
|
||||
/// `root\pf_dualsense` devnode (the driver loads on it and maps the section). The devnode lives
|
||||
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
/// Create the sealed channel (unnamed DATA section + `Global\pfds-boot-<index>` mailbox), stamp
|
||||
/// the pad index + neutral report + the magic LAST, then spawn the `pf_pad_<index>` devnode (the
|
||||
/// driver loads on it and receives the DATA handle over the bootstrap). The devnode lives for the
|
||||
/// pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
fn open(index: u8) -> Result<DsWinPad> {
|
||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
|
||||
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// Stamp the pad index (the driver validates it on attach) + the neutral input report, then
|
||||
// the magic LAST (the driver only accepts the section once magic is set). The device-type
|
||||
// stays 0 (DualSense — the section arrives zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX/OFF_INPUT are in range.
|
||||
unsafe {
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS_INPUT_REPORT_LEN], {
|
||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
||||
@@ -286,7 +293,7 @@ impl DsWinPad {
|
||||
}
|
||||
// Spawn the per-session devnode via SwDeviceCreate; `SwDeviceClose` removes it on drop. On the
|
||||
// rare failure we keep the section + data plane and fall back to an out-of-band `pf_dualsense`
|
||||
// devnode (installer / dev-box devgen).
|
||||
// devnode (installer / dev-box devgen) — its persistent driver polls the same mailbox name.
|
||||
let inst = format!("pf_pad_{index}");
|
||||
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
@@ -302,14 +309,17 @@ impl DsWinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery so the driver holds the DATA section before hidclass asks it for
|
||||
// descriptors (the driver reads `device_type` from the section to pick its HID identity).
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(DsWinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_dualsense",
|
||||
"pf_dualsense.inf",
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
seq: 0,
|
||||
@@ -326,30 +336,40 @@ impl DsWinPad {
|
||||
serialize_state(&mut r, st, self.seq, self.ts);
|
||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||
std::ptr::copy_nonoverlapping(
|
||||
r.as_ptr(),
|
||||
self.channel.data_base().add(OFF_INPUT),
|
||||
r.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Poll the section's output slot; parse a new `0x02` report (rumble / LEDs / triggers) into a
|
||||
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything
|
||||
/// new. Also feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps
|
||||
/// `driver_proto` while it has the section mapped).
|
||||
/// new. Also ticks the sealed-channel delivery and feeds the driver-attach health watcher (the
|
||||
/// driver's ~125 Hz timer stamps `driver_proto` while it has the section mapped).
|
||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||
self.channel.pump();
|
||||
let mut fb = DsFeedback::default();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe {
|
||||
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
};
|
||||
self.attach.observe(proto);
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let seq =
|
||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
if seq != self.last_out_seq {
|
||||
self.last_out_seq = seq;
|
||||
let mut out = [0u8; 64];
|
||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.channel.data_base().add(OFF_OUTPUT),
|
||||
out.as_mut_ptr(),
|
||||
64,
|
||||
)
|
||||
};
|
||||
parse_ds_output(pad, &out, &mut fb);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
//! Virtual Sony DualShock 4 on Windows via the UMDF minidriver — the PS4 sibling of
|
||||
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the
|
||||
//! `Global\pfds-shm-<idx>` shared section the driver maps), same controller model ([`DsState`]); only
|
||||
//! the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the report codec
|
||||
//! ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4) into the
|
||||
//! section so the one UMDF driver serves the DS4 descriptor / attributes / features instead of the
|
||||
//! DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar (0xCD `Led`); a DS4
|
||||
//! has no adaptive triggers / player LEDs.
|
||||
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the sealed
|
||||
//! shared-memory channel bootstrapped via `Global\pfds-boot-<idx>`), same controller model
|
||||
//! ([`DsState`]); only the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the
|
||||
//! report codec ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4)
|
||||
//! into the DATA section so the one UMDF driver serves the DS4 descriptor / attributes / features
|
||||
//! instead of the DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar
|
||||
//! (0xCD `Led`); a DS4 has no adaptive triggers / player LEDs.
|
||||
|
||||
use super::dualsense_proto::DsState;
|
||||
use super::dualsense_windows::{
|
||||
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT,
|
||||
OFF_OUTPUT, OFF_OUT_SEQ, SHM_MAGIC, SHM_SIZE,
|
||||
OFF_OUTPUT, OFF_OUT_SEQ, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE,
|
||||
};
|
||||
use super::dualshock4_proto::{
|
||||
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
||||
};
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::Result;
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::HSTRING;
|
||||
|
||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the mapped
|
||||
/// shared section. Dropping it removes the devnode and unmaps + closes the section.
|
||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the sealed
|
||||
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
|
||||
struct Ds4WinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
counter: u8,
|
||||
@@ -36,16 +36,19 @@ struct Ds4WinPad {
|
||||
}
|
||||
|
||||
impl Ds4WinPad {
|
||||
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
|
||||
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
|
||||
/// Create the sealed channel, stamp `device_type = DualShock 4` + the pad index + a neutral
|
||||
/// report + the magic LAST, then spawn the `pf_ds4_<index>` devnode (the driver loads on it and
|
||||
/// receives the DATA handle over the bootstrap).
|
||||
fn open(index: u8) -> Result<Ds4WinPad> {
|
||||
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// device-type FIRST (so it's visible the moment magic is), pad index, neutral report,
|
||||
// magic LAST.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = DEVTYPE_DUALSHOCK4;
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS4_INPUT_REPORT_LEN], {
|
||||
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
|
||||
serialize_state(&mut r, &DsState::neutral(), 0, 0);
|
||||
@@ -68,14 +71,18 @@ impl Ds4WinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery — for the DS4 this is what closes the identity race: the driver
|
||||
// must read `device_type = 1` from the delivered DATA section before hidclass asks it for
|
||||
// descriptors, or the pad would enumerate with the (default) DualSense identity.
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(Ds4WinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_dualshock4",
|
||||
"pf_dualsense.inf", // one driver package serves both HID identities
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
counter: 0,
|
||||
@@ -92,29 +99,40 @@ impl Ds4WinPad {
|
||||
serialize_state(&mut r, st, self.counter, self.ts);
|
||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||
std::ptr::copy_nonoverlapping(
|
||||
r.as_ptr(),
|
||||
self.channel.data_base().add(OFF_INPUT),
|
||||
r.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Poll the section's output slot; parse a new `0x05` report (rumble / lightbar) into a
|
||||
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new. Also
|
||||
/// feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps `driver_proto`).
|
||||
/// ticks the sealed-channel delivery and feeds the driver-attach health watcher (the driver's
|
||||
/// ~125 Hz timer stamps `driver_proto`).
|
||||
fn service(&mut self) -> Ds4Feedback {
|
||||
self.channel.pump();
|
||||
let mut fb = Ds4Feedback::default();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe {
|
||||
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
};
|
||||
self.attach.observe(proto);
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let seq =
|
||||
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
if seq != self.last_out_seq {
|
||||
self.last_out_seq = seq;
|
||||
let mut out = [0u8; 64];
|
||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.channel.data_base().add(OFF_OUTPUT),
|
||||
out.as_mut_ptr(),
|
||||
64,
|
||||
)
|
||||
};
|
||||
parse_ds4_output(&out, &mut fb);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
//! Per-pad Windows resource RAII for the gamepad backends (DualSense / DualShock 4 / XUSB).
|
||||
//! Per-pad Windows resource RAII + the **sealed gamepad channel** broker (DualSense / DualShock 4 /
|
||||
//! XUSB backends).
|
||||
//!
|
||||
//! Each virtual pad owns two OS resources: the named shared-memory section (+ its mapped view) the
|
||||
//! `pf_dualsense`/`pf_xusb` driver reads, and the `SwDeviceCreate`'d software devnode the driver loads
|
||||
//! on. Before this module, all three backends hand-rolled the same `CreateFileMappingW` +
|
||||
//! `MapViewOfFile` and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` —
|
||||
//! easy to drift or leak on an error path. [`Shm`] and [`SwDevice`] own those resources with RAII, so a
|
||||
//! backend just holds them and the cleanup (and ordering) happens by construction.
|
||||
//! Each virtual pad owns three OS resources: the **unnamed** DATA section the `pf_dualsense`/`pf_xusb`
|
||||
//! driver works against (`XusbShm`/`PadShm`), the tiny **named** bootstrap mailbox
|
||||
//! (`pf_driver_proto::gamepad::PadBootstrap`) that hands the driver a duplicated handle to it, and the
|
||||
//! `SwDeviceCreate`'d software devnode the driver loads on. [`Shm`] and [`SwDevice`] own the resources
|
||||
//! with RAII; [`PadChannel`] owns the two sections plus the delivery handshake.
|
||||
//!
|
||||
//! **Why the channel is sealed** (`design/gamepad-channel-sealing.md`): the DATA section used to be a
|
||||
//! `Global\pf…-shm-<index>` named section with an SY+LS DACL, which let any *sibling LocalService*
|
||||
//! process open it by name to read the live controller input or inject/forge input and rumble — the
|
||||
//! same name-open vector the frame ring closed (`design/idd-push-security.md`). The DATA section is now
|
||||
//! UNNAMED with a SYSTEM-only DACL and reaches the driver exclusively as a handle this host duplicated
|
||||
//! into its WUDFHost (a duplicated handle carries the source's access, so no LS ACE is needed). The pad
|
||||
//! drivers are UMDF HID minidrivers with **no control device** (hidclass owns the stack), so unlike the
|
||||
//! frame channel there is no IOCTL to deliver the handle or learn the WUDFHost pid — hence the
|
||||
//! late-bound [`PadBootstrap`] mailbox handshake, the one *named* object left. It carries only pids and
|
||||
//! a handle VALUE (meaningless outside the target process), so tampering with it yields at worst a
|
||||
//! gamepad DoS, never a read or an injection; the empirical floor from the frame work holds here too
|
||||
//! (a LocalService token is DACL-denied `OpenProcess` on a UMDF WUDFHost for every access right).
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::os::windows::io::{FromRawHandle, OwnedHandle};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use pf_driver_proto::gamepad::{PadBootstrap, BOOT_MAGIC, GAMEPAD_PROTO_VERSION};
|
||||
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
||||
use std::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::{w, HSTRING, PCWSTR};
|
||||
@@ -17,7 +32,10 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_PROB, CR_SUCCESS, DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED,
|
||||
};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
||||
use windows::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows::Win32::Foundation::{
|
||||
DuplicateHandle, GetLastError, SetLastError, DUPLICATE_HANDLE_OPTIONS, ERROR_ALREADY_EXISTS,
|
||||
HANDLE, INVALID_HANDLE_VALUE, WIN32_ERROR,
|
||||
};
|
||||
use windows::Win32::Security::Authorization::{
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
||||
};
|
||||
@@ -26,54 +44,102 @@ use windows::Win32::System::Memory::{
|
||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
|
||||
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
|
||||
};
|
||||
use windows::Win32::System::Threading::{
|
||||
GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
/// A named, anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps
|
||||
/// the view, then the [`OwnedHandle`] closes the section handle (in that order). Replaces the three
|
||||
/// backends' hand-duplicated `CreateFileMappingW` + `MapViewOfFile` + manual `Drop`.
|
||||
///
|
||||
/// SDDL `D:(A;;GA;;;SY)(A;;GA;;;LS)`: GENERIC_ALL to **SYSTEM** (the host creates the section and
|
||||
/// writes the live HID input report into it) and **LocalService** (the account the UMDF driver's
|
||||
/// WUDFHost runs under, which reads it). The old SDDL granted **Everyone** (`WD`) — on the (mistaken)
|
||||
/// assumption the driver needed a restricted token's broad access — letting any local user
|
||||
/// `OpenFileMapping` the section to inject controller input or tamper the trusted channel
|
||||
/// (security-review 2026-06-28 #5). Verified on the RTX box (2026-06-29): the WUDFHost token is
|
||||
/// `S-1-5-19` (LocalService), SYSTEM integrity, with **zero restricted SIDs** — so scoping to SY+LS is
|
||||
/// sufficient for the driver and excludes normal (medium-IL, non-service) user processes.
|
||||
/// Least access the pad driver needs on the duplicated DATA section: it only MAPS it read/write, so
|
||||
/// `SECTION_MAP_READ | SECTION_MAP_WRITE` (== the driver's `FILE_MAP_RW`). Granted explicitly in
|
||||
/// [`PadChannel::deliver_to`] instead of `DUPLICATE_SAME_ACCESS` (least privilege for the sealed
|
||||
/// section — the driver's handle then can't take ownership / change security / delete the object).
|
||||
const SECTION_MAP_RW: u32 = 0x0004 | 0x0002;
|
||||
|
||||
/// An anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps the
|
||||
/// view, then the [`OwnedHandle`] closes the section handle (in that order). Created either
|
||||
/// [unnamed](Self::create_unnamed) (the sealed DATA section — reachable only by handle duplication) or
|
||||
/// [named](Self::create_named) (the bootstrap mailbox the driver opens by name).
|
||||
pub(super) struct Shm {
|
||||
/// Owns the section handle (closed on drop). Held only for ownership — never read after construction.
|
||||
_handle: OwnedHandle,
|
||||
/// Owns the section handle (closed on drop). Also the duplication source for the sealed channel —
|
||||
/// see [`Shm::raw_handle`].
|
||||
handle: OwnedHandle,
|
||||
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
||||
}
|
||||
|
||||
/// Build a `SECURITY_ATTRIBUTES` from an SDDL literal (`psd` is OS-allocated and leaked — acceptable
|
||||
/// for the handful of pad channels a host creates; it must outlive the returned `SECURITY_ATTRIBUTES`).
|
||||
fn sddl_sa(sddl: PCWSTR) -> Result<SECURITY_ATTRIBUTES> {
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (leaked — see above).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
sddl,
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
Ok(SECURITY_ATTRIBUTES {
|
||||
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Shm {
|
||||
/// Create + zero a `size`-byte section named `name`, mapped read/write. The section handle is owned
|
||||
/// immediately, so any failure below (or the returned `Shm`'s drop) closes it.
|
||||
pub(super) fn create(name: &HSTRING, size: usize) -> Result<Shm> {
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (freed at process
|
||||
// exit — acceptable for a host-lifetime object).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
/// Create + zero an **unnamed** `size`-byte section, mapped read/write — the sealed DATA section.
|
||||
/// SDDL `D:P(A;;GA;;;SY)` (SYSTEM-only, protected): with no name there is nothing to enumerate,
|
||||
/// open, or squat, and the driver reaches it through a duplicated handle, which carries the
|
||||
/// source's access without re-checking the object DACL (the exact property the frame ring
|
||||
/// validated on-glass — `design/idd-push-security.md`).
|
||||
pub(super) fn create_unnamed(size: usize) -> Result<Shm> {
|
||||
let sa = sddl_sa(w!("D:P(A;;GA;;;SY)"))?;
|
||||
Self::create_inner(&sa, PCWSTR::null(), size).context("create unnamed gamepad DATA section")
|
||||
}
|
||||
|
||||
/// Create + zero a **named** `size`-byte section, mapped read/write — the bootstrap mailbox. SDDL
|
||||
/// `D:(A;;GA;;;SY)(A;;GA;;;LS)`: SYSTEM (this host) + LocalService (the driver's WUDFHost opens it
|
||||
/// by name). Safe to leave name-openable because it carries nothing exploitable (see the module
|
||||
/// docs). **Squat-checked**: `Global\` names are creatable by any service holding
|
||||
/// `SeCreateGlobalPrivilege` (LocalService has it), so if the name already exists —
|
||||
/// `ERROR_ALREADY_EXISTS`, meaning `CreateFileMappingW` silently *opened* a pre-existing object we
|
||||
/// don't control — we close and retry briefly (our own driver holds the name for microseconds per
|
||||
/// poll tick), then fail loudly rather than run the handshake through an attacker-owned (or
|
||||
/// another host instance's) mailbox.
|
||||
pub(super) fn create_named(name: &HSTRING, size: usize) -> Result<Shm> {
|
||||
let sa = sddl_sa(w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"))?;
|
||||
for attempt in 0..5 {
|
||||
if attempt > 0 {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
// SAFETY: clearing the thread error slot so ERROR_ALREADY_EXISTS below is unambiguous.
|
||||
unsafe { SetLastError(WIN32_ERROR(0)) };
|
||||
let shm = Self::create_inner(&sa, PCWSTR(name.as_ptr()), size)
|
||||
.with_context(|| format!("create gamepad bootstrap mailbox {name}"))?;
|
||||
// SAFETY: read immediately after the create; windows-rs only touches the error slot on
|
||||
// failure, so a success here preserves CreateFileMappingW's ALREADY_EXISTS signal.
|
||||
if unsafe { GetLastError() } != ERROR_ALREADY_EXISTS {
|
||||
return Ok(shm);
|
||||
}
|
||||
// `shm` drops here → unmap + close our handle to the foreign object, then retry.
|
||||
}
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the SDDL above.
|
||||
bail!(
|
||||
"bootstrap mailbox {name} already exists and stayed alive across retries — another \
|
||||
punktfunk-host instance is serving this pad index, or a local service is squatting the \
|
||||
name (gamepad DoS attempt?)"
|
||||
);
|
||||
}
|
||||
|
||||
fn create_inner(sa: &SECURITY_ATTRIBUTES, name: PCWSTR, size: usize) -> Result<Shm> {
|
||||
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the caller's SDDL; the
|
||||
// descriptor behind `sa` outlives this call (leaked by `sddl_sa`).
|
||||
let map = unsafe {
|
||||
CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
Some(sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
size as u32,
|
||||
PCWSTR(name.as_ptr()),
|
||||
name,
|
||||
)?
|
||||
};
|
||||
// SAFETY: `map` is a fresh section handle we own; take ownership immediately so that the early
|
||||
@@ -84,14 +150,11 @@ impl Shm {
|
||||
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size) };
|
||||
if view.Value.is_null() {
|
||||
// `handle` drops here → closes the section. No view to unmap.
|
||||
return Err(anyhow!("MapViewOfFile failed for {name}"));
|
||||
return Err(anyhow!("MapViewOfFile failed"));
|
||||
}
|
||||
// SAFETY: `view` points at `size` writable bytes (just mapped).
|
||||
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
|
||||
Ok(Shm {
|
||||
_handle: handle,
|
||||
view,
|
||||
})
|
||||
Ok(Shm { handle, view })
|
||||
}
|
||||
|
||||
/// The mapped section's base pointer. Stable for the `Shm`'s lifetime (moving the `Shm` does not
|
||||
@@ -99,11 +162,16 @@ impl Shm {
|
||||
pub(super) fn base(&self) -> *mut u8 {
|
||||
self.view.Value as *mut u8
|
||||
}
|
||||
|
||||
/// The section handle as a borrowed `HANDLE` (the sealed channel's duplication source).
|
||||
fn raw_handle(&self) -> HANDLE {
|
||||
HANDLE(self.handle.as_raw_handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shm {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `view` came from `MapViewOfFile`; unmap it BEFORE the `_handle` field closes the
|
||||
// SAFETY: `view` came from `MapViewOfFile`; unmap it BEFORE the `handle` field closes the
|
||||
// section (struct fields drop only after this `Drop::drop` returns).
|
||||
unsafe {
|
||||
let _ = UnmapViewOfFile(self.view);
|
||||
@@ -111,6 +179,230 @@ impl Drop for Shm {
|
||||
}
|
||||
}
|
||||
|
||||
// ── The sealed-channel bootstrap broker ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Global delivery sequence for [`PadBootstrap::handle_seq`] — host-wide monotonic and never 0, so two
|
||||
/// consecutive pads on the same index can't hand the (persistent, out-of-band-devnode) driver the same
|
||||
/// seq twice. Starts at 1.
|
||||
static BOOT_SEQ: AtomicU32 = AtomicU32::new(1);
|
||||
|
||||
/// Hard cap on delivery attempts per pad: each attempt duplicates a handle into a WUDFHost, so a
|
||||
/// tampered mailbox flapping `driver_pid` must not mint unbounded remote handles (DoS containment).
|
||||
/// A legitimate pad needs exactly one (a driver restart within one pad lifetime is not a thing —
|
||||
/// the WUDFHost dies with the devnode).
|
||||
const MAX_DELIVERY_ATTEMPTS: u32 = 16;
|
||||
|
||||
/// One pad's sealed host↔driver channel: the unnamed DATA section (the real `XusbShm`/`PadShm`), the
|
||||
/// named bootstrap mailbox, and the delivery state machine ([`Self::pump`]) that hands the driver's
|
||||
/// WUDFHost a duplicated DATA handle once it publishes its pid. Owns both sections (RAII teardown —
|
||||
/// dropping the channel closes the mailbox, whose *name* then disappears, which is how a persistent
|
||||
/// (out-of-band-devnode) driver detects the host is gone).
|
||||
pub(super) struct PadChannel {
|
||||
data: Shm,
|
||||
boot: Shm,
|
||||
boot_name: String,
|
||||
/// Last `driver_pid` acted on (delivered or rejected) — never retry the same value, so a failed
|
||||
/// verify can't be spun into a hot loop by a static mailbox.
|
||||
last_seen_pid: u32,
|
||||
attempts: u32,
|
||||
delivered: bool,
|
||||
warned_proto: bool,
|
||||
warned_cap: bool,
|
||||
}
|
||||
|
||||
impl PadChannel {
|
||||
/// Create the unnamed DATA section (`data_size` bytes, zeroed — the caller stamps its layout and
|
||||
/// magic) plus the named bootstrap mailbox, stamped `host_proto` first and `BOOT_MAGIC` last so a
|
||||
/// driver only trusts a fully-initialized mailbox.
|
||||
pub(super) fn create(boot_name: String, data_size: usize) -> Result<PadChannel> {
|
||||
let data = Shm::create_unnamed(data_size)?;
|
||||
let boot = Shm::create_named(
|
||||
&HSTRING::from(boot_name.as_str()),
|
||||
core::mem::size_of::<PadBootstrap>(),
|
||||
)?;
|
||||
let base = boot.base();
|
||||
// SAFETY: `base` is the live, page-aligned mailbox view (>= size_of::<PadBootstrap>()); the
|
||||
// field offsets are pinned by the proto's asserts and naturally aligned, so the atomic views
|
||||
// are valid. `host_proto` is published BEFORE `magic` (Release) — a driver that observes the
|
||||
// magic (Acquire) sees the version.
|
||||
unsafe {
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, host_proto)) as *const AtomicU32))
|
||||
.store(GAMEPAD_PROTO_VERSION, Ordering::Relaxed);
|
||||
fence(Ordering::Release);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, magic)) as *const AtomicU32))
|
||||
.store(BOOT_MAGIC, Ordering::Release);
|
||||
}
|
||||
Ok(PadChannel {
|
||||
data,
|
||||
boot,
|
||||
boot_name,
|
||||
last_seen_pid: 0,
|
||||
attempts: 0,
|
||||
delivered: false,
|
||||
warned_proto: false,
|
||||
warned_cap: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// The DATA section's mapped base (the host side of `XusbShm`/`PadShm`).
|
||||
pub(super) fn data_base(&self) -> *mut u8 {
|
||||
self.data.base()
|
||||
}
|
||||
|
||||
/// The bootstrap mailbox name (log labelling).
|
||||
pub(super) fn boot_name(&self) -> &str {
|
||||
&self.boot_name
|
||||
}
|
||||
|
||||
/// Atomic `u32` load from a mailbox field.
|
||||
fn boot_load(&self, off: usize) -> u32 {
|
||||
// SAFETY: the mailbox view is live (owned by `self.boot`), page-aligned, and every
|
||||
// `PadBootstrap` u32 field offset is 4-aligned (proto asserts), so the atomic view is valid;
|
||||
// no reference into the shared region outlives the load.
|
||||
unsafe { (*(self.boot.base().add(off) as *const AtomicU32)).load(Ordering::Acquire) }
|
||||
}
|
||||
|
||||
/// One tick of the delivery state machine — called from the pad's regular service pump (≤4 ms
|
||||
/// cadence) and from [`Self::deliver_eager`]. Cheap when idle: two atomic loads.
|
||||
pub(super) fn pump(&mut self) {
|
||||
// Version diagnostics: the driver writes its own proto version even when it refuses to
|
||||
// publish a pid (host/driver mismatch), so the operator sees WHY the pad never attaches.
|
||||
let drv_proto = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_proto));
|
||||
if drv_proto != 0 && drv_proto != GAMEPAD_PROTO_VERSION && !self.warned_proto {
|
||||
self.warned_proto = true;
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
driver_proto = drv_proto,
|
||||
host_proto = GAMEPAD_PROTO_VERSION,
|
||||
"gamepad driver/host protocol mismatch on the bootstrap mailbox — update the \
|
||||
drivers: punktfunk-host.exe driver install --gamepad"
|
||||
);
|
||||
}
|
||||
let pid = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_pid));
|
||||
if pid == 0 || pid == self.last_seen_pid {
|
||||
return;
|
||||
}
|
||||
self.last_seen_pid = pid;
|
||||
if self.attempts >= MAX_DELIVERY_ATTEMPTS {
|
||||
if !self.warned_cap {
|
||||
self.warned_cap = true;
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
attempts = self.attempts,
|
||||
"gamepad channel delivery cap reached — the bootstrap mailbox keeps changing \
|
||||
its driver pid (tampering?); no further handles will be duplicated"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.attempts += 1;
|
||||
match self.deliver_to(pid) {
|
||||
Ok(seq) => {
|
||||
self.delivered = true;
|
||||
tracing::info!(
|
||||
mailbox = %self.boot_name,
|
||||
wudf_pid = pid,
|
||||
seq,
|
||||
"sealed gamepad channel delivered (DATA handle duplicated into the driver's \
|
||||
WUDFHost)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
mailbox = %self.boot_name,
|
||||
pid,
|
||||
error = %format!("{e:#}"),
|
||||
"sealed gamepad channel delivery failed — will retry when the mailbox reports \
|
||||
a different driver pid"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate the DATA section into `pid`'s handle table (after verifying it is a genuine
|
||||
/// WUDFHost) and publish the handle value + owning pid, bumping `handle_seq` LAST. The driver
|
||||
/// adopts the handle by consuming the delivery; an unconsumed duplicate dies with the target
|
||||
/// process (nothing to reap — there is no fallible step after the duplication).
|
||||
fn deliver_to(&self, pid: u32) -> Result<u32> {
|
||||
// SAFETY: plain FFI; the handle (checked by `?`) is owned solely here and moved into the
|
||||
// `OwnedHandle` (single owner, closes on drop); `verify_is_wudfhost` borrows it for the
|
||||
// synchronous check and forms no lasting alias.
|
||||
let process = unsafe {
|
||||
let h = OpenProcess(
|
||||
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
false,
|
||||
pid,
|
||||
)
|
||||
.context("OpenProcess(PROCESS_DUP_HANDLE) on the mailbox-reported pid")?;
|
||||
let process = OwnedHandle::from_raw_handle(h.0 as _);
|
||||
crate::capture::idd_push::verify_is_wudfhost(
|
||||
HANDLE(process.as_raw_handle()),
|
||||
pid,
|
||||
"gamepad-channel",
|
||||
)?;
|
||||
process
|
||||
};
|
||||
let mut remote = HANDLE::default();
|
||||
// SAFETY: `self.data.raw_handle()` is the live section handle this channel owns;
|
||||
// `process` is the live PROCESS_DUP_HANDLE target; `&mut remote` is a valid out-param.
|
||||
// Least privilege: the pad driver only MAPS the DATA section read/write (its `FILE_MAP_RW` =
|
||||
// `SECTION_MAP_READ | SECTION_MAP_WRITE`), so grant exactly that instead of copying our
|
||||
// full-access creator handle via `DUPLICATE_SAME_ACCESS` (Chen: don't over-grant unnamed
|
||||
// shared objects — a compromised driver's handle then can't `WRITE_DAC`/`DELETE` the section).
|
||||
unsafe {
|
||||
DuplicateHandle(
|
||||
GetCurrentProcess(),
|
||||
self.data.raw_handle(),
|
||||
HANDLE(process.as_raw_handle()),
|
||||
&mut remote,
|
||||
SECTION_MAP_RW,
|
||||
false,
|
||||
DUPLICATE_HANDLE_OPTIONS(0),
|
||||
)
|
||||
.context("DuplicateHandle(gamepad DATA section) into the driver's WUDFHost")?;
|
||||
}
|
||||
let value = remote.0 as usize as u64;
|
||||
let base = self.boot.base();
|
||||
let seq = BOOT_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
// SAFETY: live, page-aligned mailbox view; `data_handle` is 8-aligned and `handle_pid`/
|
||||
// `handle_seq` 4-aligned (proto asserts). The handle value + owning pid are published BEFORE
|
||||
// the seq (Release) — a driver that observes the new seq (Acquire) sees a complete delivery.
|
||||
unsafe {
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, data_handle)) as *const AtomicU64))
|
||||
.store(value, Ordering::Relaxed);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_pid)) as *const AtomicU32))
|
||||
.store(pid, Ordering::Relaxed);
|
||||
fence(Ordering::Release);
|
||||
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_seq)) as *const AtomicU32))
|
||||
.store(seq, Ordering::Release);
|
||||
}
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
/// Bounded wait at pad-open: pump until the mailbox produces a driver pid we act on (delivered or
|
||||
/// rejected) or `timeout` passes. Closes the identity race for the DualShock 4 (the driver reads
|
||||
/// `device_type` from the DATA section when hidclass asks for descriptors — the channel should be
|
||||
/// attached by then); the regular service pump takes over afterwards either way.
|
||||
pub(super) fn deliver_eager(&mut self, timeout: Duration) {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
self.pump();
|
||||
if self.last_seen_pid != 0 || Instant::now() >= deadline {
|
||||
if !self.delivered {
|
||||
tracing::debug!(
|
||||
mailbox = %self.boot_name,
|
||||
"eager gamepad-channel delivery window passed without an attach — the \
|
||||
service pump keeps polling (driver-attach diagnosis follows if it stays \
|
||||
silent)"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `SwDeviceCreate`'d software devnode; drop removes it via `SwDeviceClose`. Replaces the manual
|
||||
/// `SwDeviceClose` each backend used to call in its `Drop`.
|
||||
pub(super) struct SwDevice(HSWDEVICE);
|
||||
@@ -151,7 +443,7 @@ pub(super) struct DriverAttach {
|
||||
inf: &'static str,
|
||||
/// The driver's own debug log, referenced in the diagnosis line.
|
||||
driver_log: &'static str,
|
||||
/// Section name, for log lines.
|
||||
/// Bootstrap-mailbox name, for log lines (the DATA section is unnamed).
|
||||
shm_name: String,
|
||||
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
|
||||
instance_id: Option<String>,
|
||||
@@ -241,8 +533,8 @@ impl DriverAttach {
|
||||
devnode = %devnode,
|
||||
driver_log = self.driver_log,
|
||||
"gamepad driver has not attached to the shared section — the virtual pad exists but no \
|
||||
driver is serving it (games will not see it); an old (pre-health) driver also reads as \
|
||||
not-attached: update with punktfunk-host.exe driver install --gamepad"
|
||||
driver is serving it (games will not see it); an old (pre-sealed-channel) driver also \
|
||||
reads as not-attached: update with punktfunk-host.exe driver install --gamepad"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
//! Windows virtual Xbox 360 gamepad via the punktfunk **XUSB companion** UMDF driver
|
||||
//! (`packaging/windows/xusb-driver`) — the in-tree replacement for ViGEmBus. One virtual Xbox 360
|
||||
//! (`packaging/windows/drivers/pf-xusb`) — the in-tree replacement for ViGEmBus. One virtual Xbox 360
|
||||
//! controller per client pad index, visible to classic **XInput** (`XInputGetState`) with no kernel
|
||||
//! bus driver: each pad `SwDeviceCreate`s a `pf_xusb_<index>` devnode (the driver loads on it and
|
||||
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into the shared section
|
||||
//! `Global\pfxusb-shm-<index>`. GameStream/Moonlight already speak the XInput conventions (low-16
|
||||
//! button bits, sticks −32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
|
||||
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into an **unnamed** shared
|
||||
//! DATA section the driver reaches over the **sealed channel** ([`PadChannel`] — handle duplicated
|
||||
//! into its WUDFHost, bootstrapped via `Global\pfxusb-boot-<index>`; see
|
||||
//! `design/gamepad-channel-sealing.md`). GameStream/Moonlight already speak the XInput conventions
|
||||
//! (low-16 button bits, sticks −32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
|
||||
//!
|
||||
//! Rumble flows back the other way: a game writes force-feedback via `XInputSetState`, the driver
|
||||
//! parses the `SET_STATE` packet into the shared section, and [`GamepadManager::pump_rumble`] relays
|
||||
//! level changes to the client (the universal 0xCA plane), mirroring the Linux `EV_FF` read path.
|
||||
//!
|
||||
//! NB: the driver currently maps `Global\pfxusb-shm-0` (hardcoded), so a single pad (index 0) is
|
||||
//! fully correct; mixed multi-pad needs the driver to read its own index first (same limitation as
|
||||
//! the DualSense backend).
|
||||
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::ffi::c_void;
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use std::time::Duration;
|
||||
use windows::core::{w, GUID, HRESULT, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
@@ -41,6 +41,7 @@ const OFF_RY: usize = core::mem::offset_of!(XusbShm, thumb_ry);
|
||||
const OFF_RUMBLE_SEQ: usize = core::mem::offset_of!(XusbShm, rumble_seq);
|
||||
const OFF_RUMBLE: usize = core::mem::offset_of!(XusbShm, rumble_large); // large @28, small @29
|
||||
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
|
||||
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
|
||||
|
||||
/// Context for the `SwDeviceCreate` completion callback: an event to signal, the HRESULT it reports,
|
||||
/// and the PnP instance id PnP assigned (captured for devnode health diagnostics).
|
||||
@@ -100,7 +101,7 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// The pad index, stamped into the device Location — the driver reads it to map `pfxusb-shm-<index>`
|
||||
// The pad index, stamped into the device Location — the driver reads it to poll `pfxusb-boot-<index>`
|
||||
// (multi-pad). The buffer must outlive the SwDeviceCreate call (it does; we wait on the event).
|
||||
let loc: Vec<u16> = format!("{index}")
|
||||
.encode_utf16()
|
||||
@@ -171,12 +172,13 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
|
||||
Ok((hsw, ctx.instance_id()))
|
||||
}
|
||||
|
||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
|
||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the sealed shared-memory channel.
|
||||
struct XusbWinPad {
|
||||
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// Owns `Global\pfxusb-shm-<index>` (the section + its mapped view; drop unmaps + closes).
|
||||
shm: super::gamepad_raii::Shm,
|
||||
/// The sealed channel: the unnamed DATA section (the `XusbShm`) + the bootstrap mailbox + the
|
||||
/// handle-delivery state machine (drop closes both sections).
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
packet: u32,
|
||||
@@ -184,17 +186,18 @@ struct XusbWinPad {
|
||||
}
|
||||
|
||||
impl XusbWinPad {
|
||||
/// Create + map `Global\pfxusb-shm-<index>`, stamp the magic, then spawn the devnode.
|
||||
/// Create the sealed channel (unnamed DATA section + `Global\pfxusb-boot-<index>` mailbox), stamp
|
||||
/// the pad index then the magic LAST, spawn the devnode, and eagerly deliver the DATA handle once
|
||||
/// the driver publishes its pid.
|
||||
fn open(index: u8) -> Result<XusbWinPad> {
|
||||
// Permissive-DACL named section the WUDFHost (whatever account) can open; `Shm` owns the
|
||||
// section handle + its mapped view (zero-filled) and unmaps/closes on drop.
|
||||
let shm_name = pf_driver_proto::gamepad::xusb_shm_name(index);
|
||||
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
|
||||
let base = shm.base();
|
||||
// Zero the section then stamp the magic LAST (the driver only accepts it once magic is set).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
let boot_name = pf_driver_proto::gamepad::xusb_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// The section arrives zeroed; stamp the pad index (the driver validates it against its own
|
||||
// devnode index on attach) then the magic LAST (the driver only accepts it once magic is set).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX is in range.
|
||||
unsafe {
|
||||
std::ptr::write_bytes(base, 0, SHM_SIZE);
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
let (hsw, instance_id) = match create_swdevice(index) {
|
||||
@@ -205,14 +208,18 @@ impl XusbWinPad {
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery: the driver's EvtDeviceAdd publishes its pid right away; handing it
|
||||
// the DATA handle before we return means the pad is live for the game's first XInput poll.
|
||||
// On a missing/old driver this waits out the window once and the service pump takes over.
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(XusbWinPad {
|
||||
_sw,
|
||||
shm,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_xusb",
|
||||
"pf_xusb.inf",
|
||||
"C:\\Users\\Public\\pfxusb-driver.log",
|
||||
shm_name,
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
packet: 0,
|
||||
@@ -225,7 +232,7 @@ impl XusbWinPad {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
||||
self.packet = self.packet.wrapping_add(1);
|
||||
let base = self.shm.base();
|
||||
let base = self.channel.data_base();
|
||||
// SAFETY: `base` is the start of the mapped section (`SHM_SIZE` bytes, owned by `Shm`); every
|
||||
// `OFF_*` is a fixed in-range offset into it and `write_unaligned` handles the unaligned field
|
||||
// writes. Single owner (`&mut self`), so no concurrent writer races these stores.
|
||||
@@ -242,10 +249,12 @@ impl XusbWinPad {
|
||||
}
|
||||
|
||||
/// Poll the section for a game's rumble (the driver bumps `rumble_seq` on each SET_STATE). Returns
|
||||
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also feeds the driver-attach
|
||||
/// health watcher (the driver stamps `driver_proto` at device add + on every serviced IOCTL).
|
||||
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also ticks the sealed-channel
|
||||
/// delivery (a late-binding driver gets its handle here) and feeds the driver-attach health
|
||||
/// watcher (the driver stamps `driver_proto` once it maps the delivered section + per IOCTL).
|
||||
fn service(&mut self) -> Option<(u8, u8)> {
|
||||
let base = self.shm.base();
|
||||
self.channel.pump();
|
||||
let base = self.channel.data_base();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
|
||||
self.attach.observe(proto);
|
||||
|
||||
Reference in New Issue
Block a user