use std::{
ffi::c_void,
fs::{File, OpenOptions},
io, mem,
os::windows::{
ffi::OsStrExt,
fs::OpenOptionsExt,
io::{AsRawHandle, FromRawHandle, OwnedHandle},
},
path::{Path, PathBuf},
ptr, slice,
};
use windows_sys::Win32::{
Foundation::{ERROR_SUCCESS, HANDLE, INVALID_HANDLE_VALUE, LocalFree},
Security::{
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION, AclSizeInformation,
Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
GetSecurityInfo, SDDL_REVISION_1, SE_FILE_OBJECT, SetSecurityInfo,
},
DACL_SECURITY_INFORMATION, EqualSid, GetAce, GetAclInformation, GetSecurityDescriptorDacl,
GetTokenInformation, INHERIT_ONLY_ACE, IsWellKnownSid, OWNER_SECURITY_INFORMATION,
PROTECTED_DACL_SECURITY_INFORMATION, PSID, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
TokenUser, WinBuiltinAdministratorsSid, WinLocalSystemSid,
},
Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, CreateDirectoryW, FILE_ATTRIBUTE_DIRECTORY,
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE,
FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TYPE_DISK, GetFileInformationByHandle, GetFileType,
READ_CONTROL, ReOpenFile, WRITE_DAC,
},
System::{
SystemServices::{
ACCESS_ALLOWED_ACE_TYPE, ACCESS_DENIED_ACE_TYPE, ACCESS_DENIED_CALLBACK_ACE_TYPE,
ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE, ACCESS_DENIED_OBJECT_ACE_TYPE,
},
Threading::{GetCurrentProcess, OpenProcessToken},
},
};
pub(crate) fn create_private_dir_all(path: &Path) -> io::Result<()> {
match create_private_dir(path) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(_) if path.is_dir() => return Ok(()),
Err(error) => return Err(error),
}
match path.parent() {
Some(parent) => create_private_dir_all(parent)?,
None => return Err(io::Error::other("failed to create whole tree")),
}
match create_private_dir(path) {
Err(_) if path.is_dir() => Ok(()),
result => result,
}
}
pub(crate) fn create_private_dir(path: &Path) -> io::Result<()> {
let descriptor = owner_only(true)?;
let attributes = SECURITY_ATTRIBUTES {
nLength: mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor.0,
bInheritHandle: 0,
};
let path = wide(path)?;
if unsafe { CreateDirectoryW(path.as_ptr(), &attributes) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(crate) fn private_tempdir_in(prefix: &str, parent: &Path) -> io::Result<PrivateTempDir> {
const RANDOM_BYTES: usize = 16;
const CREATE_ATTEMPTS: usize = 128;
if prefix.contains(['/', '\\', '\0']) {
return Err(io::ErrorKind::InvalidInput.into());
}
let parent = std::path::absolute(parent)?;
for _ in 0..CREATE_ATTEMPTS {
let mut random = [0_u8; RANDOM_BYTES];
getrandom::fill(&mut random).map_err(io::Error::other)?;
let path = parent.join(format!("{prefix}{:032x}", u128::from_ne_bytes(random)));
match create_private_dir(&path) {
Ok(()) => return Ok(PrivateTempDir(Some(path))),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"could not create a unique private temporary directory",
))
}
pub(crate) struct PrivateTempDir(Option<PathBuf>);
impl PrivateTempDir {
pub(crate) fn path(&self) -> &Path {
self.0
.as_deref()
.expect("temporary directory is still owned")
}
pub(crate) fn keep(mut self) -> PathBuf {
self.0.take().expect("temporary directory is still owned")
}
}
impl Drop for PrivateTempDir {
fn drop(&mut self) {
if let Some(path) = &self.0 {
let _ = std::fs::remove_dir_all(path);
}
}
}
pub(crate) fn is_private_dir(path: &Path) -> io::Result<bool> {
let directory = OpenOptions::new()
.access_mode(READ_CONTROL)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
is_owner_only(directory.as_raw_handle())
}
pub(crate) fn is_private_file(file: &File) -> io::Result<bool> {
let handle = file.as_raw_handle();
if unsafe { GetFileType(handle) } != FILE_TYPE_DISK {
return Ok(false);
}
let mut info = BY_HANDLE_FILE_INFORMATION::default();
if unsafe { GetFileInformationByHandle(handle, &mut info) } == 0 {
return Err(io::Error::last_os_error());
}
if info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT) != 0
|| info.nNumberOfLinks != 1
{
return Ok(false);
}
is_owner_only(handle)
}
pub(crate) fn private_options() -> OpenOptions {
OpenOptions::new()
}
pub(crate) fn restrict_file(file: &File) -> io::Result<()> {
let handle = unsafe {
ReOpenFile(
file.as_raw_handle(),
READ_CONTROL | WRITE_DAC,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
0,
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let handle = unsafe { OwnedHandle::from_raw_handle(handle) };
restrict(handle.as_raw_handle(), false)
}
fn restrict(handle: HANDLE, directory: bool) -> io::Result<()> {
let descriptor = owner_only(directory)?;
let (mut present, mut defaulted) = (0, 0);
let mut dacl: *mut ACL = ptr::null_mut();
if unsafe { GetSecurityDescriptorDacl(descriptor.0, &mut present, &mut dacl, &mut defaulted) }
== 0
{
return Err(io::Error::last_os_error());
}
let error = unsafe {
SetSecurityInfo(
handle,
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
ptr::null_mut(),
ptr::null_mut(),
dacl,
ptr::null(),
)
};
if error != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(error as i32));
}
Ok(())
}
pub(crate) fn is_owner_only(handle: HANDLE) -> io::Result<bool> {
let mut owner: PSID = ptr::null_mut();
let mut dacl: *mut ACL = ptr::null_mut();
let mut descriptor = ptr::null_mut();
let error = unsafe {
GetSecurityInfo(
handle,
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&mut owner,
ptr::null_mut(),
&mut dacl,
ptr::null_mut(),
&mut descriptor,
)
};
if error != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(error as i32));
}
let _descriptor = Local(descriptor);
let user = current_user()?;
let user = user.sid();
let owned = unsafe {
EqualSid(owner, user) != 0
|| IsWellKnownSid(owner, WinBuiltinAdministratorsSid) != 0
|| IsWellKnownSid(owner, WinLocalSystemSid) != 0
};
if !owned || dacl.is_null() {
return Ok(false);
}
let mut size = ACL_SIZE_INFORMATION::default();
if unsafe {
GetAclInformation(
dacl,
(&raw mut size).cast(),
mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
} == 0
{
return Err(io::Error::last_os_error());
}
for index in 0..size.AceCount {
let mut ace: *mut c_void = ptr::null_mut();
if unsafe { GetAce(dacl, index, &mut ace) } == 0 {
return Err(io::Error::last_os_error());
}
let header = unsafe { ace.cast::<ACE_HEADER>().read_unaligned() };
if u32::from(header.AceFlags) & INHERIT_ONLY_ACE != 0 {
continue;
}
match u32::from(header.AceType) {
ACCESS_ALLOWED_ACE_TYPE => {
let sid = unsafe { &raw mut (*ace.cast::<ACCESS_ALLOWED_ACE>()).SidStart };
if unsafe { EqualSid(sid.cast(), user) } == 0 {
return Ok(false);
}
}
ACCESS_DENIED_ACE_TYPE
| ACCESS_DENIED_OBJECT_ACE_TYPE
| ACCESS_DENIED_CALLBACK_ACE_TYPE
| ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE => {}
_ => return Ok(false),
}
}
Ok(true)
}
fn owner_only(directory: bool) -> io::Result<Local> {
let sid = user_identity()?;
let inheritance = if directory { "OICI" } else { "" };
let sddl: Vec<u16> = format!("O:{sid}D:P(A;{inheritance};FA;;;{sid})")
.encode_utf16()
.chain([0])
.collect();
let mut descriptor = ptr::null_mut();
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
ptr::null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(Local(descriptor))
}
pub(crate) fn user_identity() -> io::Result<String> {
let user = current_user()?;
let mut text = ptr::null_mut();
if unsafe { ConvertSidToStringSidW(user.sid(), &mut text) } == 0 {
return Err(io::Error::last_os_error());
}
let text = Local(text.cast());
let sid = unsafe {
let start = text.0.cast::<u16>();
let length = (0..).take_while(|&index| *start.add(index) != 0).count();
String::from_utf16_lossy(slice::from_raw_parts(start, length))
};
Ok(sid)
}
pub(crate) fn private_pipe(
options: &tokio::net::windows::named_pipe::ServerOptions,
address: &Path,
) -> io::Result<tokio::net::windows::named_pipe::NamedPipeServer> {
let descriptor = owner_only(false)?;
let mut attributes = SECURITY_ATTRIBUTES {
nLength: mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor.0,
bInheritHandle: 0,
};
unsafe { options.create_with_security_attributes_raw(address, (&raw mut attributes).cast()) }
}
struct Local(*mut c_void);
impl Drop for Local {
fn drop(&mut self) {
unsafe { LocalFree(self.0) };
}
}
struct User(Vec<u64>);
impl User {
fn sid(&self) -> PSID {
unsafe { (*self.0.as_ptr().cast::<TOKEN_USER>()).User.Sid }
}
}
fn current_user() -> io::Result<User> {
let mut token = ptr::null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let token = unsafe { OwnedHandle::from_raw_handle(token) };
let mut length = 0;
unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenUser,
ptr::null_mut(),
0,
&mut length,
)
};
let mut buffer = vec![0_u64; (length as usize).div_ceil(std::mem::size_of::<u64>())];
if unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenUser,
buffer.as_mut_ptr().cast(),
length,
&mut length,
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(User(buffer))
}
fn wide(path: &Path) -> io::Result<Vec<u16>> {
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.contains(&0) {
return Err(io::ErrorKind::InvalidInput.into());
}
wide.push(0);
Ok(wide)
}
#[cfg(test)]
mod tests {
use super::{is_private_dir, private_tempdir_in};
#[test]
fn temporary_directory_has_an_explicit_owner_only_dacl_under_a_shared_parent() {
let parent = crate::test_os::tempdir();
crate::test_os::share_dir(parent.path());
assert!(!is_private_dir(parent.path()).unwrap());
let directory = private_tempdir_in("draft-", parent.path()).unwrap();
let path = directory.path().to_owned();
assert!(is_private_dir(&path).unwrap());
assert!(crate::test_os::private_dir(&path));
assert!(!crate::test_os::has_inherited_aces(&path));
std::fs::write(path.join("draft"), "private").unwrap();
assert!(crate::test_os::private_file(&path.join("draft")));
drop(directory);
assert!(!path.exists());
}
#[test]
fn kept_temporary_directory_transfers_cleanup_responsibility() {
let parent = crate::test_os::tempdir();
let directory = private_tempdir_in("kept-", parent.path()).unwrap();
let path = directory.keep();
assert!(path.is_dir());
assert!(is_private_dir(&path).unwrap());
}
}