fix(inject): heap the SwDeviceCreate callback context; stop latching a pad slot on a failed create

Two medium findings from the round-1 sweep, each applied to both siblings.

- create_swdevice stack-allocated the SwCreateCtx that the async PnP completion
  callback writes through (result + up to 127 u16 of instance id) and then
  SetEvents. The wait is bounded at 10s, so on a wedged-PnP timeout the callback
  can still be PENDING: the frame is popped, the input thread reuses that stack,
  and a late callback corrupts it and SetEvents an already-closed (possibly
  recycled) handle. The context is now heap-allocated and reclaimed only where
  the callback provably ran; on the timeout path the box is deliberately leaked
  and the event left open, so a late write always targets live memory. Costs a
  one-off ~264 B + one HANDLE on that rare path. Applied to the DualSense path
  and its XUSB sibling in gamepad_windows.rs.

- Ds4WinPad::open swallowed a create_swdevice failure into a WARN and returned
  Ok with no devnode. PadSlots::ensure then stored Some(pad) AND called
  gate.on_success(), so the slot short-circuited on is_some() forever and the
  capped-backoff retry that exists precisely to self-heal a transient PnP failure
  never ran — the game saw no controller for the rest of the session unless the
  client unplugged the pad. Now propagates, matching the XUSB sibling. Same fix
  applied to steam_deck_windows.rs.

Windows .173: pf-inject 53/0. Linux .21: pf-inject 74/0 (8 ignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:19:19 +02:00
parent fe4af1761e
commit 134fba1424
4 changed files with 52 additions and 35 deletions
@@ -82,12 +82,16 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
// `result` starts as E_FAIL, NOT S_OK: if the wait below times out, a zero-initialised HRESULT
// would read as success and mask the failure (found by the 2026-07 driver-health audit).
let mut ctx = SwCreateCtx {
// HEAP-allocated for the same reason as the DualSense sibling: the callback writes through this
// pointer and SetEvents, and the wait below is bounded — a stack context would be popped while a
// late callback still holds it. On the timeout path the box is deliberately leaked and the event
// left open so a late write/SetEvent always targets live memory/handle.
let ctx = Box::into_raw(Box::new(SwCreateCtx {
event,
result: E_FAIL,
instance_id: [0; 128],
};
// SAFETY: info + buffers + ctx outlive the call (we wait on the event before returning).
}));
// SAFETY: info + buffers outlive the call; `ctx` is a live heap allocation outliving every path.
let hsw = match unsafe {
SwDeviceCreate(
w!("punktfunk"),
@@ -95,13 +99,14 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
&info,
None,
Some(sw_create_cb),
Some(&mut ctx as *mut SwCreateCtx as *const c_void),
Some(ctx as *const c_void),
)
} {
Ok(h) => h,
Err(e) => {
// SAFETY: event is valid.
// SAFETY: the call failed, so no callback is pending and `ctx` is ours to reclaim.
unsafe {
drop(Box::from_raw(ctx));
let _ = CloseHandle(event);
}
return Err(anyhow!("SwDeviceCreate(pf_xusb) failed: {e}"));
@@ -109,17 +114,20 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
};
// SAFETY: event valid; block until PnP finishes enumerating, then check the callback result.
let wait = unsafe { WaitForSingleObject(event, 10_000) };
// SAFETY: event is valid.
unsafe {
let _ = CloseHandle(event);
}
if wait != WAIT_OBJECT_0 {
// Timed out — intentionally leak `ctx` and leave `event` open (see above).
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate(pf_xusb) enumeration callback never fired (10s) — PnP may be wedged"
));
}
// The callback ran (it signalled the event), so nothing else will touch `ctx`/`event`.
// SAFETY: `ctx` came from `Box::into_raw` and is reclaimed exactly once here.
let ctx = unsafe {
let _ = CloseHandle(event);
Box::from_raw(ctx)
};
if ctx.result.is_err() {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };