fix(ipc): harden local IPC authorization and portable-service bootstrap flow (#14671)

* fix(ipc): harden ipc access

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

* fix(ipc): full cmd path, comments, simple refactor

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

* fix(ipc): portable service, ipc exit

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

* fix(ipc): Remove unused logs

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

* fix(ipc): Use SetEntriesInAclW instead of icacls

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

* fix(ipc): Comments

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

* fix(ipc): check is_reparse_point

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

* fix(ipc): shmem name, no fallback

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

* fix(ipc): Simple refactor

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

* fix(ipc): better exit and clear

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

* fix(ipc): portable service, better exit

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

* fix(ipc): comments, id -u

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

* fix: comments linux headless, rx desktop ready

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

* fix(ipc): magic number

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

* fix(ipc): update deps

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

* Update Cargo.lock

* Update Cargo.lock

* fix(ipc): harden ipc, test `identity_unavailable`

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

* fix(ipc): portable service, check dir of shmem

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

* fix(ipc): macos, better check exe allowed

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

* fix(ipc): update hbb_common

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

* fix(ipc): update hbb_common

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

* fix(ipc): harden ipc, better active uid for uinput

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

* fix(ipc): harden portable service token validation

Compare portable service IPC tokens in constant time and document the
CSPRNG source used for one-time token generation. Clarify Windows IPC
authorization comments around canonical path matching and partial peer
identity lookup.

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

* fix(ipc): simple refactor

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

* fix(ipc): harden portable service token handling

Generate the portable service IPC token directly from OsRng, keep token
comparison in the IPC layer as a fixed-length byte-wise check, and document
the malformed-frame behavior for protected service IPC.

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

* fix(ipc): comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2026-05-09 18:15:00 +08:00
committed by GitHub
parent 72d27c3c47
commit 9df486a689
12 changed files with 4500 additions and 249 deletions

View File

@@ -29,6 +29,12 @@ use wallpaper;
pub const PA_SAMPLE_RATE: u32 = 48000;
static mut UNMODIFIED: bool = true;
#[derive(Clone, Debug)]
struct ActiveUserLookupCache {
uid: String,
username: String,
}
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
@@ -50,6 +56,8 @@ lazy_static::lazy_static! {
}
}
};
static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
std::sync::Mutex::new(None);
// https://github.com/rustdesk/rustdesk/issues/13705
// Check if `sudo -E` actually preserves environment.
//
@@ -82,6 +90,27 @@ lazy_static::lazy_static! {
};
}
#[inline]
fn update_active_user_lookup_cache(desktop: &Desktop) {
if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {
if desktop.uid.is_empty() || desktop.username.is_empty() {
*cache = None;
} else {
*cache = Some(ActiveUserLookupCache {
uid: desktop.uid.clone(),
username: desktop.username.clone(),
});
}
}
}
#[inline]
fn get_active_user_id_name_from_cache() -> Option<(String, String)> {
let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?;
let entry = cache.as_ref()?;
Some((entry.uid.clone(), entry.username.clone()))
}
thread_local! {
// XDO context - created via libxdo-sys (which uses dynamic loading stub).
// If libxdo is not available, xdo will be null and xdo-based functions become no-ops.
@@ -789,6 +818,7 @@ pub fn start_os_service() {
let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) {
desktop.refresh();
update_active_user_lookup_cache(&desktop);
// Duplicate logic here with should_start_server
// Login wayland will try to start a headless --server.
@@ -861,13 +891,29 @@ pub fn start_os_service() {
}
#[inline]
/// Returns the cached active `(uid, username)` snapshot when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_user_id_name() -> (String, String) {
if let Some(id_name) = get_active_user_id_name_from_cache() {
return id_name;
}
let vec_id_name = get_values_of_seat0(&[1, 2]);
(vec_id_name[0].clone(), vec_id_name[1].clone())
}
#[inline]
/// Returns the cached active uid when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_userid() -> String {
if let Some((uid, _)) = get_active_user_id_name_from_cache() {
return uid;
}
get_values_of_seat0(&[1])[0].clone()
}
#[inline]
/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache.
pub fn get_active_userid_fresh() -> String {
get_values_of_seat0(&[1])[0].clone()
}
@@ -922,7 +968,12 @@ fn _get_display_manager() -> String {
}
#[inline]
/// Returns the cached active username when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_username() -> String {
if let Some((_, username)) = get_active_user_id_name_from_cache() {
return username;
}
get_values_of_seat0(&[2])[0].clone()
}

