#![deny(unsafe_code)]
#![forbid(clippy::arithmetic_side_effects)]
#![forbid(clippy::cast_possible_truncation)]
#![forbid(clippy::cast_possible_wrap)]
use std::{
fmt,
mem::MaybeUninit,
os::fd::{AsFd, FromRawFd, RawFd},
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Condvar, Mutex,
},
thread,
};
use ctr::cipher::{StreamCipher, StreamCipherSeek};
use hmac::Mac;
use libseccomp::{ScmpAction, ScmpFilterContext};
use nix::{
errno::Errno,
fcntl::{posix_fadvise, OFlag, PosixFadviseAdvice},
sched::{unshare, CloneFlags},
unistd::{gettid, lseek64, Gid, Pid, Uid, Whence},
};
use serde::{Serialize, Serializer};
use zeroize::Zeroizing;
#[cfg(target_arch = "x86")]
use crate::cookie::CookieIdx::Ftruncate64Arg3;
use crate::{
alert,
compat::seccomp_load,
config::*,
confine::{
confine_scmp_clone, confine_scmp_clone3, confine_scmp_close, confine_scmp_fadvise,
confine_scmp_fcntl, confine_scmp_ftruncate, confine_scmp_madvise, confine_scmp_open_stat,
confine_scmp_prctl, confine_scmp_read, confine_scmp_setid, confine_scmp_sigaction,
confine_scmp_write, confine_scmp_wx_syd, Sydcall,
},
cookie::safe_ftruncate64,
err::{err2no, SydResult},
error,
fd::{closeexcept, seal_memfd_all, SafeOwnedFd},
hash::{
aes_ctr, hmac_sha256, AesKey, CryptKey, SydHashMap, BLOCK_SIZE, CRYPT_MAGIC_OFFSET,
HMAC_TAG_SIZE, IV, SYD3_HDR_OFFSET, SYD3_HDR_SIZE,
},
id::SydId,
info,
io::{read_chunk, write_all},
landlock::Errata,
landlock_policy::LandlockPolicy,
lookup::FileInfo,
ofd::lock_fd,
path::{XPath, XPathBuf},
retry::{retry_on_eintr, retry_on_intr},
sandbox::Options,
sealbox::SealedSlice,
xfmt,
};
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum AesMod {
Read,
Append,
Write,
}
impl fmt::Display for AesMod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read => write!(f, "read"),
Self::Append => write!(f, "append"),
Self::Write => write!(f, "write"),
}
}
}
impl Serialize for AesMod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl From<OFlag> for AesMod {
fn from(flags: OFlag) -> Self {
if flags.contains(OFlag::O_WRONLY) || flags.contains(OFlag::O_RDWR) {
if flags.contains(OFlag::O_APPEND) {
Self::Append
} else {
Self::Write
}
} else {
Self::Read
}
}
}
#[derive(Clone)]
pub(crate) struct AesVal {
pub(crate) crypt_fd: RawFd,
pub(crate) enc_fd: RawFd,
pub(crate) info: FileInfo,
pub(crate) mode: AesMod,
pub(crate) tid: Pid,
}
pub(crate) type AesMap = SydHashMap<XPathBuf, AesVal>;
pub(crate) type AesLock = Arc<(Mutex<AesMap>, Condvar)>;
pub(crate) struct AesFilter {
bpf: SealedSlice<u8>,
flags: libc::c_ulong,
}
impl AesFilter {
pub(crate) fn new(bpf: SealedSlice<u8>, flags: libc::c_ulong) -> Self {
Self { bpf, flags }
}
}
pub(crate) struct AesConfig {
keys: AesKey,
filter: Option<Arc<AesFilter>>,
memfd: bool,
}
impl AesConfig {
pub(crate) fn new(keys: AesKey, filter: Option<Arc<AesFilter>>, memfd: bool) -> Self {
Self {
keys,
filter,
memfd,
}
}
}
struct AesGuard<'a> {
files: &'a AesLock,
pending: &'a AtomicUsize,
path: XPathBuf,
tid: Pid,
}
impl Drop for AesGuard<'_> {
fn drop(&mut self) {
let (aes_map, cvar) = &**self.files;
let mut aes_map = aes_map.lock().unwrap_or_else(|e| e.into_inner());
if aes_map
.get(&self.path)
.is_some_and(|val| val.tid == self.tid)
{
aes_map.remove(&self.path);
}
let was_last = self.pending.fetch_sub(1, Ordering::Release) == 1;
drop(aes_map);
if was_last {
cvar.notify_all();
}
}
}
pub(crate) struct AesWorker {
crypt_keys: AesKey,
crypt_iv: IV,
crypt_data: AesVal,
crypt_path: XPathBuf,
files: AesLock,
filter: Option<Arc<AesFilter>>,
memfd: bool,
pending: Arc<AtomicUsize>,
}
impl AesWorker {
pub(crate) fn prepare_confine(
options: Options,
transit_uids: &[(Uid, Uid)],
transit_gids: &[(Gid, Gid)],
_dry_run: bool,
) -> SydResult<ScmpFilterContext> {
let mut ctx = ScmpFilterContext::new(ScmpAction::KillProcess)?;
ctx.set_ctl_nnp(true)?;
ctx.set_ctl_ssb(options.allow_unsafe_exec_speculative())?;
ctx.set_ctl_tsync(false)?;
ctx.set_act_badarch(ScmpAction::KillProcess)?;
let _ = ctx.set_ctl_optimize(2);
let restrict_cookie = !options.allow_unsafe_nocookie();
confine_scmp_wx_syd(&mut ctx)?;
confine_scmp_open_stat(&mut ctx, true , true )?;
let rw_max: u64 = IO_BUF_SIZE.try_into().or(Err(Errno::EOVERFLOW))?;
confine_scmp_read(&mut ctx, rw_max, restrict_cookie)?;
confine_scmp_write(
&mut ctx,
Some(rw_max),
false,
restrict_cookie,
)?;
confine_scmp_clone(&mut ctx)?;
confine_scmp_clone3(&mut ctx)?;
confine_scmp_fadvise(&mut ctx)?;
confine_scmp_madvise(&mut ctx)?;
confine_scmp_ftruncate(&mut ctx, restrict_cookie)?;
confine_scmp_close(&mut ctx, restrict_cookie)?;
confine_scmp_fcntl(&mut ctx, AES_FCNTL_OPS)?;
confine_scmp_prctl(&mut ctx, AES_PRCTL_OPS)?;
confine_scmp_sigaction(&mut ctx)?;
for sysname in AES_SYSCALLS
.iter()
.chain(ALLOC_SYSCALLS)
.chain(FUTEX_SYSCALLS)
.chain(GETID_SYSCALLS)
.chain(VDSO_SYSCALLS)
{
match Sydcall::from_name(sysname) {
Ok(syscall) => {
ctx.add_rule(ScmpAction::Allow, syscall)?;
}
Err(_) => {
info!("ctx": "confine", "op": "allow_aes_syscall",
"msg": xfmt!("invalid or unsupported syscall {sysname}"));
}
}
}
let safe_setuid = options.allow_safe_setuid();
let safe_setgid = options.allow_safe_setgid();
if safe_setuid || safe_setgid {
confine_scmp_setid(
"aes",
&mut ctx,
safe_setuid,
safe_setgid,
transit_uids,
transit_gids,
)?;
}
Ok(ctx)
}
pub(crate) fn try_spawn(
cfg: &AesConfig,
crypt_iv: IV,
crypt_data: AesVal,
crypt_path: &XPath,
files: &AesLock,
pending: &Arc<AtomicUsize>,
) -> Result<Pid, Errno> {
let ready = Arc::new((Mutex::new(None::<Pid>), Condvar::new()));
retry_on_intr(|| {
let ready = Arc::clone(&ready);
let worker = AesWorker {
crypt_keys: Arc::clone(&cfg.keys),
crypt_iv: crypt_iv.clone(),
crypt_data: crypt_data.clone(),
crypt_path: XPathBuf::try_from(crypt_path)?,
files: Arc::clone(files),
filter: cfg.filter.clone(),
memfd: cfg.memfd,
pending: Arc::clone(pending),
};
thread::Builder::new()
.name(SydId::get_name("syd_aes").to_string())
.stack_size(AES_STACK_SIZE)
.spawn(move || {
if let Err(errno) =
Self::unshare(worker.crypt_data.crypt_fd, worker.crypt_data.enc_fd)
{
alert!("ctx": "boot", "op": "unshare_aes_thread",
"msg": xfmt!("failed to unshare AES fds: {errno}"),
"err": errno as i32);
std::process::exit(101);
}
let mut worker = worker;
worker.crypt_data.tid = gettid();
let (lock, cvar) = &*ready;
{
let mut guard = lock.lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(worker.crypt_data.tid);
}
cvar.notify_one();
worker.run()
})
.map_err(|err| err2no(&err))
})?;
let (lock, cvar) = &*ready;
{
let mut guard = lock.lock().unwrap_or_else(|e| e.into_inner());
loop {
if let Some(tid) = *guard {
break Ok(tid);
}
guard = cvar.wait(guard).unwrap_or_else(|e| e.into_inner());
}
}
}
fn run(self) -> SydResult<()> {
let AesWorker {
crypt_keys,
crypt_iv,
crypt_data,
crypt_path,
files,
filter,
memfd,
pending,
} = self;
if let Some(filter) = filter {
let abi = crate::landlock::ABI::new_current();
let errata = crate::landlock::Errata::query();
let policy = LandlockPolicy {
scoped_abs: true,
scoped_sig: errata.contains(Errata::SCOPED_SIGNAL_SAME_TGID),
..Default::default()
};
let _ = policy.restrict_self(abi);
if let Err(errno) = seccomp_load(&filter.bpf, filter.flags) {
alert!("ctx": "boot", "op": "confine_aes_thread",
"msg": xfmt!("failed to confine AES thread: {errno}"),
"err": errno as i32);
std::process::exit(101);
}
} else {
error!("ctx": "confine", "op": "confine_aes_thread",
"msg": "AES thread is running unconfined in debug mode");
}
#[expect(unsafe_code)]
let crypt_fd = unsafe { SafeOwnedFd::from_raw_fd(crypt_data.crypt_fd) };
let _guard = AesGuard {
files: &files,
pending: &pending,
path: crypt_path,
tid: crypt_data.tid,
};
match retry_on_eintr(|| lock_fd(&crypt_fd, true, true)) {
Ok(()) => Self::sync(&crypt_fd, &crypt_keys, crypt_iv, crypt_data, memfd),
Err(errno) => Err(errno.into()),
}
}
fn sync<Fd: AsFd>(
crypt_fd: Fd,
crypt_keys: &CryptKey,
crypt_iv: IV,
crypt_data: AesVal,
memfd: bool,
) -> SydResult<()> {
if memfd {
seal_memfd_all(&crypt_fd)?;
}
let file_mode = crypt_data.mode;
#[expect(unsafe_code)]
let enc_fd = unsafe { SafeOwnedFd::from_raw_fd(crypt_data.enc_fd) };
let mut is_append = match file_mode {
AesMod::Read => return Ok(()),
AesMod::Append => true,
_ => false,
};
let data_size: u64 = lseek64(&crypt_fd, 0, Whence::SeekEnd)?
.try_into()
.or(Err(Errno::EOVERFLOW))?;
if data_size == 0 {
retry_on_eintr(|| safe_ftruncate64(&enc_fd, 0))?;
return Ok(());
}
let mut file_size: u64 = lseek64(&enc_fd, 0, Whence::SeekEnd)?
.try_into()
.or(Err(Errno::EOVERFLOW))?;
if is_append && file_size == 0 {
is_append = false;
}
if is_append && data_size <= file_size.saturating_sub(SYD3_HDR_SIZE) {
return Ok(());
}
let mut cipher = aes_ctr(crypt_keys.enc(), &crypt_iv)?;
let mut mac = hmac_sha256(crypt_keys.mac())?;
mac.update(CRYPT_MAGIC);
mac.update(crypt_iv.as_ref());
Self::advise_io(&crypt_fd, &enc_fd)?;
let mut buf = Zeroizing::new([MaybeUninit::<u8>::uninit(); IO_BUF_SIZE]);
let plain_len: usize = if is_append {
file_size = file_size
.checked_sub(SYD3_HDR_SIZE)
.ok_or(Errno::EOVERFLOW)?;
let last_block_offset = Self::get_last_block_offset(file_size)?;
cipher.seek(last_block_offset);
Self::seek2append(&crypt_fd, last_block_offset, file_size)?;
assert!(
data_size >= file_size,
"BUG: stale bytes in last partial block, report a bug!"
);
lseek64(&enc_fd, SYD3_HDR_OFFSET, Whence::SeekSet)?;
let mut remaining: usize = last_block_offset.try_into().or(Err(Errno::EOVERFLOW))?;
while remaining > 0 {
let len = remaining.min(IO_BUF_SIZE);
let chunk = read_chunk(&enc_fd, &mut buf[..len])?;
mac.update(&*chunk);
remaining = remaining.checked_sub(len).ok_or(Errno::EOVERFLOW)?;
}
let off = SYD3_HDR_OFFSET
.checked_add(last_block_offset.try_into().or(Err(Errno::EOVERFLOW))?)
.ok_or(Errno::EOVERFLOW)?;
lseek64(&enc_fd, off, Whence::SeekSet)?;
data_size
.checked_sub(last_block_offset)
.ok_or(Errno::EOVERFLOW)?
.try_into()
.or(Err(Errno::EOVERFLOW))?
} else {
lseek64(&crypt_fd, 0, Whence::SeekSet)?;
if file_size > 0 {
retry_on_eintr(|| safe_ftruncate64(&enc_fd, 0))?;
lseek64(&enc_fd, 0, Whence::SeekSet)?;
}
Self::write_header(&enc_fd, &crypt_iv)?;
data_size.try_into().or(Err(Errno::EOVERFLOW))?
};
let mut remaining = plain_len;
while remaining > 0 {
let len = remaining.min(IO_BUF_SIZE);
let chunk = read_chunk(&crypt_fd, &mut buf[..len])?;
cipher.apply_keystream(chunk);
mac.update(&*chunk);
write_all(&enc_fd, &*chunk)?;
remaining = remaining.checked_sub(len).ok_or(Errno::EOVERFLOW)?;
}
let tag = mac.finalize().into_bytes();
lseek64(&enc_fd, CRYPT_MAGIC_OFFSET, Whence::SeekSet)?;
write_all(&enc_fd, &tag[..])?;
Ok(())
}
fn unshare(crypt_fd: RawFd, enc_fd: RawFd) -> Result<(), Errno> {
unshare(CloneFlags::CLONE_FILES | CloneFlags::CLONE_FS | CloneFlags::CLONE_SYSVSEM)?;
#[expect(clippy::cast_sign_loss)]
let mut keep = [
crypt_fd as libc::c_uint,
enc_fd as libc::c_uint,
crate::log::LOG_FD.load(Ordering::Relaxed) as libc::c_uint,
];
keep.sort_unstable();
closeexcept(&keep)
}
fn advise_io<Fd1: AsFd, Fd2: AsFd>(crypt_fd: Fd1, enc_fd: Fd2) -> Result<(), Errno> {
posix_fadvise(&enc_fd, 0, 0, PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL)?;
posix_fadvise(&crypt_fd, 0, 0, PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL)?;
posix_fadvise(&crypt_fd, 0, 0, PosixFadviseAdvice::POSIX_FADV_WILLNEED)?;
Ok(())
}
fn get_last_block_offset(file_size: u64) -> Result<u64, Errno> {
let remainder = file_size
.checked_rem(BLOCK_SIZE as u64)
.ok_or(Errno::EOVERFLOW)?;
file_size.checked_sub(remainder).ok_or(Errno::EOVERFLOW)
}
fn seek2append<Fd: AsFd>(
crypt_fd: Fd,
last_block_offset: u64,
file_size: u64,
) -> Result<(), Errno> {
if last_block_offset < file_size {
let off: i64 = last_block_offset.try_into().or(Err(Errno::EOVERFLOW))?;
lseek64(crypt_fd, off, Whence::SeekSet)
} else {
let off: i64 = file_size.try_into().or(Err(Errno::EOVERFLOW))?;
lseek64(crypt_fd, off, Whence::SeekSet)
}
.map(drop)
}
fn write_header<Fd: AsFd>(enc_fd: Fd, iv: &IV) -> Result<(), Errno> {
write_all(&enc_fd, CRYPT_MAGIC)?;
write_all(&enc_fd, &[0u8; HMAC_TAG_SIZE])?;
write_all(&enc_fd, iv.as_ref())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::BLOCK_SIZE;
#[test]
fn test_get_last_block_offset_1() {
let result = AesWorker::get_last_block_offset(0);
assert_eq!(result, Ok(0));
}
#[test]
fn test_get_last_block_offset_2() {
let result = AesWorker::get_last_block_offset(BLOCK_SIZE as u64);
assert_eq!(result, Ok(BLOCK_SIZE as u64));
}
#[test]
fn test_get_last_block_offset_3() {
let result = AesWorker::get_last_block_offset(BLOCK_SIZE as u64 + 1);
assert_eq!(result, Ok(BLOCK_SIZE as u64));
}
#[test]
fn test_get_last_block_offset_4() {
let result = AesWorker::get_last_block_offset(2 * BLOCK_SIZE as u64);
assert_eq!(result, Ok(2 * BLOCK_SIZE as u64));
}
#[test]
fn test_get_last_block_offset_5() {
let result = AesWorker::get_last_block_offset(1);
assert_eq!(result, Ok(0));
}
#[test]
fn test_get_last_block_offset_6() {
let result = AesWorker::get_last_block_offset(BLOCK_SIZE as u64 - 1);
assert_eq!(result, Ok(0));
}
}