use std::path::Path;
use std::process::{Command, ExitStatus};
use anyhow::{Context as _, Result};
use landlock::{
ABI, Access, AccessFs, AccessNet, CompatLevel, Compatible, NetPort, Ruleset, RulesetAttr,
RulesetCreated, RulesetCreatedAttr, RulesetError, Scope, path_beneath_rules,
};
use openmoat_core::{EvalContext, Policy};
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
SeccompRule,
};
use crate::sandbox::seccomp::SOCKET_ARGS;
use crate::sandbox::{Grants, Report, landlock_rules};
const ABI_REQUIRED: ABI = ABI::V4;
const DENIED: [i64; 6] = [
libc::SYS_io_uring_setup,
libc::SYS_io_uring_enter,
libc::SYS_io_uring_register,
libc::SYS_ptrace,
libc::SYS_process_vm_readv,
libc::SYS_process_vm_writev,
];
#[cfg(target_arch = "x86_64")]
const X32_DENIED: &[i64] = &[41, 425, 426, 427, 521, 539, 540];
#[cfg(not(target_arch = "x86_64"))]
const X32_DENIED: &[i64] = &[];
const X32: i64 = 0x4000_0000;
pub struct Confined {
command: Command,
report: Report,
ruleset: RulesetCreated,
filter: BpfProgram,
}
pub fn confine(
policy: &Policy,
ctx: &EvalContext,
grants: Grants,
program: &Path,
) -> Result<Confined> {
let generated = landlock_rules(policy, ctx, grants)?;
let rules = &generated.rules;
let ports = rules
.connect_port
.map(|port| Ok(NetPort::new(port, AccessNet::ConnectTcp)));
let build = || -> Result<RulesetCreated, RulesetError> {
Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::from_all(ABI_REQUIRED))?
.handle_access(AccessNet::from_all(ABI_REQUIRED))?
.set_compatibility(CompatLevel::BestEffort)
.scope(Scope::from_all(ABI::V6))?
.create()?
.add_rules(path_beneath_rules(
&rules.read,
AccessFs::from_read(ABI_REQUIRED),
))?
.add_rules(path_beneath_rules(
&rules.write,
AccessFs::from_all(ABI_REQUIRED),
))?
.add_rules(ports)
};
let ruleset = build().context("`moat run` needs Landlock ABI 4 (Linux 6.7 or later)")?;
Ok(Confined {
command: Command::new(program),
report: generated.report,
ruleset,
filter: seccomp_filter().context("generating the seccomp filter")?,
})
}
fn seccomp_filter() -> Result<BpfProgram> {
let mut socket = Vec::new();
for (arg, allowed) in (0..).zip(SOCKET_ARGS) {
let other = allowed.iter().map(|&value| {
SeccompCondition::new(arg, SeccompCmpArgLen::Dword, SeccompCmpOp::Ne, value.into())
});
socket.push(SeccompRule::new(other.collect::<Result<_, _>>()?)?);
}
let x32 = X32_DENIED.iter().map(|nr| X32 + nr);
let refused = DENIED.into_iter().chain(x32).map(|nr| (nr, Vec::new()));
let rules = refused.chain([(libc::SYS_socket, socket)]).collect();
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow,
SeccompAction::Errno(libc::EPERM.unsigned_abs()),
std::env::consts::ARCH.try_into()?,
)?;
Ok(filter.try_into()?)
}
impl Confined {
pub fn command(&mut self) -> &mut Command {
&mut self.command
}
pub fn report(&self) -> &Report {
&self.report
}
pub fn status(self) -> Result<ExitStatus> {
let Self {
mut command,
ruleset,
filter,
..
} = self;
std::thread::spawn(move || {
ruleset
.restrict_self()
.context("applying the Landlock rules")?;
seccompiler::apply_filter(&filter).context("applying the seccomp filter")?;
command.status().context("starting the agent")
})
.join()
.ok()
.context("the thread that starts the agent panicked")?
}
}
#[cfg(test)]
mod tests {
use std::net::{TcpListener, UdpSocket};
use std::os::unix::net::{UnixDatagram, UnixStream};
use super::*;
#[test]
fn the_filter_refuses_unix_and_udp_sockets_and_allows_tcp_and_socketpair() {
let filter = seccomp_filter().expect("the filter compiles");
let errno = |result: std::io::Result<()>| result.err().and_then(|e| e.raw_os_error());
let (unix, udp, tcp, pair) = std::thread::spawn(move || {
seccompiler::apply_filter(&filter).expect("the filter applies");
(
errno(UnixDatagram::unbound().map(drop)),
errno(UdpSocket::bind("127.0.0.1:0").map(drop)),
errno(TcpListener::bind("127.0.0.1:0").map(drop)),
errno(UnixStream::pair().map(drop)),
)
})
.join()
.expect("the filtered thread finishes");
assert_eq!((unix, udp), (Some(libc::EPERM), Some(libc::EPERM)));
assert_eq!((tcp, pair), (None, None));
assert!(
UdpSocket::bind("127.0.0.1:0").is_ok(),
"this thread is not filtered"
);
}
}