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>
43 lines
2.3 KiB
Rust
43 lines
2.3 KiB
Rust
//! Windows display-topology helpers (plan §W6), extracted from the host's `windows/{win_display,
|
|
//! monitor_devnode,display_events}.rs` so the IDD-push capturer (`pf-capture`) and the pf-vdisplay
|
|
//! backend (the host) depend on them as a leaf PEER instead of the capturer reaching back into the
|
|
//! orchestrator. Windows-only; compiles to an empty lib elsewhere.
|
|
//!
|
|
//! - [`win_display`]: CCD/GDI path activation, mode-setting, HDR advanced-colour toggles, and the
|
|
//! source-desktop geometry the capturer duplicates.
|
|
//! - [`monitor_devnode`]: PnP monitor devnode enable/disable (the parallel-display isolation lever).
|
|
//! - [`display_events`]: the `WM_DISPLAYCHANGE` / device-arrival watch that lets a capture stall say
|
|
//! whether an OS display event coincided with it.
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub mod display_events;
|
|
/// Bind display-config writes to the input desktop so a UAC / lock screen can't refuse them.
|
|
#[cfg(target_os = "windows")]
|
|
mod input_desktop;
|
|
#[cfg(target_os = "windows")]
|
|
pub mod monitor_devnode;
|
|
#[cfg(target_os = "windows")]
|
|
pub mod win_display;
|
|
|
|
/// `Some((own_session, console_session))` when this process is NOT in the active console session —
|
|
/// the state where every `SetDisplayConfig`/CDS write fails `ERROR_ACCESS_DENIED`, GDI reads
|
|
/// describe the wrong session's displays, and input compose kicks go nowhere (the lid-closed /
|
|
/// non-console field failure mode). The IDD-push capturer uses it to phrase a diagnostic when the
|
|
/// driver won't attach. (A byte-copy of the host's `interactive::console_session_mismatch`: it lives
|
|
/// here too so `pf-capture` gets it as a leaf peer instead of reaching into the orchestrator.)
|
|
#[cfg(target_os = "windows")]
|
|
pub fn console_session_mismatch() -> Option<(u32, u32)> {
|
|
use windows::Win32::System::RemoteDesktop::{
|
|
ProcessIdToSessionId, WTSGetActiveConsoleSessionId,
|
|
};
|
|
use windows::Win32::System::Threading::GetCurrentProcessId;
|
|
let mut own: u32 = 0;
|
|
// SAFETY: `own` is a live local out-param for this synchronous call; no pointer escapes it.
|
|
if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut own) }.is_err() {
|
|
return None;
|
|
}
|
|
// SAFETY: takes no arguments and returns the console session id by value.
|
|
let console = unsafe { WTSGetActiveConsoleSessionId() };
|
|
(console != 0xFFFF_FFFF && own != console).then_some((own, console))
|
|
}
|