Fix/windows installer temp junction (#16871)

* fix(windows): support secure installer temp junctions

Preserve Windows Temp when persistent ACLs are available, validate resolved junction targets, and fall back to ProgramData only when the target volume lacks ACL support. Guard parent directories while creating protected installer files and cover the Windows sharing behavior with regression tests.

Refs rustdesk/rustdesk#16841

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(windows): simplify installer temp junction fallback

Signed-off-by: fufesou <linlong1266@gmail.com>

* test(windows): cover installer staging path selection

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou authored and GitHub committed 2026-11-05 15:52:35 +08:00
1 parent 79c63639ea
commit 13cccb2f7b
3 files changed
+134 -9

No files matched your search

@@ -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<PathBuf> {
.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<PathBuf> {
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<bool> {
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<bool> {
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<()> {
@@ -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());
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ pub(super) fn get_system_executable(relative_path: &str) -> ResultType<PathBuf>
Ok(path)
}
fn get_known_folder(id: &windows::core::GUID) -> ResultType<PathBuf> {
pub(super) fn get_known_folder(id: &windows::core::GUID) -> ResultType<PathBuf> {
let value = unsafe { SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None) }?;
let path = unsafe { value.to_string() };
unsafe { Com::CoTaskMemFree(Some(value.0.cast())) };