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>
12 KiB
IDD-push frame channel — security model (the sealed channel)
Status: implemented (host capture/windows/idd_push.rs + driver
packaging/windows/drivers/pf-vdisplay/src/{control,monitor,frame_transport}.rs, contract
crates/pf-driver-proto v2). Windows CI-validated; on-glass validation pending.
What is being protected
The IDD-push path moves whole-desktop frames — including the secure desktop (UAC prompts, the
lock screen) — from the pf-vdisplay driver (UMDF, running in a WUDFHost.exe under LocalService)
into the SYSTEM host for encoding. That data is SYSTEM-tier-sensitive, and because we bypass the OS
capture APIs (Desktop Duplication / WGC), we own the isolation those APIs would have provided.
DDA's isolation property is that capturer and consumer are the same process: there is no openable channel at all — to reach the frames you must own the capturing process. The sealed channel reproduces exactly that property for our two-process design.
The design
┌──────────────────────────┐ control device (SY+BA only) ┌───────────────────────────┐
│ Host (SYSTEM service) │ ── IOCTL_SET_FRAME_CHANNEL: handle ────▶ │ pf-vdisplay driver │
│ creates header/event/ │ VALUES only (integers) │ (WUDFHost, LocalService) │
│ ring textures UNNAMED, │ │ maps/opens the duplicated │
│ DuplicateHandle()s them │ ◀── frames via keyed-mutex textures ──── │ handles; publishes frames │
│ INTO WUDFHost, encodes │ (no names anywhere) │ │
└──────────────────────────┘ └───────────────────────────┘
trust boundary: only these two processes ever hold a handle to any frame object
- Every frame object is unnamed (header section, frame-ready event, all ring textures —
CreateFileMappingW/CreateEventW/CreateSharedHandlewith a null name). An unnamed object is in no namespace: it cannot be enumerated (NtQueryDirectoryObjectcan't see it), cannot be opened by name, and cannot be pre-created ("squatted"). It can be shared only by handle duplication. - The host is the broker. SYSTEM opens the driver's WUDFHost with
PROCESS_DUP_HANDLE(the pid comes from theIOCTL_ADDreply, per-monitor, so a WUDFHost restart can't leave us duplicating into a dead process) andDuplicateHandles each object in. The reverse direction — LocalService injecting into SYSTEM — is correctly denied by the OS, which is why the broker must be the host. - The bootstrap carries only integers.
IOCTL_SET_FRAME_CHANNELdelivers the duplicated handle values. A handle value is only meaningful inside the target process's handle table: a third party that read (or even forged) the message would learn nothing openable and could at most feed values that don't resolve — a DoS of its own session, not a read. The bootstrap's ACL is therefore not load-bearing; we still restrict the control device toD:P(A;;GA;;;SY)(A;;GA;;;BA)(INFSecurity), because ADD/REMOVE/CLEAR_ALL shouldn't be world-callable either.
Net result: the only way to reach the frames is to already run code as SYSTEM (the host) or inside that specific WUDFHost (the driver) — DDA's property, achieved in user mode.
Why user-mode, not a kernel driver
Ring level does not govern cross-process memory visibility — the handle/VAD access checks do; a user
process cannot ReadProcessMemory a LocalService process regardless of rings. What kernel-mode
would change is the blast radius of a driver bug: UMDF caps a pf-vdisplay compromise at the
LocalService token, a KMDF display driver would make it ring-0 full-system. Least-blast-radius is
the reason punktfunk ships zero kernel drivers (the gamepad stack dropped ViGEmBus for UMDF for
the same reason). The correct control for "SYSTEM-tier data in the channel" is sealing the channel —
done above — not raising the ring.
Handle-lifetime invariants (the auditable list)
- Frame objects unnamed; bootstrap carries only handle values. ✔ by construction
bInheritHandle: falseon every object — no child inherits a handle. ✔- Zero-init header + atomic
magic-last publish (the driver never acts on a half-initialized ring); generation-tagged publish tokens reject stale-ring frames. ✔ - Attacker-influenced header fields are bounds-checked before use (generation/seq/slot unpacking;
ring_lenclamped; the driver validatesIOCTL_SET_FRAME_CHANNELbefore adopting anything). ✔ - Adopt-on-success-only: the driver owns (and eventually closes) the delivered handles iff the
IOCTL completed successfully; on ANY error completion it leaves them untouched and the host reaps
its remote duplicates (
DUPLICATE_CLOSE_SOURCE). Exactly one side closes each value — no double-close of possibly-reused handle values, no leak on a half-delivered channel. ✔ - Single ownership inside the driver: each delivery lives in exactly one place (monitor stash →
publisher), and whichever owner dies — replaced stash, dropped publisher, removed monitor, reaped
watchdog, departed device — closes the handles (
FrameChannel/publisherDrop). Host-side objects are RAII (MappedSection,OwnedHandle); nothing survives the capturer. ✔ - The object DACL is
D:P(A;;GA;;;SY)— SYSTEM only, protected. Since the driver reaches the objects via duplicated handles (which carry their own access;OpenSharedResource1on a handle does not re-check the object DACL), the LocalService ACE was dropped — the minimal DACL. ✔ (on-glass confirmed 2026-07-03: the driver still attaches + delivers frames with SYSTEM-only objects.) - The duplication target is verified. Before duplicating frame handles into
AddReply.wudf_pid, the host confirms that pid is%SystemRoot%\System32\WUDFHost.exe(verify_is_wudfhost). A spoofed devnode advertising our interface GUID cannot redirect frames to an arbitrary process. ✔ - Handles are duplicated with least privilege, not
DUPLICATE_SAME_ACCESS. The driver's copy of the header section isSECTION_MAP_READ|WRITE(matched by the driver mappingFILE_MAP_READ|WRITE, notFILE_MAP_ALL_ACCESS), the frame-ready event isEVENT_MODIFY_STATE(the driver only signals it), and the ring textures keep their already-scopedCreateSharedHandleaccess (DXGI_SHARED_RESOURCE_READ|WRITE). So a compromised driver's handles can map/signal but cannotWRITE_DAC/WRITE_OWNER/DELETEthe objects — the "give unnamed shared objects proper (minimal) security attributes, becauseDuplicateHandlecan still reach them" discipline (Raymond Chen, devblogs 2015-06-04). Marginal here (the driver is already a trusted frame endpoint) but correct hygiene, and it applies identically to the gamepad DATA section. ✔ (on-glass confirmed 2026-07-03: the driver attaches + streamsframes=7035with the least-access header handle.)
Ring recreation (mid-session HDR flip) and host build-retries re-deliver a complete fresh handle set; the driver treats a pending delivery as newest-wins (a retry's ring is a different header mapping, whose generation bump an old publisher can never observe).
Empirical verification (2026-07-03, RTX box)
The headline claim — "reaching a frame requires already being one of the two endpoint processes" —
was tested, not just argued. A LocalService-token process (scheduled task, the sibling-service
stand-in) attempting OpenProcess on the pf_vdisplay WUDFHost was denied every access right:
PROCESS_DUP_HANDLE, PROCESS_VM_READ, PROCESS_QUERY_INFORMATION, and even
PROCESS_QUERY_LIMITED_INFORMATION → ERROR_ACCESS_DENIED. The QUERY_LIMITED denial is decisive:
it is a read-class right MIC permits across integrity levels, so its denial is a DACL exclusion of
the LocalService SID, not an integrity ceiling — meaning even a higher-integrity LocalService
service is denied (LocalService lacks SeDebugPrivilege, so it cannot bypass the DACL). Combined
with the objects being unnamed, a sibling LocalService has no reachable path to a frame: no
name to open, no way to dup the handles out of WUDFHost, no way to read WUDFHost's memory. The
baseline (an elevated admin, holding SeDebugPrivilege) opened WUDFHost freely — expected, and the
reason "admin/SYSTEM = total" stays on the residual list below.
Residual limits — the honest floor
- The virtual display is a real monitor. Any process in the interactive session can capture it through the ordinary OS APIs (DDA/WGC/BitBlt), exactly as it can capture any physical monitor. That floor is identical for every virtual-display streaming stack (Sunshine + VDD, Apollo/SudoVDA); the sealed channel keeps our transport above that floor rather than below it. This is the single most realistic way for unprivileged session code to see the streamed pixels, and it is outside our channel entirely.
- The gamepad channels are now sealed too (2026-07-03,
design/gamepad-channel-sealing.md, gamepad proto v2 — on-glass validation pending): the pad DATA sections (XusbShm/PadShm) are UNNAMED withD:P(A;;GA;;;SY), handle-duplicated into the pad's WUDFHost by the host broker (inject/windows/gamepad_raii.rsPadChannel, reusing this design'sverify_is_wudfhost+ adopt-on-success discipline), and the driver validates the mapped section's magic +pad_indexbefore use. The pad drivers have no control device (hidclass), so the handshake runs over a tiny named bootstrap mailbox (Global\pf…-boot-<index>, SY+LS,PadBootstrap) that carries only pids and a handle value — nothing exploitable; the residual is that a sibling LocalService can tamper the mailbox for a gamepad DoS (never a read or an injection; deliveries are capped, and the mailbox is squat-checked at create). The old sibling-LS read/inject vector onGlobal\pf…-shm-*is gone — the names no longer exist. - Admin / SYSTEM = total. The control device is
D:P(A;;GA;;;SY)(A;;GA;;;BA), so an admin can driveIOCTL_SET_FRAME_CHANNEL(DoS a live session) and, withSeDebugPrivilege, dup a section into WUDFHost to exfiltrate; and an admin can plant a fake devnode with our interface GUID to impersonate the driver. All admin-gated (no non-privileged escalation), but the control plane is explicitly not a boundary against admin. The host↔driver channel has no mutual authentication beyond theGET_INFOversion handshake + theverify_is_wudfhostimage check. WDA_EXCLUDEFROMCAPTUREwindows are visible. IDD-push taps the present side, not the capture side, so windows that exclude themselves from capture still appear in the stream — true of every virtual-display streaming stack. Untested on our lab box; treat as expected behavior.- DRM/HDCP: protected content is blanked by DWM at composition, and HDCP is a monitor↔GPU handshake an indirect display cannot satisfy — neither is bypassed by this path.
- IDD-push is currently the sole Windows capture path (DDA and the WGC relay were removed). An OS-mediated-capture-only mode would trade away secure-desktop capture and latency; if a deployment requires it, that's a feature request, not a toggle that exists today.