use std::sync::{Mutex, MutexGuard};
static WINDOWS_SYMLINK_PRIVILEGE_LOCK: Mutex<()> = Mutex::new(());
#[must_use]
#[doc(hidden)]
pub struct WindowsSymlinkPrivilegeGuard {
privilege: Option<WindowsTokenPrivilegeGuard>,
_lock: MutexGuard<'static, ()>,
}
impl WindowsSymlinkPrivilegeGuard {
pub fn acquire() -> Self {
let lock = WINDOWS_SYMLINK_PRIVILEGE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let privilege = match WindowsTokenPrivilegeGuard::enable_symlink_creation() {
Ok(privilege) => privilege,
Err(error) => {
tracing::debug!(
error = %error,
"Could not enable the optional Windows symlink privilege"
);
None
}
};
Self {
privilege,
_lock: lock,
}
}
pub fn assigned_privilege_enabled(&self) -> bool {
self.privilege.is_some()
}
}
#[doc(hidden)]
pub fn denial_diagnostic(assigned_privilege_enabled: bool) -> &'static str {
if assigned_privilege_enabled {
"SeCreateSymbolicLinkPrivilege was enabled, but the target directory ACL or endpoint \
security denied symbolic-link creation; verify the target ACL and allow the approved \
A3S Box executable and target directory in endpoint security; ERROR_ACCESS_DENIED (5) \
or ERROR_PRIVILEGE_NOT_HELD (1314)"
} else {
"enable Windows Developer Mode or grant SeCreateSymbolicLinkPrivilege and allow the \
target directory; ERROR_ACCESS_DENIED (5) or ERROR_PRIVILEGE_NOT_HELD (1314)"
}
}
struct WindowsTokenPrivilegeGuard {
token: windows_sys::Win32::Foundation::HANDLE,
previous: windows_sys::Win32::Security::TOKEN_PRIVILEGES,
}
impl WindowsTokenPrivilegeGuard {
fn enable_symlink_creation() -> std::io::Result<Option<Self>> {
use std::mem::size_of;
use std::ptr::null;
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, SetLastError, ERROR_NOT_ALL_ASSIGNED, ERROR_SUCCESS, LUID,
};
use windows_sys::Win32::Security::{
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES,
SE_CREATE_SYMBOLIC_LINK_NAME, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES,
TOKEN_PRIVILEGES, TOKEN_QUERY,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let mut token = 0;
if unsafe {
OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&mut token,
)
} == 0
{
return Err(std::io::Error::last_os_error());
}
let mut luid = LUID {
LowPart: 0,
HighPart: 0,
};
if unsafe { LookupPrivilegeValueW(null(), SE_CREATE_SYMBOLIC_LINK_NAME, &mut luid) } == 0 {
let error = std::io::Error::last_os_error();
unsafe { CloseHandle(token) };
return Err(error);
}
let requested = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
let mut previous = TOKEN_PRIVILEGES {
PrivilegeCount: 0,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: LUID {
LowPart: 0,
HighPart: 0,
},
Attributes: 0,
}],
};
let mut previous_length = 0;
unsafe { SetLastError(ERROR_SUCCESS) };
let adjusted = unsafe {
AdjustTokenPrivileges(
token,
0,
&requested,
size_of::<TOKEN_PRIVILEGES>() as u32,
&mut previous,
&mut previous_length,
)
};
let adjustment_error = unsafe { GetLastError() };
if adjusted == 0 {
unsafe { CloseHandle(token) };
return if adjustment_error == ERROR_SUCCESS {
Err(std::io::Error::other(
"AdjustTokenPrivileges failed without a Windows error code",
))
} else {
Err(std::io::Error::from_raw_os_error(adjustment_error as i32))
};
}
if adjustment_error != ERROR_SUCCESS {
unsafe { CloseHandle(token) };
if adjustment_error == ERROR_NOT_ALL_ASSIGNED {
return Ok(None);
}
return Err(std::io::Error::from_raw_os_error(adjustment_error as i32));
}
Ok(Some(Self { token, previous }))
}
}
impl Drop for WindowsTokenPrivilegeGuard {
fn drop(&mut self) {
use std::ptr::null_mut;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Security::AdjustTokenPrivileges;
unsafe {
AdjustTokenPrivileges(self.token, 0, &self.previous, 0, null_mut(), null_mut());
CloseHandle(self.token);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn denial_diagnostic_distinguishes_endpoint_policy() {
let enabled = denial_diagnostic(true);
assert!(enabled.contains("SeCreateSymbolicLinkPrivilege was enabled"));
assert!(enabled.contains("endpoint security"));
assert!(!enabled.contains("enable Windows Developer Mode"));
let unavailable = denial_diagnostic(false);
assert!(unavailable.contains("enable Windows Developer Mode"));
assert!(unavailable.contains("grant SeCreateSymbolicLinkPrivilege"));
}
#[test]
fn acquired_scope_preserves_a_real_link_when_the_identity_is_capable() {
let temporary = tempfile::tempdir().unwrap();
std::fs::write(temporary.path().join("target"), b"probe").unwrap();
let result = {
let _guard = WindowsSymlinkPrivilegeGuard::acquire();
std::os::windows::fs::symlink_file("target", temporary.path().join("link"))
};
match result {
Ok(()) => {
let link = temporary.path().join("link");
assert!(std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
assert_eq!(
std::fs::read_link(link).unwrap(),
std::path::Path::new("target")
);
}
Err(error) if matches!(error.raw_os_error(), Some(5) | Some(1314)) => {
eprintln!("skipping Windows symlink privilege test: {error}");
}
Err(error) => panic!("Windows symlink privilege scope failed unexpectedly: {error}"),
}
let _second = WindowsSymlinkPrivilegeGuard::acquire();
}
}