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>
18 KiB
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.ViewCellholds 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 overMappedView. 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 rawWDFREQUESTinto aRequesttoken once (the onlyunsafeat 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) withPod+offset_of!asserts;xusb_boot_name/pad_boot_name(Global\pf…-boot-<index>) REPLACE the old*_shm_namefns (the DATA-section name is gone);XusbShm/PadShmgainedpad_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-deppf-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_EXISTSon 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).PadChannelowns both + the delivery state machine: polldriver_pid→OpenProcess→verify_is_wudfhost(now shared with the frame broker incapture/windows/idd_push.rs) →DuplicateHandle→ publishdata_handle/handle_pid, bumphandle_seqlast (Release). Pumped from each backend's existing service tick (≤4 ms) + a bounded eager delivery (1.5 s) at pad-open so the DS4'sdevice_typeis 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-tickpump_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 writesdriver_protoalways but publishes its pid only whenhost_protomatches (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 onhandle_seq, reset when the mailbox disappears so a new host session's counter can't collide), mapped, and validated:magicANDpad_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-<index>(64 B, [pf_driver_proto::gamepad::XusbShm]) — virtual Xbox 360 / XInput.Global\pfds-shm-<index>(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
inputregion), and - inject/forge gamepad input or rumble (write the
inputregion → the driver feeds it to whatever game has focus; write theoutputregion + bumpout_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;SwDevicewraps theSwDeviceCreatedevnode.gamepad_windows.rs(XUSB),dualsense_windows.rs(DualSense/DS4),dualshock4_windows.rs— each creates itsShm, thencreate_swdevice(index)/create_swdevice(profile)which stamps the pad index intoinfo.pszDeviceLocation(a UTF-16 decimal string) and createspf_xusb_<index>/pf_pad_<index>.
Driver (packaging/windows/drivers/pf-{xusb,dualsense}/src/lib.rs):
query_shm_index(device)—WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)→ parses the decimal →SHM_INDEXstatic.- On first control activity it builds
format!("Global\\pf…-shm-{}", SHM_INDEX),OpenFileMappingW+MapViewOfFile. The dualsense driver also runs a ~125 Hz timer (writesdriver_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:
- Host, per pad: create the DATA section unnamed (
CreateFileMappingWwithPCWSTR::null(), DACLD: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 viaSwDeviceCreate, stamping the pad index intopszDeviceLocationas today (the index still identifies which pad's bootstrap the driver should use). - Driver
EvtDeviceAdd: read the index (unchangedquery_shm_index). Writestd::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. - 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),DuplicateHandlethe 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-<index>, ~32 B, SY+LS): host creates it; driver opens it by name (index from location), writesdriver_pid, spins ondata_handle!= 0; host pollsdriver_pid, dups the DATA section in, writesdata_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 viaWdfDeviceAssignProperty(DEVPROPKEY_pf_pad_pid); host reads it viaCM_Get_DevNode_PropertyW/SetupDiGetDevicePropertyW, dups in, writes aDEVPROPKEY_pf_pad_handleproperty; 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.rsChannelBroker— 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 }) withPod+offset_of!asserts, bumpGAMEPAD_PROTO_VERSION, and (Option A) keeppad_shm_name/xusb_shm_nameonly 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'sOpenSharedResource/MapViewOfFileon 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-deniedOpenProcesson 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.
- Build + sign + redeploy
pf-dualsenseandpf-xusb(same loop as pf-vdisplay:packaging/windows/drivers/deploy-dev.ps1per driver, orredeploy-*; DriverVer must strictly increase). BumpGAMEPAD_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. - Connect a real client with a physical controller; confirm in a game that input works and rumble/LED return.
- Driver log (
C:\Users\Public\pfds-driver.log/pfxusb-driver.login debug builds): confirm the driver reports its pid, receives a handle, and maps the DATA section (add adbglog!"sealed pad channel mapped"). - Re-run the sibling-LS
OpenFileMappingtest: from a LocalService scheduled task, attempt to open the oldGlobal\pf…-shm-<index>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 produceddesign/idd-push-security.md.) - 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_VERSIONbump 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 blockEvtDeviceAddon 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 existingXusbShm/PadShmmagic already gives this). - Two drivers, one contract. DualSense and DualShock 4 share
pf-dualsense/PadShm; XUSB is separate. Factor the bootstrap intopf_driver_proto::gamepadso 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_protocontrol/frame).