Files
punktfunk/docs/windows-secure-desktop.md
T
enricobuehler 5c2bcbc2a2
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
docs(windows): secure-desktop two-process design + WGC impersonation attempt (vestigial)
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>
2026-06-16 07:08:50 +00:00

85 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.