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
+236
View File
@@ -0,0 +1,236 @@
# 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-<index>`) 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-<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 `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_<index>` / `pf_pad_<index>`.
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 23'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), 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-<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 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`).
+145
View File
@@ -0,0 +1,145 @@
# 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
```
1. **Every frame object is unnamed** (header section, frame-ready event, all ring textures —
`CreateFileMappingW`/`CreateEventW`/`CreateSharedHandle` with a null name). An unnamed object is
in no namespace: it cannot be enumerated (`NtQueryDirectoryObject` can't see it), cannot be
opened by name, and cannot be pre-created ("squatted"). It can be shared **only** by handle
duplication.
2. **The host is the broker.** SYSTEM opens the driver's WUDFHost with `PROCESS_DUP_HANDLE` (the pid
comes from the `IOCTL_ADD` reply, per-monitor, so a WUDFHost restart can't leave us duplicating
into a dead process) and `DuplicateHandle`s 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.
3. **The bootstrap carries only integers.** `IOCTL_SET_FRAME_CHANNEL` delivers 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 to `D:P(A;;GA;;;SY)(A;;GA;;;BA)`
(INF `Security`), 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)
1. Frame objects unnamed; bootstrap carries only handle values. ✔ by construction
2. `bInheritHandle: false` on every object — no child inherits a handle. ✔
3. Zero-init header + atomic `magic`-last publish (the driver never acts on a half-initialized
ring); generation-tagged publish tokens reject stale-ring frames. ✔
4. Attacker-influenced header fields are bounds-checked before use (generation/seq/slot unpacking;
`ring_len` clamped; the driver validates `IOCTL_SET_FRAME_CHANNEL` before adopting anything). ✔
5. **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. ✔
6. 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`/publisher `Drop`). Host-side
objects are RAII (`MappedSection`, `OwnedHandle`); nothing survives the capturer. ✔
7. 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; `OpenSharedResource1` on 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.)*
8. **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. ✔
9. **Handles are duplicated with least privilege, not `DUPLICATE_SAME_ACCESS`.** The driver's copy of
the header section is `SECTION_MAP_READ|WRITE` (matched by the driver mapping `FILE_MAP_READ|WRITE`,
not `FILE_MAP_ALL_ACCESS`), the frame-ready event is `EVENT_MODIFY_STATE` (the driver only signals
it), and the ring textures keep their already-scoped `CreateSharedHandle` access
(`DXGI_SHARED_RESOURCE_READ|WRITE`). So a compromised driver's handles can map/signal but cannot
`WRITE_DAC`/`WRITE_OWNER`/`DELETE` the objects — the "give unnamed shared objects proper (minimal)
security attributes, because `DuplicateHandle` can 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 + streams `frames=7035` with 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 with `D:P(A;;GA;;;SY)`, handle-duplicated into the pad's WUDFHost by the host broker
(`inject/windows/gamepad_raii.rs` `PadChannel`, reusing this design's `verify_is_wudfhost` +
adopt-on-success discipline), and the driver validates the mapped section's magic + `pad_index`
before 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 on
`Global\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 drive
`IOCTL_SET_FRAME_CHANNEL` (DoS a live session) and, with `SeDebugPrivilege`, 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 the `GET_INFO`
version handshake + the `verify_is_wudfhost` image check.
* **`WDA_EXCLUDEFROMCAPTURE` windows 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.