[fix] restore remote input for elevated Windows apps

This commit is contained in:
dijunkun
2026-10-09 16:02:27 +08:00
parent a06ed5806a
commit 5a7e816fb7
5 changed files with 53 additions and 11 deletions
@@ -238,8 +238,11 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
SetLastError(ERROR_SUCCESS);
const bool sent = owner_.devices_.SendKeyboardCommand(
key_code, is_down, scan_code, extended);
const DWORD error = GetLastError();
const bool blocked =
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
return DesktopInputResult{
sent, IsDesktopTransitionInputError(GetLastError())};
sent, IsDesktopTransitionInputError(error) || blocked, blocked};
},
[&]() -> DesktopInputResult {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
@@ -404,8 +404,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
SetLastError(ERROR_SUCCESS);
const bool sent = runtime->devices_.SendMouseCommand(
remote_action, runtime->selected_display_);
const DWORD error = GetLastError();
const bool blocked =
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
return DesktopInputResult{
sent, IsDesktopTransitionInputError(GetLastError())};
sent, IsDesktopTransitionInputError(error) || blocked, blocked};
},
[&]() -> DesktopInputResult {
int absolute_x = 0;
@@ -39,6 +39,9 @@ struct DesktopInputResult {
// A pipe timeout may have happened after a click/key was applied: never
// replay it.
bool not_injected = false;
// The ordinary desktop injector rejected its single input event (for
// example UIPI). The authorized service can retry even on the same desktop.
bool retry_with_service = false;
};
template <typename ProbeUserDesktop, typename SendUser, typename SendSecure>
@@ -48,6 +51,10 @@ bool DispatchDesktopInput(ProbeUserDesktop probe_user, SendUser send_user,
const auto result = user ? send_user() : send_secure();
if (result.delivered) return true;
if (!result.not_injected) return false;
if (user && result.retry_with_service) {
// Do not replay a failed/ambiguous service request through another path.
return send_secure().delivered;
}
const bool current_user = probe_user();
if (current_user == user) return false;
// Handle a switch between the probe and injection exactly once. Ordering is
@@ -38,6 +38,7 @@ constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
constexpr ULONGLONG kSasSecureDesktopGraceMs = 15000;
constexpr ULONGLONG kUserDesktopInputIdleMs = 5000;
using SendSasFunction = VOID(WINAPI*)(BOOL);
@@ -1057,6 +1058,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_state_age_ms_ = 0;
session_helper_report_uptime_ms_ = 0;
secure_input_helper_started_at_tick_ = 0;
user_desktop_input_until_tick_ = 0;
sas_secure_desktop_until_tick_ = 0;
session_helper_process_handle_ = nullptr;
session_helper_stop_event_ = nullptr;
@@ -1460,6 +1462,7 @@ void CrossDeskServiceHost::RefreshSessionState() {
ResetSessionHelperReportedStateLocked("session_changed", 0);
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
user_desktop_input_until_tick_ = 0;
}
process_session_id_ = process_session;
logon_ui_visible_ = logon_ui;
@@ -1574,7 +1577,8 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
return false;
}
return HasSecureInputUiLocked() || GetEffectiveSessionLockedLocked();
return HasSecureInputUiLocked() || GetEffectiveSessionLockedLocked() ||
GetTickCount64() < user_desktop_input_until_tick_;
}
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
@@ -1622,6 +1626,10 @@ std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
return "Default";
}
if (interactive_stage == "user-desktop") {
return "Default";
}
if (input_desktop_available_ && !input_desktop_name_.empty() &&
(interactive_stage == "credential-ui" || consent_ui_visible_)) {
return input_desktop_name_;
@@ -2174,8 +2182,12 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
ResetSessionHelperReportedStateLocked("session_changed", 0);
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
user_desktop_input_until_tick_ = 0;
}
if (session_id == active_session_id_) {
if (event_type == WTS_SESSION_LOGOFF) {
user_desktop_input_until_tick_ = 0;
}
// Do not let an older helper sample override a session notification.
ResetSessionHelperReportedStateLocked("session_event", 0);
last_session_transition_tick_ = GetTickCount64();
@@ -2501,20 +2513,28 @@ std::string CrossDeskServiceHost::ResolveSecureInputTarget(
{
std::lock_guard<std::mutex> lock(state_mutex_);
target.session_id = active_session_id_;
if (target.session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target.session_id != client_session_id ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
target.stage = ResolveInteractiveStageLocked();
target.desktop = ResolveInteractiveDesktopLocked(target.stage);
helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target.session_id;
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
if (target.stage == "user-desktop") {
// Ordinary GUI input can be blocked by an elevated foreground window.
// Only a Windows-validated client in the active console session may
// start/retain this helper; do not misreport the desktop as UAC/locked.
user_desktop_input_until_tick_ =
GetTickCount64() + kUserDesktopInputIdleMs;
can_inject = true;
}
}
if (target.session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target.session_id != client_session_id ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
WakeSessionState();
return BuildErrorJson("secure_input_not_active");
@@ -2799,8 +2819,14 @@ std::string QueryDesktopInput(const std::string& command, DWORD timeout_ms) {
const std::string error = json.value("error", "");
// Retry only explicit pre-injection rejections during helper startup.
// Never replay an uncertain timeout or a partially injected mouse action.
if (!IsDesktopInputSetupPending(error) || IsCurrentSessionUserDesktopActive())
// The SYSTEM helper may now be starting for an elevated application on
// Default too. Old services still reject that with secure_input_not_active;
// preserve their ordinary-desktop return instead of retrying indefinitely.
if (!IsDesktopInputSetupPending(error) ||
(error == "secure_input_not_active" &&
IsCurrentSessionUserDesktopActive())) {
return response;
}
const auto remaining = pipe_deadline_detail::Remaining(deadline);
if (remaining == 0) return response;
Sleep((std::min)(DWORD{10}, remaining));
@@ -137,6 +137,9 @@ class CrossDeskServiceHost {
ULONGLONG session_helper_report_state_age_ms_ = 0;
ULONGLONG session_helper_report_uptime_ms_ = 0;
ULONGLONG secure_input_helper_started_at_tick_ = 0;
// Short idle grace after a validated user-desktop input request. This keeps
// the SYSTEM helper available for elevated windows such as Task Manager.
ULONGLONG user_desktop_input_until_tick_ = 0;
ULONGLONG sas_secure_desktop_until_tick_ = 0;
bool session_locked_ = false;
bool logon_ui_visible_ = false;