use std::fs;
use std::io;
use std::os::windows::ffi::OsStrExt as _;
use std::path::Path;
use crate::platform::private_dir::OwnerPrivateDirectoryOutcome;
const PRIVATE_DIR_SDDL: &str = "D:P(A;OICI;FA;;;OW)(A;OICI;FA;;;SY)";
pub fn ensure_owner_private_directory(path: &Path) -> io::Result<OwnerPrivateDirectoryOutcome> {
fs::create_dir_all(path)?;
if owner_private_directory(path).unwrap_or(false) {
return Ok(OwnerPrivateDirectoryOutcome::AlreadyPrivate);
}
apply_protected_dacl_sddl(path, PRIVATE_DIR_SDDL)?;
if owner_private_directory(path)? {
Ok(OwnerPrivateDirectoryOutcome::Hardened)
} else {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"private-directory permissions were not applied to {}",
path.display()
),
))
}
}
pub fn owner_private_directory(path: &Path) -> io::Result<bool> {
let actual = file_security_descriptor(path)?;
if !actual.dacl_is_protected()? {
return Ok(false);
}
let expected = LocalSecurityDescriptor::from_sddl(PRIVATE_DIR_SDDL)?;
Ok(actual.dacl()?.bytes()? == expected.dacl()?.bytes()?)
}
fn apply_protected_dacl_sddl(path: &Path, sddl: &str) -> io::Result<()> {
use windows_sys::Win32::Security::PROTECTED_DACL_SECURITY_INFORMATION;
apply_dacl_sddl(path, sddl, PROTECTED_DACL_SECURITY_INFORMATION)
}
fn apply_dacl_sddl(
path: &Path,
sddl: &str,
inheritance_control: windows_sys::Win32::Security::OBJECT_SECURITY_INFORMATION,
) -> io::Result<()> {
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::Security::Authorization::{SetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
let descriptor = LocalSecurityDescriptor::from_sddl(sddl)?;
let dacl = descriptor.dacl()?;
let wide = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<_>>();
let status = unsafe {
SetNamedSecurityInfoW(
wide.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | inheritance_control,
std::ptr::null_mut(),
std::ptr::null_mut(),
dacl.as_ptr(),
std::ptr::null_mut(),
)
};
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::from_raw_os_error(status as i32))
}
}
fn file_security_descriptor(path: &Path) -> io::Result<LocalSecurityDescriptor> {
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows_sys::Win32::Security::{DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR};
let wide = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<_>>();
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let status = unsafe {
GetNamedSecurityInfoW(
wide.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut descriptor,
)
};
if status != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(status as i32));
}
if descriptor.is_null() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"GetNamedSecurityInfoW returned a null security descriptor",
));
}
Ok(LocalSecurityDescriptor(descriptor))
}
struct LocalSecurityDescriptor(windows_sys::Win32::Security::PSECURITY_DESCRIPTOR);
impl LocalSecurityDescriptor {
fn from_sddl(sddl: &str) -> io::Result<Self> {
use windows_sys::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
let wide = sddl
.encode_utf16()
.chain(std::iter::once(0))
.collect::<Vec<_>>();
let mut descriptor = std::ptr::null_mut();
let ok = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
wide.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
std::ptr::null_mut(),
)
};
if ok == 0 || descriptor.is_null() {
Err(io::Error::last_os_error())
} else {
Ok(Self(descriptor))
}
}
fn dacl_is_protected(&self) -> io::Result<bool> {
use windows_sys::Win32::Security::{
GetSecurityDescriptorControl, SECURITY_DESCRIPTOR_CONTROL, SE_DACL_PROTECTED,
};
let mut control: SECURITY_DESCRIPTOR_CONTROL = 0;
let mut revision = 0;
let ok = unsafe { GetSecurityDescriptorControl(self.0, &mut control, &mut revision) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(control & SE_DACL_PROTECTED != 0)
}
fn dacl(&self) -> io::Result<SecurityDescriptorDacl<'_>> {
use windows_sys::Win32::Security::{GetSecurityDescriptorDacl, ACL};
let mut present = 0;
let mut defaulted = 0;
let mut pointer: *mut ACL = std::ptr::null_mut();
let ok = unsafe {
GetSecurityDescriptorDacl(self.0, &mut present, &mut pointer, &mut defaulted)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let pointer = std::ptr::NonNull::new(pointer).ok_or_else(|| {
io::Error::new(io::ErrorKind::PermissionDenied, "directory has no DACL")
})?;
Ok(SecurityDescriptorDacl {
pointer,
descriptor: std::marker::PhantomData,
})
}
}
struct SecurityDescriptorDacl<'descriptor> {
pointer: std::ptr::NonNull<windows_sys::Win32::Security::ACL>,
descriptor: std::marker::PhantomData<&'descriptor LocalSecurityDescriptor>,
}
impl SecurityDescriptorDacl<'_> {
fn as_ptr(&self) -> *mut windows_sys::Win32::Security::ACL {
self.pointer.as_ptr()
}
fn bytes(&self) -> io::Result<Vec<u8>> {
use windows_sys::Win32::Security::{
AclSizeInformation, GetAclInformation, ACL_SIZE_INFORMATION,
};
let mut information = ACL_SIZE_INFORMATION {
AceCount: 0,
AclBytesInUse: 0,
AclBytesFree: 0,
};
let ok = unsafe {
GetAclInformation(
self.as_ptr(),
(&mut information as *mut ACL_SIZE_INFORMATION).cast(),
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let byte_len = information.AclBytesInUse as usize;
if byte_len < std::mem::size_of::<windows_sys::Win32::Security::ACL>() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"DACL byte length is smaller than its header",
));
}
Ok(unsafe { std::slice::from_raw_parts(self.as_ptr().cast::<u8>(), byte_len).to_vec() })
}
}
impl Drop for LocalSecurityDescriptor {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::Foundation::LocalFree(self.0.cast());
}
}
}
#[cfg(test)]
mod tests {
use std::fs::{self, File};
use super::*;
#[test]
fn ensure_private_dir_is_a_noop_for_an_already_private_populated_tree() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("private");
ensure_owner_private_directory(&directory).unwrap();
for index in 0..1_500 {
let shard = directory.join(format!("shard-{index:04}"));
fs::create_dir(&shard).unwrap();
fs::write(shard.join("artifact.bin"), b"payload").unwrap();
}
assert_eq!(
ensure_owner_private_directory(&directory).unwrap(),
OwnerPrivateDirectoryOutcome::AlreadyPrivate
);
File::open(directory.join("shard-1499/artifact.bin")).unwrap();
}
#[test]
fn nonstandard_two_ace_policy_is_rejected_and_repaired() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("private");
fs::create_dir_all(&directory).unwrap();
apply_protected_dacl_sddl(&directory, "D:P(A;OICI;FA;;;SY)(OA;OICI;FA;;;OW)")
.unwrap();
assert!(!owner_private_directory(&directory).unwrap());
assert_eq!(
ensure_owner_private_directory(&directory).unwrap(),
OwnerPrivateDirectoryOutcome::Hardened
);
assert!(owner_private_directory(&directory).unwrap());
}
#[test]
fn unprotected_identical_acl_is_rejected_and_repaired() {
use windows_sys::Win32::Security::UNPROTECTED_DACL_SECURITY_INFORMATION;
let temporary = tempfile::tempdir().unwrap();
let parent = temporary.path().join("parent");
let directory = parent.join("private");
fs::create_dir_all(&directory).unwrap();
apply_protected_dacl_sddl(&parent, "D:P(A;;FA;;;OW)(A;;FA;;;SY)").unwrap();
apply_protected_dacl_sddl(&directory, PRIVATE_DIR_SDDL).unwrap();
let protected = file_security_descriptor(&directory).unwrap();
let protected_bytes = protected.dacl().unwrap().bytes().unwrap();
apply_dacl_sddl(
&directory,
PRIVATE_DIR_SDDL,
UNPROTECTED_DACL_SECURITY_INFORMATION,
)
.unwrap();
let unprotected = file_security_descriptor(&directory).unwrap();
assert!(!unprotected.dacl_is_protected().unwrap());
assert_eq!(unprotected.dacl().unwrap().bytes().unwrap(), protected_bytes);
assert!(!owner_private_directory(&directory).unwrap());
assert_eq!(
ensure_owner_private_directory(&directory).unwrap(),
OwnerPrivateDirectoryOutcome::Hardened
);
}
#[test]
fn ensure_private_dir_keeps_existing_children_accessible() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("private");
let child = directory.join("nested");
fs::create_dir_all(&child).unwrap();
let file = child.join("service.bin");
fs::write(&file, b"payload").unwrap();
ensure_owner_private_directory(&directory).unwrap();
File::open(&file).unwrap();
fs::write(&file, b"payload2").unwrap();
fs::read_dir(&child).unwrap();
}
#[test]
fn ensure_private_dir_does_not_brick_hardlinked_files_outside() {
let temporary = tempfile::tempdir().unwrap();
let outside = temporary.path().join("outside.bin");
fs::write(&outside, b"binary").unwrap();
let directory = temporary.path().join("private");
fs::create_dir_all(&directory).unwrap();
fs::hard_link(&outside, directory.join("inside.bin")).unwrap();
ensure_owner_private_directory(&directory).unwrap();
File::open(&outside).unwrap();
fs::write(&outside, b"binary2").unwrap();
}
#[test]
fn legacy_non_inheritable_dacl_is_rejected_and_healed_by_reapply() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("private");
let file = directory.join("service.bin");
fs::create_dir_all(&directory).unwrap();
fs::write(&file, b"payload").unwrap();
apply_protected_dacl_sddl(&directory, "D:P(A;;FA;;;OW)").unwrap();
assert!(!owner_private_directory(&directory).unwrap());
assert!(File::open(&file).is_err());
assert_eq!(
ensure_owner_private_directory(&directory).unwrap(),
OwnerPrivateDirectoryOutcome::Hardened
);
File::open(&file).unwrap();
}
}