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:
2026-07-03 12:08:56 +00:00
parent 16a7991cea
commit 8b47be668f
35 changed files with 2604 additions and 1174 deletions
@@ -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"
);
}
}