# Handoff — sealing the gamepad SHM channels Status: **implemented (Option A), 2026-07-03 — Windows CI + on-glass validation pending.** The design below was implemented as proposed; the "Implementation notes" section records what was actually built and the deltas. Remaining: build + sign + redeploy both pad drivers, then the hardware validation plan (§Validation) — it needs a physical controller on the box. This closes the one open residual left by the IDD-push sealed-channel work (`design/idd-push-security.md`): frames were sealed; the gamepad input/output channel was not. ## Unsafe hygiene (2026-07-03 follow-up — the drivers' `unsafe` was confined) After the seal landed, the pad drivers' `unsafe` footprint (raw `OpenFileMapping`/`MapViewOfFile`, `read_unaligned`, the whole bootstrap state machine as bare-pointer arithmetic) was pulled into a new audited crate **`pf-umdf-util`** (`packaging/windows/drivers/pf-umdf-util/`), so the drivers benefit from Rust instead of being C-in-Rust: - `section::MappedView` — a mapped section wrapped as bounds- + alignment-checked accessors (`load_u32`/`store_u32`/`read_bytes`/…). Callers never see the base pointer; an out-of-range offset asserts instead of corrupting. `ViewCell` holds the adopted view as a leaked `&'static` (the re-delivery-must-not-unmap rule, now type-enforced). - `channel::ChannelClient` — the ENTIRE sealed-channel driver side (publish pid → adopt handle → validate magic+`pad_index`), as a **`#![forbid(unsafe_code)]`** module over `MappedView`. One implementation both pad drivers share (was hand-duplicated). - `wdf::{Request, query_location_index, retrieve_next_request}` — the WDF request/memory/property FFI behind safe methods; a callback turns its raw `WDFREQUEST` into a `Request` token once (the only `unsafe` at the driver boundary), and completion consumes the token. Result: `pf-xusb`/`pf-dualsense` business logic is **100 % safe Rust**; the only remaining `unsafe` in them is the unavoidable WDF *setup* FFI in `DriverEntry`/`EvtDeviceAdd`/the timer, each with a `// SAFETY:` proof. The display driver `pf-vdisplay` is inherently FFI-bound (D3D11 / IddCx DDIs / cross-process textures) so it can't be unsafe-*free*, but it's now unsafe-*audited*: every `unsafe {}` carries a proof. Both invariants are lint-gated across the whole drivers workspace (`#![deny(unsafe_op_in_unsafe_fn)]` + `#![deny(clippy::undocumented_unsafe_blocks)]`) and enforced by a new `cargo clippy -D warnings` step in `windows-drivers.yml`. Verified on the RTX box (.173): the whole workspace builds + clippies + fmt-checks clean; both gamepad DLLs still produce. ## Implementation notes (what was built, 2026-07-03) - **Contract** (`pf_driver_proto::gamepad`, `GAMEPAD_PROTO_VERSION = 2`): `PadBootstrap` (32 B — `magic "PFBT"`, `host_proto`, `driver_pid`, `driver_proto`, `data_handle: u64`, `handle_pid`, `handle_seq`) with `Pod` + `offset_of!` asserts; `xusb_boot_name`/`pad_boot_name` (`Global\pf…-boot-`) REPLACE the old `*_shm_name` fns (the DATA-section name is gone); `XusbShm`/`PadShm` gained `pad_index` (carved from reserved space) so the DRIVER validates a delivery resolves to *its own* pad — the authentic-side answer to the "redirect the dup into a different pad's WUDFHost" hardening note (the section content is host-written and unreachable by a sibling LS, so the check can't be spoofed). Both pad drivers now path-dep `pf-driver-proto` (as pf-vdisplay does) instead of hand-synced literals. - **Host** (`inject/windows/gamepad_raii.rs`): `Shm::create_unnamed` (DATA, `D:P(A;;GA;;;SY)`) + `Shm::create_named` (mailbox, SY+LS, **squat-checked** — `ERROR_ALREADY_EXISTS` on create is close+retry×5 then a hard error, so the handshake never runs through a pre-created object; this also turns the previously-silent two-hosts-same-index cross-wire into a loud failure). `PadChannel` owns both + the delivery state machine: poll `driver_pid` → `OpenProcess` → `verify_is_wudfhost` (now shared with the frame broker in `capture/windows/idd_push.rs`) → `DuplicateHandle` → publish `data_handle`/`handle_pid`, bump `handle_seq` last (Release). Pumped from each backend's existing service tick (≤4 ms) + a bounded **eager delivery** (1.5 s) at pad-open so the DS4's `device_type` is readable before hidclass asks for descriptors. Delivery attempts are **capped at 16 per pad** so a tampered flapping mailbox can't mint unbounded remote handles. Same pid never retried (failed verify can't be spun into a hot loop). - **Drivers** (`pf-xusb`, `pf-dualsense`): per-tick `pump_bootstrap()` (the DS timer / every XUSB IOCTL + a bounded EvtDeviceAdd worker thread for XUSB's no-game-running case) opens the mailbox *by name each time* — the name existing doubles as host-liveness, replacing the old per-access section open; mailbox gone → detach (DS additionally resets the pended-read report to neutral instead of the old frozen-last-state behavior). The driver writes `driver_proto` always but publishes its pid **only when `host_proto` matches** (fail closed both ways: v1 host never creates a mailbox a v2 driver polls; a v1 driver opens a name that no longer exists). A delivery is adopted once (CAS on `handle_seq`, reset when the mailbox disappears so a new host session's counter can't collide), mapped, and validated: `magic` AND `pad_index == SHM_INDEX` — else unmapped + ignored (the handle is deliberately NOT closed on validation failure: a tampered value could name an unrelated handle in the driver's own table). The adopted view is cached and never unmapped (re-delivery swaps + leaks the old 64/256 B mapping on purpose — a concurrent reader may hold it). Driver log line for validation step 3: `sealed pad channel mapped (index …)`. - **Not built:** Option B (devnode custom properties). The residual named mailbox is documented and DoS-bounded; migrate later if it's ever deemed worth removing. ## The problem (why this exists) Each virtual pad's host↔driver channel is a **named** shared-memory section: - `Global\pfxusb-shm-` (64 B, [`pf_driver_proto::gamepad::XusbShm`]) — virtual Xbox 360 / XInput. - `Global\pfds-shm-` (256 B, [`pf_driver_proto::gamepad::PadShm`]) — virtual DualSense / DualShock 4. Both are created by the SYSTEM host with DACL `D:(A;;GA;;;SY)(A;;GA;;;LS)` (`inject/windows/gamepad_raii.rs` `Shm::create`) so the driver's WUDFHost (LocalService) can open them by name. That means **a sibling LocalService process can `OpenFileMapping` the section by name** and: - **read** the victim's live controller input (buttons/sticks/gyro/touchpad — host→driver `input` region), and - **inject/forge** gamepad input or rumble (write the `input` region → the driver feeds it to whatever game has focus; write the `output` region + bump `out_seq` → forge rumble/LED back to the client). This is the *same* name-open vector we closed for frames, one module over. Severity is lower than desktop capture (it's game-controller I/O, scoped to the focused app, and requires the attacker to already have LocalService code execution), but it is real and it is inconsistent to leave named next to a sealed frame ring. **Not a stopgap:** randomizing the section name is inadequate — the object namespace is enumerable with `NtQueryDirectoryObject`, so a random name is discoverable. (Same reason it was rejected for frames.) The fix is to remove the name. ## Why it isn't already sealed the frame way The frame channel seals cleanly because pf-vdisplay has a **control device** (the IddCx device interface): the host duplicates the unnamed handles into the driver's WUDFHost and delivers the values over `IOCTL_SET_FRAME_CHANNEL`, and the driver reports its own pid in the `IOCTL_ADD` reply. The pad drivers (`pf-dualsense`, `pf-xusb`) are **UMDF HID minidrivers with no control device** — hidclass owns the device stack and blocks a freely-openable control interface. That is *why* they use a named section in the first place. So there is no IOCTL to (a) hand the driver a duplicated handle or (b) learn the driver's WUDFHost pid. Compounding it: `pszDeviceLocation` (the existing host→driver property) is fixed at `SwDeviceCreate` time — **before** the WUDFHost process exists — so the host can't duplicate a handle into a not-yet-created process and stamp its value there. A bidirectional, late-bound handshake is required. ## Current architecture (what to modify) Host (`crates/punktfunk-host/src/inject/windows/`): - `gamepad_raii.rs` — `Shm::create(name, size)` creates the **named** section (SY+LS SDDL) + maps it; `SwDevice` wraps the `SwDeviceCreate` devnode. - `gamepad_windows.rs` (XUSB), `dualsense_windows.rs` (DualSense/DS4), `dualshock4_windows.rs` — each creates its `Shm`, then `create_swdevice(index)` / `create_swdevice(profile)` which stamps the pad **index** into `info.pszDeviceLocation` (a UTF-16 decimal string) and creates `pf_xusb_` / `pf_pad_`. Driver (`packaging/windows/drivers/pf-{xusb,dualsense}/src/lib.rs`): - `query_shm_index(device)` — `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` → parses the decimal → `SHM_INDEX` static. - On first control activity it builds `format!("Global\\pf…-shm-{}", SHM_INDEX)`, `OpenFileMappingW` + `MapViewOfFile`. The dualsense driver also runs a ~125 Hz timer (writes `driver_heartbeat`) — an existing poll loop to piggyback a bootstrap-wait on. Contract (`crates/pf-driver-proto/src/lib.rs` `mod gamepad`): owns `XusbShm`/`PadShm` layouts, the magics, `xusb_shm_name`/`pad_shm_name`, `device_type`, `GAMEPAD_PROTO_VERSION`, and the driver_proto/heartbeat fields. ## Proposed design — a late-bound bootstrap handshake Split each pad's channel into **(1) an unnamed DATA section** (the real `XusbShm`/`PadShm`, host↔driver) and **(2) a tiny bootstrap mailbox** that carries only a magic + the driver's pid + a handle value. The handshake: 1. **Host**, per pad: create the DATA section **unnamed** (`CreateFileMappingW` with `PCWSTR::null()`, DACL `D:P(A;;GA;;;SY)` — SYSTEM-only, exactly as the sealed frame ring now uses; the driver reaches it by duplicated handle, which carries access, so no LS ACE is needed). Then create the devnode via `SwDeviceCreate`, stamping the pad index into `pszDeviceLocation` **as today** (the index still identifies *which* pad's bootstrap the driver should use). 2. **Driver** `EvtDeviceAdd`: read the index (unchanged `query_shm_index`). Write `std::process::id()` where the host can read it, then **poll** (piggyback the existing timer) for a delivered handle value; map the DATA section from it once non-zero. 3. **Host**: learn the driver's pid, `OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION)`, **verify it is the WUDFHost servicing this pad's devnode** (see hardening note), `DuplicateHandle` the DATA section into the WUDFHost, and deliver the resulting handle value back to the driver. Two viable transports for steps 2–3's pid-out / handle-in (pick one): - **Option A — named bootstrap mailbox** (`Global\pf…-boot-`, ~32 B, SY+LS): host creates it; driver opens it by name (index from location), writes `driver_pid`, spins on `data_handle` != 0; host polls `driver_pid`, dups the DATA section in, writes `data_handle` + a ready seq. **Safe to leave named + SY+LS** because it carries *only* a pid (not sensitive) and a handle value (meaningless outside the target WUDFHost) — identical to the frame channel's "the bootstrap ACL is not load-bearing" argument. A sibling LS that reads it learns nothing exploitable; one that tampers it can at worst feed a bogus pid/handle → the driver maps a value that doesn't resolve in its own table → **DoS, not a breach** (the attacker cannot place a valid section handle in the WUDFHost, so it cannot make the driver map an attacker-controlled section). *Fastest to build — reuses the existing named-section + poll machinery.* - **Option B — devnode custom properties** (no `Global\` object at all): driver writes its pid via `WdfDeviceAssignProperty(DEVPROPKEY_pf_pad_pid)`; host reads it via `CM_Get_DevNode_PropertyW` / `SetupDiGetDevicePropertyW`, dups in, writes a `DEVPROPKEY_pf_pad_handle` property; driver re-queries it in its timer. Tighter (property store isn't world-readable like the Global namespace) but more moving parts and UMDF-property-write ergonomics to prove out. *Cleaner end-state.* Recommendation: **build Option A first** (small, mirrors the frame channel, gets the DATA section unnamed — which is the actual isolation win, proven by #3 below), then optionally migrate the bootstrap to Option B if the residual named mailbox is deemed worth removing. ## Reuse the frame-channel precedent - **Ownership/adopt-on-success** discipline from `capture/windows/idd_push.rs` `ChannelBroker` — exactly one side ever closes a duplicated handle value; reap remote duplicates (`DUPLICATE_CLOSE_SOURCE`) on any failure. - **`verify_is_wudfhost`** (`idd_push.rs`) — before duplicating into the driver-reported pid, confirm it's `%SystemRoot%\System32\WUDFHost.exe`. **Strengthen it here**: also confirm the pid is the host *servicing this pad's devnode* (walk devnode → process, e.g. via the driver writing a per-pad nonce it echoes, or a devnode/PID association) so a tampered bootstrap can't redirect the dup into a *different* pad's WUDFHost. - **Contract in `pf_driver_proto::gamepad`** — add the bootstrap layout (`PadBootstrap { magic, driver_pid, data_handle: u64, seq }`) with `Pod` + `offset_of!` asserts, bump `GAMEPAD_PROTO_VERSION`, and (Option A) keep `pad_shm_name`/`xusb_shm_name` only for the bootstrap mailbox, dropping the data-section name. - **SDDL** on the DATA section: `D:P(A;;GA;;;SY)` (SYSTEM-only) — validated safe for a duplicated-handle consumer on the frame ring (the driver's `OpenSharedResource`/`MapViewOfFile` on a handle does not re-check the object DACL). ## Security properties after the change - The **DATA section is unnamed** and only ever handle-duplicated into the pad WUDFHost. Empirically (`design/idd-push-security.md`, RTX box 2026-07-03) a **LocalService token is DACL-denied `OpenProcess` on a UMDF WUDFHost for every access right incl. `QUERY_LIMITED`** — so a sibling LS cannot dup the handle out or read the WUDFHost's memory. Unnamed + unopenable-host ⇒ no sibling-LS path to the input/output data. This is the same guarantee the frame channel now has, and it rests on the same verified property. - **Residual (Option A):** the bootstrap mailbox stays named + SY+LS, but carries only a pid + handle value → worst case a sibling LS causes a **gamepad DoS**, never a read or injection. Option B removes even that. - **Unchanged inherent limits:** admin/SYSTEM = total; the game reading the pad sees the input by design. ## Validation plan (needs hardware) The blocker for calling this done is that it **requires a physical controller on the box** — the memory notes repeatedly flag the gamepad path as "needs a physical pad to live-verify," and neither the probe nor a synthetic client exercises a real game reading the virtual pad. 1. Build + sign + redeploy `pf-dualsense` and `pf-xusb` (same loop as pf-vdisplay: `packaging/windows/drivers/deploy-dev.ps1` per driver, or `redeploy-*`; DriverVer must strictly increase). Bump `GAMEPAD_PROTO_VERSION` — a v_new host against a v_old pad driver (or vice-versa) must fail closed, so deploy host + both pad drivers together. 2. Connect a real client with a physical controller; confirm in a game that input works and rumble/LED return. 3. Driver log (`C:\Users\Public\pfds-driver.log` / `pfxusb-driver.log` in debug builds): confirm the driver reports its pid, receives a handle, and maps the DATA section (add a `dbglog!` "sealed pad channel mapped"). 4. Re-run the **sibling-LS `OpenFileMapping` test**: from a LocalService scheduled task, attempt to open the old `Global\pf…-shm-` name — it must now **fail (name gone)**, and attempting to open the bootstrap (Option A) must yield only pid+handle bytes. (Reuse the scheduled-task P/Invoke harness from the #3 frame test — see the session that produced `design/idd-push-security.md`.) 5. Multi-pad: two controllers → two devnodes, two unnamed DATA sections, two bootstraps by index; confirm no cross-talk and clean teardown (`SwDeviceClose` + host handle close; the WUDFHost dies with its devnode). ## Risks / gotchas - **Regression risk to a working feature.** Gamepad input currently works on glass; this reroutes its bootstrap. Keep the change behind the `GAMEPAD_PROTO_VERSION` bump and be ready to revert both drivers. - **Chicken-and-egg timing.** The driver loads and wants the handle before the host has dup'd it — the poll loop must tolerate a bounded wait (mirror the frame path's `wait_for_attach`, ~4 s) and the driver must not block `EvtDeviceAdd` on it (spin in the timer, not the add callback). - **Handle value in shared memory is a `u64`.** A WUDFHost handle value is process-local; writing it to the bootstrap is safe (meaningless elsewhere), but the driver must treat it as untrusted (validate the mapped DATA section's magic before use — the existing `XusbShm`/`PadShm` magic already gives this). - **Two drivers, one contract.** DualSense and DualShock 4 share `pf-dualsense`/`PadShm`; XUSB is separate. Factor the bootstrap into `pf_driver_proto::gamepad` so both drivers + the host use one definition (as the frame channel does). ## Effort Medium — comparable to the frame sealed-channel change but across **two** drivers plus the host inject code, and gated on **physical-controller validation** that can't be driven over SSH. Files: `pf_driver_proto` (gamepad module), `inject/windows/{gamepad_raii,gamepad_windows,dualsense_windows,dualshock4_windows}.rs`, `packaging/windows/drivers/pf-{xusb,dualsense}/src/lib.rs`. Reference implementation: the frame sealed channel (`capture/windows/idd_push.rs` + `packaging/windows/drivers/pf-vdisplay/src/{control,monitor,frame_transport}.rs` + `pf_driver_proto` `control`/`frame`).