fix(windows): protect installer handoff and use native service control (#16562)

* fix(windows): use native APIs for manual service control

rustdesk/rustdesk#16557

Replace elevated service-control batch scripts with SCM operations
and native tray shortcuts. Preserve SYSTEM configuration import
and propagate helper failures.

* fix(windows): preserve service state during manual control

Keep existing service registration and synchronize failed operations with SCM state. Run the installed legacy importer directly as SYSTEM, bound its exit waits, and keep auxiliary cleanup errors separate from service lifecycle results.

* fix(windows): preserve legacy service import without debug privilege

* fix(windows): replace installer certutil and findstr

Use native SHA-256 verification and shortcut decoding, and execute the protected batch script in a single cmd process. Preserve protected staging, verification receipts and failure exit codes.

Restore the master service-control flow and remove the separate SCM and Task Scheduler implementation to keep the antivirus fix focused and preserve legacy configuration import.

Validation: Windows Rust library build, nine focused tests, and isolated single-cmd success, failure, tamper rejection and cleanup checks passed. Bitdefender validation remains pending.

* fix(windows): preserve installer helper path compatibility

* fix(windows): preserve caret-only installer helper paths

* fix(windows): protect installer data and use native service control

Stage installer scripts, shortcuts and verified updates with restricted ACLs
before elevation. Remove the elevated portable-verifier launch while keeping
certutil and findstr out of the shared installer handoff.

Use the installed Flutter runner for native normal-service create/start and
stop/delete operations. Preserve existing registrations on repeated setup,
legacy configuration import, and compatibility with older installed runners.
Report installer failures and document the security and compatibility reasons.

Validation: Windows Rust test build and Release runner build passed; six
isolated service checks and runner exit-code checks passed. Focused Rust
installer/update tests passed 8/9; the directory-cleanup ACL test also fails in
the earlier binary. Final service lifecycle and Bitdefender testing is pending.

* fix(windows): clean staged installer files on UAC cancellation

Retain DELETE handles until the elevated cmd connects to a local pipe, then
release the handles before allowing the BAT to run. This preserves protected
staging while allowing cancellation cleanup and cmd file access.

Handle interrupted handoffs, supply the byte-count pointer required on
Windows 7, and explain early service dispatch in the C++ runner. Keep the
staging tests inline with their implementation.

Validation: Windows Rust test build, focused installer-file and handoff
tests, rustfmt, and staged diff checks passed. The pre-existing directory
ACL test failure is unchanged. Actual UAC cancellation, Windows 7 runtime,
and final-build Bitdefender validation remain pending.

* fix(windows): roll back new service registration on start failure

Delete a registration created by this attempt if startup fails, while preserving existing services and already-running success. Keep the original startup error and report rollback failures separately.

Validation: eight isolated service checks and the Release Flutter runner build passed. Real service startup failure and final-build Bitdefender validation remain pending.

* fix(windows): continue uninstall after service stop errors

Log an unsuccessful stop and still request deletion so the uninstall
script can reach its existing taskkill and cleanup commands. Treat an
already marked service as a satisfied deletion request, while preserving
other deletion errors and the normal stop wait.

Validation: all twelve isolated service-control checks passed, including
already-marked, stop-rejected, stop-timeout and deletion-denied cases.
The Release Flutter runner build and existing argument/exit-code checks
passed. Real SCM failure scenarios and Bitdefender remain unverified.

* fix(windows): finish installer setup before service startup

Complete SAS policy and printer setup before native service activation so a startup failure cannot skip independent installation steps. Preserve the startup error, rollback and legacy command sequences, and document uninstall_service's continuation result.

Validation: Windows Rust test compilation, four isolated command cases and two existing installer handoff tests passed. CodeRabbit completed with no findings. Actual installer, SCM and Bitdefender validation remains pending.

* fix(windows): use native service control for Sciter

Share the existing native service operations with Sciter and dispatch
--service-control before UI initialization. Keep Flutter's entry point and
the legacy commands for a different installed executable unchanged.

Validated the Windows x64 Sciter build, Flutter-feature test build,
x86 C++ bridge compilation, and real EXE/BAT error propagation. Final
Bitdefender installation and service lifecycle validation remain pending.

* fix(windows): propagate negative service-control exit codes

Treat every nonzero service-control exit status as a failure so crash and loader error codes cannot be reported as success. Verified six real cmd.exe exit-code cases; CodeRabbit review completed with no findings. Actual service crashes were not exercised.

* fix(windows): handle UAC cancellation and scope service rollback

Preserve the typed elevation error and its diagnostic details so canceled UAC prompts do not show an installation-failure dialog.

Use create-service for normal installation and create-service-with-rollback for manual Start Service. Retain a new registration after a normal installation startup error; roll back only the manual path that restores stop-service=Y.

Validation: Windows Rust test compilation, Release Flutter runner and Sciter builds, cancellation/error-chain checks, isolated service-result cases, and real executable argument/exit propagation checks passed. Actual UAC cancellation and Bitdefender were not rerun.
This commit is contained in:
fufesou authored and GitHub committed 2026-10-22 13:34:49 +08:00
1 parent f010d33ca6
commit 2b6fedfc8e
13 files changed
+1103 -360

No files matched your search

+6 -1
View File
@@ -2,7 +2,12 @@
fn build_windows() {
let file = "src/platform/windows.cc";
let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows");
let mut build = cc::Build::new();
if !cfg!(feature = "flutter") {
build.cpp(true).std("c++17").define("RUSTDESK_SCITER", None);
println!("cargo:rerun-if-changed=flutter/windows/runner/service_control.h");
}
build.file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file);
println!("cargo:rerun-if-changed={}", file2);
+4
View File
@@ -10,6 +10,7 @@
#include "win32_desktop.h"
#include "flutter_window.h"
#include "utils.h"
#include "service_control.h"
typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*);
typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int);
@@ -23,6 +24,9 @@ const wchar_t* getWindowClassName();
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command)
{
if (auto result = service_control::HandleCommand(command_line)) {
return static_cast<int>(*result);
}
HINSTANCE hInstance = LoadLibraryA("librustdesk.dll");
if (!hInstance)
{
+183
View File
@@ -0,0 +1,183 @@
#ifndef RUSTDESK_RUNNER_SERVICE_CONTROL_H_
#define RUSTDESK_RUNNER_SERVICE_CONTROL_H_
#include <windows.h>
#include <shellapi.h>
#include <winsvc.h>
#include <cstdio>
#include <cwchar>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
namespace service_control {
using ServiceHandle = std::unique_ptr<std::remove_pointer_t<SC_HANDLE>,
decltype(&CloseServiceHandle)>;
constexpr DWORD kStopTimeoutMilliseconds = 30000;
constexpr DWORD kPollIntervalMilliseconds = 100;
inline DWORD StopService(SC_HANDLE service) {
SERVICE_STATUS_PROCESS status{};
DWORD bytes_needed = 0;
if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
reinterpret_cast<BYTE*>(&status), sizeof(status),
&bytes_needed)) {
return GetLastError();
}
if (status.dwCurrentState == SERVICE_STOPPED) {
return ERROR_SUCCESS;
}
if (status.dwCurrentState != SERVICE_STOP_PENDING) {
SERVICE_STATUS stopped{};
if (!ControlService(service, SERVICE_CONTROL_STOP, &stopped)) {
const DWORD error = GetLastError();
return error == ERROR_SERVICE_NOT_ACTIVE ? ERROR_SUCCESS : error;
}
}
// The installer may remove files next; accepting STOP alone does not mean
// the service has reached SERVICE_STOPPED.
const ULONGLONG started = GetTickCount64();
while (status.dwCurrentState != SERVICE_STOPPED) {
if (GetTickCount64() - started >= kStopTimeoutMilliseconds) {
return ERROR_TIMEOUT;
}
Sleep(kPollIntervalMilliseconds);
if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
reinterpret_cast<BYTE*>(&status), sizeof(status),
&bytes_needed)) {
return GetLastError();
}
}
return ERROR_SUCCESS;
}
inline DWORD RemoveService(const wchar_t* name) {
ServiceHandle manager(OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT),
CloseServiceHandle);
if (!manager) {
return GetLastError();
}
ServiceHandle service(
OpenServiceW(manager.get(), name,
SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE),
CloseServiceHandle);
if (!service) {
const DWORD error = GetLastError();
return error == ERROR_SERVICE_DOES_NOT_EXIST ? ERROR_SUCCESS : error;
}
const DWORD stopped = StopService(service.get());
if (stopped != ERROR_SUCCESS) {
// The uninstall script must still reach its taskkill cleanup.
std::fwprintf(stderr,
L"Service did not stop cleanly: Windows error %lu\n", stopped);
}
if (DeleteService(service.get())) {
return ERROR_SUCCESS;
}
const DWORD error = GetLastError();
return error == ERROR_SERVICE_MARKED_FOR_DELETE ? ERROR_SUCCESS : error;
}
inline DWORD InstallService(const wchar_t* name, const wchar_t* executable,
bool rollback_on_failure) {
if (!*executable || wcschr(executable, L'"')) {
return ERROR_INVALID_PARAMETER;
}
const std::wstring binary_path =
L"\"" + std::wstring(executable) + L"\" --service";
const std::wstring display_name = std::wstring(name) + L" Service";
ServiceHandle manager(
OpenSCManagerW(nullptr, nullptr,
SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE),
CloseServiceHandle);
if (!manager) {
return GetLastError();
}
ServiceHandle service(
CreateServiceW(manager.get(), name, display_name.c_str(),
SERVICE_START | DELETE, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
binary_path.c_str(), nullptr,
nullptr, nullptr, nullptr, nullptr),
CloseServiceHandle);
const bool created = service != nullptr;
// --after-install can reuse a registration. Preserve its executable and
// account, as the previous create-then-start script did.
if (!service && GetLastError() == ERROR_SERVICE_EXISTS) {
service.reset(OpenServiceW(manager.get(), name, SERVICE_START));
}
if (!service) {
return GetLastError();
}
if (StartServiceW(service.get(), 0, nullptr)) {
return ERROR_SUCCESS;
}
const DWORD error = GetLastError();
// Only manual Start Service restores stop-service=Y on failure. Other
// installation paths retain the registration for a later startup attempt.
if (rollback_on_failure && created && error != ERROR_SERVICE_ALREADY_RUNNING &&
!DeleteService(service.get())) {
const DWORD rollback_error = GetLastError();
std::fwprintf(stderr,
L"Failed to roll back newly created service: Windows error %lu\n",
rollback_error);
}
return error == ERROR_SERVICE_ALREADY_RUNNING ? ERROR_SUCCESS : error;
}
inline DWORD RunCommand(int count, wchar_t** args) {
constexpr int kRemoveArgumentCount = 4;
constexpr int kInstallArgumentCount = 5;
if (count < kRemoveArgumentCount) {
return ERROR_BAD_ARGUMENTS;
}
if (!*args[3]) {
return ERROR_INVALID_PARAMETER;
}
const std::wstring_view operation(args[2]);
const bool rollback_on_failure = operation == L"create-service-with-rollback";
if ((operation == L"create-service" || rollback_on_failure) &&
count == kInstallArgumentCount) {
return InstallService(args[3], args[4], rollback_on_failure);
}
if (operation == L"remove" && count == kRemoveArgumentCount) {
return RemoveService(args[3]);
}
return ERROR_BAD_ARGUMENTS;
}
// Keep these commands in the existing runner so installation/removal can control
// services before loading librustdesk.dll or initializing the UI. A Rust-side
// handler would first require that DLL to load. The header-only implementation
// keeps main.cpp as a dispatch hook without adding a CMake source entry; it is
// compiled into the existing EXE, not a separately packaged helper.
inline std::optional<DWORD> HandleCommand(const wchar_t* command_line) {
constexpr std::wstring_view option = L"--service-control";
const std::wstring_view arguments(command_line);
if (arguments.compare(0, option.size(), option) != 0 ||
(arguments.size() > option.size() && arguments[option.size()] != L' ' &&
arguments[option.size()] != L'\t')) {
return std::nullopt;
}
int count = 0;
wchar_t** args = CommandLineToArgvW(GetCommandLineW(), &count);
if (!args) {
return GetLastError();
}
const DWORD result = RunCommand(count, args);
LocalFree(args);
if (result != ERROR_SUCCESS) {
std::fwprintf(stderr,
L"RustDesk service control failed with Windows error %lu\n",
result);
}
return result;
}
} // namespace service_control
#endif // RUSTDESK_RUNNER_SERVICE_CONTROL_H_
+4
View File
@@ -18,6 +18,10 @@ fn main() {
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
fn main() -> std::process::ExitCode {
#[cfg(windows)]
if let Some(exit_code) = platform::handle_service_control() {
return exit_code;
}
#[cfg(all(windows, not(feature = "inline")))]
unsafe {
winapi::um::shellscalingapi::SetProcessDpiAwareness(2);
+17
View File
@@ -15,6 +15,23 @@
#include <sddl.h>
#include <memory>
#ifdef RUSTDESK_SCITER
// Share the runner's service operations without loading either UI framework.
#include "../../flutter/windows/runner/service_control.h"
extern "C" DWORD run_service_control()
{
int count = 0;
wchar_t** args = CommandLineToArgvW(GetCommandLineW(), &count);
if (!args) {
return GetLastError();
}
const DWORD result = service_control::RunCommand(count, args);
LocalFree(args);
return result;
}
#endif
extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id);
void flog(char const *fmt, ...)
+138 -35
View File
@@ -97,6 +97,7 @@ use windows_service::{
use winreg::{enums::*, RegKey};
mod acl;
pub(crate) mod installer_files;
mod installer_handoff;
mod installer_shell;
mod verified_update;
@@ -107,10 +108,10 @@ pub use acl::{
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
};
use installer_handoff::run_cmds;
use installer_handoff::{run_cmds, InstallCommands};
use installer_shell::{
embedded_shortcut_commands, embedded_tray_shortcut_commands, escape_nested_cmd_ampersands,
shortcut_bytes, validate_install_value,
escape_nested_cmd_ampersands, shortcut_bytes, staged_shortcut_commands,
staged_tray_shortcut_commands, validate_install_value,
};
pub use verified_update::{
copy_and_verify_update_file_sha256, schedule_current_verified_update_file_cleanup,
@@ -1675,18 +1676,16 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
// The elevated runner expands this to `%~f0.dir`, beside its protected copy.
// Do not stage privileged shortcut artifacts in the user-writable `%TEMP%`.
let tmp_path = "%RUSTDESK_OUTPUT_DIR%".to_owned();
let mk_shortcut_commands = embedded_shortcut_commands(
let (mk_shortcut_commands, mk_shortcut_file) = staged_shortcut_commands(
shortcut_bytes(&exe, None, shortcut_icon_location.as_deref())?,
&format!("{app_name}.lnk"),
"mk_shortcut",
);
let uninstall_shortcut_commands = embedded_shortcut_commands(
)?;
let (uninstall_shortcut_commands, uninstall_shortcut_file) = staged_shortcut_commands(
shortcut_bytes(&exe, Some("--uninstall"), Some("msiexec.exe"))?,
&format!("Uninstall {app_name}.lnk"),
"uninstall_shortcut",
);
let tray_shortcut_commands =
embedded_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?;
)?;
let (tray_shortcut_commands, tray_shortcut_file) =
staged_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?;
let mut reg_value_desktop_shortcuts = "0".to_owned();
let mut reg_value_start_menu_shortcuts = "0".to_owned();
let mut reg_value_printer = "0".to_owned();
@@ -1760,6 +1759,17 @@ copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\
"".to_owned()
};
let after_install = get_after_install(
&exe,
Some(reg_value_start_menu_shortcuts),
Some(reg_value_desktop_shortcuts),
Some(reg_value_printer),
);
let after_install = native_after_install_commands(
format!("{after_install}\r\n{install_remote_printer}"),
&exe,
true,
)?;
// Remember to check if `update_me` need to be changed if changing the `cmds`.
// No need to merge the existing dup code, because the code in these two functions are too critical.
// New code should be written in a common function.
@@ -1791,25 +1801,25 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
{dels}
{import_config}
{after_install}
{install_remote_printer}
{sleep}
",
display_icon = shortcut_icon_location.as_deref().unwrap_or(exe.as_str()),
nested_exe = escape_nested_cmd_ampersands(&exe),
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
after_install = get_after_install(
&exe,
Some(reg_value_start_menu_shortcuts),
Some(reg_value_desktop_shortcuts),
Some(reg_value_printer)
),
sleep = if debug { "timeout 300" } else { "" },
dels = if debug { "" } else { &dels },
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
import_config = get_import_config(&exe),
);
run_cmds(cmds, debug, "install")?;
run_cmds(
InstallCommands {
script: cmds,
files: vec![mk_shortcut_file, uninstall_shortcut_file, tray_shortcut_file],
},
debug,
"install",
)?;
run_after_run_cmds(silent);
Ok(())
}
@@ -1817,29 +1827,30 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
pub fn run_after_install() -> ResultType<()> {
let (_, _, _, exe) = get_install_info();
run_cmds(
get_after_install(&exe, None, None, None),
native_after_install_commands(get_after_install(&exe, None, None, None), &exe, false)?,
true,
"after_install",
)
}
pub fn run_before_uninstall() -> ResultType<()> {
run_cmds(get_before_uninstall(true), true, "before_install")
run_cmds(get_before_uninstall(true)?, true, "before_install")
}
fn get_before_uninstall(kill_self: bool) -> String {
fn get_before_uninstall(kill_self: bool) -> ResultType<String> {
let app_name = crate::get_app_name();
let (_, _, _, exe) = get_install_info();
let remove_service = service_control_command("remove", &exe, false)?;
let ext = app_name.to_lowercase();
let filter = if kill_self {
"".to_string()
} else {
format!(" /FI \"PID ne {}\"", get_current_pid())
};
format!(
Ok(format!(
"
chcp 65001
sc stop {app_name}
sc delete {app_name}
{remove_service}
taskkill /F /IM {broker_exe}
taskkill /F /IM {app_name}.exe{filter}
reg delete HKEY_CLASSES_ROOT\\.{ext} /f
@@ -1847,7 +1858,7 @@ fn get_before_uninstall(kill_self: bool) -> String {
netsh advfirewall firewall delete rule name=\"{app_name} Service\"
",
broker_exe = WIN_TOPMOST_INJECTED_PROCESS_EXE,
)
))
}
/// Constructs the uninstall command string for the application.
@@ -1894,7 +1905,7 @@ fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> ResultType<String>
if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\"
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
",
before_uninstall=get_before_uninstall(kill_self),
before_uninstall=get_before_uninstall(kill_self)?,
uninstall_amyuni_idd=get_uninstall_amyuni_idd(),
app_name = crate::get_app_name(),
))
@@ -3323,15 +3334,24 @@ pub fn try_lock_tray_single_instance() -> bool {
}
}
/// Returns `true` on failure or cancellation so the caller keeps running.
/// Successful removal exits this process instead of returning.
pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
log::info!("Uninstalling service...");
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
let (_, _, _, exe) = get_install_info();
let remove_service = match service_control_command("remove", &exe, false) {
Ok(commands) => commands,
Err(err) => {
log::error!("Failed to prepare service removal: {err}");
return true;
}
};
Config::set_option("stop-service".into(), "Y".into());
let cmds = format!(
"
chcp 65001
sc stop {app_name}
sc delete {app_name}
{remove_service}
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
taskkill /F /IM {broker_exe}
taskkill /F /IM {app_name}.exe{filter}
@@ -3348,7 +3368,7 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
std::process::exit(0);
}
fn get_install_service_commands(path: &str, exe: &str) -> ResultType<String> {
fn get_install_service_commands(path: &str, exe: &str) -> ResultType<InstallCommands> {
let app_name = crate::get_app_name();
for value in [path, exe] {
validate_install_value(value)?;
@@ -3363,10 +3383,10 @@ fn get_install_service_commands(path: &str, exe: &str) -> ResultType<String> {
if let Some(icon) = shortcut_icon_location.as_deref() {
validate_install_value(icon)?;
}
let tray_shortcut_commands =
embedded_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?;
let (tray_shortcut_commands, tray_shortcut_file) =
staged_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?;
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
Ok(format!(
let script = format!(
"
chcp 65001
taskkill /F /IM {app_name}.exe{filter}
@@ -3376,8 +3396,12 @@ copy /Y \"%RUSTDESK_OUTPUT_DIR%\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsof
{create_service}
",
import_config = get_import_config(exe),
create_service = get_create_service(exe),
))
create_service = service_control_command("create-service-with-rollback", exe, false)?,
);
Ok(InstallCommands {
script,
files: vec![tray_shortcut_file],
})
}
pub fn install_service() -> bool {
@@ -3928,7 +3952,86 @@ pub fn update_me_msi(msi: &str, expected_sha256: &str, quiet: bool) -> ResultTyp
verified_update::install_verified_msi(msi, expected_sha256, quiet)
}
fn native_after_install_commands(
commands: String,
exe: &str,
installed_now: bool,
) -> ResultType<String> {
let service = get_create_service(exe);
if service.is_empty() {
return Ok(commands);
}
let native_service = service_control_command("create-service", exe, installed_now)?;
if native_service == service {
return Ok(commands);
}
// Complete independent setup before service-start errors can exit the BAT.
Ok(format!(
"{}\r\n{native_service}",
commands.replacen(&service, "", 1)
))
}
#[cfg(not(feature = "flutter"))]
pub fn handle_service_control() -> Option<std::process::ExitCode> {
if std::env::args_os().nth(1).as_deref() != Some(std::ffi::OsStr::new("--service-control")) {
return None;
}
extern "C" {
fn run_service_control() -> u32;
}
let error = unsafe { run_service_control() };
Some(if error == ERROR_SUCCESS {
std::process::ExitCode::SUCCESS
} else {
eprintln!(
"RustDesk service control failed: {}",
std::io::Error::from_raw_os_error(error as i32)
);
std::process::ExitCode::FAILURE
})
}
fn service_control_command(operation: &str, exe: &str, installed_now: bool) -> ResultType<String> {
let app_name = crate::get_app_name();
let is_create = matches!(operation, "create-service" | "create-service-with-rollback");
let legacy = match operation {
"create-service" | "create-service-with-rollback" => get_create_service(exe),
"remove" => format!("sc stop {app_name}\r\nsc delete {app_name}"),
_ => bail!("Unsupported service operation: {operation}"),
};
if legacy.is_empty() || (is_create && Config::get_option("stop-service") == "Y") {
return Ok(legacy);
}
// A fresh installation has just copied this runner. Otherwise only use the
// new command when the caller is the installed executable; other versions
// may not recognize it.
if !installed_now {
let current = std::env::current_exe()?;
let current = current
.to_str()
.ok_or_else(|| anyhow!("Current executable path is not valid Unicode"))?;
if !current.eq_ignore_ascii_case(exe) {
log::debug!("Keeping legacy service commands for a different installed executable");
return Ok(legacy);
}
}
validate_install_value(exe)?;
let executable_argument = if is_create {
format!(" \"{exe}\"")
} else {
String::new()
};
Ok(format!(
"start \"\" /b /wait \"{exe}\" --service-control {operation} \"{app_name}\"{executable_argument}\r\n\
if not \"%errorlevel%\"==\"0\" exit /b %errorlevel%"
))
}
fn get_import_config(exe: &str) -> String {
// --import-config does not register a service dispatcher, so SCM can report
// a start failure even when import runs. Preserve this legacy contract for
// older installed executables.
if config::is_outgoing_only() {
return "".to_string();
}
+373
View File
@@ -0,0 +1,373 @@
use super::{installer_handoff::directory, wide_string};
use hbb_common::{anyhow::anyhow, log, ResultType};
use std::{
fs::{self, File},
io::{self, Read},
os::windows::io::{AsRawHandle, FromRawHandle},
path::{Path, PathBuf},
};
use windows::{
core::{w, Owned, PCWSTR},
Win32::{
Foundation::{DuplicateHandle, GENERIC_READ, GENERIC_WRITE, HANDLE, HLOCAL},
Security::{
Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
},
PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
},
Storage::FileSystem::{
CreateFileW, FileDispositionInfo, SetFileInformationByHandle, CREATE_NEW, DELETE,
FILE_ATTRIBUTE_NORMAL, FILE_DISPOSITION_INFO, FILE_SHARE_READ,
},
System::Threading::GetCurrentProcess,
},
};
// Stage data before elevation instead of relaunching a verifier from a portable
// directory whose executable or DLLs could be replaced while UAC is pending.
pub(super) struct ProtectedInstallFile {
path: PathBuf,
cleanup_handle: Option<Owned<HANDLE>>,
}
impl ProtectedInstallFile {
pub(super) fn new(path: &Path, bytes: &[u8]) -> ResultType<Self> {
Self::create(path, &mut io::Cursor::new(bytes), None)
}
pub(super) fn copy_verified(source: &Path, expected_sha256: &str) -> ResultType<Self> {
let mut source = File::open(source)?;
let path = directory::path()?.with_extension("tmp");
Self::create(&path, &mut source, Some(expected_sha256))
}
fn create(
path: &Path,
source: &mut impl Read,
expected_sha256: Option<&str>,
) -> ResultType<Self> {
let mut file = create_protected_file(path)?;
let result = (|| -> ResultType<Owned<HANDLE>> {
io::copy(source, &mut file)?;
file.sync_all()?;
if let Some(expected) = expected_sha256 {
crate::update_hash::verify_sha256_reader(&mut file, expected).map_err(|error| {
anyhow!(
"Failed to verify staged installer file {}: {error:?}",
path.display()
)
})?;
}
duplicate_cleanup_handle(&file)
})();
let cleanup_handle = result.map_err(|error| {
// The initial handle can remove an incomplete file without reopening its protected path.
if let Err(cleanup_error) = delete_staged_file(HANDLE(file.as_raw_handle())) {
log::warn!(
"Failed to remove incomplete installer file {}: {cleanup_error}",
path.display()
);
}
error
})?;
Ok(Self {
path: path.to_owned(),
cleanup_handle: Some(cleanup_handle),
})
}
pub(super) fn path(&self) -> &Path {
&self.path
}
pub(super) fn release(&mut self) {
// cmd.exe cannot read/copy these files while a DELETE handle is open.
// The handoff gate keeps it waiting until every staged handle is closed.
drop(self.cleanup_handle.take());
}
}
impl Drop for ProtectedInstallFile {
fn drop(&mut self) {
// Cancellation cannot reopen the protected path with DELETE permission.
let result = match &self.cleanup_handle {
Some(handle) => delete_staged_file(**handle),
None => match fs::remove_file(&self.path) {
Err(error) if error.kind() != io::ErrorKind::NotFound => Err(error.into()),
_ => Ok(()),
},
};
if let Err(error) = result {
log::warn!(
"Protected installer file may remain at {}: {error}",
self.path.display()
);
}
}
}
fn create_protected_file(path: &Path) -> ResultType<File> {
let mut descriptor = PSECURITY_DESCRIPTOR::default();
// Restrict OWNER RIGHTS at creation so the unelevated creator cannot reopen
// the file for writing or grant itself that permission after staging.
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
w!("D:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FRFX;;;OW)"),
SDDL_REVISION_1,
&mut descriptor,
None,
)?;
}
let _descriptor = unsafe { Owned::new(HLOCAL(descriptor.0)) };
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor.0,
bInheritHandle: false.into(),
};
let path_string = path
.to_str()
.ok_or_else(|| anyhow!("Invalid protected installer path"))?;
let path_string = wide_string(path_string);
let handle = unsafe {
CreateFileW(
PCWSTR(path_string.as_ptr()),
GENERIC_READ.0 | GENERIC_WRITE.0 | DELETE.0,
FILE_SHARE_READ,
Some(&attributes),
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
None,
)
}
.map_err(|error| {
anyhow!(
"Failed to create protected installer file {}: {error}",
path.display()
)
})?;
Ok(unsafe { File::from_raw_handle(handle.0) })
}
fn duplicate_cleanup_handle(file: &File) -> ResultType<Owned<HANDLE>> {
let mut handle = HANDLE::default();
// Retain deletion capability, not a writable handle, while waiting for UAC.
unsafe {
let process = GetCurrentProcess();
DuplicateHandle(
process,
HANDLE(file.as_raw_handle()),
process,
&mut handle,
DELETE.0,
false,
Default::default(),
)?;
Ok(Owned::new(handle))
}
}
fn delete_staged_file(handle: HANDLE) -> ResultType<()> {
let disposition = FILE_DISPOSITION_INFO {
DeleteFile: true.into(),
};
unsafe {
SetFileInformationByHandle(
handle,
FileDispositionInfo,
&disposition as *const _ as *const _,
std::mem::size_of_val(&disposition) as u32,
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use windows::{
core::BOOL,
Win32::{
Foundation::HANDLE,
Security::{
AccessCheck,
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
CreateRestrictedToken, CreateWellKnownSid, DuplicateToken, SecurityImpersonation,
WinBuiltinAdministratorsSid, DACL_SECURITY_INFORMATION, DISABLE_MAX_PRIVILEGE,
GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
PRIVILEGE_SET, PSID, SECURITY_MAX_SID_SIZE, SID_AND_ATTRIBUTES, TOKEN_DUPLICATE,
TOKEN_QUERY,
},
Storage::FileSystem::{
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
},
System::Threading::{GetCurrentProcess, OpenProcessToken},
},
};
#[test]
fn cancelled_staging_removes_files_without_parent_delete_permission() {
use windows::Win32::Security::{ImpersonateLoggedOnUser, RevertToSelf};
let root = std::env::temp_dir().join(format!("rustdesk-cancel-{}", uuid::Uuid::new_v4()));
fs::create_dir(&root).unwrap();
let path = root.join("package.tmp");
let file = ProtectedInstallFile::new(&path, b"cancelled update").unwrap();
// Omit DELETE_CHILD as in Windows Temp, but retain fixture ACL control.
set_parent_acl(&root, w!("D:P(A;;0x1600af;;;OW)"));
unsafe {
let token = ordinary_owner_token();
ImpersonateLoggedOnUser(*token).unwrap();
drop(file);
RevertToSelf().unwrap();
}
set_parent_acl(&root, w!("D:P(A;;FA;;;OW)"));
let remaining = fs::symlink_metadata(&path);
fs::remove_dir_all(&root).unwrap();
assert!(
matches!(remaining, Err(error) if error.kind() == io::ErrorKind::NotFound),
"Cancelled staging must remove the protected package"
);
}
fn set_parent_acl(path: &Path, sddl: PCWSTR) {
use windows::Win32::Security::{SetFileSecurityW, PROTECTED_DACL_SECURITY_INFORMATION};
let path = wide_string(path.to_str().unwrap());
let mut descriptor = PSECURITY_DESCRIPTOR::default();
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl,
SDDL_REVISION_1,
&mut descriptor,
None,
)
.unwrap();
let _descriptor = Owned::new(HLOCAL(descriptor.0));
assert!(SetFileSecurityW(
PCWSTR(path.as_ptr()),
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
descriptor
)
.as_bool());
}
}
#[test]
fn staged_files_protect_contents_and_remove_unverified_data() {
const HASH: &str = "304ca1638c5effa6832e0e15b958a8f74847efe4df9c3f3187216e921c168fed";
let root = std::env::temp_dir().join(format!("rustdesk-staging-{}", uuid::Uuid::new_v4()));
fs::create_dir(&root).unwrap();
let path = root.join("input.tmp");
let file =
ProtectedInstallFile::create(&path, &mut io::Cursor::new(b"rustdesk"), Some(HASH))
.unwrap();
assert_eq!(
ordinary_owner_access(file.path()),
(FILE_GENERIC_READ | FILE_GENERIC_EXECUTE).0
);
assert_eq!(fs::read(file.path()).unwrap(), b"rustdesk");
assert!(ProtectedInstallFile::new(&path, b"replacement").is_err());
let rejected = root.join("rejected.tmp");
assert!(ProtectedInstallFile::create(
&rejected,
&mut io::Cursor::new(b"changed"),
Some(HASH)
)
.is_err());
assert!(!rejected.exists());
drop(file);
fs::remove_dir(root).unwrap();
}
fn ordinary_owner_access(path: &Path) -> u32 {
let path = wide_string(path.to_str().unwrap());
let mut descriptor = PSECURITY_DESCRIPTOR::default();
unsafe {
GetNamedSecurityInfoW(
PCWSTR(path.as_ptr()),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
None,
None,
None,
None,
&mut descriptor,
)
.ok()
.unwrap();
}
let _descriptor = unsafe { Owned::new(HLOCAL(descriptor.0)) };
let token = ordinary_owner_token();
let mapping = GENERIC_MAPPING {
GenericRead: FILE_GENERIC_READ.0,
GenericWrite: FILE_GENERIC_WRITE.0,
GenericExecute: FILE_GENERIC_EXECUTE.0,
GenericAll: FILE_ALL_ACCESS.0,
};
let mut privileges = PRIVILEGE_SET::default();
let mut size = std::mem::size_of::<PRIVILEGE_SET>() as u32;
let mut granted = 0;
let mut allowed = BOOL::default();
unsafe {
AccessCheck(
descriptor,
*token,
winapi::um::winnt::MAXIMUM_ALLOWED,
&mapping,
Some(&mut privileges),
&mut size,
&mut granted,
&mut allowed,
)
.unwrap();
}
assert!(allowed.as_bool());
granted
}
fn ordinary_owner_token() -> Owned<HANDLE> {
let mut original = HANDLE::default();
unsafe {
OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY | TOKEN_DUPLICATE,
&mut original,
)
.unwrap();
}
let original = unsafe { Owned::new(original) };
let mut sid = [0; SECURITY_MAX_SID_SIZE as usize];
let sid_pointer = PSID(sid.as_mut_ptr().cast());
let mut size = sid.len() as u32;
unsafe {
CreateWellKnownSid(
WinBuiltinAdministratorsSid,
None,
Some(sid_pointer),
&mut size,
)
.unwrap();
}
let disabled = [SID_AND_ATTRIBUTES {
Sid: sid_pointer,
Attributes: 0,
}];
let mut restricted = HANDLE::default();
unsafe {
CreateRestrictedToken(
*original,
DISABLE_MAX_PRIVILEGE,
Some(&disabled),
None,
None,
&mut restricted,
)
.unwrap();
}
let restricted = unsafe { Owned::new(restricted) };
let mut impersonation = HANDLE::default();
unsafe {
DuplicateToken(*restricted, SecurityImpersonation, &mut impersonation).unwrap();
}
unsafe { Owned::new(impersonation) }
}
}
+166 -239
View File
@@ -1,147 +1,95 @@
mod directory;
pub(super) mod directory;
mod gate;
use super::{
installer_files::ProtectedInstallFile,
installer_shell::{
get_system_executable, path_for_cmd_assignment, path_for_cmd_environment,
run_elevated_and_wait, trusted_install_environment,
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH,
get_system_executable, launch_elevated, path_for_cmd_environment,
trusted_install_environment, BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH,
WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS,
},
validate_install_app_name, ResultType,
};
use hbb_common::{
anyhow::anyhow,
bail, log,
sha2::{Digest, Sha256},
};
use std::{
fs,
io::{self, Write},
path::{Path, PathBuf},
};
use gate::InstallGate;
use hbb_common::{bail, log};
use std::{fs, io, path::Path};
const CERTUTIL_RELATIVE_PATH: &str = "certutil.exe";
const CHCP_RELATIVE_PATH: &str = "chcp.com";
const FINDSTR_RELATIVE_PATH: &str = "findstr.exe";
const UTF8_CODE_PAGE: u32 = 65001;
const INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE: u32 = 0x5253_0001;
const INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0002;
const INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_0003;
const INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_0004;
const BATCH_CODE_PAGE_FAILURE_EXIT_CODE: u32 = 0x5253_0005;
const BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE: u32 = 0x5253_0006;
const BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE: u32 = 0x5253_0007;
const SHA256_HASH_LENGTH: usize = 32;
type BatchHash = [u8; SHA256_HASH_LENGTH];
struct InstallCommandScript {
path: PathBuf,
expected_hash: BatchHash,
pub(super) struct InstallCommands {
pub script: String,
pub files: Vec<ProtectedInstallFile>,
}
impl Drop for InstallCommandScript {
fn drop(&mut self) {
if let Err(err) = fs::remove_file(&self.path) {
if err.kind() != io::ErrorKind::NotFound {
log::warn!(
"Failed to remove temporary installer file {:?}: {err}",
self.path
);
}
impl From<String> for InstallCommands {
fn from(script: String) -> Self {
Self {
script,
files: Vec::new(),
}
}
}
fn prepare_install_commands(commands: &str) -> ResultType<String> {
let commands = commands.replace("\r\n", "\n").replace('\n', "\r\n");
struct InstallCommandScript {
file: ProtectedInstallFile,
_files: Vec<ProtectedInstallFile>,
}
impl InstallCommandScript {
fn release_files(&mut self) {
self.file.release();
for file in &mut self._files {
file.release();
}
}
}
fn prepare_install_commands(cmds: &InstallCommands) -> ResultType<String> {
let commands = cmds.script.replace("\r\n", "\n").replace('\n', "\r\n");
let chcp_path = get_system_executable(CHCP_RELATIVE_PATH)?;
let chcp = path_for_cmd_environment(&chcp_path)?;
let files = cmds
.files
.iter()
.map(|file| path_for_cmd_environment(file.path()).map(|path| format!("\"{path}\"")))
.collect::<ResultType<Vec<_>>>()?
.join(" ");
// Call a fixed label so early EXIT /B still reaches cleanup, without reparsing a path.
// Exit the dedicated cmd after self-deletion; EXIT /B would reopen the missing BAT.
Ok(format!(
"@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n\
set \"ERRORLEVEL=\"\r\n\
call :rustdesk_install\r\n\
set \"RUSTDESK_INSTALL_EXIT_CODE=%ERRORLEVEL%\"\r\n\
rd /s /q \"%~f0.dir\" > nul 2>&1\r\n\
(del /q \"%~f0\" {files} > nul 2>&1 & exit %RUSTDESK_INSTALL_EXIT_CODE%)\r\n\
:rustdesk_install\r\n\
\"{chcp}\" {UTF8_CODE_PAGE} > nul || exit /b \
{BATCH_CODE_PAGE_FAILURE_EXIT_CODE}\r\n\
{}\r\n\
if exist \"%~f0.dir\" exit /b {BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE}\r\n\
md \"%~f0.dir\" || exit /b {BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE}\r\n\
set \"RUSTDESK_OUTPUT_DIR=%~f0.dir\"\r\n{commands}\r\nexit /b 0\r\n",
trusted_install_environment()?
))
}
fn write_install_script(cmds: String) -> ResultType<InstallCommandScript> {
let directory = std::env::temp_dir();
path_for_cmd_environment(&directory)?;
fn write_install_script(cmds: InstallCommands, path: &Path) -> ResultType<InstallCommandScript> {
let commands = prepare_install_commands(&cmds)?;
let expected_hash = Sha256::digest(commands.as_bytes()).into();
let path = directory.join(format!(
"rustdesk_install_{}.bat",
uuid::Uuid::new_v4().simple()
));
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)?;
let script = InstallCommandScript {
path,
expected_hash,
};
file.write_all(commands.as_bytes())?;
file.sync_all()?;
Ok(script)
Ok(InstallCommandScript {
file: ProtectedInstallFile::new(path, commands.as_bytes())?,
_files: cmds.files,
})
}
fn install_hash_pattern(hash: &BatchHash) -> String {
hash.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<Vec<_>>()
.join(" *")
}
fn verified_install_bootstrap(
fn protected_install_parameters(
script: &InstallCommandScript,
runner_directory: &Path,
gate: &InstallGate,
) -> ResultType<String> {
let source = path_for_cmd_assignment(&script.path)?;
let directory = path_for_cmd_assignment(runner_directory)?;
let runner = runner_directory.join("install.bat");
let runner = path_for_cmd_assignment(&runner)?;
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
let certutil_path = get_system_executable(CERTUTIL_RELATIVE_PATH)?;
let findstr_path = get_system_executable(FINDSTR_RELATIVE_PATH)?;
let cmd = path_for_cmd_assignment(&cmd_path)?;
let certutil = path_for_cmd_assignment(&certutil_path)?;
let findstr = path_for_cmd_assignment(&findstr_path)?;
// Short names preserve headroom under the Windows 7 ShellExecuteExW 2,048
// UTF-16-character parameter limit. Paths are stored with delayed expansion
// disabled, then expanded indirectly so literal `!` survives: S=source,
// R=runner, Q=cmd.exe, H=certutil.exe, F=findstr.exe, D=directory, E=exit code.
Ok(format!(
"setlocal DisableDelayedExpansion & set \"S={source}\" & set \"R={runner}\" & \
set \"Q={cmd}\" & set \"H={certutil}\" & set \"F={findstr}\" & \
set \"ERRORLEVEL=\" & set \"D={directory}\" & set \"E=0\" & setlocal EnableDelayedExpansion & \
if exist \"!R!\" (set \"E={INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE}\") else (\
copy /Y \"!S!\" \"!R!\" > nul || \
(set \"E={INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE}\") & \
if \"!E!\"==\"0\" (\"!H!\" -hashfile \"!R!\" SHA256 > \"!R!.hash\" || \
set \"E={INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE}\") & \
if \"!E!\"==\"0\" (\"!F!\" /R /I /X /C:\"{}\" \"!R!.hash\" > nul || \
set \"E={INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE}\") & \
if \"!E!\"==\"0\" (\"!Q!\" /D /E:ON /V:OFF /C \"\"!R!\"\" & \
set \"E=!errorlevel!\")) & \
rd /s /q \"!D!\" > nul 2>&1 & exit /b !E!",
install_hash_pattern(&script.expected_hash),
))
}
fn verified_install_parameters(
script: &InstallCommandScript,
directory: &Path,
) -> ResultType<String> {
let parameters = format!(
"/D /E:ON /V:ON /C {}",
verified_install_bootstrap(script, directory)?
);
let path = path_for_cmd_environment(script.file.path())?;
let pipe = gate.name();
let parameters =
format!("/D /E:ON /V:OFF /C \"set /p RUSTDESK_HANDOFF=<\"{pipe}\" >nul && \"{path}\"\"");
let length = parameters.encode_utf16().count();
if length >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS {
bail!("Elevated command is too long: {length} UTF-16 characters");
@@ -149,19 +97,28 @@ fn verified_install_parameters(
Ok(parameters)
}
pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
pub(super) fn run_cmds(cmds: impl Into<InstallCommands>, show: bool, tip: &str) -> ResultType<()> {
validate_install_app_name(&crate::get_app_name())?;
let script = write_install_script(cmds)?;
let path = directory::path()?.with_extension("bat");
let directory = path.with_extension("bat.dir");
let mut script = write_install_script(cmds.into(), &path)?;
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
let directory = directory::path()?;
let parameters = verified_install_parameters(&script, &directory)?;
let gate = InstallGate::new()?;
let parameters = protected_install_parameters(&script, &gate)?;
directory::create(&directory)?;
let exit_code = run_elevated_and_wait(&cmd_path, &parameters, show).map_err(|err| {
anyhow!(
"Installer handoff failed; protected directory may remain at {}: {err}",
let process = launch_elevated(&cmd_path, &parameters, show).map_err(|err| {
log::debug!("Failed to launch elevated installer: {err:#}");
err.context(format!(
"Installer handoff failed; protected directory may remain at {}",
directory.display()
)
))
})?;
gate.wait_for_client(process.handle())?;
// Keep DELETE-capable handles through UAC cancellation, but close them before
// cmd opens the BAT or copies its inputs. The protected DACL stays unchanged.
script.release_files();
gate.release(process.handle())?;
let exit_code = process.wait()?;
// Cleanup must not change the result of an installation that already ran.
match fs::symlink_metadata(&directory) {
Ok(_) => log::warn!(
@@ -185,141 +142,111 @@ pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
fn elevated_install_failure_reason(exit_code: u32) -> &'static str {
match exit_code {
INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE => "protected runner already exists",
INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE => "failed to copy protected runner",
INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE => "failed to hash protected runner",
INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE => "protected runner hash mismatch",
BATCH_CODE_PAGE_FAILURE_EXIT_CODE => "failed to set the installer code page",
BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE => "installer output directory already exists",
BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE => {
"failed to create the installer output directory"
}
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE => "failed to decode an embedded shortcut",
BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE => "failed to copy a prepared shortcut",
_ => "installer command failed",
}
}
#[cfg(test)]
mod tests {
use super::super::installer_shell::{
embedded_shortcut_commands, shortcut_bytes, WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS,
};
use super::super::installer_shell::shortcut_bytes;
use super::*;
use ::windows::Win32::System::Threading;
use std::os::windows::process::CommandExt;
use std::{
os::windows::{io::AsRawHandle, process::CommandExt},
process::Stdio,
};
use windows::Win32::Foundation::HANDLE;
#[test]
fn native_install_handoff_verifies_before_execution() {
let marker = std::env::temp_dir().join(format!(
"rustdesk_install_marker_{}",
uuid::Uuid::new_v4().simple()
));
let runner_dir = std::env::temp_dir().join(format!(
"rustdesk_install_!RUSTDESK_HANDOFF_EXPAND!&^@()runner_{}",
uuid::Uuid::new_v4().simple()
));
std::fs::create_dir(&runner_dir).expect("runner directory should be created");
let shortcut_commands = embedded_shortcut_commands(
shortcut_bytes(r"C:\RustDesk.exe", None, None)
.expect("native shortcut should be generated"),
"test.lnk",
"test",
);
let script = write_install_script(format!(
"if \"%PROGRAMDATA%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
if \"%PUBLIC%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
{shortcut_commands}\r\n\
> \"{}\" echo verified",
marker.display()
))
.expect("install script should be created");
let bootstrap = verified_install_bootstrap(&script, &runner_dir)
.expect("native verifier bootstrap should be generated");
assert_native_handoff_structure(&script, &shortcut_commands, &runner_dir);
let output = run_install_bootstrap_for_test(&bootstrap);
assert!(
output.status.success(),
"unchanged script failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(marker.exists(), "verified install script must execute");
std::fs::remove_file(&marker).expect("test marker should be removed");
assert!(
!runner_dir.exists(),
"protected runner directory must be removed"
);
std::fs::create_dir(&runner_dir).expect("runner directory should be created");
assert_replaced_install_script_is_rejected(&script, &runner_dir, &marker);
assert!(
!runner_dir.exists(),
"failed handoff directory must be removed"
);
fn protected_handoff_preserves_shortcuts_failures_and_cleanup() {
const COMMAND_FAILURE: u32 = 77;
for exit_code in [0, COMMAND_FAILURE] {
let root =
std::env::temp_dir().join(format!("rustdesk-handoff-{}", uuid::Uuid::new_v4()));
let source = root.join("source !&^@().bat");
let runner = source.with_extension("bat.dir");
fs::create_dir_all(&runner).unwrap();
let bytes = shortcut_bytes(r"C:\RustDesk.exe", Some("--tray"), None).unwrap();
let shortcut =
ProtectedInstallFile::new(&root.join("shortcut !&^@().tmp"), &bytes).unwrap();
let marker = root.join("copied.lnk");
let commands = InstallCommands {
script: format!(
"copy /B /Y \"{}\" \"{}\" > nul\r\nexit /b {exit_code}",
shortcut.path().display(),
marker.display()
),
files: vec![shortcut],
};
let mut script = write_install_script(commands, &source).unwrap();
if !super::super::is_elevated(None).unwrap() {
assert!(fs::write(script.file.path(), "exit /b 0\r\n").is_err());
}
let output = run_script(&mut script, true);
let copied = fs::read(&marker).unwrap();
let cleaned = !runner.exists()
&& !script.file.path().exists()
&& script._files.iter().all(|file| !file.path().exists());
fs::remove_dir_all(&root).unwrap();
assert_eq!(
output.status.code().map(|code| code as u32),
Some(exit_code),
"{output:?}"
);
assert_eq!(copied, bytes);
assert!(cleaned, "The runner and protected inputs must be removed");
}
}
fn assert_native_handoff_structure(
script: &InstallCommandScript,
shortcut_commands: &str,
runner_dir: &Path,
) {
let bootstrap = verified_install_bootstrap(script, runner_dir)
.expect("native verifier bootstrap should be generated");
assert!(shortcut_commands.contains("certutil"));
assert!(shortcut_commands.contains("-decode"));
assert!(!shortcut_commands.to_ascii_lowercase().contains("cscript"));
assert!(!shortcut_commands
.to_ascii_lowercase()
.contains("powershell"));
let win7_hash_pattern = script
.expected_hash
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<Vec<_>>()
.join(" *");
assert!(bootstrap.contains(&format!("/R /I /X /C:\"{win7_hash_pattern}\"")));
let parameters = verified_install_parameters(script, runner_dir)
.expect("elevated parameters should be generated");
assert!(bootstrap.contains("certutil.exe"));
assert!(bootstrap.contains("findstr.exe"));
assert!(!bootstrap.to_ascii_lowercase().contains("powershell"));
assert!(parameters.encode_utf16().count() < WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS);
#[test]
fn interrupted_handoff_does_not_execute_the_batch() {
let root =
std::env::temp_dir().join(format!("rustdesk-interrupted-{}", uuid::Uuid::new_v4()));
fs::create_dir(&root).unwrap();
let source = root.join("source.bat");
let marker = root.join("executed");
let commands = format!("echo unexpected > \"{}\"", marker.display());
let mut script = write_install_script(commands.into(), &source).unwrap();
let output = run_script(&mut script, false);
assert!(!output.status.success(), "{output:?}");
assert!(!marker.exists());
drop(script);
assert!(!source.exists());
fs::remove_dir(&root).unwrap();
// A child exiting without connecting must also retire the pending pipe I/O.
let gate = InstallGate::new().unwrap();
let mut child =
std::process::Command::new(get_system_executable(CMD_RELATIVE_PATH).unwrap())
.args(["/D", "/C", "exit", "0"])
.creation_flags(::windows::Win32::System::Threading::CREATE_NO_WINDOW.0)
.spawn()
.unwrap();
assert!(gate.wait_for_client(HANDLE(child.as_raw_handle())).is_err());
assert!(child.wait().unwrap().success());
}
fn assert_replaced_install_script_is_rejected(
script: &InstallCommandScript,
runner_dir: &Path,
marker: &Path,
) {
std::fs::write(
&script.path,
format!("> \"{}\" echo hijacked\r\n", marker.display()),
)
.expect("install script should be replaceable");
let replaced = verified_install_bootstrap(&script, &runner_dir)
.expect("replacement verifier should be generated");
let output = run_install_bootstrap_for_test(&replaced);
assert!(
!output.status.success(),
"replaced script unexpectedly passed verification"
);
assert_eq!(
output.status.code(),
Some(INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE as i32)
);
assert!(!marker.exists(), "replaced script must not execute");
}
fn run_install_bootstrap_for_test(bootstrap: &str) -> std::process::Output {
let cmd = get_system_executable(CMD_RELATIVE_PATH).expect("system cmd.exe should resolve");
let mut command = std::process::Command::new(cmd);
command
.env("PROGRAMDATA", "rustdesk_untrusted")
.env("PUBLIC", "rustdesk_untrusted")
.env("RUSTDESK_HANDOFF_EXPAND", "expanded");
command.raw_arg(format!("/D /E:ON /V:ON /C {bootstrap}"));
command
.creation_flags(Threading::CREATE_NO_WINDOW.0)
.output()
.expect("native verifier should run")
fn run_script(script: &mut InstallCommandScript, release: bool) -> std::process::Output {
let gate = InstallGate::new().unwrap();
let parameters = protected_install_parameters(script, &gate).unwrap();
let mut child =
std::process::Command::new(get_system_executable(CMD_RELATIVE_PATH).unwrap())
.raw_arg(parameters)
.creation_flags(::windows::Win32::System::Threading::CREATE_NO_WINDOW.0)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let process = HANDLE(child.as_raw_handle());
gate.wait_for_client(process).unwrap();
assert!(child.try_wait().unwrap().is_none());
if release {
script.release_files();
gate.release(process).unwrap();
} else {
drop(gate);
}
child.wait_with_output().unwrap()
}
}
@@ -22,7 +22,7 @@ use windows::{
},
};
pub(super) fn path() -> ResultType<PathBuf> {
pub(in crate::platform::windows) fn path() -> ResultType<PathBuf> {
let system = get_system_executable("")?;
let windows = system
.parent()
@@ -0,0 +1,135 @@
use super::super::wide_string;
use hbb_common::{bail, log, ResultType};
use std::io;
use windows::{
core::{Owned, PCWSTR},
Win32::{
Foundation::{
ERROR_IO_PENDING, ERROR_NOT_FOUND, ERROR_PIPE_CONNECTED, HANDLE, WAIT_OBJECT_0,
},
Storage::FileSystem::{
WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, PIPE_ACCESS_OUTBOUND,
},
System::{
Pipes::{
ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId,
PIPE_REJECT_REMOTE_CLIENTS,
},
Threading::{CreateEventW, GetProcessId, WaitForMultipleObjects, INFINITE},
IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED},
},
},
};
const READY: &[u8] = b"1\r\n";
const PIPE_INSTANCES: u32 = 1;
pub(super) struct InstallGate {
pipe: Owned<HANDLE>,
name: String,
}
impl InstallGate {
pub(super) fn new() -> ResultType<Self> {
let name = format!(
r"\\.\pipe\rustdesk-install-{}",
uuid::Uuid::new_v4().simple()
);
let wide_name = wide_string(&name);
let pipe = unsafe {
CreateNamedPipeW(
PCWSTR(wide_name.as_ptr()),
PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
PIPE_REJECT_REMOTE_CLIENTS,
PIPE_INSTANCES,
READY.len() as u32,
0,
0,
None,
)
};
if pipe.is_invalid() {
return Err(io::Error::last_os_error().into());
}
Ok(Self {
pipe: unsafe { Owned::new(pipe) },
name,
})
}
pub(super) fn name(&self) -> &str {
&self.name
}
pub(super) fn wait_for_client(&self, process: HANDLE) -> ResultType<()> {
let event = unsafe { Owned::new(CreateEventW(None, true, false, None)?) };
let mut overlapped = OVERLAPPED {
hEvent: *event,
..Default::default()
};
if let Err(error) = unsafe { ConnectNamedPipe(*self.pipe, Some(&mut overlapped)) } {
if error.code() == ERROR_IO_PENDING.to_hresult() {
self.wait_for_io(&overlapped, process)?;
} else if error.code() != ERROR_PIPE_CONNECTED.to_hresult() {
return Err(error.into());
}
}
let mut client = 0;
unsafe { GetNamedPipeClientProcessId(*self.pipe, &mut client)? };
if client != unsafe { GetProcessId(process) } {
bail!("Unexpected installer handoff pipe client");
}
Ok(())
}
// cmd's SET /P must read a line successfully before it opens the BAT. Closing
// the pipe without this signal fails the command instead of running the BAT.
pub(super) fn release(&self, process: HANDLE) -> ResultType<()> {
let event = unsafe { Owned::new(CreateEventW(None, true, false, None)?) };
let mut overlapped = OVERLAPPED {
hEvent: *event,
..Default::default()
};
// Windows 7 requires a byte-count pointer even for overlapped writes.
let mut written = 0;
if let Err(error) = unsafe {
WriteFile(
*self.pipe,
Some(READY),
Some(&mut written),
Some(&mut overlapped),
)
} {
if error.code() != ERROR_IO_PENDING.to_hresult() {
return Err(error.into());
}
self.wait_for_io(&overlapped, process)?;
}
// Keep the server handle alive until cmd exits so the signal stays buffered.
Ok(())
}
fn wait_for_io(&self, overlapped: &OVERLAPPED, process: HANDLE) -> ResultType<()> {
let wait =
unsafe { WaitForMultipleObjects(&[overlapped.hEvent, process], false, INFINITE) };
let mut transferred = 0;
if wait == WAIT_OBJECT_0 {
unsafe { GetOverlappedResult(*self.pipe, overlapped, &mut transferred, false)? };
return Ok(());
}
let error = io::Error::last_os_error();
// Retire pending I/O before the stack-allocated OVERLAPPED and event go away.
if let Err(error) = unsafe { CancelIoEx(*self.pipe, Some(overlapped)) } {
if error.code() != ERROR_NOT_FOUND.to_hresult() {
log::warn!("Failed to cancel installer handoff I/O: {error}");
}
}
// Cancellation normally completes with ERROR_OPERATION_ABORTED; preserve
// the original wait failure after observing that completion.
let _ = unsafe { GetOverlappedResult(*self.pipe, overlapped, &mut transferred, true) };
if wait.0 == WAIT_OBJECT_0.0 + 1 {
bail!("Elevated installer exited before the file handoff completed");
}
bail!("Installer handoff wait failed ({}): {error}", wait.0)
}
}
+41 -41
View File
@@ -1,10 +1,7 @@
use super::{wide_string, ResultType};
use hbb_common::{
anyhow::anyhow,
bail,
base64::{engine::general_purpose::STANDARD, Engine as _},
log,
use super::{
installer_files::ProtectedInstallFile, installer_handoff::directory, wide_string, ResultType,
};
use hbb_common::{anyhow::anyhow, bail, log};
use std::{
ffi::OsString,
io, mem,
@@ -26,7 +23,7 @@ use windows::{
};
pub(super) const CMD_RELATIVE_PATH: &str = "cmd.exe";
pub(super) const BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE: u32 = 0x5253_0008;
pub(super) const BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0008;
pub(super) const WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS: usize = 2048;
const SHORTCUT_ICON_INDEX: i32 = 0;
@@ -71,26 +68,29 @@ pub(super) fn shortcut_bytes(
Ok(bytes)
}
pub(super) fn embedded_shortcut_commands(bytes: Vec<u8>, filename: &str, name: &str) -> String {
let encoded = STANDARD.encode(bytes);
let encoded_path = format!("%~f0.{name}.b64");
format!(
"> \"{encoded_path}\" echo {encoded}\r\n\
certutil -f -decode \"{encoded_path}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE}"
)
pub(super) fn staged_shortcut_commands(
bytes: Vec<u8>,
filename: &str,
) -> ResultType<(String, ProtectedInstallFile)> {
validate_install_value(filename)?;
let file = ProtectedInstallFile::new(&directory::path()?.with_extension("lnk"), &bytes)?;
let source = path_for_cmd_environment(file.path())?;
let commands = format!(
"copy /B /Y \"{source}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE}"
);
Ok((commands, file))
}
pub(super) fn embedded_tray_shortcut_commands(
pub(super) fn staged_tray_shortcut_commands(
app_name: &str,
exe: &str,
icon_location: Option<&str>,
) -> ResultType<String> {
) -> ResultType<(String, ProtectedInstallFile)> {
let filename = format!("{app_name} Tray.lnk");
Ok(embedded_shortcut_commands(
staged_shortcut_commands(
shortcut_bytes(exe, Some("--tray"), icon_location)?,
&filename,
"tray_shortcut",
))
)
}
pub(super) fn validate_install_value(value: &str) -> ResultType<()> {
@@ -135,14 +135,6 @@ pub(super) fn path_for_cmd_environment(path: &Path) -> ResultType<&str> {
Ok(value)
}
// Bootstrap assignments are parsed before DisableDelayedExpansion takes effect.
// Escape carets first so `^!` preserves each literal exclamation mark.
pub(super) fn path_for_cmd_assignment(path: &Path) -> ResultType<String> {
Ok(path_for_cmd_environment(path)?
.replace('^', "^^")
.replace('!', "^!"))
}
pub(super) fn trusted_install_environment() -> ResultType<String> {
let system = get_system_executable("")?;
let program_data = get_known_folder(&FOLDERID_ProgramData)?;
@@ -201,7 +193,25 @@ fn initialize_shell_com() -> ResultType<Option<ShellComGuard>> {
Ok(Some(ShellComGuard))
}
struct ProcessHandle(HANDLE);
pub(super) struct ProcessHandle(HANDLE);
impl ProcessHandle {
pub(super) fn handle(&self) -> HANDLE {
self.0
}
pub(super) fn wait(self) -> ResultType<u32> {
let wait_result = unsafe { Threading::WaitForSingleObject(self.0, Threading::INFINITE) };
if wait_result == Foundation::WAIT_FAILED {
return Err(io::Error::last_os_error().into());
}
if wait_result != Foundation::WAIT_OBJECT_0 {
bail!("Unexpected elevated process wait result: {}", wait_result.0);
}
let mut exit_code = 0;
unsafe { Threading::GetExitCodeProcess(self.0, &mut exit_code) }?;
Ok(exit_code)
}
}
impl Drop for ProcessHandle {
fn drop(&mut self) {
@@ -217,11 +227,11 @@ fn elevated_working_directory(executable: &Path) -> ResultType<&Path> {
.ok_or_else(|| anyhow!("Elevated executable has no parent directory"))
}
pub(super) fn run_elevated_and_wait(
pub(super) fn launch_elevated(
executable: &Path,
parameters: &str,
show: bool,
) -> ResultType<u32> {
) -> ResultType<ProcessHandle> {
let parameter_chars = parameters.encode_utf16().count();
if parameter_chars >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS {
bail!("Elevated command is too long: {parameter_chars} UTF-16 characters");
@@ -249,17 +259,7 @@ pub(super) fn run_elevated_and_wait(
if info.hProcess.0.is_null() {
bail!("Windows did not return an elevated process handle");
}
let process = ProcessHandle(info.hProcess);
let wait_result = unsafe { Threading::WaitForSingleObject(process.0, Threading::INFINITE) };
if wait_result == Foundation::WAIT_FAILED {
return Err(io::Error::last_os_error().into());
}
if wait_result != Foundation::WAIT_OBJECT_0 {
bail!("Unexpected elevated process wait result: {}", wait_result.0);
}
let mut exit_code = 0;
unsafe { Threading::GetExitCodeProcess(process.0, &mut exit_code) }?;
Ok(exit_code)
Ok(ProcessHandle(info.hProcess))
}
// Escape `^` before using it to escape `&` so both survive nested cmd.exe parsing.
+23 -40
View File
@@ -1,10 +1,11 @@
mod file;
use super::{
get_custom_client_staging_dir, get_reg_of_hkcr,
handle_custom_client_staging_dir_before_update, installer_handoff::run_cmds,
installer_shell::path_for_cmd_environment, remove_custom_client_staging_dir, ResultType,
REG_NAME_INSTALL_PRINTER,
get_custom_client_staging_dir, get_reg_of_hkcr, handle_custom_client_staging_dir_before_update,
installer_files::ProtectedInstallFile,
installer_handoff::{run_cmds, InstallCommands},
installer_shell::path_for_cmd_environment,
remove_custom_client_staging_dir, ResultType, REG_NAME_INSTALL_PRINTER,
};
use hbb_common::{allow_err, anyhow::anyhow, bail, log};
use std::path::Path;
@@ -15,10 +16,7 @@ const VERIFIED_UPDATE_FILE_PREFIX: &str = "rustdesk-verified-";
const VERIFIED_EXE_FILENAME: &str = "update.exe";
const VERIFIED_MSI_FILENAME: &str = "update.msi";
const VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0009;
const VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_000A;
const VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_000B;
const VERIFIED_EXE_LAUNCH_FAILURE_EXIT_CODE: u32 = 0x5253_000C;
const SHA256_HASH_LENGTH: usize = 32;
pub fn update_to_verified(file: &str, expected_sha256: &str, expected_size: u64) -> ResultType<()> {
let extension = file::update_file_extension(file).unwrap_or_default();
@@ -77,50 +75,33 @@ fn launch_verified_update(
Ok(())
}
fn verified_update_hash_pattern(expected_sha256: &str) -> ResultType<String> {
let hash = hex::decode(expected_sha256.trim())
.map_err(|err| anyhow!("Invalid expected update SHA-256: {err}"))?;
if hash.len() != SHA256_HASH_LENGTH {
bail!("Invalid expected update SHA-256 length");
}
// FINDSTR /R /I /X /C matches the complete certutil hash line; ` *` allows
// optional spaces between byte pairs.
Ok(hash
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<Vec<_>>()
.join(" *"))
}
fn launch_verified_exe(exe: &str, expected_sha256: &str) -> ResultType<()> {
let source = path_for_cmd_environment(Path::new(exe))?;
let hash_pattern = verified_update_hash_pattern(expected_sha256)?;
// Keep the hash outside the EXE directory because update_me() copies that
// entire directory into the installation.
let update = ProtectedInstallFile::copy_verified(Path::new(exe), expected_sha256)?;
let source = path_for_cmd_environment(update.path())?;
let commands = format!(
"set \"RUSTDESK_VERIFIED_EXE_SOURCE={source}\"\r\n\
set \"RUSTDESK_VERIFIED_EXE=%RUSTDESK_OUTPUT_DIR%\\{VERIFIED_EXE_FILENAME}\"\r\n\
copy /B /Y \"%RUSTDESK_VERIFIED_EXE_SOURCE%\" \"%RUSTDESK_VERIFIED_EXE%\" > nul || exit /b {VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE}\r\n\
certutil.exe -hashfile \"%RUSTDESK_VERIFIED_EXE%\" SHA256 > \"%~f0.exe.sha256\" || exit /b {VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE}\r\n\
findstr.exe /R /I /X /C:\"{hash_pattern}\" \"%~f0.exe.sha256\" > nul || exit /b {VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE}\r\n\
start \"\" /wait \"%RUSTDESK_VERIFIED_EXE%\" --update || exit /b {VERIFIED_EXE_LAUNCH_FAILURE_EXIT_CODE}"
);
run_cmds(commands, false, "update-exe")
run_cmds(
InstallCommands {
script: commands,
files: vec![update],
},
false,
"update-exe",
)
}
fn verified_msi_install_commands(
msi: &Path,
expected_sha256: &str,
quiet: bool,
) -> ResultType<String> {
fn verified_msi_install_commands(msi: &Path, quiet: bool) -> ResultType<String> {
use windows::Win32::Foundation::{
ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, ERROR_SUCCESS_REBOOT_REQUIRED,
};
// The open source handle prevents replacing that file, but not retargeting an
// ancestor junction. Copy and reverify it in the protected runner directory.
// The input is already verified and protected, including its parent path.
// Execute the copy from the protected runner to exclude adjacent untrusted files.
let source = path_for_cmd_environment(msi)?;
let hash_pattern = verified_update_hash_pattern(expected_sha256)?;
let quiet_args = if quiet { " /qn LAUNCH_TRAY_APP=N" } else { "" };
let app_name = crate::get_app_name();
let subkey = format!(".{}", app_name.to_lowercase());
@@ -141,8 +122,6 @@ fn verified_msi_install_commands(
"set \"RUSTDESK_VERIFIED_MSI_SOURCE={source}\"\r\n\
set \"RUSTDESK_VERIFIED_MSI=%RUSTDESK_OUTPUT_DIR%\\{VERIFIED_MSI_FILENAME}\"\r\n\
copy /B /Y \"%RUSTDESK_VERIFIED_MSI_SOURCE%\" \"%RUSTDESK_VERIFIED_MSI%\" > nul || exit /b {VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE}\r\n\
certutil.exe -hashfile \"%RUSTDESK_VERIFIED_MSI%\" SHA256 > \"%RUSTDESK_VERIFIED_MSI%.sha256\" || exit /b {VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE}\r\n\
findstr.exe /R /I /X /C:\"{hash_pattern}\" \"%RUSTDESK_VERIFIED_MSI%.sha256\" > nul || exit /b {VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE}\r\n\
msiexec.exe /i \"%RUSTDESK_VERIFIED_MSI%\"{quiet_args}{printer_args} REBOOT=ReallySuppress /norestart\r\n\
set \"RUSTDESK_MSI_EXIT_CODE=%ERRORLEVEL%\"\r\n\
if \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{reboot_initiated}\" exit /b {success}\r\n\
@@ -159,8 +138,12 @@ pub(super) fn install_verified_msi(
expected_sha256: &str,
quiet: bool,
) -> ResultType<()> {
let update = ProtectedInstallFile::copy_verified(Path::new(msi), expected_sha256)?;
run_cmds(
verified_msi_install_commands(Path::new(msi), expected_sha256, quiet)?,
InstallCommands {
script: verified_msi_install_commands(update.path(), quiet)?,
files: vec![update],
},
false,
"update-msi",
)
+12 -3
View File
@@ -107,9 +107,18 @@ pub fn goto_install() {
pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) {
#[cfg(windows)]
std::thread::spawn(move || {
allow_err!(crate::platform::windows::install_me(
&_options, _path, _silent, _debug
));
if let Err(err) = crate::platform::windows::install_me(&_options, _path, _silent, _debug) {
log::error!("Installation failed: {err:#}");
if !_silent
&& !err
.downcast_ref::<::windows::core::Error>()
.is_some_and(|err| {
err.code() == ::windows::Win32::Foundation::ERROR_CANCELLED.to_hresult()
})
{
crate::platform::windows::message_box(&format!("Installation failed:\n{err:#}!"));
}
}
std::process::exit(0);
});
}