use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows_sys::Win32::Foundation::{LocalFree, ERROR_SUCCESS};
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Security::Authorization::{
ConvertSecurityDescriptorToStringSecurityDescriptorW, GetNamedSecurityInfoW,
SetNamedSecurityInfoW, SDDL_REVISION_1, SE_FILE_OBJECT,
};
use windows_sys::Win32::Security::{
AddAccessAllowedAce, AddAccessDeniedAce, AddAce, GetSecurityDescriptorDacl, InitializeAcl,
ACE_HEADER, ACL, ACL_REVISION, DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
};
const SEAL_RIGHTS: u32 = 0x2 | 0x4;
const DENY_ACE_TYPE: u8 = 1;
#[cfg(test)]
const ALLOW_ACE_TYPE: u8 = 0;
fn world_sid() -> &'static [u8] {
const WORLD: &[u8] = &[1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
WORLD
}
fn seal_ace_size() -> usize {
std::mem::size_of::<ACE_HEADER>() + 4 + world_sid().len()
}
pub fn deny_in_place_writes(path: &Path) -> io::Result<()> {
let (descriptor, dacl, null_dacl) = read_dacl(path)?;
let result = (|| {
if has_seal(dacl) {
return Ok(());
}
let rebuilt = rebuild_with_seal(dacl, null_dacl, true)?;
set_dacl(path, &rebuilt)
})();
unsafe { LocalFree(descriptor) };
result?;
if !in_place_writes_denied(path)? {
let observed = dacl_sddl(path).unwrap_or_else(|| "<sddl unavailable>".into());
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"the filesystem did not retain the in-place write seal (no ACL support?); \
observed DACL after write: {observed}"
),
));
}
Ok(())
}
fn dacl_sddl(path: &Path) -> Option<String> {
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let mut dacl: *mut ACL = std::ptr::null_mut();
let rc = unsafe {
GetNamedSecurityInfoW(
wide(path).as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut dacl,
std::ptr::null_mut(),
&mut descriptor,
)
};
if rc != ERROR_SUCCESS || descriptor.is_null() {
return None;
}
let mut text: *mut u16 = std::ptr::null_mut();
let ok = unsafe {
ConvertSecurityDescriptorToStringSecurityDescriptorW(
descriptor,
SDDL_REVISION_1,
DACL_SECURITY_INFORMATION,
&mut text,
std::ptr::null_mut(),
)
};
let sddl = if ok != 0 && !text.is_null() {
let mut chars = Vec::new();
let mut p = text;
unsafe {
while *p != 0 {
chars.push(*p);
p = p.add(1);
}
}
Some(String::from_utf16_lossy(&chars))
} else {
None
};
unsafe {
if !text.is_null() {
LocalFree(text as HLOCAL);
}
LocalFree(descriptor as HLOCAL);
}
sddl
}
pub fn allow_in_place_writes(path: &Path) -> io::Result<()> {
let (descriptor, dacl, null_dacl) = read_dacl(path)?;
let result = (|| {
if !has_seal(dacl) {
return Ok(());
}
let rebuilt = rebuild_with_seal(dacl, null_dacl, false)?;
set_dacl(path, &rebuilt)
})();
unsafe { LocalFree(descriptor) };
result
}
pub fn in_place_writes_denied(path: &Path) -> io::Result<bool> {
let (descriptor, dacl, _null_dacl) = read_dacl(path)?;
let denied = has_seal(dacl);
unsafe { LocalFree(descriptor) };
Ok(denied)
}
fn set_dacl(path: &Path, acl: &[u8]) -> io::Result<()> {
let rc = unsafe {
SetNamedSecurityInfoW(
wide(path).as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
acl.as_ptr() as *mut ACL,
std::ptr::null_mut(),
)
};
if rc != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(rc as i32));
}
Ok(())
}
fn read_dacl(path: &Path) -> io::Result<(
*mut std::ffi::c_void,
*const ACL,
bool,
)> {
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let mut dacl: *mut ACL = std::ptr::null_mut();
let rc = unsafe {
GetNamedSecurityInfoW(
wide(path).as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut dacl,
std::ptr::null_mut(),
&mut descriptor,
)
};
if rc != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(rc as i32));
}
if descriptor.is_null() {
return Err(io::Error::other(
"GetNamedSecurityInfoW returned a null security descriptor",
));
}
let mut present = 0;
let mut defaulted = 0;
if unsafe { GetSecurityDescriptorDacl(descriptor, &mut present, &mut dacl, &mut defaulted) }
== 0
{
unsafe { LocalFree(descriptor) };
return Err(io::Error::last_os_error());
}
if present == 0 || dacl.is_null() {
return Ok((descriptor, empty_acl(), true));
}
Ok((descriptor, dacl, false))
}
fn empty_acl() -> *const ACL {
static EMPTY: ACL = ACL {
AclRevision: ACL_REVISION as u8,
Sbz1: 0,
AceCount: 0,
AclSize: std::mem::size_of::<ACL>() as u16,
Sbz2: 0,
};
&EMPTY
}
fn has_seal(dacl: *const ACL) -> bool {
let acl = unsafe { &*dacl };
let bytes = unsafe { std::slice::from_raw_parts(dacl.cast::<u8>(), acl.AclSize as usize) };
aces(bytes).any(|offset| is_seal_ace(&bytes[offset.0..offset.1]))
}
fn aces(bytes: &[u8]) -> impl Iterator<Item = (usize, usize)> + '_ {
let mut offset = std::mem::size_of::<ACL>();
std::iter::from_fn(move || {
if offset + std::mem::size_of::<ACE_HEADER>() > bytes.len() {
return None;
}
let header: ACE_HEADER = unsafe {
std::ptr::read_unaligned(bytes[offset..].as_ptr().cast::<ACE_HEADER>())
};
let len = header.AceSize as usize;
if len < std::mem::size_of::<ACE_HEADER>() || offset + len > bytes.len() {
return None;
}
let range = (offset, offset + len);
offset += len;
Some(range)
})
}
fn is_seal_ace(ace: &[u8]) -> bool {
let header_len = std::mem::size_of::<ACE_HEADER>();
let need = header_len + 4 + world_sid().len();
if ace.len() != need {
return false;
}
let header: ACE_HEADER = unsafe { std::ptr::read_unaligned(ace.as_ptr().cast::<ACE_HEADER>()) };
if header.AceType != DENY_ACE_TYPE {
return false;
}
let mask = u32::from_ne_bytes(
ace[header_len..header_len + 4]
.try_into()
.expect("four bytes follow every ACE header"),
);
mask == SEAL_RIGHTS && &ace[header_len + 4..] == world_sid()
}
fn rebuild_with_seal(dacl: *const ACL, null_dacl: bool, seal: bool) -> io::Result<Vec<u8>> {
let acl = unsafe { &*dacl };
let current = unsafe { std::slice::from_raw_parts(dacl.cast::<u8>(), acl.AclSize as usize) };
let kept: Vec<(usize, usize)> = aces(current)
.filter(|(start, end)| !is_seal_ace(¤t[*start..*end]))
.collect();
let extra = if seal {
seal_ace_size()
} else {
0
} + if seal && null_dacl {
std::mem::size_of::<ACE_HEADER>() + 4 + world_sid().len()
} else {
0
};
let total = std::mem::size_of::<ACL>() + extra + kept.iter().map(|(_, len)| *len).sum::<usize>();
let mut buffer = vec![0_u8; total];
if unsafe { InitializeAcl(buffer.as_mut_ptr().cast::<ACL>(), buffer.len() as u32, ACL_REVISION) }
== 0
{
return Err(io::Error::last_os_error());
}
let mut aces_in_rebuilt = 0_u32;
if seal {
if unsafe {
AddAccessDeniedAce(
buffer.as_mut_ptr().cast::<ACL>(),
ACL_REVISION,
SEAL_RIGHTS,
world_sid().as_ptr().cast_mut().cast(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
aces_in_rebuilt += 1;
if null_dacl {
if unsafe {
AddAccessAllowedAce(
buffer.as_mut_ptr().cast::<ACL>(),
ACL_REVISION,
0x1F01FF, world_sid().as_ptr().cast_mut().cast(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
aces_in_rebuilt += 1;
}
}
for (start, end) in &kept {
if unsafe {
AddAce(
buffer.as_mut_ptr().cast::<ACL>(),
ACL_REVISION,
aces_in_rebuilt,
current[*start..].as_ptr().cast_mut().cast(),
(end - start) as u32,
)
} == 0
{
return Err(io::Error::last_os_error());
}
aces_in_rebuilt += 1;
}
Ok(buffer)
}
fn wide(path: &Path) -> Vec<u16> {
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_the_deny_ace_written_by_windows() {
let rebuilt = rebuild_with_seal(empty_acl(), true, true).expect("build sealed ACL");
let entries: Vec<_> = aces(&rebuilt).collect();
assert_eq!(entries.len(), 2, "deny followed by the null DACL's grant");
let (start, end) = entries[0];
assert!(is_seal_ace(&rebuilt[start..end]), "the native deny ACE must match");
let (start, end) = entries[1];
assert!(!is_seal_ace(&rebuilt[start..end]), "the native allow ACE must not match");
}
#[test]
fn seal_ace_is_recognized_and_rejected_structurally() {
let header_len = std::mem::size_of::<ACE_HEADER>();
let mut ace = vec![0_u8; seal_ace_size()];
ace[0] = DENY_ACE_TYPE;
ace[1] = 0;
ace[2..4].copy_from_slice(&(seal_ace_size() as u16).to_ne_bytes());
ace[header_len..header_len + 4].copy_from_slice(&SEAL_RIGHTS.to_ne_bytes());
ace[header_len + 4..].copy_from_slice(world_sid());
assert!(is_seal_ace(&ace), "the constructed seal must match");
let mut wrong_mask = ace.clone();
wrong_mask[header_len..header_len + 4].copy_from_slice(&0x1u32.to_ne_bytes());
assert!(!is_seal_ace(&wrong_mask), "mask must match exactly");
let mut wrong_type = ace.clone();
wrong_type[0] = ALLOW_ACE_TYPE;
assert!(!is_seal_ace(&wrong_type), "an allow ACE is not a seal");
let mut truncated = ace;
truncated.pop();
assert!(!is_seal_ace(&truncated), "length must match");
}
#[test]
fn world_sid_is_the_packed_s1_1_0() {
assert_eq!(world_sid(), &[1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]);
assert_eq!(world_sid().len(), 12);
}
}