A LocalService principal could take over a virtual pad's shared input section and
forge HID input into the interactive desktop.
The host duplicates each pad's unnamed DATA section into the driver's WUDFHost, and
through gamepad proto v2 it learned that process from `driver_pid` in the named
bootstrap mailbox. That mailbox has to be LocalService-writable — that is what the
driver's own WUDFHost runs as — and the delivery gate, verify_is_wudfhost, only checks
that the target's IMAGE is %SystemRoot%\System32\WUDFHost.exe. That image is
world-executable. So anything running as LocalService — notably the deliberately
de-privileged plugin runner — could spawn its own WUDFHost (CREATE_SUSPENDED parks it
indefinitely with the right image path), publish that pid, and be handed
SECTION_MAP_READ|WRITE on a live section. For pf-mouse that section drives a real
absolute pointer, so it was desktop control; for the pads it was forged gamepad input
plus a read of the remote user's controller state.
The module docs claimed mailbox tampering "yields at worst a gamepad DoS, never a read
or an injection". That was wrong, and the reasoning behind it — that a LocalService
token is DACL-denied OpenProcess on a UMDF WUDFHost — only covers the REAL host, not
one the attacker spawned itself.
The pid now comes from the device stack (ChannelProof, proto 2 -> 3). The host asks the
devnode it SwDeviceCreate'd who is serving it, looked up by the instance id PnP handed
back, so a planted look-alike devnode is not a candidate and the kernel — not anything
the attacker supplies — does the routing. Only the driver PnP actually bound to that
device can answer. `driver_pid` survives as a liveness hint; a tamperer can still deny a
pad, which squatting the name always allowed, but can no longer choose the recipient.
Two rules keep the state machine honest around it: a delivery stands until its target
process EXITS (judged on a retained SYNCHRONIZE handle, so a recycled pid cannot fake
it, and UMDF's restart-after-driver-crash still re-attaches), and a pad with no
SwDeviceCreate devnode refuses to deliver rather than fall back — unless an operator
sets PUNKTFUNK_PAD_CHANNEL_TRUST_MAILBOX, which says so loudly.
Three transports, because Windows carries different things to different driver shapes,
and the obvious two did not survive contact with hidclass. Measured on .173 (Win11
26200): HidD_GetIndexedString is NOT forwarded to a UMDF HID minidriver at all — it
failed for every index including ones the driver demonstrably serves through the named
wrappers; and a private device interface registers and enumerates but cannot be OPENED
(ERROR_GEN_FAILURE), because hidclass owns IRP_MJ_CREATE on a devnode it is the FDO for.
That is exactly why pf-xusb was never affected: it is not a HID minidriver, so nothing
sits above it. What works:
* pf-xusb — a private IOCTL on its own GUID_DEVINTERFACE_XUSB.
* pf-mouse — the HID serial string. Verified: PFCP:3:0:7296, and 7296 was a genuine
service-spawned WUDFHost.exe. Safe here alone: nothing reads the virtual
mouse's serial, whereas a pad's is SDL/Steam dedup material.
* pf-gamepad — a HID feature report, and it cost NO report-descriptor change. The
captured descriptors already declare far more Feature ids than the driver
ever served: 0x85 is declared on DualSense, DualShock 4 and Edge alike and
used to fail with STATUS_INVALID_PARAMETER, so hidclass lets it through and
nothing can have depended on the old failure. The Deck's one feature report
is unnumbered and Steam drives it command->response, so its proof rides that
existing contract via a private two-byte command. Verified: feature 0x85
returned magic "PFCP", proto 3, pad_index 0, wudf_pid 18456 — and 18456 was
a WUDFHost — with the product string still 'DualSense Wireless Controller'.
Also renamed pf-dualsense -> pf-gamepad. One driver has always served four identities, so
the old name read as if the other three lived elsewhere. ONLY the package identity moved
(crate, INF/CAT/DLL, UMDF service, build script, CI lines, log file, env var). The four
HARDWARE IDS are deliberately unchanged — they bind every devnode the host creates and
every installed system — as are the Global\pfds-boot-<i> mailbox and PAD_MAGIC, which are
wire contract. `driver install --gamepad` now retires the pre-rename store package first,
matched on pf_dualsense.dll because that string appears only in the OLD inf; matching on
the hardware ids would delete what we are about to install. On .173 that separated 14
stale packages from the 1 new one with 0 ambiguous, and the renamed package binds the old
hwid (devgen root\pf_dualsense -> oem143.inf = pf_gamepad.inf).
The repo's own pre-commit/pre-push rustfmt hooks named the old crate, so they caught the
rename before the commit did — they now check pf-gamepad, and pf-mouse alongside it, which
they had been missing relative to the CI line.
Host and drivers MUST ship together: v2<->v3 fails closed in both directions by design,
with the existing "update host + drivers together" diagnostic.
The rename moved files that also carry the security change, so splitting this into two
commits would mean reconstructing an intermediate state that was never gated. It is one
commit on purpose.
Gated on the windows-amd64 runner with cargo clean first (the box's clock lags, so stale
artifacts would read as a vacuous green): clippy -D warnings clean for pf-inject,
pf-capture and pf-driver-proto, drivers workspace build + the CI clippy line clean,
cargo check --release -p punktfunk-host clean, 19 + 58 tests green. Also fixes pf-mouse
still writing its debug log to world-writable C:\Users\Public, which the 2026-07-17
review moved for the other three drivers and missed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pf-xusb — virtual Xbox 360 XUSB companion (UMDF2, classic XInput)
A pure-user-mode UMDF2 driver that makes a virtual Xbox 360 controller visible to classic
XInputGetState with no kernel bus driver (no ViGEmBus) — the HIDMaestro approach. It is the
Windows counterpart to ViGEm's X360 target, owned in-tree.
Why this is not the HID driver
XInput does not use HID. xinput1_4.dll enumerates the XUSB device-interface GUID
{EC87F1E3-C13B-4100-B5F7-8B84D54260CB} (SetupDiEnumDeviceInterfaces), opens the Nth present
instance (= player slot 0–3) with CreateFile, and polls it with buffered IOCTLs. So this driver:
- is not a HID minidriver (no
MsHidUmdf) — it's a plain UMDF2 function driver underWUDFRd, System setup class; - registers the XUSB interface with
WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL); - answers the XUSB IOCTLs (all
METHOD_BUFFERED, delivered to user mode by the reflector) from controller state the host publishes into an unnamed shared DATA section reached over the sealed pad channel (punktfunk-planning:gamepad-channel-sealing.md): the host duplicates the section handle into this driver's WUDFHost, bootstrapped via the namedGlobal\pfxusb-boot-<index>mailbox (pf_driver_proto::gamepad::PadBootstrap); a game's rumble (SET_STATE) is published back for the host to forward to the client.
The WAIT_* IOCTLs return STATUS_INVALID_DEVICE_REQUEST, which makes xinput1_4 fall back to
synchronous GET_STATE polling — so no manual queue / timer is needed for classic XInput. (WGI/
GameInput admission additionally needs a xinputhid UpperFilters registry tripwire + the async
WAIT_FOR_INPUT pump — not implemented; classic XInput does not need it.)
Verified wire formats (source: HIDMaestro driver/companion.c, nefarius/XInputHooker XUSB.h, ViGEm)
| IOCTL | Code | Reply |
|---|---|---|
GET_INFORMATION |
0x80006000 |
12 B: [0]=ver 0x0103, [2]=count 0x01, [8]=VID 045E, [10]=PID 028E — marks the slot connected |
GET_CAPABILITIES |
0x8000E004 |
24 B (or 36 B V2 if outLen>=36): Type 0x03/SubType 0x01, motor max 0xFFFF (advertise rumble) |
GET_STATE |
0x8000E00C |
29 B: [0]ver [2]count [5]u32 packet# [0x0B]u16 wButtons [0x0D]LT [0x0E]RT [0x0F..0x16]4×i16 sticks |
SET_STATE |
0x8000A010 |
input 5 B {00, led, large, small, subcmd}: subcmd 0x02=rumble (large [2], small [3]), 0x01=player-LED |
GET_LED_STATE |
0x8000E008 |
{0,0,0x06} |
GET_BATTERY_INFORMATION |
0x8000E018 |
{0,0x01,0x03,0} |
WAIT_GUIDE_BUTTON / WAIT_FOR_INPUT |
0x8000E014 / 0x8000E3AC |
STATUS_INVALID_DEVICE_REQUEST → GET_STATE fallback |
wButtons is the XINPUT_GAMEPAD_* bitmap (DPAD_UP 0x0001 … A 0x1000 B 0x2000 X 0x4000
Y 0x8000). dwPacketNumber (GET_STATE [5]) must increment whenever the payload changes.
Shared-memory layout (unnamed DATA section, 64 B) — host writes state, driver writes rumble
pf_driver_proto::gamepad::XusbShm (the crate owns the offsets; both sides compile against it):
magic u32 @0 ("PFXU" 0x55584650) · packet u32 @4 (host bumps → dwPacketNumber) · wButtons u16 @8 · LT @10 · RT @11 · LX/LY/RX/RY i16 @12/@14/@16/@18 · rumble_seq u32 @24 (driver bumps) ·
large @28 · small @29 · health marks @32/@36 · pad_index u32 @40 (validated against the
devnode's Location index when the delivered handle is mapped).
Validated live (2026-06-22, maintainer's RTX test box)
XInputGetState(0) returns CONNECTED with the pushed buttons/sticks and an incrementing
dwPacketNumber; XInputSetState(0xC000, 0x4000) reaches the driver as 00 00 c0 40 02 → host sees
large=192 small=64. Test tools (on that box): xusbtest.exe (creates the pf_xusb
devnode + cycling state via shm) and xinputtest.exe (XInputGetState/SetState harness).
Build / sign / install (same recipe as the DualSense driver)
Built as a member of the in-tree packaging/windows/drivers/ workspace — one
cargo build --release builds all three drivers; build-gamepad-drivers.ps1 (one level up) wraps
the whole build/sign/stage flow in CI. The manual steps:
cargo build --releasein the workspace (envLIBCLANG_PATH,Version_Number=10.0.26100.0) →target\x86_64-pc-windows-msvc\release\pf_xusb.dll.- Clear the FORCE_INTEGRITY PE bit (bit
0x80ate_lfanew+0x5eofpf_xusb.dll). signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll.Inf2Cat /driver:<pkg> /os:10_X64→ re-signpf_xusb.catwith the same thumbprint.pnputil /add-driver pf_xusb.inf(no/install; the host SwDeviceCreate'spf_xusbper session).
Host integration (done)
crates/punktfunk-host/src/inject/windows/gamepad_windows.rs is the Windows GamepadManager (used by
PadBackend::Xbox360): it SwDeviceCreate's the pf_xusb companion, delivers the unnamed DATA
section over the sealed channel (PadChannel), writes
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
is no ViGEmBus dependency anymore. The driver is built + signed from source in CI
(build-gamepad-drivers.ps1) and installed by the Inno Setup installer via
punktfunk-host.exe driver install --gamepad.
Multi-pad
The host stamps each pad's index into the device Location (pszDeviceLocation); the driver reads it
via WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation) in EvtDeviceAdd and polls its own
pfxusb-boot-<index> bootstrap mailbox (the delivered DATA section's pad_index is validated against it). UmdfHostProcessSharing=ProcessSharingDisabled (the INF) gives each pad its own
WUDFHost, so the per-pad SHM_INDEX static doesn't collide. Validated live: two pads → two distinct
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
index — which only routes shared memory.)