View File

@@ -73,10 +73,19 @@ use winapi::{
};
use windows::Win32::{
Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE},
Security::{
GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser,
WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER,
},
System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
System::Threading::{
OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken,
QueryFullProcessImageNameW as WinQueryFullProcessImageNameW,
PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION,
},
};
use windows_service::{
define_windows_service,
@@ -88,6 +97,14 @@ use windows_service::{
};
use winreg::{enums::*, RegKey};
mod acl;
pub(crate) use acl::current_process_user_sid_string;
pub use acl::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
};
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
pub const EXPLORER_EXE: &'static str = "explorer.exe";
pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW";
@@ -565,6 +582,55 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD {
unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) }
}
#[inline]
fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option<u32> {
let share_rdp_enabled = is_share_rdp();
if get_available_sessions(false)
.iter()
.any(|e| e.sid == session_id)
{
return Some(session_id);
}
let current_active_session =
unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) };
if current_active_session == u32::MAX {
None
} else {
Some(current_active_session)
}
}
#[inline]
fn authorize_service_scoped_ipc_connection(
stream: &ipc::Connection,
expected_active_session_id: Option<u32>,
) -> bool {
let (authorized, peer_pid, peer_session_id, peer_is_system) =
stream.service_authorization_status_for_session(expected_active_session_id);
if !authorized {
ipc::log_rejected_windows_ipc_connection(
crate::POSTFIX_SERVICE,
peer_pid,
peer_session_id,
expected_active_session_id,
peer_is_system,
);
return false;
}
if let Err(err) =
ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE)
{
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
crate::POSTFIX_SERVICE,
peer_pid,
err
);
return false;
}
true
}
extern "system" {
fn BlockInput(v: BOOL) -> BOOL;
}
@@ -631,6 +697,15 @@ async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
Ok(res) => match res {
Some(Ok(stream)) => {
let mut stream = ipc::Connection::new(stream);
// Keep IPC authorization consistent with the session we are currently serving.
// Recompute expected session right before authorization to avoid using a stale
// session_id after awaiting incoming.next().
let expected_active_session_id =
resolve_expected_active_session_id_for_service(session_id);
if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id)
{
continue;
}
if let Ok(Some(data)) = stream.next_timeout(1000).await {
match data {
ipc::Data::Close => {
@@ -1141,6 +1216,22 @@ pub fn get_active_user_home() -> Option<PathBuf> {
None
}
#[cfg(not(feature = "flutter"))]
#[inline]
pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> {
// Keep parity with history for now: derive LocalAppData from user profile path.
// If users report redirected/non-standard LocalAppData issues, switch to:
// `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution.
let user_dir = hbb_common::directories_next::UserDirs::new()?;
let dir = user_dir
.home_dir()
.join("AppData")
.join("Local")
.join("rustdesk-sciter");
let dst = dir.join("rustdesk.exe");
Some((dir, dst))
}
pub fn is_prelogin() -> bool {
let Some(username) = get_current_session_username() else {
return false;
@@ -2327,16 +2418,33 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
is_run_as_system,
crate::username(),
);
let arg_elevate = if is_setup {
let mut arg_elevate = if is_setup {
"--noinstall --elevate"
} else {
"--elevate"
};
let arg_run_as_system = if is_setup {
}
.to_owned();
let mut arg_run_as_system = if is_setup {
"--noinstall --run-as-system"
} else {
"--run-as-system"
};
}
.to_owned();
let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args();
if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() {
log::error!("Invalid portable service shared memory argument, aborting elevation flow");
// This is a malformed bootstrap argument in a privilege-sensitive path.
// Keep fail-closed process termination here to avoid continuing elevation
// with inconsistent shared-memory contract.
std::process::exit(1);
}
if let Some(shmem_name) = shmem_name_from_args {
let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name);
arg_elevate.push(' ');
arg_elevate.push_str(&shmem_arg);
arg_run_as_system.push(' ');
arg_run_as_system.push_str(&shmem_arg);
}
if is_root() {
if is_run_as_system {
log::info!("run portable service");
@@ -2347,7 +2455,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
Ok(elevated) => {
if elevated {
if !is_run_as_system {
if run_as_system(arg_run_as_system).is_ok() {
if run_as_system(arg_run_as_system.as_str()).is_ok() {
std::process::exit(0);
} else {
log::error!(
@@ -2358,7 +2466,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
}
} else {
if !is_elevate {
if let Ok(true) = elevate(arg_elevate) {
if let Ok(true) = elevate(arg_elevate.as_str()) {
std::process::exit(0);
} else {
log::error!("Failed to elevate, error {}", io::Error::last_os_error());
@@ -2416,6 +2524,115 @@ pub fn is_elevated(process_id: Option<DWORD>) -> ResultType<bool> {
}
}
#[inline]
unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType<Vec<u8>> {
let mut token_user_size = 0u32;
let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size);
match get_info_result {
Ok(()) => {
if token_user_size == 0 {
bail!(
"Failed to get {} token user size: unexpected zero buffer size",
subject
);
}
}
Err(e) => {
// Allow expected size-probe failures if Windows still returns required size.
let is_insufficient_buffer =
e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32);
let is_bad_length =
e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32);
if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 {
bail!("Failed to get {} token user size: {}", subject, e);
}
}
}
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token,
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?;
let min_size = std::mem::size_of::<TOKEN_USER>();
if buffer.len() < min_size {
bail!(
"Failed to parse {} token user: buffer too small (got {}, need >= {})",
subject,
buffer.len(),
min_size
);
}
Ok(buffer)
}
/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process.
///
/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18).
///
/// TODO: After a few releases of real-world validation, consider replacing
/// the legacy `is_local_system()` with this implementation.
pub fn is_process_running_as_system(process_id: DWORD) -> ResultType<bool> {
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let mut token = WinHANDLE::default();
let result = (|| -> ResultType<bool> {
WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?;
let token_subject = format!("process {}", process_id);
let buffer = read_token_user_buffer(token, token_subject.as_str())?;
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool())
})();
if !token.is_invalid() {
let _ = WinCloseHandle(token);
}
let _ = WinCloseHandle(process);
result
}
}
pub fn get_process_executable_path(process_id: DWORD) -> ResultType<PathBuf> {
const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024;
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let result = (|| -> ResultType<PathBuf> {
let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN];
let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32;
WinQueryFullProcessImageNameW(
process,
windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0),
windows::core::PWSTR(buffer.as_mut_ptr()),
&mut length,
)
.map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?;
if length == 0 {
bail!(
"Failed to query process {} image path: empty result",
process_id
);
}
buffer.truncate(length as usize);
Ok(PathBuf::from(OsString::from_wide(&buffer)))
})();
let _ = WinCloseHandle(process);
result
}
}
pub fn is_foreground_window_elevated() -> ResultType<bool> {
unsafe {
let mut process_id: DWORD = 0;
@@ -2708,16 +2925,6 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
return Ok(());
}
pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> {
std::process::Command::new("icacls")
.arg(dir.as_os_str())
.arg("/grant")
.arg(format!("*S-1-1-0:(OI)(CI){}", permission))
.arg("/T")
.spawn()?;
Ok(())
}
#[inline]
fn str_to_device_name(name: &str) -> [u16; 32] {
let mut device_name: Vec<u16> = wide_string(name);
@@ -4281,6 +4488,87 @@ pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
#[cfg(test)]
mod tests {
use super::*;
// Test-only reusable Win32 HANDLE RAII helper.
// If a future non-test path needs the same pattern, move it out of this test module.
//
// This struct is similar to `hbb_common::platform::windows::RAIIHandle`,
// but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate.
struct HandleGuard(WinHANDLE);
impl HandleGuard {
#[inline]
fn new(handle: WinHANDLE) -> Self {
Self(handle)
}
#[inline]
fn get(&self) -> WinHANDLE {
self.0
}
}
impl Drop for HandleGuard {
fn drop(&mut self) {
unsafe {
if !self.0.is_invalid() {
let _ = WinCloseHandle(self.0);
}
}
}
}
#[test]
fn test_is_process_running_as_system_invalid_pid_errors() {
assert!(is_process_running_as_system(u32::MAX).is_err());
}
#[test]
fn test_is_process_running_as_system_matches_current_process_token_user() {
let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() };
let actual = is_process_running_as_system(pid).unwrap();
let expected = unsafe {
// Keep this test consistent: use only the `windows` crate APIs/types.
let process = HandleGuard::new(
WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
.expect("WinOpenProcess should succeed for current process"),
);
let mut token = WinHANDLE::default();
WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token)
.expect("WinOpenProcessToken should succeed for current process");
let token = HandleGuard::new(token);
let mut token_user_size = 0u32;
let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size);
assert_ne!(token_user_size, 0, "TokenUser size should be non-zero");
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token.get(),
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.expect("WinGetTokenInformation(TokenUser) should succeed for current process");
let min_size = std::mem::size_of::<TOKEN_USER>();
assert!(
buffer.len() >= min_size,
"TokenUser buffer too small (got {}, need >= {})",
buffer.len(),
min_size
);
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool();
expected
};
assert_eq!(actual, expected);
}
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());

903
src/platform/windows/acl.rs Normal file
View File

@@ -0,0 +1,903 @@
// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary
use super::{read_token_user_buffer, wide_string, ResultType};
use hbb_common::{anyhow::anyhow, bail};
use std::{
fs, io,
os::windows::{ffi::OsStrExt, fs::MetadataExt},
path::Path,
};
use windows::{
core::{PCWSTR, PWSTR},
Win32::{
Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL},
Security::{
Authorization::{
ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW,
SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS,
SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
},
ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
TOKEN_QUERY, TOKEN_USER,
},
Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE},
System::Threading::{GetCurrentProcess, OpenProcessToken},
},
};
const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400;
#[inline]
fn is_reparse_point(metadata: &fs::Metadata) -> bool {
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0
}
fn apply_grant_sid_allow_ace_to_path(
path: &Path,
sid_ptr: *mut std::ffi::c_void,
access_mask: u32,
is_group: bool,
is_dir: bool,
) -> ResultType<()> {
// Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW.
// https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c--
let mut old_dacl: *mut ACL = std::ptr::null_mut();
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let get_named_result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut old_dacl),
None,
&mut security_descriptor,
)
};
if get_named_result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
get_named_result.0
);
}
let _sd_guard = LocalAllocGuard(security_descriptor.0);
let inherit_flags = if is_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let explicit_access = [make_sid_trustee_entry(
sid_ptr,
access_mask,
inherit_flags,
is_group,
)];
let old_acl_option = if old_dacl.is_null() {
None
} else {
Some(old_dacl as *const ACL)
};
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result = unsafe {
SetEntriesInAclW(
Some(explicit_access.as_slice()),
old_acl_option,
&mut new_acl,
)
};
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be
/// readable/executable across user contexts.
///
/// `access_mask` is the Win32 file access mask to grant recursively.
pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> {
let metadata = fs::symlink_metadata(dir).map_err(|e| {
anyhow!(
"Failed to inspect ACL target directory '{}': {}",
dir.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"ACL target directory is a reparse point and is rejected: '{}'",
dir.display()
);
}
if !metadata.file_type().is_dir() {
bail!("ACL target is not a directory: '{}'", dir.display());
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?;
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let metadata = fs::symlink_metadata(&path)
.map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?;
if is_reparse_point(&metadata) {
continue;
}
let is_dir = metadata.file_type().is_dir();
apply_grant_sid_allow_ace_to_path(
&path,
everyone_sid.as_sid_ptr(),
access_mask,
true,
is_dir,
)?;
if !is_dir {
continue;
}
for entry in fs::read_dir(&path)
.map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))?
{
let entry = entry.map_err(|e| {
anyhow!(
"Failed to read ACL target dir entry under '{}': {}",
path.display(),
e
)
})?;
stack.push(entry.path());
}
}
Ok(())
}
/// Returns the current process user SID as a standard SID string
/// (for example: `S-1-5-18`).
///
/// Source:
/// - Official SID-to-string API (`ConvertSidToStringSidW`):
/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw
pub(crate) fn current_process_user_sid_string() -> ResultType<String> {
let mut token = HANDLE::default();
let result = (|| -> ResultType<String> {
unsafe {
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open current process token: {}", e))?;
}
let buffer = unsafe { read_token_user_buffer(token, "current process")? };
let token_user: TOKEN_USER =
unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) };
if token_user.User.Sid.0.is_null() {
bail!("Token SID is null");
}
let mut sid_string_ptr = PWSTR::null();
unsafe {
ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| {
anyhow!(
"ConvertSidToStringSidW failed for current process token SID: {}",
e
)
})?;
}
if sid_string_ptr.is_null() {
bail!("ConvertSidToStringSidW returned null SID string pointer");
}
let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void);
unsafe {
sid_string_ptr
.to_string()
.map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e))
}
})();
if !token.is_invalid() {
unsafe {
let _ = CloseHandle(token);
}
}
result
}
/// Hardens ACLs for portable-service shared-memory path (directory or file).
///
/// Why:
/// - Shared memory used by portable service carries runtime control/data and must not inherit
/// broad/default ACLs.
/// - We explicitly grant only trusted principals and remove broad groups to reduce local
/// privilege-boundary bypass risk.
///
/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`):
/// - common (directory + file):
/// - `S-1-5-18` (LocalSystem): full control
/// - `S-1-5-32-544` (Built-in Administrators): full control
/// - `current_process_user_sid_string()` result: full control
/// - directory (`portable_service_shmem` parent):
/// - keep `Authenticated Users` directory-level write so other local accounts can
/// create their own runtime shmem files after account switching
/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself;
/// it is intentionally not inherited by children.
/// Reference:
/// - File access rights:
/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
/// - ACE inheritance rules:
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules
/// - remove `Everyone` and `Users` grants
/// - file (`shared_memory*` flink):
/// - remove broad grants:
/// - `S-1-1-0` (Everyone)
/// - `S-1-5-11` (Authenticated Users)
/// - `S-1-5-32-545` (Users)
///
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, true)
}
#[inline]
pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
validate_portable_service_shmem_dir_target(path)
}
#[inline]
pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, false)
}
#[derive(Debug)]
pub(super) struct LocalAllocGuard(*mut std::ffi::c_void);
impl LocalAllocGuard {
#[inline]
pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void {
self.0
}
}
impl Drop for LocalAllocGuard {
fn drop(&mut self) {
if self.0.is_null() {
return;
}
// Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW /
// ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed.
unsafe {
let _ = LocalFree(Some(HLOCAL(self.0)));
}
}
}
#[inline]
pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType<LocalAllocGuard> {
let sid_utf16 = wide_string(sid);
let mut sid_ptr = PSID::default();
unsafe {
ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr)
.map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?;
}
if sid_ptr.0.is_null() {
bail!("ConvertStringSidToSidW returned null SID for '{}'", sid);
}
Ok(LocalAllocGuard(sid_ptr.0))
}
#[inline]
fn make_sid_trustee_entry(
sid_ptr: *mut std::ffi::c_void,
access_permissions: u32,
inheritance: ACE_FLAGS,
is_group: bool,
) -> EXPLICIT_ACCESS_W {
// `is_group` is explicitly provided by the caller from the concrete SID semantic
// (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user).
EXPLICIT_ACCESS_W {
grfAccessPermissions: access_permissions,
grfAccessMode: SET_ACCESS,
grfInheritance: inheritance,
Trustee: TRUSTEE_W {
pMultipleTrustee: std::ptr::null_mut(),
MultipleTrusteeOperation: Default::default(),
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: if is_group {
TRUSTEE_IS_GROUP
} else {
TRUSTEE_IS_USER
},
// SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID.
ptstrName: PWSTR::from_raw(sid_ptr as *mut u16),
},
}
}
fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> {
let metadata = fs::symlink_metadata(path).map_err(|e| {
anyhow!(
"Failed to inspect portable service shared-memory ACL directory '{}': {}",
path.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'",
path.display()
);
}
if !metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is not a directory: '{}'",
path.display()
);
}
Ok(())
}
fn set_path_permission_for_portable_service_shmem_impl(
path: &Path,
expect_dir: bool,
) -> ResultType<()> {
if expect_dir {
validate_portable_service_shmem_dir_target(path)?;
} else {
let metadata_result = fs::symlink_metadata(path);
match metadata_result {
Ok(metadata) => {
if metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is a directory, expected file-like path: '{}'",
path.display()
);
}
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'",
path.display()
);
}
}
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::PermissionDenied =>
{
// Keep going and let Win32 ACL APIs return the final OS error.
// `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into
// a false "not found" signal under restricted directory ACLs.
}
Err(e) => {
bail!(
"Failed to inspect portable service shared-memory ACL target '{}': {}",
path.display(),
e
);
}
}
}
let user_sid = current_process_user_sid_string()?;
let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?;
let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?;
let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?;
let authenticated_users_sid = if expect_dir {
Some(sid_string_to_local_alloc_guard("S-1-5-11")?)
} else {
None
};
let inherit_flags = if expect_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let mut entries = vec![
make_sid_trustee_entry(
local_system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
make_sid_trustee_entry(
administrators_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
true,
),
make_sid_trustee_entry(
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
];
if let Some(auth_sid) = authenticated_users_sid.as_ref() {
// Keep the shared parent directory multi-user writable at directory level.
entries.push(make_sid_trustee_entry(
auth_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0,
NO_INHERITANCE,
true,
));
}
// Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected.
// This avoids carrying over broad legacy ACEs from inherited/default ACLs.
// Reference:
// - SetEntriesInAclW:
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw
// - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION):
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result =
unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) };
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
security_info,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
current_process_user_sid_string, set_path_permission,
set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard,
LocalAllocGuard, ResultType,
};
use hbb_common::bail;
use std::{
fs,
os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file},
path::{Path, PathBuf},
};
use windows::{
core::PCWSTR,
Win32::{
Security::{
AclSizeInformation,
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl,
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION,
DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
},
Storage::FileSystem::{
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
},
},
};
const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0;
fn unique_acl_test_path(prefix: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"rustdesk_acl_{}_{}_{}",
prefix,
std::process::id(),
hbb_common::rand::random::<u32>()
))
}
fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_dir(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create directory reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_file(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create file reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> {
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut sd = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut dacl),
None,
&mut sd,
)
};
if result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
result.0
);
}
if dacl.is_null() || sd.0.is_null() {
bail!("DACL/security descriptor missing for '{}'", path.display());
}
Ok((dacl, LocalAllocGuard(sd.0)))
}
fn has_allow_ace_with_mask(
dacl: *const ACL,
sid_ptr: *mut std::ffi::c_void,
mask: u32,
) -> bool {
let mut info = ACL_SIZE_INFORMATION::default();
if unsafe {
GetAclInformation(
dacl,
&mut info as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
}
.is_err()
{
return false;
}
for index in 0..info.AceCount {
let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() {
continue;
}
let header = unsafe { &*(ace_ptr as *const ACE_HEADER) };
if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 {
continue;
}
let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) };
let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void);
if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok()
&& (allowed.Mask & mask) == mask
{
return true;
}
}
false
}
fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool {
has_allow_ace_with_mask(dacl, sid_ptr, 0)
}
fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool {
let mut control: u16 = 0;
let mut revision: u32 = 0;
if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() {
return false;
}
(control & SE_DACL_PROTECTED.0) != 0
}
#[test]
fn test_portable_service_shmem_dir_acl_policy() {
let dir = unique_acl_test_path("dir");
fs::create_dir_all(&dir).unwrap();
set_path_permission_for_portable_service_shmem_dir(&dir).unwrap();
let (dacl, sd_guard) = get_file_dacl(&dir).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
auth_users_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_policy() {
let dir = unique_acl_test_path("file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("shared_memory_portable_service_test");
fs::write(&file, b"x").unwrap();
set_path_permission_for_portable_service_shmem_file(&file).unwrap();
let (dacl, sd_guard) = get_file_dacl(&file).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(!has_any_allow_ace_for_sid(
dacl,
auth_users_sid.as_sid_ptr()
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_set_path_permission_rx_applies_recursively() {
let root = unique_acl_test_path("set_path_permission");
let child_dir = root.join("child");
let child_file = child_dir.join("helper.exe");
fs::create_dir_all(&child_dir).unwrap();
fs::write(&child_file, b"x").unwrap();
if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) {
let text = err.to_string();
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
if text.contains("win32_error=5") || text.contains("Access is denied") {
eprintln!(
"skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}",
text
);
return;
}
panic!("set_path_permission failed unexpectedly: {}", text);
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0;
for target in [&root, &child_dir, &child_file] {
let (dacl, _sd_guard) = get_file_dacl(target).unwrap();
assert!(
has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask),
"Everyone RX grant missing on '{}'",
target.display()
);
}
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_file_target() {
let dir = unique_acl_test_path("dir_target_file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("target.txt");
fs::write(&file, b"x").unwrap();
let result = set_path_permission_for_portable_service_shmem_dir(&file);
assert!(result.is_err());
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_dir_target() {
let dir = unique_acl_test_path("file_target_dir");
fs::create_dir_all(&dir).unwrap();
let result = set_path_permission_for_portable_service_shmem_file(&dir);
assert!(result.is_err());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_missing_target() {
let path = unique_acl_test_path("missing").join("shared_memory_missing");
let result = set_path_permission_for_portable_service_shmem_file(&path);
assert!(result.is_err());
}
#[test]
fn test_set_path_permission_rejects_reparse_entrypoint() {
let root = unique_acl_test_path("reparse_entry");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_set_path_permission_rejects_reparse_entrypoint",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_dir");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_portable_service_shmem_dir_acl_rejects_reparse_target",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_dir(&link_dir);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_file");
let real_file = root.join("real.txt");
let link_file = root.join("link.txt");
fs::create_dir_all(&root).unwrap();
fs::write(&real_file, b"x").unwrap();
if !try_create_file_reparse_point(
&real_file,
&link_file,
"test_portable_service_shmem_file_acl_rejects_reparse_target",
) {
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_file(&link_file);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_file(&link_file);
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
}
}