diff --git a/src/platform/windows/installer_handoff/directory.rs b/src/platform/windows/installer_handoff/directory.rs index 7c23c9cee..dfc3d883e 100644 --- a/src/platform/windows/installer_handoff/directory.rs +++ b/src/platform/windows/installer_handoff/directory.rs @@ -1,8 +1,12 @@ +#[cfg(test)] +#[path = "directory/tests/security.rs"] +mod security_tests; + use super::super::{ - installer_shell::{get_system_executable, path_for_cmd_environment}, + installer_shell::{get_known_folder, get_system_executable, path_for_cmd_environment}, wide_string, }; -use hbb_common::{anyhow::anyhow, bail, ResultType}; +use hbb_common::{anyhow::anyhow, bail, log, ResultType}; use std::{ fs, mem, os::windows::fs::MetadataExt, @@ -19,6 +23,7 @@ use windows::{ PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, }, Storage::FileSystem::{CreateDirectoryW, FILE_ATTRIBUTE_REPARSE_POINT}, + UI::Shell::FOLDERID_ProgramData, }, }; @@ -28,24 +33,93 @@ pub(in crate::platform::windows) fn path() -> ResultType { .parent() .ok_or_else(|| anyhow!("System directory has no parent"))?; let temporary = windows.join("Temp"); - for parent in temporary.ancestors() { + let parent = temporary_parent(&temporary)?; + Ok(parent.join(format!( + "rustdesk-install-{}", + uuid::Uuid::new_v4().simple() + ))) +} + +fn temporary_parent(temporary: &Path) -> ResultType { + let parent = if has_reparse_parent(temporary)? { + let fallback = get_known_folder(&FOLDERID_ProgramData)?; + if has_reparse_parent(&fallback)? { + bail!( + "Installer fallback directory contains a reparse point: {}", + fallback.display() + ); + } + log::warn!( + "Installer temporary directory contains a reparse point; using {} instead of {}", + fallback.display(), + temporary.display() + ); + fallback + } else { + temporary.to_owned() + }; + if !supports_acls(&parent).map_err(|error| { + anyhow!( + "Failed to inspect installer volume {}: {error}", + parent.display() + ) + })? { + bail!( + "Installer temporary volume does not support persistent ACLs: {}", + parent.display() + ); + } + Ok(parent) +} + +fn has_reparse_parent(path: &Path) -> ResultType { + for parent in path.ancestors() { let metadata = fs::symlink_metadata(parent).map_err(|err| { anyhow!( "Failed to inspect installer temporary parent {}: {err}", parent.display() ) })?; - if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 { + if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 { + return Ok(true); + } + if !metadata.is_dir() { bail!( "Installer temporary parent is not a regular directory: {}", parent.display() ); } } - Ok(temporary.join(format!( - "rustdesk-install-{}", - uuid::Uuid::new_v4().simple() - ))) + Ok(false) +} + +fn supports_acls(path: &Path) -> ResultType { + use std::os::windows::{fs::OpenOptionsExt, io::AsRawHandle}; + use windows::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{ + GetVolumeInformationByHandleW, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, + }, + }; + + let directory = fs::OpenOptions::new() + .access_mode(FILE_READ_ATTRIBUTES.0) + .share_mode((FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE).0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0) + .open(path)?; + let mut flags = 0; + unsafe { + GetVolumeInformationByHandleW( + HANDLE(directory.as_raw_handle()), + None, + None, + None, + Some(&mut flags), + None, + )?; + } + Ok(flags & winapi::um::winnt::FILE_PERSISTENT_ACLS != 0) } pub(super) fn create(path: &Path) -> ResultType<()> { diff --git a/src/platform/windows/installer_handoff/directory/tests/security.rs b/src/platform/windows/installer_handoff/directory/tests/security.rs new file mode 100644 index 000000000..83bacbfde --- /dev/null +++ b/src/platform/windows/installer_handoff/directory/tests/security.rs @@ -0,0 +1,51 @@ +use super::*; +use crate::platform::windows::installer_files::ProtectedInstallFile; +use std::{os::windows::process::CommandExt, process::Command}; +use windows::Win32::System::Threading::CREATE_NO_WINDOW; + +#[test] +fn installer_path_uses_windows_temp_unless_redirected() { + let system = get_system_executable("").unwrap(); + let temporary = system.parent().unwrap().join("Temp"); + let expected = if has_reparse_parent(&temporary).unwrap() { + get_known_folder(&FOLDERID_ProgramData).unwrap() + } else { + temporary + }; + assert_eq!(path().unwrap().parent(), Some(expected.as_path())); +} + +#[test] +fn junction_installer_parent_uses_program_data_for_staging() { + let root = std::env::temp_dir().join(format!("rustdesk-junction-{}", uuid::Uuid::new_v4())); + fs::create_dir(&root).unwrap(); + let link = root.join("redirected"); + let mut results = Vec::new(); + for target in [ + &root, + &get_system_executable("").unwrap(), + &root.join("missing"), + ] { + let output = Command::new(get_system_executable("cmd.exe").unwrap()) + .args(["/D", "/C", "mklink", "/J"]) + .arg(&link) + .arg(target) + .creation_flags(CREATE_NO_WINDOW.0) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + results.push(temporary_parent(&link)); + fs::remove_dir(&link).unwrap(); + } + fs::remove_dir(root).unwrap(); + for result in results { + let parent = result.unwrap(); + assert_eq!(parent, get_known_folder(&FOLDERID_ProgramData).unwrap()); + let path = parent.join(format!("rustdesk-fallback-{}.tmp", uuid::Uuid::new_v4())); + let staged = ProtectedInstallFile::new(&path, b"installer").unwrap(); + let contents = fs::read(staged.path()).unwrap(); + drop(staged); + assert_eq!(contents, b"installer"); + assert!(!path.exists()); + } +} diff --git a/src/platform/windows/installer_shell.rs b/src/platform/windows/installer_shell.rs index 7efc2503b..6ae389e0b 100644 --- a/src/platform/windows/installer_shell.rs +++ b/src/platform/windows/installer_shell.rs @@ -115,7 +115,7 @@ pub(super) fn get_system_executable(relative_path: &str) -> ResultType Ok(path) } -fn get_known_folder(id: &windows::core::GUID) -> ResultType { +pub(super) fn get_known_folder(id: &windows::core::GUID) -> ResultType { let value = unsafe { SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None) }?; let path = unsafe { value.to_string() }; unsafe { Com::CoTaskMemFree(Some(value.0.cast())) };