#![allow(unsafe_code)]
use std::ffi::{CStr, CString, c_int, c_long, c_ulong};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use super::seccomp::Insn;
pub(crate) fn errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
fn check(rc: c_long) -> Result<c_long, i32> {
if rc < 0 { Err(errno()) } else { Ok(rc) }
}
fn check_int(rc: c_int) -> Result<(), i32> {
if rc < 0 { Err(errno()) } else { Ok(()) }
}
#[repr(C)]
struct RulesetAttr {
handled_access_fs: u64,
handled_access_net: u64,
scoped: u64,
}
#[repr(C, packed)]
struct PathBeneathAttr {
allowed_access: u64,
parent_fd: i32,
}
#[repr(C)]
struct NetPortAttr {
allowed_access: u64,
port: u64,
}
const CREATE_RULESET_VERSION: u32 = 1 << 0;
const CREATE_RULESET_ERRATA: u32 = 1 << 1;
const RULE_PATH_BENEATH: c_int = 1;
const RULE_NET_PORT: c_int = 2;
fn ruleset_query(flag: u32) -> Result<u32, i32> {
let rc = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<u8>(),
0usize,
flag,
)
};
check(rc).map(|v| u32::try_from(v).unwrap_or(0))
}
pub(crate) fn landlock_abi() -> Result<u32, i32> {
ruleset_query(CREATE_RULESET_VERSION)
}
pub(crate) fn landlock_errata() -> u32 {
ruleset_query(CREATE_RULESET_ERRATA).unwrap_or(0)
}
pub(crate) fn landlock_ruleset(fs: u64, net: u64, scoped: u64) -> Result<OwnedFd, i32> {
let attr = RulesetAttr {
handled_access_fs: fs,
handled_access_net: net,
scoped,
};
let rc = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
&raw const attr,
std::mem::size_of::<RulesetAttr>(),
0u32,
)
};
let fd = c_int::try_from(check(rc)?).map_err(|_| libc::EBADF)?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
pub(crate) fn landlock_allow_path(
ruleset: &OwnedFd,
parent: &OwnedFd,
access: u64,
) -> Result<(), i32> {
let attr = PathBeneathAttr {
allowed_access: access,
parent_fd: parent.as_raw_fd(),
};
let rc = unsafe {
libc::syscall(
libc::SYS_landlock_add_rule,
ruleset.as_raw_fd(),
RULE_PATH_BENEATH,
&raw const attr,
0u32,
)
};
check(rc).map(drop)
}
pub(crate) fn landlock_allow_port(ruleset: &OwnedFd, port: u16, access: u64) -> Result<(), i32> {
let attr = NetPortAttr {
allowed_access: access,
port: u64::from(port),
};
let rc = unsafe {
libc::syscall(
libc::SYS_landlock_add_rule,
ruleset.as_raw_fd(),
RULE_NET_PORT,
&raw const attr,
0u32,
)
};
check(rc).map(drop)
}
pub(crate) fn open_path(path: &CStr) -> Result<(OwnedFd, bool), i32> {
let fd = unsafe { libc::open(path.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
if fd < 0 {
return Err(errno());
}
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let st = unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(fd.as_raw_fd(), &raw mut st) != 0 {
return Err(errno());
}
st
};
Ok((fd, st.st_mode & libc::S_IFMT == libc::S_IFDIR))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Identity {
pub(crate) dev: u64,
pub(crate) ino: u64,
}
pub(crate) fn hold(path: &CStr) -> Result<OwnedFd, i32> {
let fd = unsafe {
libc::open(
path.as_ptr(),
libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if fd < 0 {
return Err(errno());
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
pub(crate) fn identity_of(fd: &OwnedFd) -> Result<Identity, i32> {
let st = unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(fd.as_raw_fd(), &raw mut st) != 0 {
return Err(errno());
}
st
};
Ok(Identity {
dev: st.st_dev as u64,
ino: st.st_ino as u64,
})
}
pub(crate) fn identity(path: &CStr) -> Result<Identity, i32> {
let st = unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::lstat(path.as_ptr(), &raw mut st) != 0 {
return Err(errno());
}
st
};
Ok(Identity {
dev: st.st_dev as u64,
ino: st.st_ino as u64,
})
}
pub(crate) fn ids() -> (u32, u32) {
unsafe { (libc::getuid(), libc::getgid()) }
}
pub(crate) fn thread_count() -> usize {
std::fs::read_dir("/proc/self/task").map_or(0, Iterator::count)
}
pub(crate) fn pid() -> libc::pid_t {
unsafe { libc::getpid() }
}
pub(crate) fn parent_pid() -> libc::pid_t {
unsafe { libc::getppid() }
}
pub(crate) mod steps {
pub(crate) const SESSION: u32 = 1;
pub(crate) const LIFETIME: u32 = 2;
pub(crate) const NAMESPACES: u32 = 3;
pub(crate) const ID_MAP: u32 = 4;
pub(crate) const MOUNTS: u32 = 5;
pub(crate) const CAPABILITIES: u32 = 6;
pub(crate) const RLIMIT: u32 = 7;
pub(crate) const FDS: u32 = 8;
pub(crate) const NO_NEW_PRIVS: u32 = 9;
pub(crate) const LANDLOCK: u32 = 10;
pub(crate) const SECCOMP: u32 = 11;
pub(crate) const VERIFY: u32 = 12;
pub(crate) const LISTENER: u32 = 13;
}
const STEP_MARKER: i32 = 0x5043_0000;
pub(crate) const fn step_code(step: u32, errno: i32) -> i32 {
STEP_MARKER | ((step as i32 & 0xf) << 12) | (errno & 0xfff)
}
pub(crate) fn decode_step(code: i32) -> Option<(u32, i32)> {
((code as u32 & 0xffff_0000) == STEP_MARKER as u32)
.then_some((((code >> 12) & 0xf) as u32, code & 0xfff))
}
#[derive(Debug)]
pub(crate) enum ChildMount {
Bind {
source: CString,
source_id: Identity,
target: CString,
target_id: Identity,
attrs: u64,
optional: Option<OwnedFd>,
},
Tmpfs {
target: CString,
target_id: Identity,
attrs: u64,
mode: CString,
optional: Option<OwnedFd>,
},
}
impl ChildMount {
fn optional(&self) -> Option<&OwnedFd> {
match self {
ChildMount::Bind { optional, .. } | ChildMount::Tmpfs { optional, .. } => {
optional.as_ref()
}
}
}
pub(crate) fn removed(&self) -> bool {
self.optional().is_some_and(|held| {
unsafe {
let mut st: libc::stat = std::mem::zeroed();
libc::fstat(held.as_raw_fd(), &raw mut st) == 0 && st.st_nlink == 0
}
})
}
pub(crate) fn planned(&self) -> Vec<(&CStr, Identity)> {
match self {
ChildMount::Bind {
source,
source_id,
target,
target_id,
..
} => {
let mut out = vec![(target.as_c_str(), *target_id)];
if source != target {
out.push((source.as_c_str(), *source_id));
}
out
}
ChildMount::Tmpfs {
target, target_id, ..
} => vec![(target.as_c_str(), *target_id)],
}
}
}
pub(crate) const MOUNT_ATTR_RDONLY: u64 = 0x1;
pub(crate) const MOUNT_ATTR_NOSUID: u64 = 0x2;
pub(crate) const MOUNT_ATTR_NODEV: u64 = 0x4;
pub(crate) const MOUNT_ATTR_NOEXEC: u64 = 0x8;
const OPEN_TREE_CLONE: u64 = 0x1;
const MOVE_MOUNT_F_EMPTY_PATH: u64 = 0x4;
const MOVE_MOUNT_T_EMPTY_PATH: u64 = 0x40;
const FSOPEN_CLOEXEC: u64 = 0x1;
const FSMOUNT_CLOEXEC: u64 = 0x1;
const FSCONFIG_SET_STRING: u64 = 1;
const FSCONFIG_CMD_CREATE: u64 = 6;
const RESOLVE_NO_SYMLINKS: u64 = 0x4;
#[repr(C)]
struct OpenHow {
flags: u64,
mode: u64,
resolve: u64,
}
#[repr(C)]
struct MountAttr {
attr_set: u64,
attr_clr: u64,
propagation: u64,
userns_fd: u64,
}
struct Fd(c_int);
impl Drop for Fd {
fn drop(&mut self) {
unsafe { libc::close(self.0) };
}
}
fn syscall_fd(rc: c_long) -> Result<Fd, i32> {
if rc < 0 {
Err(errno())
} else {
Ok(Fd(rc as c_int))
}
}
fn open_planned(path: &CStr, id: Identity) -> Result<Fd, i32> {
let how = OpenHow {
flags: (libc::O_PATH | libc::O_CLOEXEC) as u64,
mode: 0,
resolve: RESOLVE_NO_SYMLINKS,
};
let fd = syscall_fd(unsafe {
libc::syscall(
libc::SYS_openat2,
libc::AT_FDCWD,
path.as_ptr(),
&raw const how,
std::mem::size_of::<OpenHow>(),
)
})?;
let st = unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(fd.0, &raw mut st) != 0 {
return Err(errno());
}
st
};
if st.st_dev as u64 != id.dev || st.st_ino as u64 != id.ino {
return Err(libc::ESTALE);
}
Ok(fd)
}
fn set_attrs(mount: &Fd, attrs: u64) -> Result<(), i32> {
if attrs == 0 {
return Ok(());
}
let attr = MountAttr {
attr_set: attrs,
attr_clr: 0,
propagation: 0,
userns_fd: 0,
};
let flags = libc::AT_EMPTY_PATH;
let rc = unsafe {
libc::syscall(
libc::SYS_mount_setattr,
mount.0,
c"".as_ptr(),
flags as c_ulong,
&raw const attr,
std::mem::size_of::<MountAttr>(),
)
};
check(rc).map(drop)
}
fn attach(mount: &Fd, target: &Fd) -> Result<(), i32> {
let rc = unsafe {
libc::syscall(
libc::SYS_move_mount,
mount.0,
c"".as_ptr(),
target.0,
c"".as_ptr(),
(MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) as c_ulong,
)
};
check(rc).map(drop)
}
fn bind_planned(source: &Fd, target: &Fd, attrs: u64) -> Result<(), i32> {
let flags = OPEN_TREE_CLONE as c_ulong
| libc::O_CLOEXEC as c_ulong
| libc::AT_EMPTY_PATH as c_ulong
| libc::AT_RECURSIVE as c_ulong;
let rc = unsafe { libc::syscall(libc::SYS_open_tree, source.0, c"".as_ptr(), flags) };
let tree = syscall_fd(rc)?;
set_attrs(&tree, attrs)?;
attach(&tree, target)
}
fn tmpfs_planned(target: &Fd, mode: &CStr, attrs: u64) -> Result<(), i32> {
unsafe {
let fs = syscall_fd(libc::syscall(
libc::SYS_fsopen,
c"tmpfs".as_ptr(),
FSOPEN_CLOEXEC as c_ulong,
))?;
check(libc::syscall(
libc::SYS_fsconfig,
fs.0,
FSCONFIG_SET_STRING as c_ulong,
c"mode".as_ptr(),
mode.as_ptr(),
0 as c_int,
))?;
check(libc::syscall(
libc::SYS_fsconfig,
fs.0,
FSCONFIG_CMD_CREATE as c_ulong,
std::ptr::null::<libc::c_char>(),
std::ptr::null::<libc::c_char>(),
0 as c_int,
))?;
let mount = syscall_fd(libc::syscall(
libc::SYS_fsmount,
fs.0,
FSMOUNT_CLOEXEC as c_ulong,
attrs as c_ulong,
))?;
attach(&mount, target)
}
}
fn make(m: &ChildMount) -> Result<(), i32> {
match m {
ChildMount::Bind {
source,
source_id,
target,
target_id,
attrs,
..
} => {
let target_fd = open_planned(target, *target_id)?;
if source == target {
bind_planned(&target_fd, &target_fd, *attrs)
} else {
let source_fd = open_planned(source, *source_id)?;
bind_planned(&source_fd, &target_fd, *attrs)
}
}
ChildMount::Tmpfs {
target,
target_id,
attrs,
mode,
..
} => tmpfs_planned(&open_planned(target, *target_id)?, mode, *attrs),
}
}
#[derive(Debug)]
pub(crate) struct Isolate {
pub(crate) flags: c_int,
pub(crate) uid_map: Vec<u8>,
pub(crate) gid_map: Vec<u8>,
pub(crate) mounts: std::sync::Arc<[ChildMount]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Session {
New,
Group,
Keep,
Lead,
}
pub(crate) struct ChildPlan {
pub(crate) session: Session,
pub(crate) parent: libc::pid_t,
pub(crate) isolate: Option<Isolate>,
pub(crate) limits: Vec<(c_int, u64, u64)>,
pub(crate) keep: Vec<c_int>,
pub(crate) ruleset: OwnedFd,
pub(crate) filter: Vec<Insn>,
pub(crate) notify: Option<Notify>,
pub(crate) bridge: Option<Bridge>,
}
pub(crate) struct Bridge {
pub(crate) ports: Vec<u16>,
pub(crate) socket: OwnedFd,
pub(crate) control: Vec<u8>,
}
impl Bridge {
#[cfg(feature = "proxy")]
pub(crate) fn new(ports: Vec<u16>, socket: OwnedFd) -> Bridge {
let space = unsafe { libc::CMSG_SPACE(std::mem::size_of::<c_int>() as u32) } as usize;
Bridge {
ports,
socket,
control: vec![0; space],
}
}
fn open(&mut self) -> Result<(), i32> {
let datagram = libc::SOCK_DGRAM | libc::SOCK_CLOEXEC;
let control = unsafe { libc::socket(libc::AF_INET, datagram, 0) };
if control < 0 {
return Err(errno());
}
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
for (dst, src) in ifr.ifr_name.iter_mut().zip(b"lo") {
*dst = *src as libc::c_char;
}
let up = unsafe {
let got = libc::ioctl(control, libc::SIOCGIFFLAGS as _, &raw mut ifr);
if got == 0 {
ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
libc::ioctl(control, libc::SIOCSIFFLAGS as _, &raw const ifr)
} else {
got
}
};
let up = if up == 0 { Ok(()) } else { Err(errno()) };
unsafe { libc::close(control) };
up?;
for &port in &self.ports {
let stream = libc::SOCK_STREAM | libc::SOCK_CLOEXEC;
let l = unsafe { libc::socket(libc::AF_INET, stream, 0) };
if l < 0 {
return Err(errno());
}
let addr = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: port.to_be(),
sin_addr: libc::in_addr {
s_addr: u32::from(std::net::Ipv4Addr::LOCALHOST).to_be(),
},
sin_zero: [0; 8],
};
let rc = unsafe {
let rc = libc::bind(
l,
(&raw const addr).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
);
if rc == 0 { libc::listen(l, 128) } else { rc }
};
let sent = if rc == 0 {
send(self.socket.as_raw_fd(), &[1], Some(l), &mut self.control)
} else {
Err(errno())
};
unsafe { libc::close(l) };
sent?;
}
Ok(())
}
}
pub(crate) struct Notify {
pub(crate) filter: Vec<Insn>,
pub(crate) socket: OwnedFd,
pub(crate) control: Vec<u8>,
}
impl Notify {
pub(crate) fn new(filter: Vec<Insn>, socket: OwnedFd) -> Notify {
let space = unsafe { libc::CMSG_SPACE(std::mem::size_of::<c_int>() as u32) } as usize;
Notify {
filter,
socket,
control: vec![0; space],
}
}
}
fn install_filter(filter: &mut [Insn], flags: libc::c_ulong) -> Result<c_int, i32> {
let prog = libc::sock_fprog {
len: u16::try_from(filter.len()).map_err(|_| libc::E2BIG)?,
filter: filter.as_mut_ptr().cast(),
};
let rc = unsafe {
libc::syscall(
libc::SYS_seccomp,
libc::SECCOMP_SET_MODE_FILTER,
flags,
&raw const prog,
)
};
if rc < 0 {
Err(errno())
} else {
Ok(c_int::try_from(rc).unwrap_or(-1))
}
}
fn send(socket: c_int, bytes: &[u8], fd: Option<c_int>, control: &mut [u8]) -> Result<(), i32> {
let mut iov = libc::iovec {
iov_base: bytes.as_ptr().cast_mut().cast(),
iov_len: bytes.len(),
};
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &raw mut iov;
msg.msg_iovlen = 1;
if let Some(fd) = fd {
msg.msg_control = control.as_mut_ptr().cast();
msg.msg_controllen = control.len() as _;
unsafe {
let header = libc::CMSG_FIRSTHDR(&raw const msg);
(*header).cmsg_level = libc::SOL_SOCKET;
(*header).cmsg_type = libc::SCM_RIGHTS;
(*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<c_int>() as u32) as _;
std::ptr::write_unaligned(libc::CMSG_DATA(header).cast::<c_int>(), fd);
}
}
let rc = unsafe { libc::sendmsg(socket, &raw const msg, libc::MSG_NOSIGNAL) };
if rc < 0 { Err(errno()) } else { Ok(()) }
}
fn write_file(path: &CStr, data: &[u8]) -> Result<(), i32> {
unsafe {
let fd = libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
if fd < 0 {
return Err(errno());
}
let n = libc::write(fd, data.as_ptr().cast(), data.len());
let e = errno();
libc::close(fd);
if usize::try_from(n).ok() != Some(data.len()) {
return Err(if n < 0 { e } else { libc::EIO });
}
}
Ok(())
}
fn mount(
source: Option<&CStr>,
target: &CStr,
fstype: Option<&CStr>,
flags: c_ulong,
data: Option<&CStr>,
) -> Result<(), i32> {
let p = |c: Option<&CStr>| c.map_or(std::ptr::null(), CStr::as_ptr);
let rc = unsafe { libc::mount(p(source), target.as_ptr(), p(fstype), flags, p(data).cast()) };
check_int(rc)
}
fn prctl(option: c_int, arg: c_ulong) -> Result<c_int, i32> {
let rc = unsafe { libc::prctl(option, arg, 0 as c_ulong, 0 as c_ulong, 0 as c_ulong) };
if rc < 0 { Err(errno()) } else { Ok(rc) }
}
#[repr(C)]
struct CapHeader {
version: u32,
pid: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct CapData {
effective: u32,
permitted: u32,
inheritable: u32,
}
const CAP_VERSION_3: u32 = 0x2008_0522;
fn drop_capabilities(bounding: bool) -> Result<(), i32> {
if bounding {
for cap in 0..64 {
match prctl(libc::PR_CAPBSET_DROP, cap) {
Ok(_) => {}
Err(libc::EINVAL) => break,
Err(e) => return Err(e),
}
}
}
match prctl(
libc::PR_CAP_AMBIENT,
libc::PR_CAP_AMBIENT_CLEAR_ALL as c_ulong,
) {
Ok(_) | Err(libc::EINVAL) => {}
Err(e) => return Err(e),
}
let header = CapHeader {
version: CAP_VERSION_3,
pid: 0,
};
let data = [CapData {
effective: 0,
permitted: 0,
inheritable: 0,
}; 2];
let rc = unsafe { libc::syscall(libc::SYS_capset, &raw const header, data.as_ptr()) };
check(rc).map(drop)
}
pub(crate) fn capabilities() -> (u64, u64, u64) {
let mut header = CapHeader {
version: CAP_VERSION_3,
pid: 0,
};
let mut data = [CapData {
effective: 0,
permitted: 0,
inheritable: 0,
}; 2];
unsafe { libc::syscall(libc::SYS_capget, &raw mut header, data.as_mut_ptr()) };
let join = |lo: u32, hi: u32| u64::from(lo) | (u64::from(hi) << 32);
let mut bounding = 0u64;
for cap in 0..64 {
match prctl(libc::PR_CAPBSET_READ, cap) {
Ok(1) => bounding |= 1 << cap,
Ok(_) => {}
Err(_) => break,
}
}
(
join(data[0].effective, data[1].effective),
join(data[0].permitted, data[1].permitted),
bounding,
)
}
fn close_range(first: u32, last: u32, flags: u32) -> c_long {
unsafe { libc::syscall(libc::SYS_close_range, first, last, flags) }
}
fn mark_fds(keep: &[c_int]) -> Result<(), i32> {
mark_others(keep)?;
for &fd in keep {
check_int(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) })?;
}
Ok(())
}
fn mark_others(keep: &[c_int]) -> Result<(), i32> {
const CLOSE_RANGE_CLOEXEC: u32 = 1 << 2;
let mut first: u32 = 3;
let mut ranges_ok = true;
for &fd in keep.iter().chain(std::iter::once(&-1)) {
let last = if fd < 0 {
u32::MAX
} else {
match u32::try_from(fd) {
Ok(fd) if fd >= first => fd - 1,
Ok(fd) => {
first = first.max(fd + 1);
continue;
}
Err(_) => continue,
}
};
if last >= first && close_range(first, last, CLOSE_RANGE_CLOEXEC) != 0 {
ranges_ok = false;
break;
}
if fd >= 0 {
first = u32::try_from(fd).unwrap_or(u32::MAX).saturating_add(1);
}
}
if ranges_ok {
return Ok(());
}
#[allow(clippy::unnecessary_cast)]
let max = getrlimit(libc::RLIMIT_NOFILE as c_int).map_or(1024, |(soft, _)| soft.min(1 << 20));
for fd in 3..c_int::try_from(max).unwrap_or(c_int::MAX) {
if keep.contains(&fd) {
continue;
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
match errno() {
libc::EBADF => {}
e => return Err(e),
}
}
}
Ok(())
}
impl ChildPlan {
fn run(&mut self) -> std::io::Result<()> {
let fail =
|step: u32, errno: i32| std::io::Error::from_raw_os_error(step_code(step, errno));
let rc = unsafe {
match self.session {
Session::New => libc::setsid(),
Session::Group => libc::setpgid(0, 0),
Session::Keep => 0,
Session::Lead if libc::getsid(0) == libc::getpid() => 0,
Session::Lead => libc::setsid(),
}
};
check_int(rc).map_err(|e| fail(steps::SESSION, e))?;
let parent = self.parent;
let death = move || -> std::io::Result<()> {
prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as c_ulong)
.map_err(|e| fail(steps::LIFETIME, e))?;
if unsafe { libc::getppid() } != parent {
return Err(fail(steps::LIFETIME, libc::ESRCH));
}
Ok(())
};
death()?;
let t1 = self.isolate.is_some();
if let Some(iso) = &self.isolate {
check_int(unsafe { libc::unshare(iso.flags) })
.map_err(|e| fail(steps::NAMESPACES, e))?;
write_file(c"/proc/self/setgroups", b"deny")
.and_then(|()| write_file(c"/proc/self/uid_map", &iso.uid_map))
.and_then(|()| write_file(c"/proc/self/gid_map", &iso.gid_map))
.map_err(|e| fail(steps::ID_MAP, e))?;
mount(None, c"/", None, libc::MS_REC | libc::MS_PRIVATE, None)
.map_err(|e| fail(steps::MOUNTS, e))?;
for m in iso.mounts.iter() {
match make(m) {
Err(libc::ENOENT | libc::ESTALE | libc::ELOOP) if m.removed() => {}
Err(libc::ENOENT) if m.optional().is_some() => {
return Err(fail(steps::MOUNTS, libc::ESTALE));
}
r => r.map_err(|e| fail(steps::MOUNTS, e))?,
}
}
}
if let Some(bridge) = &mut self.bridge {
bridge.open().map_err(|e| fail(steps::LISTENER, e))?;
}
drop_capabilities(t1).map_err(|e| fail(steps::CAPABILITIES, e))?;
if t1 {
death()?;
}
for &(resource, soft, hard) in &self.limits {
let limit = libc::rlimit {
rlim_cur: soft,
rlim_max: hard,
};
#[allow(clippy::useless_conversion, clippy::unnecessary_cast)]
let rc = unsafe { libc::setrlimit(resource as _, &raw const limit) };
check_int(rc).map_err(|e| fail(steps::RLIMIT, e))?;
}
mark_fds(&self.keep).map_err(|e| fail(steps::FDS, e))?;
prctl(libc::PR_SET_NO_NEW_PRIVS, 1).map_err(|e| fail(steps::NO_NEW_PRIVS, e))?;
let rc = unsafe {
libc::syscall(
libc::SYS_landlock_restrict_self,
self.ruleset.as_raw_fd(),
0u32,
)
};
check(rc).map_err(|e| fail(steps::LANDLOCK, e))?;
match &mut self.notify {
Some(notify) => {
let socket = notify.socket.as_raw_fd();
match install_filter(&mut notify.filter, libc::SECCOMP_FILTER_FLAG_NEW_LISTENER) {
Ok(listener) => {
let sent = send(socket, &[1], Some(listener), &mut notify.control);
unsafe { libc::close(listener) };
sent.map_err(|e| fail(steps::LISTENER, e))?;
}
Err(refused) => {
install_filter(&mut self.filter, 0).map_err(|e| fail(steps::SECCOMP, e))?;
let mut status = [0u8; 5];
status[1..].copy_from_slice(&refused.to_le_bytes());
send(socket, &status, None, &mut notify.control)
.map_err(|e| fail(steps::LISTENER, e))?;
}
}
}
None => {
install_filter(&mut self.filter, 0).map_err(|e| fail(steps::SECCOMP, e))?;
}
}
let seccomp = prctl(libc::PR_GET_SECCOMP, 0).map_err(|e| fail(steps::VERIFY, e))?;
let nnp = prctl(libc::PR_GET_NO_NEW_PRIVS, 0).map_err(|e| fail(steps::VERIFY, e))?;
if seccomp != 2 || nnp != 1 {
return Err(fail(steps::VERIFY, libc::EPERM));
}
Ok(())
}
pub(crate) fn install(self, command: &mut std::process::Command) {
use std::os::unix::process::CommandExt;
let mut plan = self;
unsafe {
command.pre_exec(move || plan.run());
}
}
}
pub(crate) fn probe_userns(flags: c_int) -> Result<(), i32> {
let (uid, gid) = ids();
let tmp = identity(c"/tmp")?;
let uid_map = format!("{uid} {uid} 1\n").into_bytes();
let gid_map = format!("{gid} {gid} 1\n").into_bytes();
let mut fds = [0 as c_int; 2];
check_int(unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) })?;
let (read, write) = unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) };
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(errno());
}
if pid == 0 {
let result = (|| {
check_int(unsafe { libc::unshare(flags) })?;
write_file(c"/proc/self/setgroups", b"deny")?;
write_file(c"/proc/self/uid_map", &uid_map)?;
write_file(c"/proc/self/gid_map", &gid_map)?;
mount(None, c"/", None, libc::MS_REC | libc::MS_PRIVATE, None)?;
let target = open_planned(c"/tmp", tmp)?;
bind_planned(&target, &target, MOUNT_ATTR_RDONLY)?;
tmpfs_planned(&target, c"0700", 0)
})();
let code: i32 = result.err().unwrap_or(0);
unsafe {
libc::write(write.as_raw_fd(), (&raw const code).cast(), 4);
libc::_exit(0);
}
}
drop(write);
let mut code: i32 = libc::EIO;
unsafe {
libc::read(read.as_raw_fd(), (&raw mut code).cast(), 4);
let mut status = 0;
libc::waitpid(pid, &raw mut status, 0);
}
if code == 0 { Ok(()) } else { Err(code) }
}
pub(crate) fn kill(pid: libc::pid_t) {
unsafe { libc::kill(pid, libc::SIGKILL) };
}
pub(crate) fn leads() -> (bool, bool) {
unsafe {
let pid = libc::getpid();
(libc::getsid(0) == pid, libc::getpgrp() == pid)
}
}
pub(crate) fn killpg(pgid: libc::pid_t) {
unsafe { libc::killpg(pgid, libc::SIGKILL) };
}
pub(crate) fn alive(pid: libc::pid_t) -> bool {
unsafe { libc::kill(pid, 0) == 0 || errno() == libc::EPERM }
}
#[allow(clippy::useless_conversion, clippy::unnecessary_cast)]
pub(crate) fn getrlimit(resource: c_int) -> Option<(u64, u64)> {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
(unsafe { libc::getrlimit(resource as _, &raw mut limit) } == 0)
.then_some((limit.rlim_cur, limit.rlim_max))
}
pub(crate) fn fd_is_open(fd: c_int) -> bool {
unsafe { libc::fcntl(fd, libc::F_GETFD) != -1 }
}
pub(crate) fn clear_cloexec(fd: c_int) -> Result<(), i32> {
check_int(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) })
}
pub(crate) fn leadership() -> (bool, bool) {
unsafe {
let me = libc::getpid();
(libc::getsid(0) == me, libc::getpgrp() == me)
}
}
pub(crate) fn syscall_errno(nr: c_long, args: [c_long; 5]) -> i32 {
let rc = unsafe { libc::syscall(nr, args[0], args[1], args[2], args[3], args[4]) };
if rc < 0 { errno() } else { 0 }
}
pub(crate) fn spawn_sleeper(setsid: bool) -> std::io::Result<u32> {
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new("/bin/sleep");
cmd.arg("30");
if setsid {
unsafe {
cmd.pre_exec(|| {
libc::setsid();
Ok(())
});
}
}
let child = cmd.spawn()?;
Ok(child.id())
}
pub(crate) fn plant_before_spawn(
cmd: &mut std::process::Command,
planned: &std::path::Path,
aside: &std::path::Path,
replacement: &std::path::Path,
link: bool,
) -> std::io::Result<()> {
use std::os::unix::process::CommandExt;
let (from, aside, replacement) = (c_path(planned)?, c_path(aside)?, c_path(replacement)?);
unsafe {
cmd.pre_exec(move || {
let os = std::io::Error::from_raw_os_error;
check_int(libc::rename(from.as_ptr(), aside.as_ptr())).map_err(os)?;
check_int(if link {
libc::symlink(replacement.as_ptr(), from.as_ptr())
} else {
libc::rename(replacement.as_ptr(), from.as_ptr())
})
.map_err(os)?;
Ok(())
});
}
Ok(())
}
fn c_path(p: &std::path::Path) -> std::io::Result<CString> {
use std::os::unix::ffi::OsStrExt;
CString::new(p.as_os_str().as_bytes()).map_err(std::io::Error::other)
}
pub(crate) fn open_pty() -> std::io::Result<(OwnedFd, std::path::PathBuf)> {
use std::os::unix::ffi::OsStrExt;
let (mut controller, mut device) = (-1, -1);
let mut name = [0 as libc::c_char; 128];
let rc = unsafe {
libc::openpty(
&raw mut controller,
&raw mut device,
name.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
let controller = unsafe {
drop(OwnedFd::from_raw_fd(device));
OwnedFd::from_raw_fd(controller)
};
let name = unsafe { CStr::from_ptr(name.as_ptr()) };
Ok((
controller,
std::ffi::OsStr::from_bytes(name.to_bytes()).into(),
))
}
pub(crate) fn tty_ioctl_errno(fd: c_int, byte: Option<u8>) -> i32 {
let c = byte.unwrap_or(0) as libc::c_char;
#[allow(clippy::useless_conversion, clippy::unnecessary_cast)]
let rc = unsafe {
match byte {
None => libc::ioctl(fd, libc::TIOCSCTTY as _, 0),
Some(_) => libc::ioctl(fd, libc::TIOCSTI as _, &raw const c),
}
};
if rc == -1 { errno() } else { 0 }
}
pub(crate) fn udp_send_errno(fd: c_int, port: u16) -> i32 {
let addr = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: port.to_be(),
sin_addr: libc::in_addr {
s_addr: u32::from(std::net::Ipv4Addr::LOCALHOST).to_be(),
},
sin_zero: [0; 8],
};
let rc = unsafe {
libc::sendto(
fd,
b"x".as_ptr().cast(),
1,
0,
(&raw const addr).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
)
};
if rc == -1 { errno() } else { 0 }
}
pub(crate) fn net_errno(kind: &str, port: u16) -> i32 {
let v4 = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: port.to_be(),
sin_addr: libc::in_addr {
s_addr: u32::from(std::net::Ipv4Addr::LOCALHOST).to_be(),
},
sin_zero: [0; 8],
};
let mut v6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
v6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
v6.sin6_port = port.to_be();
v6.sin6_addr.s6_addr = std::net::Ipv6Addr::LOCALHOST.octets();
let (family, kind_, protocol) = match kind {
"fastopen" => (libc::AF_INET, libc::SOCK_STREAM, 0),
"mptcp" => (libc::AF_INET, libc::SOCK_STREAM, libc::IPPROTO_MPTCP),
"raw" => (libc::AF_INET, libc::SOCK_RAW, libc::IPPROTO_ICMP),
"tcp6" => (libc::AF_INET6, libc::SOCK_STREAM, 0),
_ => return libc::EINVAL,
};
let fd = unsafe { libc::socket(family, kind_ | libc::SOCK_CLOEXEC, protocol) };
if fd < 0 {
return errno();
}
let len = |size: usize| size as libc::socklen_t;
let rc = unsafe {
match kind {
"fastopen" => libc::sendto(
fd,
b"x".as_ptr().cast(),
1,
libc::MSG_FASTOPEN,
(&raw const v4).cast(),
len(std::mem::size_of::<libc::sockaddr_in>()),
) as c_int,
"raw" => libc::sendto(
fd,
[8u8, 0, 0xf7, 0xff, 0, 0, 0, 0].as_ptr().cast(),
8,
0,
(&raw const v4).cast(),
len(std::mem::size_of::<libc::sockaddr_in>()),
) as c_int,
"tcp6" => libc::connect(
fd,
(&raw const v6).cast(),
len(std::mem::size_of::<libc::sockaddr_in6>()),
),
_ => libc::connect(
fd,
(&raw const v4).cast(),
len(std::mem::size_of::<libc::sockaddr_in>()),
),
}
};
let result = if rc < 0 { errno() } else { 0 };
unsafe { libc::close(fd) };
result
}
pub(crate) fn install_outer_listener(mut filter: Vec<Insn>) -> Result<OwnedFd, i32> {
prctl(libc::PR_SET_NO_NEW_PRIVS, 1)?;
let fd = install_filter(&mut filter, libc::SECCOMP_FILTER_FLAG_NEW_LISTENER)?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
pub(crate) fn socket_pair() -> std::io::Result<(OwnedFd, OwnedFd)> {
let mut fds = [0 as c_int; 2];
let rc = unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
0,
fds.as_mut_ptr(),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
}
pub(crate) enum HandOver {
Listener(OwnedFd),
Refused(i32),
}
pub(crate) fn receive(socket: &OwnedFd) -> Result<HandOver, i32> {
let mut bytes = [0u8; 5];
let mut iov = libc::iovec {
iov_base: bytes.as_mut_ptr().cast(),
iov_len: bytes.len(),
};
let space = unsafe { libc::CMSG_SPACE(std::mem::size_of::<c_int>() as u32) } as usize;
let mut control = vec![0u8; space];
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &raw mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.as_mut_ptr().cast();
msg.msg_controllen = control.len() as _;
let n = unsafe {
libc::recvmsg(
socket.as_raw_fd(),
&raw mut msg,
libc::MSG_DONTWAIT | libc::MSG_CMSG_CLOEXEC,
)
};
if n < 1 {
return Err(if n < 0 { errno() } else { libc::EPIPE });
}
let header = unsafe { libc::CMSG_FIRSTHDR(&raw const msg) };
let fd = if header.is_null() {
None
} else {
let h = unsafe { &*header };
(h.cmsg_level == libc::SOL_SOCKET && h.cmsg_type == libc::SCM_RIGHTS).then(|| {
unsafe { std::ptr::read_unaligned(libc::CMSG_DATA(header).cast::<c_int>()) }
})
};
let fd = fd.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
match (bytes[0], fd) {
(1, Some(fd)) => Ok(HandOver::Listener(fd)),
(0, None) if n == 5 => Ok(HandOver::Refused(i32::from_le_bytes([
bytes[1], bytes[2], bytes[3], bytes[4],
]))),
_ => Err(libc::EPROTO),
}
}
pub(crate) enum Wait {
Ready,
HungUp,
Idle,
}
pub(crate) fn wait_listener(listener: &OwnedFd, timeout_ms: c_int) -> Result<Wait, i32> {
let mut pfd = libc::pollfd {
fd: listener.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&raw mut pfd, 1, timeout_ms) };
if rc < 0 {
let e = errno();
return if e == libc::EINTR {
Ok(Wait::Idle)
} else {
Err(e)
};
}
if rc == 0 {
return Ok(Wait::Idle);
}
if pfd.revents & libc::POLLIN != 0 {
Ok(Wait::Ready)
} else {
Ok(Wait::HungUp)
}
}
pub(crate) struct Notification {
pub(crate) id: u64,
pub(crate) pid: u32,
pub(crate) nr: u32,
pub(crate) args: [u64; 6],
}
pub(crate) fn receive_notification(listener: &OwnedFd) -> Result<Notification, i32> {
let mut n: libc::seccomp_notif = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::ioctl(
listener.as_raw_fd(),
libc::SECCOMP_IOCTL_NOTIF_RECV,
&raw mut n,
)
};
if rc != 0 {
return Err(errno());
}
Ok(Notification {
id: n.id,
pid: n.pid,
nr: n.data.nr as u32,
args: n.data.args,
})
}
pub(crate) fn answer_eperm(listener: &OwnedFd, id: u64) -> Result<(), i32> {
let mut resp = libc::seccomp_notif_resp {
id,
val: 0,
error: -libc::EPERM,
flags: 0,
};
let rc = unsafe {
libc::ioctl(
listener.as_raw_fd(),
libc::SECCOMP_IOCTL_NOTIF_SEND,
&raw mut resp,
)
};
if rc != 0 { Err(errno()) } else { Ok(()) }
}
pub(crate) fn notification_valid(listener: &OwnedFd, id: u64) -> bool {
unsafe {
libc::ioctl(
listener.as_raw_fd(),
libc::SECCOMP_IOCTL_NOTIF_ID_VALID,
&raw const id,
) == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insn_has_the_layout_of_sock_filter() {
assert_eq!(
std::mem::size_of::<Insn>(),
std::mem::size_of::<libc::sock_filter>()
);
assert_eq!(
std::mem::align_of::<Insn>(),
std::mem::align_of::<libc::sock_filter>()
);
}
#[test]
fn step_codes_round_trip() {
assert_eq!(decode_step(step_code(steps::VERIFY, 13)), Some((12, 13)));
assert_eq!(decode_step(13), None);
}
}