use std::env;
use std::ffi::c_int;
use std::fs::{self, File};
use std::io;
use std::os::fd::AsRawFd as _;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::{RawCommand, Result};
use libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::libc::{self, rusage, wait4, WEXITSTATUS, WSTOPPED, WTERMSIG};
use nix::sys::resource::{setrlimit, Resource};
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
use nix::unistd::{alarm, dup2, execvp, fork, ForkResult};
extern "C" fn signal_handler(_: nix::libc::c_int) {}
#[derive(Debug, Clone)]
pub struct RlimitConfig {
pub resource: Resource,
pub soft_limit: u64,
pub hard_limit: u64,
}
fn apply_rlimit_configs(configs: &[RlimitConfig]) -> Result<()> {
for config in configs {
setrlimit(config.resource, config.soft_limit, config.hard_limit)?;
}
Ok(())
}
#[derive(Debug)]
#[allow(unused)]
pub struct RawRunResultInfo {
pub exit_status: c_int,
pub exit_signal: c_int,
pub exit_code: c_int,
pub real_time_cost: Duration,
pub resource_usage: Rusage,
}
#[derive(Debug)]
#[allow(unused)]
pub struct Rusage {
pub user_time: Duration,
pub system_time: Duration,
pub max_rss: i64,
pub page_faults: i64,
pub involuntary_context_switches: i64,
pub voluntary_context_switches: i64,
}
impl From<rusage> for Rusage {
fn from(rusage: rusage) -> Self {
Self {
user_time: Duration::new(
rusage.ru_utime.tv_sec as u64,
rusage.ru_utime.tv_usec as u32 * 1000,
),
system_time: Duration::new(
rusage.ru_stime.tv_sec as u64,
rusage.ru_stime.tv_usec as u32 * 1000,
),
max_rss: rusage.ru_maxrss,
page_faults: rusage.ru_majflt,
involuntary_context_switches: rusage.ru_nivcsw,
voluntary_context_switches: rusage.ru_nvcsw,
}
}
}
fn get_default_rusage() -> rusage {
rusage {
ru_utime: libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
ru_stime: libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
ru_maxrss: 0,
ru_ixrss: 0,
ru_idrss: 0,
ru_isrss: 0,
ru_minflt: 0,
ru_majflt: 0,
ru_nswap: 0,
ru_inblock: 0,
ru_oublock: 0,
ru_msgsnd: 0,
ru_msgrcv: 0,
ru_nsignals: 0,
ru_nvcsw: 0,
ru_nivcsw: 0,
}
}
pub struct Sandbox {
scmp_filter: ScmpFilterContext,
rlimit_configs: &'static [RlimitConfig],
time_out: u32,
project_path: PathBuf,
command: RawCommand,
input_redirect: File,
output_redirect: File,
error_redirect: File,
child_pid: i32,
begin_time: Instant,
}
impl Sandbox {
pub fn new(
scmp_black_list: &'static [&'static str],
rlimit_configs: &'static [RlimitConfig],
time_out: u32,
project_path: PathBuf,
command: RawCommand,
input_path: &Path,
output_path: &Path,
error_path: &Path,
) -> Result<Self> {
let mut scmp_filter = ScmpFilterContext::new_filter(ScmpAction::Allow)?;
for s in scmp_black_list {
let syscall = ScmpSyscall::from_name(s)?;
scmp_filter.add_rule_exact(ScmpAction::KillProcess, syscall)?;
}
let input_redirect = fs::OpenOptions::new().read(true).open(input_path)?;
let output_redirect = fs::OpenOptions::new().write(true).open(output_path)?;
let error_redirect = fs::OpenOptions::new().write(true).open(error_path)?;
let child_pid = -1;
let begin_time = Instant::now();
Ok(Self {
scmp_filter,
rlimit_configs,
time_out,
project_path,
command,
input_redirect,
output_redirect,
error_redirect,
child_pid,
begin_time,
})
}
fn load_io(&self) -> Result<()> {
let stdin_raw_fd = io::stdin().as_raw_fd();
dup2(self.input_redirect.as_raw_fd(), stdin_raw_fd)?;
let stdout_raw_fd = io::stdout().as_raw_fd();
dup2(self.output_redirect.as_raw_fd(), stdout_raw_fd)?;
let stderr_raw_fd = io::stderr().as_raw_fd();
dup2(self.error_redirect.as_raw_fd(), stderr_raw_fd)?;
Ok(())
}
pub fn wait(&self) -> Result<RawRunResultInfo> {
let mut status: c_int = 0;
let mut usage: rusage = get_default_rusage();
unsafe {
wait4(self.child_pid, &mut status, WSTOPPED, &mut usage);
}
Ok(RawRunResultInfo {
exit_status: status,
exit_signal: WTERMSIG(status),
exit_code: WEXITSTATUS(status),
real_time_cost: self.begin_time.elapsed(),
resource_usage: Rusage::from(usage),
})
}
pub fn spawn(&mut self) -> Result<i32> {
let now = Instant::now();
unsafe {
sigaction(
Signal::SIGALRM,
&SigAction::new(
SigHandler::Handler(signal_handler),
SaFlags::empty(),
SigSet::empty(),
),
)
.unwrap();
}
match unsafe { fork() } {
Ok(ForkResult::Parent { child, .. }) => {
self.child_pid = child.as_raw();
self.begin_time = now;
Ok(child.as_raw())
}
Ok(ForkResult::Child) => {
if env::set_current_dir(&self.project_path).is_err() {
eprintln!("Failed to load change to project directory");
unsafe { libc::_exit(100) };
}
if self.load_io().is_err() {
eprintln!("Failed to load I/O");
unsafe { libc::_exit(1) };
}
if apply_rlimit_configs(&self.rlimit_configs).is_err() {
eprintln!("Failed to load rlimit configs");
unsafe { libc::_exit(1) };
}
if self.scmp_filter.load().is_err() {
eprintln!("Failed to load seccomp filter");
unsafe { libc::_exit(1) };
}
alarm::set(self.time_out);
let RawCommand { binary, args } = self.command;
if let Err(error) = execvp(binary, args) {
eprintln!("{}", error);
}
unsafe { libc::_exit(0) };
}
Err(e) => Err(e.into()),
}
}
}