docs(windows): secure-desktop two-process design + WGC impersonation attempt (vestigial)
apple / swift (push) Successful in 55s
android / android (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled

Validated design for adding secure-desktop (UAC/lock/login) coverage on top of the shipped WGC
animation fix. Key verified constraint: WGC won't activate under SYSTEM (0x80070424) even with
thread-level ImpersonateLoggedOnUser, and DDA+SendInput on Winlogon need LOCAL_SYSTEM — so one
process can't do both. Architecture: SYSTEM host (QUIC + SudoVDA + DDA-secure + SendInput + AU mux)
+ a USER-session WGC helper (CreateProcessAsUser) that relays encoded Annex-B AUs over a named pipe;
the host muxes helper-AUs (normal desktop) vs its own DDA encoder (secure desktop), switched by a
desktop-name watcher. No shared GPU texture (rejected — MIC/keyed-mutex pain); just AU bytes.
docs/windows-secure-desktop.md has the ordered, box-testable steps.

The impersonate_active_user() in wgc.rs is kept as a harmless no-op (under a user-token process
WTSQueryUserToken fails → no impersonation → WGC works natively); it does NOT make WGC work under
SYSTEM (the two-process design uses a real user process for WGC instead). + Win32_System_RemoteDesktop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 07:08:50 +00:00
parent 589b364c01
commit 5c2bcbc2a2
3 changed files with 128 additions and 1 deletions
+84
View File
@@ -0,0 +1,84 @@
# Windows secure-desktop capture — two-process design
Status: **design validated, implementation in progress.** 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.
## The constraint (verified live on the RTX 4090)
- **WGC** (the composed-desktop capture that fixes frozen HDR animations) **will not activate under
the SYSTEM account** — `CreateForMonitor``0x80070424`. 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.