use super::{Found, RawEntry, RawKind, Refused};
use crate::access::Access;
use rustix::{
fs::{
AtFlags, CWD, Dir, FileType, FlockOperation, Gid, Mode, OFlags, Stat, fchmod, fchown,
flock, fstat, fsync, mkdirat, openat, renameat, statat, unlinkat,
},
io::Errno,
};
use std::{
io::Write as _,
os::fd::{AsFd, BorrowedFd, OwnedFd},
path::Path,
};
pub(crate) struct Root(Result<OwnedFd, Errno>);
fn directory_flags() -> OFlags {
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC
}
fn kind_of(file_type: FileType) -> RawKind {
match file_type {
FileType::RegularFile => RawKind::File,
FileType::Directory => RawKind::Directory,
FileType::Symlink => RawKind::Link,
_ => RawKind::Other,
}
}
fn classify(directory: BorrowedFd<'_>, name: &str, errno: Errno, prefix: &[&str]) -> Refused {
match statat(directory, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) if kind_of(FileType::from_raw_mode(stat.st_mode)) == RawKind::Link => {
Refused::Link(prefix.join("/"))
}
Ok(_) if errno == Errno::NOTDIR || errno == Errno::NXIO => Refused::Unexpected,
_ if errno == Errno::NOENT => Refused::Missing,
_ => Refused::Io(errno.into()),
}
}
impl Root {
pub(crate) fn new(path: &Path) -> Self {
Self(openat(
CWD,
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
))
}
pub(crate) fn try_clone(&self) -> Self {
Self(match &self.0 {
Ok(fd) => fd.try_clone().map_err(|error| {
error
.raw_os_error()
.map_or(Errno::IO, Errno::from_raw_os_error)
}),
Err(errno) => Err(*errno),
})
}
fn root(&self) -> Result<BorrowedFd<'_>, Refused> {
match &self.0 {
Ok(fd) => Ok(fd.as_fd()),
Err(Errno::NOENT) => Err(Refused::Missing),
Err(errno) => Err(Refused::Io((*errno).into())),
}
}
fn parent<T>(
&self,
components: &[&str],
access: impl FnOnce(BorrowedFd<'_>, &str) -> Result<T, Refused>,
) -> Result<T, Refused> {
let root = self.root()?;
let Some((name, parents)) = components.split_last() else {
return Err(Refused::Unexpected);
};
let mut current: Option<OwnedFd> = None;
for (index, component) in parents.iter().enumerate() {
let directory = current.as_ref().map_or(root, AsFd::as_fd);
let opened = openat(directory, *component, directory_flags(), Mode::empty())
.map_err(|errno| classify(directory, component, errno, &components[..=index]))?;
current = Some(opened);
}
access(current.as_ref().map_or(root, AsFd::as_fd), name)
}
pub(crate) fn file(&self, components: &[&str]) -> Result<(std::fs::File, u64), Refused> {
self.parent(components, |directory, name| {
let flags = OFlags::RDONLY
| OFlags::NOFOLLOW
| OFlags::CLOEXEC
| OFlags::NOCTTY
| OFlags::NONBLOCK;
let fd = openat(directory, name, flags, Mode::empty())
.map_err(|errno| classify(directory, name, errno, components))?;
let stat = fstat(&fd).map_err(|errno| Refused::Io(errno.into()))?;
if kind_of(FileType::from_raw_mode(stat.st_mode)) != RawKind::File {
return Err(Refused::Unexpected);
}
Ok((
std::fs::File::from(fd),
u64::try_from(stat.st_size).unwrap_or(0),
))
})
}
pub(crate) fn kind(&self, components: &[&str]) -> Result<Found, Refused> {
self.parent(components, |directory, name| {
match statat(directory, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) => match kind_of(FileType::from_raw_mode(stat.st_mode)) {
RawKind::File => Ok(Found::File),
RawKind::Directory => Ok(Found::Directory),
RawKind::Link => Err(Refused::Link(components.join("/"))),
RawKind::Other => Ok(Found::Other),
},
Err(Errno::NOENT) => Ok(Found::Missing),
Err(errno) => Err(Refused::Io(errno.into())),
}
})
}
pub(crate) fn directory(
&self,
components: &[&str],
create: bool,
) -> Result<Directory, Refused> {
let root = self.root()?;
let mut current: Option<OwnedFd> = None;
for (index, component) in components.iter().enumerate() {
let directory = current.as_ref().map_or(root, AsFd::as_fd);
let opened = match openat(directory, *component, directory_flags(), Mode::empty()) {
Err(Errno::NOENT) if create => {
match mkdirat(directory, *component, Mode::from_raw_mode(0o777)) {
Ok(()) | Err(Errno::EXIST) => {}
Err(errno) => return Err(Refused::Io(errno.into())),
}
openat(directory, *component, directory_flags(), Mode::empty())
}
opened => opened,
}
.map_err(|errno| classify(directory, component, errno, &components[..=index]))?;
current = Some(opened);
}
match current {
Some(directory) => Ok(Directory(directory)),
None => root
.try_clone_to_owned()
.map(Directory)
.map_err(Refused::Io),
}
}
pub(crate) fn make_directory(&self, components: &[&str]) -> Result<bool, Refused> {
let Some((name, parent)) = components.split_last() else {
return Ok(false);
};
let parent = self.directory(parent, false)?;
match mkdirat(&parent.0, *name, Mode::from_raw_mode(0o777)) {
Ok(()) => Ok(true),
Err(Errno::EXIST) => Ok(false),
Err(errno) => Err(classify(parent.0.as_fd(), name, errno, components)),
}
}
pub(crate) fn lock(&self) -> Result<Option<Lock>, Refused> {
let fd = openat(self.root()?, ".", directory_flags(), Mode::empty())
.map_err(|errno| Refused::Io(errno.into()))?;
match flock(&fd, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => Ok(Some(Lock(fd))),
Err(Errno::WOULDBLOCK) => Ok(None),
Err(errno) => Err(Refused::Io(errno.into())),
}
}
pub(crate) fn list(&self, components: &[&str]) -> Result<Listing, Refused> {
let directory = if components.is_empty() {
self.root()?.try_clone_to_owned().map_err(Refused::Io)?
} else {
self.parent(components, |directory, name| {
openat(directory, name, directory_flags(), Mode::empty())
.map_err(|errno| classify(directory, name, errno, components))
})?
};
let entries = Dir::read_from(&directory).map_err(|errno| Refused::Io(errno.into()))?;
Ok(Listing {
directory,
entries,
done: false,
})
}
}
pub(crate) struct Listing {
directory: OwnedFd,
entries: Dir,
done: bool,
}
impl Iterator for Listing {
type Item = std::io::Result<RawEntry>;
fn next(&mut self) -> Option<Self::Item> {
while !self.done {
let entry = match self.entries.next()? {
Ok(entry) => entry,
Err(errno) => {
self.done = true;
return Some(Err(errno.into()));
}
};
let name = entry.file_name();
if matches!(name.to_bytes(), b"." | b"..") {
continue;
}
let kind = match entry.file_type() {
FileType::Unknown => {
match statat(&self.directory, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) => kind_of(FileType::from_raw_mode(stat.st_mode)),
Err(errno) => return Some(Err(errno.into())),
}
}
known => kind_of(known),
};
return Some(Ok(RawEntry {
name: std::str::from_utf8(name.to_bytes()).ok().map(str::to_owned),
kind,
}));
}
None
}
}
pub(crate) struct Lock(#[allow(dead_code)] OwnedFd);
pub(crate) struct Directory(OwnedFd);
impl Directory {
pub(crate) fn access(&self, name: &str) -> Result<Option<Access>, Refused> {
match statat(&self.0, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) if kind_of(FileType::from_raw_mode(stat.st_mode)) == RawKind::File => {
self.file_access(name, &stat)
}
Ok(_) | Err(Errno::NOENT) => Ok(None),
Err(errno) => Err(Refused::Io(errno.into())),
}
}
#[cfg(target_os = "linux")]
fn file_access(&self, name: &str, _: &Stat) -> Result<Option<Access>, Refused> {
let flags =
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC | OFlags::NOCTTY;
let fd = openat(&self.0, name, flags, Mode::empty())
.map_err(|errno| classify(self.0.as_fd(), name, errno, &[name]))?;
let stat = fstat(&fd).map_err(|errno| Refused::Io(errno.into()))?;
if kind_of(FileType::from_raw_mode(stat.st_mode)) != RawKind::File {
return Ok(None);
}
refuse_access_control_lists(&fd, false)?;
Ok(Some(Access {
control: labels(&fd)?,
..access_of(&stat)
}))
}
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
fn file_access(&self, _: &str, stat: &Stat) -> Result<Option<Access>, Refused> {
Ok(Some(access_of(stat)))
}
pub(crate) fn own_access(&self) -> Result<Access, Refused> {
let stat = fstat(&self.0).map_err(|errno| Refused::Io(errno.into()))?;
#[cfg(target_os = "linux")]
refuse_access_control_lists(&self.0, true)?;
Ok(Access {
control: labels(&self.0)?,
..access_of(&stat)
})
}
pub(crate) fn set_own_access(&self, access: &Access) -> Result<(), Refused> {
check_labels(&self.0, access)?;
set_access(&self.0, access)
}
pub(crate) fn write(
&self,
name: &str,
access: Option<&Access>,
bytes: &[u8],
) -> Result<(), Refused> {
self.remove_file(name)?;
let flags = OFlags::WRONLY
| OFlags::CREATE
| OFlags::EXCL
| OFlags::NOFOLLOW
| OFlags::CLOEXEC
| OFlags::NOCTTY;
let mode = if access.is_some() { 0o600 } else { 0o666 };
let fd = openat(&self.0, name, flags, Mode::from_raw_mode(mode))
.map_err(|errno| classify(self.0.as_fd(), name, errno, &[name]))?;
let written = (|| {
let mut file = std::fs::File::from(fd);
if let Some(access) = access {
#[cfg(target_os = "linux")]
strip_access_control_list(&file)?;
check_labels(&file, access)?;
}
file.write_all(bytes).map_err(Refused::Io)?;
if let Some(access) = access {
set_access(&file, access)?;
}
file.sync_all().map_err(Refused::Io)
})();
if written.is_err() {
let _ = self.remove_file(name);
}
written
}
pub(crate) fn create(
&self,
name: &str,
access: Option<&Access>,
bytes: &[u8],
) -> Result<(), Refused> {
let flags = OFlags::WRONLY
| OFlags::CREATE
| OFlags::EXCL
| OFlags::NOFOLLOW
| OFlags::CLOEXEC
| OFlags::NOCTTY;
let mode = if access.is_some() { 0o600 } else { 0o666 };
let fd = match openat(&self.0, name, flags, Mode::from_raw_mode(mode)) {
Ok(fd) => fd,
Err(Errno::EXIST) => return Err(Refused::Io(crate::access::already_there())),
Err(errno) => return Err(classify(self.0.as_fd(), name, errno, &[name])),
};
let mut file = std::fs::File::from(fd);
let written = (|| {
#[cfg(test)]
super::after_create().map_err(Refused::Io)?;
if let Some(access) = access {
#[cfg(target_os = "linux")]
strip_access_control_list(&file)?;
check_labels(&file, access)?;
}
file.write_all(bytes).map_err(Refused::Io)?;
if let Some(access) = access {
set_access(&file, access)?;
}
file.sync_all().map_err(Refused::Io)
})();
match written {
Ok(()) => Ok(()),
Err(error) if self.remove_created(name, &file) => Err(error),
Err(error) => Err(crate::access::left_behind(error)),
}
}
fn remove_created(&self, name: &str, created: &std::fs::File) -> bool {
let (Ok(ours), Ok(there)) = (
fstat(created),
statat(&self.0, name, AtFlags::SYMLINK_NOFOLLOW),
) else {
return false;
};
if (ours.st_dev, ours.st_ino) != (there.st_dev, there.st_ino) {
return false;
}
unlinkat(&self.0, name, AtFlags::empty()).is_ok()
}
pub(crate) fn rename(&self, from: &str, to: &str) -> Result<(), Refused> {
renameat(&self.0, from, &self.0, to).map_err(|errno| Refused::Io(errno.into()))
}
pub(crate) fn remove_file(&self, name: &str) -> Result<bool, Refused> {
match unlinkat(&self.0, name, AtFlags::empty()) {
Ok(()) => Ok(true),
Err(Errno::NOENT) => Ok(false),
Err(errno) => Err(Refused::Io(errno.into())),
}
}
pub(crate) fn remove_directory(&self, name: &str) -> Result<bool, Refused> {
match unlinkat(&self.0, name, AtFlags::REMOVEDIR) {
Ok(()) => Ok(true),
Err(Errno::NOENT | Errno::NOTEMPTY | Errno::EXIST) => Ok(false),
Err(errno) => Err(Refused::Io(errno.into())),
}
}
#[cfg(target_os = "linux")]
pub(crate) fn check_final_name(&self, staged: &str, name: &str) -> Result<(), Refused> {
let Some(directory) = label(&self.0, "security.selinux")? else {
return Ok(());
};
if selinux::creates_as_set().map_err(Refused::Io)? {
return Ok(());
}
let given = selinux::compute_create(&directory, staged).map_err(Refused::Io)?;
let expected = selinux::compute_create(&directory, name).map_err(Refused::Io)?;
if given == expected {
Ok(())
} else {
Err(unkept(
"the system's policy labels a new file of its name otherwise than the file staged for it, and renaming does not relabel",
))
}
}
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub(crate) fn check_final_name(&self, _: &str, _: &str) -> Result<(), Refused> {
Ok(())
}
#[cfg(target_os = "linux")]
pub(crate) fn mount_id(&self) -> Result<Result<u64, String>, Refused> {
use rustix::fs::{StatxFlags, statx};
use std::os::fd::AsRawFd as _;
let found = statx(&self.0, "", AtFlags::EMPTY_PATH, StatxFlags::MNT_ID);
#[cfg(test)]
let (found, fdinfo) = match NO_STATX_MOUNT_ID.with(std::cell::Cell::get) {
Some(fdinfo) => (
found.map(|mut found| {
found.stx_mask &= !StatxFlags::MNT_ID.bits();
found.stx_mnt_id = 0;
found
}),
fdinfo,
),
None => (found, "/proc/self/fdinfo"),
};
#[cfg(not(test))]
let fdinfo = "/proc/self/fdinfo";
match found {
Ok(found) if found.stx_mask & StatxFlags::MNT_ID.bits() != 0 => {
return Ok(Ok(found.stx_mnt_id));
}
Ok(_) | Err(Errno::NOSYS) => {}
Err(errno) => return Err(Refused::Io(errno.into())),
}
Ok(
match super::mounts::fdinfo_mount_id(fdinfo, self.0.as_raw_fd()) {
Ok(Some(id)) => Ok(id),
Ok(None) => Err("the kernel does not say which mount it is on".to_owned()),
Err(reason) => Err(format!("statx gives no mount ID, and {reason}")),
},
)
}
#[cfg(target_os = "linux")]
pub(crate) fn unchecked_file_system(&self, mounts: &Mounts) -> Result<Option<String>, Refused> {
#[cfg(test)]
if let Some((found, magic)) = GIVEN_FILE_SYSTEM.with(std::cell::Cell::get) {
return Ok(unchecked_file_system(found.as_bytes(), magic));
}
let unknown = |reason: &str| Ok(Some(format!("one that could not be told: {reason}")));
if let Some(reason) = mounts.unreadable() {
return unknown(reason);
}
let id = match self.mount_id()? {
Ok(id) => id,
Err(reason) => return unknown(&reason),
};
let found = match mounts.type_of(id) {
Ok(found) => found,
Err(reason) => return unknown(&reason),
};
Ok(unchecked_file_system(found, self.magic()?))
}
#[cfg(target_os = "linux")]
pub(crate) fn magic(&self) -> Result<u32, Refused> {
let statistics = rustix::fs::fstatfs(&self.0).map_err(|errno| Refused::Io(errno.into()))?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Ok(statistics.f_type as u32)
}
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub(crate) fn unchecked_file_system(&self, _: &Mounts) -> Result<Option<String>, Refused> {
Ok(None)
}
pub(crate) fn sync(&self) -> Result<bool, Refused> {
#[cfg(target_os = "macos")]
match rustix::fs::fcntl_fullfsync(&self.0) {
Ok(()) => return Ok(true),
Err(Errno::NOTSUP | Errno::OPNOTSUPP | Errno::INVAL | Errno::NOTTY) => {}
Err(errno) => return Err(Refused::Io(errno.into())),
}
fsync(&self.0)
.map(|()| true)
.map_err(|errno| Refused::Io(errno.into()))
}
}
fn access_of(stat: &Stat) -> Access {
Access {
mode: mode_bits(stat.st_mode),
user: stat.st_uid,
group: stat.st_gid,
control: [0; 32],
}
}
#[allow(clippy::useless_conversion)]
fn mode_bits(mode: rustix::fs::RawMode) -> u32 {
u32::from(mode) & 0o7777
}
fn set_access(fd: impl AsFd, access: &Access) -> Result<(), Refused> {
let stat = fstat(&fd).map_err(|errno| Refused::Io(errno.into()))?;
if stat.st_gid != access.group {
fchown(&fd, None, Some(Gid::from_raw(access.group)))
.map_err(|errno| Refused::Io(errno.into()))?;
}
let mode = rustix::fs::RawMode::try_from(access.mode & 0o7777)
.map_err(|_| Refused::Io(std::io::ErrorKind::InvalidInput.into()))?;
fchmod(&fd, Mode::from_raw_mode(mode)).map_err(|errno| Refused::Io(errno.into()))?;
let stat = fstat(&fd).map_err(|errno| Refused::Io(errno.into()))?;
if mode_bits(stat.st_mode) != access.mode || stat.st_gid != access.group {
return Err(Refused::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"could not be given mode {:o} and group {}: it has mode {:o} and group {}",
access.mode,
access.group,
mode_bits(stat.st_mode),
stat.st_gid
),
)));
}
Ok(())
}
#[cfg(target_os = "linux")]
const ACCESS_CONTROL_LISTS: [&str; 4] = [
"system.posix_acl_access",
"system.nfs4_acl",
"system.nfs4_acl_xdr",
"system.richacl",
];
#[cfg(target_os = "linux")]
const CHECKED_FILE_SYSTEMS: [(&str, u32); 9] = [
("ext2", 0x0000_EF53),
("ext3", 0x0000_EF53),
("ext4", 0x0000_EF53),
("xfs", 0x5846_5342),
("btrfs", 0x9123_683E),
("tmpfs", 0x0102_1994),
("f2fs", 0xF2F5_2010),
("zfs", 0x2FC1_2FC1),
("bcachefs", 0xCA45_1A4E),
];
#[cfg(target_os = "linux")]
pub(crate) fn unchecked_file_system(found: &[u8], magic: u32) -> Option<String> {
let shown = found.escape_ascii().to_string();
match CHECKED_FILE_SYSTEMS
.iter()
.find(|(name, _)| name.as_bytes() == found)
{
Some((_, expected)) if *expected == magic => None,
Some(_) => Some(format!("{shown}, though statfs gives type {magic:#x}")),
None => Some(shown),
}
}
#[cfg(target_os = "linux")]
pub(crate) use super::mounts::Mounts;
#[cfg(not(target_os = "linux"))]
pub(crate) struct Mounts;
#[cfg(not(target_os = "linux"))]
impl Mounts {
pub(crate) fn read() -> Self {
Self
}
}
#[cfg(target_os = "linux")]
const LABELS: [&str; 2] = ["security.selinux", "security.SMACK64"];
#[cfg(target_os = "linux")]
const UNKEPT_LABELS: [&str; 3] = [
"security.SMACK64EXEC",
"security.SMACK64MMAP",
"security.SMACK64TRANSMUTE",
];
fn unkept(what: &str) -> Refused {
Refused::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!("{what}, which publishing could not keep"),
))
}
#[cfg(target_os = "linux")]
fn refuse_access_control_lists(fd: impl AsFd, directory: bool) -> Result<(), Refused> {
let found = has_any(&fd, &ACCESS_CONTROL_LISTS)?
|| (directory && has_any(&fd, &["system.posix_acl_default"])?);
if found {
return Err(unkept("it has an access control list"));
}
Ok(())
}
#[cfg(target_os = "linux")]
fn has_any(fd: impl AsFd, names: &[&str]) -> Result<bool, Refused> {
for name in names {
match rustix::fs::fgetxattr(&fd, *name, &mut [0_u8; 0][..]) {
Ok(_) | Err(Errno::RANGE) => return Ok(true),
Err(Errno::NODATA | Errno::NOTSUP) => {}
Err(errno) => return Err(Refused::Io(errno.into())),
}
}
Ok(false)
}
#[cfg(target_os = "linux")]
const LABEL_LIMIT: usize = 4096;
#[cfg(target_os = "linux")]
fn labels(fd: impl AsFd) -> Result<[u8; 32], Refused> {
use sha2::{Digest as _, Sha256};
if has_any(&fd, &UNKEPT_LABELS)? {
return Err(unkept(
"it has a Smack execution, mapping or transmuting label",
));
}
let mut values = Vec::with_capacity(LABELS.len());
for name in LABELS {
values.push((name, label(&fd, name)?));
}
#[cfg(test)]
if let Some(given) = given_label(&fd)? {
values[0].1 = Some(given);
}
if values.iter().all(|(_, value)| value.is_none()) {
return Ok([0; 32]);
}
let mut hasher = Sha256::new();
for (name, value) in values {
hasher.update(name.as_bytes());
match value {
None => hasher.update([0]),
Some(value) => {
hasher.update([1]);
hasher.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(&value);
}
}
}
Ok(hasher.finalize().into())
}
#[cfg(target_os = "linux")]
fn label(fd: impl AsFd, name: &str) -> Result<Option<Vec<u8>>, Refused> {
let size = match rustix::fs::fgetxattr(&fd, name, &mut [0_u8; 0][..]) {
Ok(size) => size,
Err(Errno::NODATA | Errno::NOTSUP) => return Ok(None),
Err(errno) => return Err(Refused::Io(errno.into())),
};
if size > LABEL_LIMIT {
return Err(unkept("it has a label longer than publishing reads"));
}
let mut value = vec![0_u8; size];
let read = rustix::fs::fgetxattr(&fd, name, &mut value[..])
.map_err(|errno| Refused::Io(errno.into()))?;
value.truncate(read);
Ok(Some(value))
}
#[cfg(all(test, target_os = "linux"))]
thread_local! {
pub(crate) static GIVEN_LABELS: std::cell::RefCell<std::collections::BTreeMap<(u64, u64), Vec<u8>>> =
const { std::cell::RefCell::new(std::collections::BTreeMap::new()) };
}
#[cfg(all(test, target_os = "linux"))]
thread_local! {
pub(crate) static NO_STATX_MOUNT_ID: std::cell::Cell<Option<&'static str>> =
const { std::cell::Cell::new(None) };
pub(crate) static GIVEN_FILE_SYSTEM: std::cell::Cell<Option<(&'static str, u32)>> =
const { std::cell::Cell::new(None) };
}
#[cfg(all(test, target_os = "linux"))]
fn given_label(fd: impl AsFd) -> Result<Option<Vec<u8>>, Refused> {
let stat = fstat(&fd).map_err(|errno| Refused::Io(errno.into()))?;
Ok(GIVEN_LABELS.with(|labels| labels.borrow().get(&(stat.st_dev, stat.st_ino)).cloned()))
}
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps)]
fn labels(_fd: impl AsFd) -> Result<[u8; 32], Refused> {
Ok([0; 32])
}
fn check_labels(fd: impl AsFd, access: &Access) -> Result<(), Refused> {
if labels(fd)? == access.control {
Ok(())
} else {
Err(unkept(
"the system gave it other mandatory access control labels (SELinux, Smack) than the entries publishing compared it with",
))
}
}
#[cfg(target_os = "linux")]
fn strip_access_control_list(fd: impl AsFd) -> Result<(), Refused> {
match rustix::fs::fremovexattr(&fd, "system.posix_acl_access") {
Ok(()) | Err(Errno::NODATA | Errno::NOTSUP) => {}
Err(errno) => return Err(Refused::Io(errno.into())),
}
if has_any(&fd, &ACCESS_CONTROL_LISTS)? {
return Err(Refused::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"the directory gives new files an access control list that cannot be removed",
)));
}
Ok(())
}
#[cfg(target_os = "linux")]
pub(crate) mod selinux {
use std::{
fs,
io::{self, Read as _, Write as _},
};
const FILE_SYSTEM: &str = "/sys/fs/selinux";
const LIMIT: u64 = 4096;
pub(crate) fn trimmed(label: &[u8]) -> &[u8] {
let end = label
.iter()
.rposition(|byte| !matches!(byte, b'\0' | b'\n'))
.map_or(0, |last| last + 1);
&label[..end]
}
pub(super) fn creates_as_set() -> io::Result<bool> {
match fs::read("/proc/thread-self/attr/fscreate") {
Ok(label) => Ok(!trimmed(&label).is_empty()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
}
pub(super) fn compute_create(directory: &[u8], name: &str) -> io::Result<Vec<u8>> {
let class = fs::read_to_string(format!("{FILE_SYSTEM}/class/file/index"))
.map_err(|error| {
io::Error::new(
error.kind(),
format!("the tree has SELinux labels, but {FILE_SYSTEM} cannot be asked which label a new file gets ({error})"),
)
})?
.trim()
.parse::<u16>()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
let thread = fs::read("/proc/thread-self/attr/current")?;
let query = request(&thread, directory, class, name)?;
let mut answer = fs::OpenOptions::new()
.read(true)
.write(true)
.open(format!("{FILE_SYSTEM}/create"))?;
if answer.write(&query)? != query.len() {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"the label question was not taken whole",
));
}
let mut label = Vec::new();
answer.take(LIMIT).read_to_end(&mut label)?;
Ok(trimmed(&label).to_vec())
}
pub(crate) fn request(
source: &[u8],
target: &[u8],
class: u16,
name: &str,
) -> io::Result<Vec<u8>> {
let (source, target) = (trimmed(source), trimmed(target));
let plain = |label: &[u8]| !label.is_empty() && label.iter().all(u8::is_ascii_graphic);
if !plain(source) || !plain(target) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"a label is not plain text",
));
}
let mut query = Vec::with_capacity(source.len() + target.len() + name.len() * 3 + 16);
query.extend_from_slice(source);
query.push(b' ');
query.extend_from_slice(target);
query.extend_from_slice(format!(" {class} ").as_bytes());
for byte in name.bytes() {
if byte.is_ascii_alphanumeric() || b"-_.".contains(&byte) {
query.push(byte);
} else {
query.extend_from_slice(format!("%{byte:02x}").as_bytes());
}
}
Ok(query)
}
}