Files
punktfunk/docs/windows-secure-desktop.md
T
enricobuehler 140209bbfc feat(host/windows): two-process secure-desktop step 5 — DDA mux on Winlogon
`virtual_stream_relay` now muxes the AU source by input desktop. A DesktopWatcher
(SYSTEM-only Winlogon-name poll) drives it: the user-session WGC helper relay
feeds the normal (Default) desktop; the host's OWN DDA capturer+encoder — opened
lazily on the first secure transition, on the same SudoVDA target with a no-op
keepalive (the host still holds the real isolation owner) — captures the secure
(Winlogon: UAC/lock/login) desktop that WGC can't see. Every switch latches
"wait for IDR" and forces the now-active source to emit a keyframe (the two
encoders keep independent infinite-GOP state, so the client must resume on an
IDR); returning to the helper also drains its stale buffered AUs first.
Reconfigure drops the stale-target DDA; keyframe requests route to the live
source. Send path (FEC/seal/paced-send) unchanged.

Also: wgc_relay gains try_recv (drain on switch-back); open_dda takes dims as
args (avoids a closure borrow of the reassigned cur_mode); the forward! macro
returns bool with `break 'outer` at the call site (no in-macro label hygiene).

cfg-gated windows-only. Live validation (UAC switch over a session) pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:55:29 +00:00

7.8 KiB
Raw Blame History

Windows secure-desktop capture — two-process design

Status: steps 15 implemented (compiles on the 4090); live validation pending. The WGC animation fix ships and works (host in user-mode); this doc is the plan for adding secure-desktop (UAC / lock / login) coverage on top of it, since WGC and the secure desktop need conflicting process tokens.

Implemented so far:

  • Step 1 — DesktopWatcher (capture/desktop_watch.rs): polls the input-desktop name → atomic Default/Winlogon. Committed 80e222d.
  • Step 3 — WGC helper subcommand (wgc_helper.rs, m3-host wgc-helper): WGC→NVENC→framed AUs on stdout, stdin keyframe control. Committed a0f6cdd.
  • Step 4 — spawn + relay (capture/wgc_relay.rs, m3::virtual_stream_relay): SYSTEM host spawns the helper via CreateProcessAsUserW into winsta0\default, relays its stdout AUs to the QUIC send thread, forwards keyframe requests, surfaces helper stderr in host tracing. Committed 9f50b39.
  • Step 5 — source mux (m3::virtual_stream_relay): the DesktopWatcher switches the AU source — helper relay on Default, the host's own DDA capturer+encoder on Winlogon; every switch latches "wait for IDR" + forces the now-active source to emit a keyframe.

Remaining: step 6 (helper relaunch watchdog on console connect/disconnect + crash, then a lock/unlock+UAC soak) and step 2 (SendInput retry-on-failure refactor — input works today via the existing path; this hardens it across the desktop boundary).

Live validation to run when the box is up (single session, host as SYSTEM via the -s -i 1 scheduled task): connect a client → confirm video via the helper relay on the normal desktop (host log source switch … normal(WGC relay) + WGC helper spawned), trigger a UAC prompt → the stream shows the UAC dialog (host log source switch … secure(DDA)), dismiss → back to the helper; the QUIC session stays up throughout.

