use std::io;
use std::os::fd::BorrowedFd;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[must_use = "the observed ACL presence must be handled"]
pub enum ExtendedAclPresence {
Absent,
Present,
}
pub fn extended_acl_presence(fd: BorrowedFd<'_>) -> io::Result<ExtendedAclPresence> {
imp::extended_acl_presence(fd)
}
#[cfg(target_os = "macos")]
mod imp {
use super::ExtendedAclPresence;
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::raw::{c_int, c_uint, c_void};
const ACL_TYPE_EXTENDED: c_uint = 0x0000_0100;
#[allow(unsafe_code)] unsafe extern "C" {
fn acl_get_fd_np(fd: c_int, acl_type: c_uint) -> *mut c_void;
fn acl_free(obj: *mut c_void) -> c_int;
}
pub(super) fn extended_acl_presence(fd: BorrowedFd<'_>) -> io::Result<ExtendedAclPresence> {
#[allow(unsafe_code)]
let acl = unsafe { acl_get_fd_np(fd.as_raw_fd(), ACL_TYPE_EXTENDED) };
if acl.is_null() {
let error = io::Error::last_os_error();
return if error.raw_os_error() == Some(libc::ENOENT) {
Ok(ExtendedAclPresence::Absent)
} else {
Err(error)
};
}
#[allow(unsafe_code)]
let freed = unsafe { acl_free(acl) };
if freed != 0 {
return Err(io::Error::last_os_error());
}
Ok(ExtendedAclPresence::Present)
}
}
#[cfg(target_os = "linux")]
mod imp {
use super::ExtendedAclPresence;
use std::ffi::CStr;
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
const ACL_XATTR_NAMES: [&CStr; 2] = [c"system.posix_acl_access", c"system.posix_acl_default"];
pub(super) fn extended_acl_presence(fd: BorrowedFd<'_>) -> io::Result<ExtendedAclPresence> {
for name in ACL_XATTR_NAMES {
#[allow(unsafe_code)]
let size =
unsafe { libc::fgetxattr(fd.as_raw_fd(), name.as_ptr(), std::ptr::null_mut(), 0) };
if size >= 0 {
return Ok(ExtendedAclPresence::Present);
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ENODATA | libc::ENOTSUP) => {}
_ => return Err(error),
}
}
Ok(ExtendedAclPresence::Absent)
}
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "linux"))))]
mod imp {
use super::ExtendedAclPresence;
use std::io;
use std::os::fd::BorrowedFd;
pub(super) fn extended_acl_presence(_fd: BorrowedFd<'_>) -> io::Result<ExtendedAclPresence> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"extended-ACL presence probing is only audited for macOS and Linux",
))
}
}
#[cfg(test)]
mod tests {
use super::{ExtendedAclPresence, extended_acl_presence};
use std::fs::File;
use std::io;
use std::os::fd::AsFd;
use std::path::Path;
use std::process::Command;
fn run_acl_tool(program: &str, args: &[&str]) -> io::Result<bool> {
match Command::new(program).args(args).output() {
Ok(output) if output.status.success() => Ok(true),
Ok(output) => Err(io::Error::other(format!(
"{program} {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
))),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
}
fn assert_presence(path: &Path, expected: ExtendedAclPresence, context: &str) {
let handle = File::open(path).expect("fixture must open");
assert_eq!(
extended_acl_presence(handle.as_fd()).expect("presence probe must succeed"),
expected,
"{context}"
);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn clean_file_and_directory_report_absent() {
let dir = tempfile::tempdir().expect("tempdir");
let file_path = dir.path().join("clean-file");
File::create(&file_path).expect("create fixture file");
assert_presence(
&file_path,
ExtendedAclPresence::Absent,
"a freshly created file must report Absent",
);
assert_presence(
dir.path(),
ExtendedAclPresence::Absent,
"a freshly created directory must report Absent",
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_access_acl_toggles_presence_through_a_retained_fd() {
let dir = tempfile::tempdir().expect("tempdir");
let file_path = dir.path().join("acl-file");
File::create(&file_path).expect("create fixture file");
let retained = File::open(&file_path).expect("open fixture");
let path_text = file_path.to_str().expect("utf8 fixture path");
let user = std::env::var("USER").unwrap_or_else(|_| "root".to_string());
let spec = format!("u:{user}:r");
match run_acl_tool("setfacl", &["-m", &spec, path_text]) {
Ok(true) => {}
Ok(false) => {
eprintln!("skipping: setfacl binary not installed");
return;
}
Err(error) => {
eprintln!("skipping: setfacl unusable here: {error}");
return;
}
}
assert_eq!(
extended_acl_presence(retained.as_fd()).expect("probe after setfacl"),
ExtendedAclPresence::Present,
"an access ACL must be observed through the retained descriptor"
);
assert!(
run_acl_tool("setfacl", &["-b", path_text]).expect("setfacl -b must succeed"),
"setfacl disappeared mid-test"
);
assert_eq!(
extended_acl_presence(retained.as_fd()).expect("probe after clear"),
ExtendedAclPresence::Absent,
"clearing the ACL must return the retained descriptor to Absent"
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_default_directory_acl_reports_present() {
let dir = tempfile::tempdir().expect("tempdir");
let sub = dir.path().join("default-acl-dir");
std::fs::create_dir(&sub).expect("create fixture dir");
let path_text = sub.to_str().expect("utf8 fixture path");
let user = std::env::var("USER").unwrap_or_else(|_| "root".to_string());
let spec = format!("u:{user}:rx");
match run_acl_tool("setfacl", &["-d", "-m", &spec, path_text]) {
Ok(true) => {}
Ok(false) => {
eprintln!("skipping: setfacl binary not installed");
return;
}
Err(error) => {
eprintln!("skipping: setfacl unusable here: {error}");
return;
}
}
assert_presence(
&sub,
ExtendedAclPresence::Present,
"a default ACL alone must report Present for a directory",
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_o_path_descriptor_is_rejected_not_misread() {
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
let dir = tempfile::tempdir().expect("tempdir");
let file_path = dir.path().join("target");
File::create(&file_path).expect("create fixture file");
let path_cstr = std::ffi::CString::new(file_path.to_str().expect("utf8 fixture path"))
.expect("no interior NUL");
#[allow(unsafe_code)]
let raw = unsafe { libc::open(path_cstr.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
assert!(raw >= 0, "O_PATH open must succeed");
#[allow(unsafe_code)]
let o_path_fd = unsafe { OwnedFd::from_raw_fd(raw) };
let error = extended_acl_presence(o_path_fd.as_fd())
.expect_err("an O_PATH descriptor must be rejected, not misread as Absent");
assert_eq!(
error.raw_os_error(),
Some(libc::EBADF),
"the kernel's EBADF must surface unchanged"
);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_chmod_acl_toggles_presence_through_a_retained_fd() {
let dir = tempfile::tempdir().expect("tempdir");
let file_path = dir.path().join("acl-file");
File::create(&file_path).expect("create fixture file");
let retained = File::open(&file_path).expect("open fixture");
let path_text = file_path.to_str().expect("utf8 fixture path");
let user = std::env::var("USER").expect("USER must identify an ACL principal");
let entry = format!("user:{user} allow read");
assert!(
run_acl_tool("/bin/chmod", &["+a", &entry, path_text]).expect("chmod +a must succeed"),
"/bin/chmod must exist on macOS"
);
assert_eq!(
extended_acl_presence(retained.as_fd()).expect("probe after chmod +a"),
ExtendedAclPresence::Present,
"an ALLOW entry must be observed through the retained descriptor"
);
assert!(
run_acl_tool("/bin/chmod", &["-N", path_text]).expect("chmod -N must succeed"),
"/bin/chmod must exist on macOS"
);
assert_eq!(
extended_acl_presence(retained.as_fd()).expect("probe after chmod -N"),
ExtendedAclPresence::Absent,
"clearing the ACL must return the retained descriptor to Absent"
);
}
}