mod child;
mod classes;
mod probe;
mod seccomp;
mod spawn;
mod sys;
mod violations;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use procage_policy::linux::RuleTarget;
use procage_policy::{
HardDenyClass, LinuxPlan, LowerError, LowerOptions, Plan, Policy, Relax, Relaxed, Target, Tier,
Tty, lift, lower,
};
pub use crate::unix::TMPDIR_GRANT;
pub use crate::violation::{Coverage, DeniedBy, Violation, Violations};
pub use child::Child;
pub use probe::{Probe, probe};
use crate::unix::TmpdirGuard;
pub const BACKEND_MAX_ABI: u8 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Step {
Session,
Lifetime,
Namespaces,
IdMap,
Mounts,
Capabilities,
Rlimit,
Fds,
NoNewPrivs,
Landlock,
Seccomp,
Verify,
Listener,
}
impl Step {
fn from_code(code: u32) -> Option<Step> {
use sys::steps::*;
Some(match code {
SESSION => Step::Session,
LIFETIME => Step::Lifetime,
NAMESPACES => Step::Namespaces,
ID_MAP => Step::IdMap,
MOUNTS => Step::Mounts,
CAPABILITIES => Step::Capabilities,
RLIMIT => Step::Rlimit,
FDS => Step::Fds,
NO_NEW_PRIVS => Step::NoNewPrivs,
LANDLOCK => Step::Landlock,
SECCOMP => Step::Seccomp,
VERIFY => Step::Verify,
LISTENER => Step::Listener,
_ => return None,
})
}
}
impl fmt::Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Step::Session => "new session",
Step::Lifetime => "parent-death signal",
Step::Namespaces => "create namespaces",
Step::IdMap => "write id maps",
Step::Mounts => "mounts",
Step::Capabilities => "drop capabilities",
Step::Rlimit => "resource limits",
Step::Fds => "close inherited descriptors",
Step::NoNewPrivs => "no_new_privs",
Step::Landlock => "apply Landlock",
Step::Seccomp => "install seccomp filter",
Step::Verify => "verify sandbox",
Step::Listener => "hand over the seccomp listener",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unsupported {
Landlock {
abi: u8,
needed: u8,
},
Tier {
requested: Tier,
userns_errno: Option<i32>,
},
Syscall {
name: String,
},
Setting {
setting: &'static str,
},
ExecThreads {
threads: usize,
},
EnvClear,
}
impl fmt::Display for Unsupported {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unsupported::Landlock { abi: 0, needed } => {
write!(f, "Landlock is not available (ABI {needed} needed)")
}
Unsupported::Landlock { abi, needed } => {
write!(f, "Landlock ABI {abi} is older than the {needed} needed")
}
Unsupported::Tier {
requested,
userns_errno,
} => {
write!(f, "tier {requested} is not available on this host")?;
match userns_errno {
Some(e) => write!(
f,
" (user namespaces: {})",
io::Error::from_raw_os_error(*e)
),
None => Ok(()),
}
}
Unsupported::Syscall { name } => {
write!(f, "syscall `{name}` has no number on this architecture")
}
Unsupported::ExecThreads { threads } => write!(
f,
"Cage::exec on tier T1 needs a single-threaded process (this one has {threads} \
threads, which user namespaces refuse): call it before starting threads, or \
choose tier T2 with CageOptions::with_tier"
),
Unsupported::EnvClear => write!(
f,
"EnvPolicy::Passthrough needs to see whether Command::env_clear was called, \
which this Rust standard library does not show; use EnvPolicy::Allowlist"
),
Unsupported::Setting { setting } => {
write!(f, "{setting} is not supported in this release")
}
}
}
}
#[derive(Debug)]
pub enum GrantError {
Undeclared {
name: String,
},
Missing {
name: String,
path: PathBuf,
error: io::Error,
},
Reserved {
name: String,
},
TmpdirWithExec,
Covers {
name: String,
hidden: PathBuf,
},
CoversClass {
name: String,
class: HardDenyClass,
},
}
impl fmt::Display for GrantError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GrantError::Undeclared { name } => {
write!(f, "the policy declares no grant named {name:?}")
}
GrantError::Missing { name, path, error } => {
write!(
f,
"grant {name}: cannot resolve {}: {error}",
path.display()
)
}
GrantError::Covers { name, hidden } => write!(
f,
"grant {name} gives write access around {}, which tier T2 cannot keep hidden \
inside a writable tree: bind the grant elsewhere, or use tier T1",
hidden.display()
),
GrantError::TmpdirWithExec => write!(
f,
"the tmpdir grant needs a handle that removes the directory, and Cage::exec \
returns none: use Cage::spawn, or bind a directory of your own to another grant"
),
GrantError::Reserved { name } => {
write!(
f,
"grant {name} is bound by procage itself (a private temporary directory)"
)
}
GrantError::CoversClass { name, class } => write!(
f,
"grant {name} reaches paths of the hard-deny class {class}, which tier T2 cannot deny inside it"
),
}
}
}
#[derive(Debug)]
pub enum SpawnError {
Unsupported(Unsupported),
Lower(LowerError),
Path {
path: String,
error: io::Error,
},
Grant(GrantError),
Fd {
fd: i32,
},
Tmpdir(io::Error),
Walk {
root: PathBuf,
limit: usize,
},
Apply {
step: Step,
errno: i32,
},
Io(io::Error),
}
impl fmt::Display for SpawnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpawnError::Unsupported(u) => write!(f, "unsupported: {u}"),
SpawnError::Lower(e) => write!(f, "{e}"),
SpawnError::Path { path, error } => write!(f, "cannot use {path}: {error}"),
SpawnError::Grant(e) => write!(f, "{e}"),
SpawnError::Fd { fd } => write!(f, "descriptor {fd} handed to the spawn is not open"),
SpawnError::Tmpdir(e) => {
write!(f, "cannot create the private temporary directory: {e}")
}
SpawnError::Walk { root, limit } => write!(
f,
"{} has more than {limit} entries to search for hard-deny paths",
root.display()
),
SpawnError::Apply { step, errno } => write!(
f,
"the child failed at step `{step}`: {}; the target did not run",
io::Error::from_raw_os_error(*errno)
),
SpawnError::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SpawnError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
SpawnError::Lower(e) => Some(e),
SpawnError::Path { error, .. } | SpawnError::Io(error) | SpawnError::Tmpdir(error) => {
Some(error)
}
SpawnError::Grant(GrantError::Missing { error, .. }) => Some(error),
_ => None,
}
}
}
impl From<LowerError> for SpawnError {
fn from(e: LowerError) -> Self {
SpawnError::Lower(e)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CageOptions {
pub lower: LowerOptions,
pub tier: Option<Tier>,
pub walk_limit: usize,
pub homes: Vec<PathBuf>,
pub landlock_abi: Option<u8>,
pub violations: bool,
}
impl Default for CageOptions {
fn default() -> Self {
Self {
lower: LowerOptions::default(),
tier: None,
walk_limit: 200_000,
homes: Vec::new(),
landlock_abi: None,
violations: false,
}
}
}
impl CageOptions {
pub fn with_lower(mut self, lower: LowerOptions) -> Self {
self.lower = lower;
self
}
pub fn with_tier(mut self, tier: Tier) -> Self {
self.tier = Some(tier);
self
}
pub fn with_walk_limit(mut self, limit: usize) -> Self {
self.walk_limit = limit;
self
}
pub fn with_home(mut self, home: impl AsRef<Path>) -> Self {
self.homes.push(home.as_ref().to_path_buf());
self
}
pub fn with_landlock_abi(mut self, abi: u8) -> Self {
self.landlock_abi = Some(abi);
self
}
pub fn with_violations(mut self) -> Self {
self.violations = true;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Grants {
bindings: Vec<(String, PathBuf)>,
}
impl Grants {
pub fn new() -> Self {
Self::default()
}
pub fn bind(mut self, name: &str, path: impl AsRef<Path>) -> Self {
self.bindings
.push((name.to_string(), path.as_ref().to_path_buf()));
self
}
}
#[derive(Debug, Clone)]
pub struct Cage {
plan: LinuxPlan,
filter: Arc<[seccomp::Insn]>,
homes: Vec<PathBuf>,
walk_limit: usize,
notifying: Option<Arc<[seccomp::Insn]>>,
}
impl Cage {
pub fn new(policy: &Policy) -> Result<Cage, SpawnError> {
Cage::with_options(policy, &CageOptions::default())
}
pub fn fit(
policy: &Policy,
options: &CageOptions,
relax: &Relax,
) -> Result<(Cage, Vec<Relaxed>), SpawnError> {
let mut fitted = policy.clone();
let mut relaxed = Vec::new();
for _ in 0..4 {
let gaps = match Cage::with_options(&fitted, options) {
Ok(cage) => return Ok((cage, relaxed)),
Err(SpawnError::Lower(LowerError::Gaps(gaps))) => gaps,
Err(e) => return Err(e),
};
let done = relax
.apply(&mut fitted, &gaps)
.map_err(|rest| SpawnError::Lower(LowerError::Gaps(rest)))?;
relaxed.extend(done.into_iter().map(|(gap, relaxation)| Relaxed {
lift: lift(policy, &gap, &options.lower, BACKEND_MAX_ABI),
gap,
relaxation,
}));
}
Cage::with_options(&fitted, options).map(|cage| (cage, relaxed))
}
pub fn with_options(policy: &Policy, options: &CageOptions) -> Result<Cage, SpawnError> {
if policy.tty == Tty::NewPty {
return Err(SpawnError::Unsupported(Unsupported::Setting {
setting: "Tty::NewPty",
}));
}
let host = probe();
let tier = match options.tier {
Some(Tier::T1) if host.tier != Some(Tier::T1) => {
return Err(SpawnError::Unsupported(Unsupported::Tier {
requested: Tier::T1,
userns_errno: host.userns_errno,
}));
}
Some(tier) => tier,
None => host.tier.unwrap_or(Tier::T2),
};
let abi = host
.landlock_abi
.min(BACKEND_MAX_ABI)
.min(options.landlock_abi.unwrap_or(u8::MAX));
if abi < tier.min_landlock_abi() {
return Err(SpawnError::Unsupported(Unsupported::Landlock {
abi,
needed: tier.min_landlock_abi(),
}));
}
let target = Target::Linux {
tier,
landlock_abi: abi,
};
let Plan::Linux(mut plan) = lower(policy, target, &options.lower)? else {
unreachable!("lowering for Linux yields a Linux plan");
};
let build = |notify| {
seccomp::build(&plan.seccomp, &seccomp::NATIVE, notify)
.map_err(|name| SpawnError::Unsupported(Unsupported::Syscall { name }))
};
let filter: Arc<[seccomp::Insn]> = build(false)?.into();
let notifying = if options.violations {
Some(Arc::from(build(true)?))
} else {
None
};
let homes = classes::homes(&options.homes);
let rules = spawn::rule_paths(&plan);
classes::home_gaps(&mut plan, &rules, &homes, &options.lower, target)?;
Ok(Cage {
plan,
filter,
homes,
walk_limit: options.walk_limit,
notifying,
})
}
pub fn plan(&self) -> &LinuxPlan {
&self.plan
}
pub fn tier(&self) -> Tier {
self.plan.tier
}
pub fn landlock_abi(&self) -> u8 {
self.plan.landlock_abi
}
fn declared(&self) -> BTreeSet<&str> {
self.plan
.landlock
.rules
.iter()
.filter_map(|r| match &r.target {
RuleTarget::Grant(name) => Some(name.as_str()),
_ => None,
})
.collect()
}
pub fn spawn(&self, command: &mut Command, grants: &Grants) -> Result<Child, SpawnError> {
self.spawn_with_fds(command, grants, &[])
}
pub fn spawn_with_fds(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> Result<Child, SpawnError> {
let session = if self.plan.hygiene.tty == Tty::Inherit {
sys::Session::Group
} else {
sys::Session::New
};
let (parent_end, notify) = match &self.notifying {
Some(filter) => {
let (parent_end, child_end) = sys::socket_pair().map_err(SpawnError::Io)?;
(
Some(parent_end),
Some(sys::Notify::new(filter.to_vec(), child_end)),
)
}
None => (None, None),
};
let (dropped, removal) = self.prepare(command, grants, fds, session, notify)?;
let inner = command.spawn().map_err(spawn::decode)?;
let reporting = match parent_end {
None => child::Reporting::Off,
Some(socket) => self.supervise(&socket),
};
Ok(Child::new(inner, dropped, removal.disarm(), reporting))
}
fn supervise(&self, socket: &std::os::fd::OwnedFd) -> child::Reporting {
let unavailable = |reason: String| child::Reporting::Unavailable(reason);
match sys::receive(socket) {
Ok(sys::HandOver::Listener(listener)) => {
let record = Arc::new(std::sync::Mutex::new(crate::violation::Record::new(
Coverage::SystemCalls,
)));
let rules: Arc<[_]> = self.plan.seccomp.clone().into();
match violations::start(listener, rules, record.clone()) {
Ok(done) => child::Reporting::On { record, done },
Err(e) => unavailable(format!("cannot start the supervisor: {e}")),
}
}
Ok(sys::HandOver::Refused(errno)) => unavailable(format!(
"the kernel refused a seccomp listener ({}); refused calls still fail",
std::io::Error::from_raw_os_error(errno)
)),
Err(errno) => unavailable(format!(
"no listener arrived from the child ({})",
std::io::Error::from_raw_os_error(errno)
)),
}
}
pub fn exec(&self, command: &mut Command, grants: &Grants) -> SpawnError {
self.exec_with_fds(command, grants, &[])
}
pub fn exec_with_fds(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> SpawnError {
use std::os::unix::process::CommandExt;
if self.declared().contains(TMPDIR_GRANT) {
return SpawnError::Grant(GrantError::TmpdirWithExec);
}
let threads = sys::thread_count();
if self.plan.namespaces.is_some() && threads > 1 {
return SpawnError::Unsupported(Unsupported::ExecThreads { threads });
}
match self.prepare(command, grants, fds, sys::Session::Keep, None) {
Ok(_) => spawn::decode(command.exec()),
Err(e) => e,
}
}
fn prepare(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
session: sys::Session,
notify: Option<sys::Notify>,
) -> Result<(Vec<String>, TmpdirGuard), SpawnError> {
let keep = kept_fds(fds.iter().map(AsRawFd::as_raw_fd))?;
if grants.bindings.iter().any(|(name, _)| name == TMPDIR_GRANT) {
return Err(SpawnError::Grant(GrantError::Reserved {
name: TMPDIR_GRANT.into(),
}));
}
let declared = self.declared();
let tmpdir = if declared.contains(TMPDIR_GRANT) {
Some(crate::unix::private_tmpdir().map_err(SpawnError::Tmpdir)?)
} else {
None
};
let removal = TmpdirGuard(tmpdir.clone());
let mut grants = grants.clone();
if let Some(dir) = &tmpdir {
grants = grants.bind(TMPDIR_GRANT, dir);
if !command.get_envs().any(|(k, _)| k == "TMPDIR") {
command.env("TMPDIR", dir);
}
}
let bound = self.bind(&grants, &declared)?;
let ruleset = spawn::ruleset(
&self.plan,
&bound,
&self.hidden_piecewise(),
self.walk_limit,
)?;
let isolate = match self.plan.namespaces {
Some(ns) => {
let writable = spawn::writable(&self.plan, &bound)?;
let mut covers = classes::t1_covers(
&self.plan.classes,
&self.homes,
&writable,
self.walk_limit,
)?;
if self.plan.mask_unix_sockets {
covers.extend(self.socket_masks(&covers)?);
}
let (uid, gid) = sys::ids();
let net = if ns.net { libc::CLONE_NEWNET } else { 0 };
Some(sys::Isolate {
flags: probe::T1_NAMESPACES | net,
uid_map: spawn::id_map(uid),
gid_map: spawn::id_map(gid),
mounts: spawn::mounts(&self.plan, &covers)?,
})
}
None => None,
};
let dropped = crate::unix::environment(command, &self.plan.hygiene.env)
.map_err(|_| SpawnError::Unsupported(Unsupported::EnvClear))?;
let parent = if session == sys::Session::Keep {
crate::unix::find_program(command).map_err(SpawnError::Io)?;
sys::parent_pid()
} else {
sys::pid()
};
sys::ChildPlan {
session,
parent,
isolate,
limits: crate::unix::limits(&self.plan.hygiene.limits),
keep,
ruleset,
filter: self.filter.to_vec(),
notify,
}
.install(command);
Ok((dropped, removal))
}
fn hidden_piecewise(&self) -> Vec<PathBuf> {
use std::os::unix::ffi::OsStrExt;
if self.plan.namespaces.is_some() {
return Vec::new();
}
let to_path =
|p: &procage_policy::AbsPath| PathBuf::from(std::ffi::OsStr::from_bytes(p.as_bytes()));
let mut out: Vec<PathBuf> = self.plan.hide_by_siblings.iter().map(to_path).collect();
for class in self.plan.classes.iter().filter(|c| c.hide) {
for home in &self.homes {
out.extend(class.home.iter().map(|rel| home.join(rel)));
}
out.extend(class.system.iter().map(to_path));
}
out.retain(|p| std::fs::symlink_metadata(p).is_ok());
out.sort();
out.dedup();
out
}
fn socket_masks(
&self,
covers: &[(PathBuf, classes::Cover)],
) -> Result<Vec<(PathBuf, classes::Cover)>, SpawnError> {
use std::os::unix::ffi::OsStrExt;
let to_path =
|p: &procage_policy::AbsPath| PathBuf::from(std::ffi::OsStr::from_bytes(p.as_bytes()));
let allowed: Vec<PathBuf> = self
.plan
.unix_socket_paths
.iter()
.map(|p| std::fs::canonicalize(to_path(p)).unwrap_or_else(|_| to_path(p)))
.collect();
let mut masked: Vec<PathBuf> = covers
.iter()
.filter(|(_, c)| matches!(c, classes::Cover::Hide))
.map(|(p, _)| p.clone())
.collect();
masked.extend(self.plan.mounts.iter().filter_map(|m| match m {
procage_policy::linux::Mount::Mask(p) => Some(to_path(p)),
_ => None,
}));
Ok(spawn::foreign_sockets(&allowed, &masked)?
.into_iter()
.map(|p| (p, classes::Cover::Hide))
.collect())
}
fn bind(
&self,
grants: &Grants,
declared: &BTreeSet<&str>,
) -> Result<BTreeMap<String, PathBuf>, SpawnError> {
let mut out = BTreeMap::new();
for (name, path) in &grants.bindings {
if !declared.contains(name.as_str()) {
return Err(SpawnError::Grant(GrantError::Undeclared {
name: name.clone(),
}));
}
let resolved = std::fs::canonicalize(path).map_err(|error| {
SpawnError::Grant(GrantError::Missing {
name: name.clone(),
path: path.clone(),
error,
})
})?;
if self.plan.tier == Tier::T2 {
let writes = self.plan.landlock.rules.iter().any(|r| {
matches!(&r.target, RuleTarget::Grant(g) if g.as_str() == name)
&& r.access
.intersect(procage_policy::linux::AccessFs::WRITE_FILE)
!= procage_policy::linux::AccessFs::NONE
});
if let Some(class) =
classes::grant_reaches(&self.plan.classes, &self.homes, &resolved, writes)
{
return Err(SpawnError::Grant(GrantError::CoversClass {
name: name.clone(),
class,
}));
}
}
out.insert(name.clone(), resolved);
}
Ok(out)
}
}
fn kept_fds(fds: impl IntoIterator<Item = i32>) -> Result<Vec<i32>, SpawnError> {
let mut keep: Vec<i32> = fds.into_iter().filter(|&fd| fd > 2).collect();
keep.sort_unstable();
keep.dedup();
match keep.iter().find(|&&fd| !sys::fd_is_open(fd)) {
Some(&fd) => Err(SpawnError::Fd { fd }),
None => Ok(keep),
}
}
#[doc(hidden)]
pub mod testing {
use super::sys;
pub fn is_session_leader() -> bool {
sys::leadership().0
}
pub fn is_group_leader() -> bool {
sys::leadership().1
}
pub fn clear_cloexec(fd: i32) -> bool {
sys::clear_cloexec(fd).is_ok()
}
pub fn fd_is_open(fd: i32) -> bool {
sys::fd_is_open(fd)
}
pub fn alive(pid: u32) -> bool {
let zombie = std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|s| {
s.rfind(')')
.and_then(|i| s[i + 1..].split_whitespace().next())
== Some("Z")
});
!zombie && libc::pid_t::try_from(pid).is_ok_and(sys::alive)
}
pub fn spawn_sleeper(setsid: bool) -> std::io::Result<u32> {
sys::spawn_sleeper(setsid)
}
pub fn nofile_and_core() -> Option<(u64, u64)> {
Some((
sys::getrlimit(libc::RLIMIT_NOFILE as libc::c_int)?.0,
sys::getrlimit(libc::RLIMIT_CORE as libc::c_int)?.0,
))
}
pub fn capabilities() -> (u64, u64, u64) {
sys::capabilities()
}
pub fn syscall_errno(name: &str) -> Option<i32> {
let none = [0; 5];
let (nr, args) = match name {
"ptrace" => (
libc::SYS_ptrace,
[7, libc::c_long::from(sys::pid()), 0, 0, 0],
),
"io_uring_setup" => (libc::SYS_io_uring_setup, none),
"bpf" => (libc::SYS_bpf, [-1, 0, 0, 0, 0]),
"keyctl" => (libc::SYS_keyctl, [9999, 0, 0, 0, 0]),
"unshare" => (libc::SYS_unshare, none),
"setsid" => (libc::SYS_setsid, none),
"setpgid" => (libc::SYS_setpgid, none),
"setns" => (libc::SYS_setns, [-1, 0, 0, 0, 0]),
"perf_event_open" => (libc::SYS_perf_event_open, [0, 0, -1, -1, 0]),
"userfaultfd" => (libc::SYS_userfaultfd, [-1, 0, 0, 0, 0]),
"socket_netlink" => (
libc::SYS_socket,
[libc::AF_NETLINK.into(), libc::SOCK_RAW.into(), 0, 0, 0],
),
"socket_vsock" => (
libc::SYS_socket,
[libc::AF_VSOCK.into(), libc::SOCK_STREAM.into(), 0, 0, 0],
),
"unshare_user" => (libc::SYS_unshare, [libc::CLONE_NEWUSER.into(), 0, 0, 0, 0]),
"clone3" => (libc::SYS_clone3, none),
"process_vm_readv" => (
libc::SYS_process_vm_readv,
[libc::c_long::from(sys::parent_pid()), 0, 0, 0, 0],
),
"open_tree" => (
libc::SYS_open_tree,
[
libc::AT_FDCWD.into(),
c"/".as_ptr() as libc::c_long,
0,
0,
0,
],
),
_ => return None,
};
Some(sys::syscall_errno(nr, args))
}
pub fn kill_errno(pid: u32, signal: i32) -> i32 {
sys::syscall_errno(
libc::SYS_kill,
[libc::c_long::from(pid), libc::c_long::from(signal), 0, 0, 0],
)
}
pub fn truncate_errno(route: &str, path: &std::path::Path) -> Option<i32> {
use std::os::unix::ffi::OsStrExt;
let path = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
let p = path.as_ptr() as libc::c_long;
let at = libc::c_long::from(libc::AT_FDCWD);
let trunc = libc::c_long::from(libc::O_TRUNC);
let how: [u64; 3] = [(libc::O_RDONLY | libc::O_TRUNC) as u64, 0, 0];
let (nr, args) = match route {
"truncate" => (libc::SYS_truncate, [p, 0, 0, 0, 0]),
"rdonly" => (libc::SYS_openat, [at, p, trunc, 0, 0]),
"noaccess" => (libc::SYS_openat, [at, p, 3 | trunc, 0, 0]),
"openat2" => (
libc::SYS_openat2,
[at, p, how.as_ptr() as libc::c_long, 24, 0],
),
#[cfg(target_arch = "x86_64")]
"open" => (libc::SYS_open, [p, trunc, 0, 0, 0]),
_ => return None,
};
Some(sys::syscall_errno(nr, args))
}
pub fn open_pty() -> std::io::Result<(std::os::fd::OwnedFd, std::path::PathBuf)> {
sys::open_pty()
}
pub fn make_controlling(fd: i32) -> i32 {
sys::tty_ioctl_errno(fd, None)
}
pub fn tiocsti_errno(fd: i32, byte: u8) -> i32 {
sys::tty_ioctl_errno(fd, Some(byte))
}
pub fn umount(path: &str) -> i32 {
let Ok(c) = std::ffi::CString::new(path) else {
return libc::EINVAL;
};
sys::syscall_errno(libc::SYS_umount2, [c.as_ptr() as libc::c_long, 0, 0, 0, 0])
}
pub fn remount_rw(path: &str) -> i32 {
let Ok(c) = std::ffi::CString::new(path) else {
return libc::EINVAL;
};
sys::syscall_errno(
libc::SYS_mount,
[
0,
c.as_ptr() as libc::c_long,
0,
(libc::MS_REMOUNT | libc::MS_BIND) as libc::c_long,
0,
],
)
}
pub fn install_outer_listener() -> std::io::Result<std::os::fd::OwnedFd> {
use procage_policy::linux::{Action, Condition, SeccompRule, Syscall};
let rule = SeccompRule {
syscall: Syscall::Kcmp,
condition: Condition::Always,
action: Action::Eperm,
reason: None,
};
let filter = super::seccomp::build(&[rule], &super::seccomp::NATIVE, true)
.map_err(std::io::Error::other)?;
sys::install_outer_listener(filter).map_err(std::io::Error::from_raw_os_error)
}
}