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 a3e1ea2b44
commit 95a08e99c3
37 changed files with 2985 additions and 1174 deletions
@@ -78,6 +78,8 @@ pub struct SwapChainProcessor {
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
// terminate flag.
unsafe impl Send for SwapChainProcessor {}
// SAFETY: as above — the raw pointer is only touched by the serialised worker, so a shared
// `&SwapChainProcessor` reference exposes no unsynchronised access.
unsafe impl Sync for SwapChainProcessor {}
impl SwapChainProcessor {
@@ -223,10 +225,11 @@ impl SwapChainProcessor {
return;
}
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF token can't
// create named objects, so the host creates the header + event + textures and we only OPEN them
// once they appear (`try_open`). Until then we just drain — exactly the STEP-5 behaviour — so a
// non-IDD-push session never stalls. Retried every ~30 loop iterations.
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring over the SEALED channel. The
// frame objects are unnamed — the host duplicates their handles into this process and delivers
// the values via IOCTL_SET_FRAME_CHANNEL, which the control plane stashes on our monitor
// (`monitor::take_frame_channel`). Until a delivery lands we just drain — exactly the STEP-5
// behaviour — so a non-IDD-push session never stalls. The stash is polled every ~30 iterations.
let mut publisher: Option<FramePublisher> = None;
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first loop iteration
@@ -243,31 +246,41 @@ impl SwapChainProcessor {
break;
}
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach to
// the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring, whose
// format now mismatches the surface → the publish() format-guard drops every frame and the
// stream freezes until the next swap-chain recreate.
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
// Re-attach triggers, either of:
// * `is_stale` — the host recreated the ring mid-session (HDR flip): it bumps OUR header's
// generation and re-delivers; without dropping here we'd keep CopyResource'ing into the
// stale ring, whose format now mismatches the surface → the publish() format-guard drops
// every frame and the stream freezes until the next swap-chain recreate.
// * a PENDING delivery (newest-wins) — a host build-retry creates a whole NEW ring with a
// DIFFERENT header mapping; the old publisher's header never changes, so `is_stale` can't
// fire. The host only delivers after fully (re)creating a ring, so a pending delivery
// always supersedes whatever we're attached to.
if publisher.as_ref().is_some_and(FramePublisher::is_stale)
|| (publisher.is_some() && crate::monitor::has_frame_channel(target_id))
{
publisher = None;
frames_since_try = u32::MAX; // re-attach immediately
}
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is idle
// (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open` is a
// cheap OpenFileMapping that fails fast until the host has created the ring.
// (E_PENDING / no frames presented yet), not only when a frame is acquired. Checking the
// stash is a cheap mutex peek that stays empty until the host's channel delivery lands; a
// taken delivery is consumed whether the attach succeeds or not (on failure its handles are
// closed, the host's wait-for-attach reads the status code, and any retry is a NEW delivery).
if publisher.is_none() {
if frames_since_try >= 30 {
frames_since_try = 0;
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
// happy under `-D warnings`; semantics are identical — attach on success, retry on Err.
if let Ok(p) = FramePublisher::try_open(
target_id,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
publisher = Some(p);
if let Some(channel) = crate::monitor::take_frame_channel(target_id) {
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
// happy under `-D warnings`; attach on success, drop the delivery on Err.
if let Ok(p) = FramePublisher::from_channel(
channel,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
publisher = Some(p);
}
}
} else {
frames_since_try += 1;
@@ -337,10 +350,10 @@ impl SwapChainProcessor {
if !raw.is_null() {
// SAFETY: `raw` is IddCx's live surface pointer (valid until the next
// ReleaseAndAcquire); `from_raw_borrowed` does not consume the refcount.
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
p.publish(&tex);
}
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) }
&& let Ok(tex) = res.cast::<ID3D11Texture2D>()
{
p.publish(&tex);
}
}
}