Files
punktfunk/crates/pf-win-display/Cargo.toml
T
enricobuehlerandClaude Opus 4.8 b5fa878bc6 fix(pf-win-display): follow the input desktop so a UAC prompt can't refuse display writes
Field-reported 2026-07-23: a consent prompt left up on an unattended host made
the box unreachable. Every connect black-screened and failed after ~60 s, and
the host had to be reached over SSH to clear the prompt.

Windows refuses ChangeDisplaySettingsEx/SetDisplayConfig issued from a thread
that is not on the desktop currently receiving input. While UAC (or the lock /
logon screen) is up that desktop is Winlogon, and the host — which the service
launches explicitly onto WinSta0\Default — got DISP_CHANGE_FAILED /
ERROR_ACCESS_DENIED for every write. A session starting in that window could
never set its virtual display's mode, so the capturer sized its ring to a stale
mode, every composed frame was dropped for a size mismatch, and bring-up burned
8 retries before giving up.

Measured on glass (RTX box, SYSTEM host in console session 2, real consent
prompt up, virtual display active):

    INPUT desktop = Winlogon
    UNBOUND  CDS_TEST      -> -1 (DISP_CHANGE_FAILED)
    UNBOUND  SDC_VALIDATE  -> 0x5 (ERROR_ACCESS_DENIED)
    BOUND    CDS_TEST      ->  0 (DISP_CHANGE_SUCCESSFUL)
    BOUND    SDC_VALIDATE  -> 0x0 (ERROR_SUCCESS)

New input_desktop.rs mirrors pf-inject's sendinput.rs retry model: issue the
write, and only when it fails the way a wrong-desktop write fails, rebind this
thread to the current input desktop and retry once. A working write is never
touched, so the normal path is unchanged. The retry predicate stays narrow
(ERROR_ACCESS_DENIED only) so the unrelated 0x57 exclusive-mode topology bug is
not re-issued and mis-attributed. Unlike sendinput's dedicated injector thread,
the binding here is SCOPED — a shared display-write thread left on a Winlogon
desktop that is later destroyed would fail every subsequent write, which is the
wedge this removes.

Applied to all eight SetDisplayConfig sites and both ChangeDisplaySettingsExW
calls in set_active_mode. The isolate site now decides its supplied config once,
outside the write, so a retry re-issues the identical config rather than logging
its escalation twice.

A save is logged, because a silent one is indistinguishable in a field log from
a write that never needed saving — which made the first on-glass verification of
this change inconclusive.

Both ACCESS_DENIED diagnostics now ask which cause applies instead of naming
only the disconnected-RDP one: that phrasing sent this investigation chasing a
phantom RDP session on a host that was already service-launched in the console
session, while a consent prompt was the actual cause.

Verified on glass: connect with a consent prompt up now reaches first frame in
3.0 s (was a black screen then "Connection failed" after ~60 s), with six
"retried bound to it and it applied desktop=Winlogon" lines and rc=0x0
throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 00:36:51 +02:00

39 lines
1.9 KiB
TOML

# The Windows display-topology cluster (plan §W6): CCD/GDI path activation, mode-setting, HDR
# advanced-colour toggles, source-rect geometry ([`win_display`]); PnP monitor devnode enable/disable
# ([`monitor_devnode`]); and the WM_DISPLAYCHANGE / device-arrival watch ([`display_events`]). A leaf
# so the IDD-push capturer (pf-capture) and the pf-vdisplay backend (host) depend on it as a PEER
# instead of the capturer reaching back into the host for display utilities. Windows-only content;
# compiles to an empty lib elsewhere.
[package]
name = "pf-win-display"
version.workspace = true
edition = "2021"
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host Windows display-topology helpers: CCD/GDI mode-set + path activation, HDR advanced colour, PnP monitor devnodes, and the display-change event watch."
publish = false
[target.'cfg(target_os = "windows")'.dependencies]
# `Mode` (the negotiated display mode) is the core wire type; `pf-paths` for the pnp-disabled-monitors
# state file.
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
pf-paths = { path = "../pf-paths" }
anyhow = "1"
tracing = "0.1"
# The pnp-disabled-monitors state file (a `Vec<String>` of instance ids) is serialized as JSON.
serde_json = "1"
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_Devices_DeviceAndDriverInstallation",
"Win32_Devices_Display",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
"Win32_System_LibraryLoader",
# console_session_mismatch: WTSGetActiveConsoleSessionId + ProcessIdToSessionId + GetCurrentProcessId.
"Win32_System_RemoteDesktop",
# input_desktop: OpenInputDesktop/SetThreadDesktop/GetUserObjectInformationW — display writes
# follow the input desktop so a UAC/lock screen can't refuse them.
"Win32_System_StationsAndDesktops",
"Win32_System_Threading",
] }