The constraint (verified live on the RTX 4090)

  • WGC (the composed-desktop capture that fixes frozen HDR animations) will not activate under the SYSTEM accountCreateForMonitor0x80070424. Thread-level ImpersonateLoggedOnUser is insufficient (tested: impersonated=true, still 0x80070424). WGC needs the process to run as the interactive user.
  • DDA + SendInput on the secure desktop (Winlogon: UAC/lock/login) require LOCAL_SYSTEM (attach to the Winlogon desktop). This is already shipped (task #17) when the host runs as SYSTEM.
  • Therefore one process can't do both. Single-process (the simpler design) is out.

Architecture: SYSTEM host + USER-session WGC helper, AU-relay (no shared GPU texture)

  • SYSTEM host (the existing m3-host, launched as SYSTEM in interactive Session 1 via the scheduled task → PsExec -s -i 1): owns the punktfunk/1 QUIC session, the single SudoVDA virtual output (+ isolate/restore RAII — the only topology owner), the DDA capture + NVENC encoder for the secure desktop, the single SendInput injector (serves both desktops), and the AU source mux that feeds the QUIC data plane.
  • USER-session WGC helper (a new m3-host subcommand, spawned by the SYSTEM host via WTSQueryUserToken(activeConsoleSessionId)DuplicateTokenEx(TokenPrimary)CreateProcessAsUserW(lpDesktop="winsta0\\default", CREATE_NO_WINDOW)): runs the existing WGC → scRGB/PQ → NVENC pipeline and ships Annex-B AUs ({data, pts_ns, keyframe}) to the SYSTEM host over a named pipe. It captures the SAME SudoVDA output by GDI name only — it must NOT create its own virtual output / touch display topology (a second topology owner re-triggers the ACCESS_LOST born-lost storm).
  • Mux: the SYSTEM host relays the helper's AUs onto QUIC while the input desktop is Default (normal — WGC, HDR/animation-correct), and switches to its own DDA encoder while it's Winlogon (secure — UAC/lock/login). The client sees one continuous stream; the encoder/FEC/AES-GCM/QUIC send path is untouched (same EncodedFrame flow). NVENC re-inits only on a size/format change across the swap (already handled); same-mode is a pointer re-register.
  • Input: stays entirely in the SYSTEM host (only it can attach to Winlogon). One windowless SendInput thread, Sunshine's retry-on-failure-only model (cache HDESK thread-local; SendInput first; only on 0-injected re-OpenInputDesktop+SetThreadDesktop and retry once) — serves both desktops with no per-event reattach. (Ctrl+Alt+Del/SAS needs SendSAS, out of scope; clicking UAC Yes/No + typing the login password are plain SendInput on Winlogon.)

Rejected: a shared NT-handle GPU texture (MIC/SDDL pain SYSTEM→user, keyed-mutex ring at 240 Hz, nvenc pointer-cache churn — all for a static lock dialog). AU bytes over a pipe are far simpler.

Detection

DesktopWatcher: a dedicated thread polling the input-desktop NAME at 3060 Hz — OpenInputDesktop(0,FALSE,0) + GetUserObjectInformationW(UOI_NAME) == "Winlogon" (secure) vs "Default" (normal) → Arc<AtomicU8>. This is the authoritative signal; WTS session notifications miss UAC entirely. (May also register WTSRegisterSessionNotification to short-circuit lock/unlock.)

Implementation steps (each independently buildable/testable on the 4090)

  1. DesktopWatcher (capture/desktop_watch.rs, ~40 lines): the poll + atomic. Test: lock / trigger UAC over the existing stream, confirm the atomic flips Default↔Winlogon within a poll interval.
  2. SendInput retry-on-failure model (inject/sendinput.rs): replace per-event reattach with the cached-HDESK + retry-once model. Test: normal input unchanged; click UAC + type the lock password land (works today via per-event reattach — this is a refactor).
  3. WGC helper subcommand (m3-host wgc-helper or similar): the existing WGC pipeline → NVENC → Annex-B AUs over a named-pipe server. Test standalone: as the user it writes a valid .h265 to the pipe (capturing the SudoVDA output by GDI name, no topology changes).
  4. Spawn + relay: SYSTEM host spawns the helper (CreateProcessAsUserW), connects the pipe, relays its AUs onto the live QUIC session. Test: normal-desktop stream sourced via the helper relay.
  5. Source mux: relay helper AUs while Default, switch to the host's own DDA encoder while Winlogon (reusing the DesktopWatcher). Test: normal (WGC, HDR) → trigger UAC → stream shows the UAC dialog (DDA) → dismiss → back to WGC; QUIC session stays up throughout. Full-coverage milestone.
  6. Relaunch watchdog + soak: SERVICE_CONTROL_SESSIONCHANGE-style relaunch of the helper on console connect/disconnect; soak a few hundred lock/unlock+UAC switches (cf. task #17's 1012-switch run) — no leak / black / disconnect. Cargo features for the fallback: Win32_System_Threading, Win32_System_Pipes, Win32_System_RemoteDesktop.

Risks / notes

  • Validate on the real 4090 only (ssh "Enrico Bühler"@192.168.1.174, Session 1 via the Interactive scheduled task) — the headless build VM can't reproduce Winlogon-on-virtual-display or WGC.
  • The helper MUST capture the SudoVDA by GDI name and never create a second virtual output (avoids the ACCESS_LOST born-lost storm — one isolate owner = the SYSTEM host).
  • Confirm reisolate fires on a FRESH mid-session DDA open at the desktop boundary (task #17 only validated DDA recovery within an already-DDA session).
  • Brief one-frame repeat/flicker at the WGC↔DDA boundary is acceptable (the local lock/UAC transition flickers too); never starve the encoder (repeat last frame across the swap gap).
  • Pragmatic alternative if full coverage isn't worth the build: PromptOnSecureDesktop=0 (UAC renders on the normal desktop → WGC captures it) covers UAC (not lock/login) with one reversible registry change.