# 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. This is
  the same exposure a person looking at the physical screen has (the flag hides a window from capture
  APIs, not from the display), so it fits inside the "a client sees what someone at the screen sees"
  model rather than exceeding it; what it exceeds is an ordinary screen-*capture* tool (OBS/WGC/DDA),
  which honors the flag. **Measured, not assumed (2026-07-04, .173):** a full-screen test window was
  streamed through three 8 s phases — no flag / `WDA_EXCLUDEFROMCAPTURE` set (affinity readback `0x11`,
  confirmed active) / flag cleared — and the window was pixel-identically visible in the decoded
  punktfunk/1 stream in all three. The flag made no difference to the stream.
* **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.
