#![forbid(unsafe_code)]
use std::{
ffi::CStr,
fs::{File, Permissions},
io::{self, Read},
ops::{Deref, DerefMut},
os::unix::{fs::PermissionsExt as _, io::AsRawFd, process::CommandExt},
process::Command,
};
use nix::{
errno::Errno,
fcntl::{fcntl, FcntlArg, SealFlag},
libc::{
c_int, c_uint, F_SEAL_FUTURE_WRITE, F_SEAL_GROW, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_WRITE,
MFD_ALLOW_SEALING, MFD_CLOEXEC, MFD_EXEC, MFD_NOEXEC_SEAL,
},
sys::memfd::MFdFlags,
};
use crate::err::err2no;
const DEFAULT_MEMFD_NAME: &CStr = c"syd";
const F_SEAL_EXEC: c_int = 0x0020;
const OPTIONS: SealOptions = SealOptions::new().close_on_exec(true).executable(true);
pub fn ensure_sealed() -> Result<(), Errno> {
#[expect(clippy::disallowed_methods)]
let mut file = File::open("/proc/self/exe").map_err(|err| err2no(&err))?;
if OPTIONS.is_sealed(&file) {
Ok(())
} else {
let mut command = SealedCommand::new(&mut file)?;
let mut args = std::env::args_os().fuse();
if let Some(arg0) = args.next() {
command.arg0(arg0);
}
command.args(args);
match std::env::var_os("RUST_BACKTRACE") {
Some(val) => command.env("SYD_RUST_BACKTRACE", val),
None => command.env_remove("SYD_RUST_BACKTRACE"),
};
command.env("RUST_BACKTRACE", "0");
Err(err2no(&command.exec()))
}
}
pub struct SealedCommand {
inner: Command,
_memfd: File,
}
impl SealedCommand {
pub fn new<R: Read>(program: &mut R) -> Result<Self, Errno> {
let mut memfd = OPTIONS.create()?;
io::copy(program, &mut memfd).or(Err(Errno::EIO))?;
OPTIONS.seal(&mut memfd)?;
let mut comm = Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd()));
comm.arg0("syd");
Ok(Self {
inner: comm,
_memfd: memfd,
})
}
}
impl Deref for SealedCommand {
type Target = Command;
fn deref(&self) -> &Command {
&self.inner
}
}
impl DerefMut for SealedCommand {
fn deref_mut(&mut self) -> &mut Command {
&mut self.inner
}
}
macro_rules! set_flag {
($flags:expr, $flag:expr, $value:expr) => {
if $value {
$flags |= $flag;
} else {
$flags &= !$flag;
}
};
}
macro_rules! seal {
(
$seal_ident:ident
$( { $( #[ $attr:meta ] )* } )? ,
$must_seal_ident:ident
$( { $( #[ $must_attr:meta ] )* } )? ,
$( ? $preflight:ident : )? $flag:expr,
$try_to:expr,
$default:expr
) => {
#[doc = concat!("If `true`, try to ", $try_to, ".")]
#[doc = ""]
#[doc = "If `false`, also set"]
#[doc = concat!("[`SealOptions::", stringify!($must_seal_ident), "`]")]
#[doc = "to `false`."]
#[doc = ""]
#[doc = concat!("This flag is `", $default, "` by default.")]
$($( #[ $attr ] )*)?
pub const fn $seal_ident(mut self, $seal_ident: bool) -> SealOptions {
if true $( && self.$preflight() )? {
set_flag!(self.seal_flags, $flag, $seal_ident);
}
if !$seal_ident {
self.must_seal_flags &= !$flag;
}
self
}
#[doc = "If `true`, also set"]
#[doc = concat!("[`SealOptions::", stringify!($seal_ident), "`] to `true`")]
#[doc = "and ensure it is successful when [`SealOptions::seal`] is called."]
#[doc = ""]
#[doc = concat!("This flag is `", $default, "` by default.")]
$($( #[ $must_attr ] )*)?
pub const fn $must_seal_ident(mut self, $must_seal_ident: bool) -> SealOptions {
if $must_seal_ident {
self.seal_flags |= $flag;
}
set_flag!(self.must_seal_flags, $flag, $must_seal_ident);
self
}
};
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[must_use]
pub struct SealOptions {
memfd_flags: c_uint,
seal_flags: c_int,
must_seal_flags: c_int,
}
impl Default for SealOptions {
fn default() -> Self {
Self::new()
}
}
impl SealOptions {
pub const fn new() -> Self {
Self {
memfd_flags: MFD_ALLOW_SEALING | MFD_CLOEXEC,
seal_flags: F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE,
must_seal_flags: F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE,
}
}
pub const fn close_on_exec(mut self, close_on_exec: bool) -> SealOptions {
set_flag!(self.memfd_flags, MFD_CLOEXEC, close_on_exec);
self
}
pub const fn executable(mut self, executable: bool) -> SealOptions {
self.memfd_flags = self.memfd_flags & !MFD_EXEC & !MFD_NOEXEC_SEAL
| if executable {
MFD_EXEC
} else {
MFD_NOEXEC_SEAL
};
self.seal_flags |= F_SEAL_EXEC;
self
}
const fn is_executable_set(&self) -> bool {
self.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL) != 0
}
seal!(
seal_seals,
must_seal_seals,
F_SEAL_SEAL,
"prevent further seals from being set on this file",
true
);
seal!(
seal_shrinking,
must_seal_shrinking,
F_SEAL_SHRINK,
"prevent shrinking this file",
true
);
seal!(
seal_growing,
must_seal_growing,
F_SEAL_GROW,
"prevent growing this file",
true
);
seal!(
seal_writing,
must_seal_writing,
F_SEAL_WRITE,
"prevent writing to this file",
true
);
seal!(
seal_future_writing {
#[doc = ""]
#[doc = "This requires at least Linux 5.1."]
},
must_seal_future_writing {
#[doc = ""]
#[doc = "This requires at least Linux 5.1."]
},
F_SEAL_FUTURE_WRITE,
"prevent directly writing to this file or creating new writable mappings, \
but allow writes to existing writable mappings",
false
);
seal!(
seal_executable {
#[doc = ""]
#[doc = "If [`SealOptions::executable`] has already been called,"]
#[doc = "this function does nothing."]
#[doc = ""]
#[doc = "This requires at least Linux 6.3."]
},
must_seal_executable {
#[doc = ""]
#[doc = "This requires at least Linux 6.3."]
},
? seal_executable_preflight : F_SEAL_EXEC,
"prevent modifying the executable permission of the file",
false
);
const fn seal_executable_preflight(&self) -> bool {
!self.is_executable_set()
}
pub fn copy_and_seal<R: Read>(&self, reader: &mut R) -> Result<File, Errno> {
let mut file = self.create()?;
io::copy(reader, &mut file).or(Err(Errno::EIO))?;
self.seal(&mut file)?;
Ok(file)
}
pub fn create(&self) -> Result<File, Errno> {
let file = match memfd_create(DEFAULT_MEMFD_NAME, self.memfd_flags) {
Ok(file) => file,
Err(Errno::EINVAL) if self.is_executable_set() => {
memfd_create(
DEFAULT_MEMFD_NAME,
self.memfd_flags & !MFD_EXEC & !MFD_NOEXEC_SEAL,
)?
}
Err(err) => return Err(err),
};
if self.is_executable_set() {
let permissions = file.metadata().or(Err(Errno::EACCES))?.permissions();
let new_permissions =
Permissions::from_mode(if self.memfd_flags & MFD_NOEXEC_SEAL != 0 {
permissions.mode() & !0o111
} else if self.memfd_flags & MFD_EXEC != 0 {
permissions.mode() | 0o111
} else {
return Ok(file);
});
if permissions != new_permissions {
file.set_permissions(new_permissions)
.or(Err(Errno::EACCES))?;
}
}
Ok(file)
}
pub fn seal(&self, file: &mut File) -> Result<(), Errno> {
for group in [
F_SEAL_EXEC, F_SEAL_FUTURE_WRITE, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE, ] {
match fcntl_add_seals(file, self.seal_flags & group) {
Ok(()) => {}
Err(Errno::EINVAL) => {}
Err(err) => return Err(err),
}
}
if self.is_sealed_inner(file)? {
Ok(())
} else {
Err(Errno::EBADF)
}
}
pub fn is_sealed(&self, file: &File) -> bool {
self.is_sealed_inner(file).unwrap_or(false)
}
fn is_sealed_inner(&self, file: &File) -> Result<bool, Errno> {
Ok(fcntl_get_seals(file)? & self.must_seal_flags == self.must_seal_flags)
}
}
fn memfd_create(name: &CStr, flags: c_uint) -> Result<File, Errno> {
nix::sys::memfd::memfd_create(name, MFdFlags::from_bits_retain(flags)).map(File::from)
}
fn fcntl_get_seals(file: &File) -> Result<c_int, Errno> {
fcntl(file, FcntlArg::F_GET_SEALS)
}
fn fcntl_add_seals(file: &File, arg: c_int) -> Result<(), Errno> {
fcntl(file, FcntlArg::F_ADD_SEALS(SealFlag::from_bits_retain(arg))).map(drop)
}
#[cfg(test)]
mod test {
use std::os::unix::fs::PermissionsExt as _;
use super::{
c_int, SealOptions, F_SEAL_EXEC, F_SEAL_FUTURE_WRITE, F_SEAL_GROW, F_SEAL_SEAL,
F_SEAL_SHRINK, F_SEAL_WRITE, MFD_ALLOW_SEALING, MFD_CLOEXEC, MFD_EXEC, MFD_NOEXEC_SEAL,
};
#[test]
fn new() {
let options = SealOptions {
memfd_flags: MFD_ALLOW_SEALING,
seal_flags: 0,
must_seal_flags: 0,
};
assert_eq!(
options
.close_on_exec(true)
.must_seal_seals(true)
.must_seal_shrinking(true)
.must_seal_growing(true)
.must_seal_writing(true)
.seal_future_writing(false)
.seal_executable(false),
SealOptions::new()
);
}
#[test]
fn flags() {
const ALL_SEALS: c_int = F_SEAL_SEAL
| F_SEAL_SHRINK
| F_SEAL_GROW
| F_SEAL_WRITE
| F_SEAL_FUTURE_WRITE
| F_SEAL_EXEC;
let mut options = SealOptions::new();
assert_eq!(options.memfd_flags & MFD_ALLOW_SEALING, MFD_ALLOW_SEALING);
assert_eq!(options.memfd_flags & MFD_CLOEXEC, MFD_CLOEXEC);
options = options.close_on_exec(false);
assert_eq!(options.memfd_flags & MFD_CLOEXEC, 0);
options = options.close_on_exec(true);
assert_eq!(options.memfd_flags & MFD_CLOEXEC, MFD_CLOEXEC);
assert_eq!(
options.seal_flags & ALL_SEALS,
ALL_SEALS & !F_SEAL_FUTURE_WRITE & !F_SEAL_EXEC
);
assert_eq!(
options.must_seal_flags & ALL_SEALS,
ALL_SEALS & !F_SEAL_FUTURE_WRITE & !F_SEAL_EXEC
);
options = options
.must_seal_future_writing(true)
.must_seal_executable(true);
assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
assert_eq!(options.must_seal_flags & ALL_SEALS, ALL_SEALS);
options = options
.seal_seals(false)
.seal_shrinking(false)
.seal_growing(false)
.seal_writing(false)
.seal_future_writing(false)
.seal_executable(false);
assert_eq!(options.seal_flags & ALL_SEALS, 0);
assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
options = options
.seal_seals(true)
.seal_shrinking(true)
.seal_growing(true)
.seal_writing(true)
.seal_future_writing(true)
.seal_executable(true);
assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
options = options
.seal_seals(false)
.seal_shrinking(false)
.seal_growing(false)
.seal_writing(false)
.seal_future_writing(false)
.seal_executable(false);
assert_eq!(options.seal_flags & ALL_SEALS, 0);
assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
options = options
.must_seal_seals(true)
.must_seal_shrinking(true)
.must_seal_growing(true)
.must_seal_writing(true)
.must_seal_future_writing(true)
.must_seal_executable(true);
assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
assert_eq!(options.must_seal_flags & ALL_SEALS, ALL_SEALS);
options = options
.must_seal_seals(false)
.must_seal_shrinking(false)
.must_seal_growing(false)
.must_seal_writing(false)
.must_seal_future_writing(false)
.must_seal_executable(false);
assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
}
#[test]
fn execute_flags() {
let mut options = SealOptions::new();
assert_eq!(options.seal_flags & F_SEAL_EXEC, 0);
options = options.seal_executable(true);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
options = options.seal_executable(false);
assert_eq!(options.seal_flags & F_SEAL_EXEC, 0);
for _ in 0..2 {
options = options.executable(true);
assert_eq!(options.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL), MFD_EXEC);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
options = options.seal_executable(false);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
options = options.executable(false);
assert_eq!(
options.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL),
MFD_NOEXEC_SEAL
);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
options = options.seal_executable(false);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
}
assert_eq!(options.must_seal_flags & F_SEAL_EXEC, 0);
options = options.must_seal_executable(true);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
assert_eq!(options.must_seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
options = options.seal_executable(false);
assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
assert_eq!(options.must_seal_flags & F_SEAL_EXEC, 0);
}
#[test]
fn executable() {
let file = SealOptions::new()
.executable(false)
.copy_and_seal(&mut &[][..])
.unwrap();
assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0);
let file = SealOptions::new()
.executable(true)
.copy_and_seal(&mut &[][..])
.unwrap();
assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0o111);
}
}