1use std::env;
2use std::ffi::c_int;
3use std::fs::{self, File};
4use std::io;
5use std::os::fd::AsRawFd as _;
6use std::path::{Path, PathBuf};
7use std::time::{Duration, Instant};
8
9use super::{RawCommand, Result};
10use libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall};
11use nix::libc::{self, rusage, wait4, WEXITSTATUS, WSTOPPED, WTERMSIG};
12use nix::sys::resource::{setrlimit, Resource};
13use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
14use nix::unistd::{alarm, dup2, execvp, fork, ForkResult};
15
16extern "C" fn signal_handler(_: nix::libc::c_int) {}
17
18#[derive(Debug, Clone)]
19pub struct RlimitConfig {
20 pub resource: Resource,
21 pub soft_limit: u64,
22 pub hard_limit: u64,
23}
24
25fn apply_rlimit_configs(configs: &[RlimitConfig]) -> Result<()> {
26 for config in configs {
27 setrlimit(config.resource, config.soft_limit, config.hard_limit)?;
28 }
29
30 Ok(())
31}
32
33#[derive(Debug)]
34#[allow(unused)]
35pub struct RawRunResultInfo {
36 pub exit_status: c_int,
37 pub exit_signal: c_int,
38 pub exit_code: c_int,
39 pub real_time_cost: Duration,
40 pub resource_usage: Rusage,
41}
42
43#[derive(Debug)]
44#[allow(unused)]
45pub struct Rusage {
46 pub user_time: Duration,
47 pub system_time: Duration,
48 pub max_rss: i64,
49 pub page_faults: i64,
50 pub involuntary_context_switches: i64,
51 pub voluntary_context_switches: i64,
52}
53
54impl From<rusage> for Rusage {
55 fn from(rusage: rusage) -> Self {
56 Self {
57 user_time: Duration::new(
58 rusage.ru_utime.tv_sec as u64,
59 rusage.ru_utime.tv_usec as u32 * 1000,
60 ),
61 system_time: Duration::new(
62 rusage.ru_stime.tv_sec as u64,
63 rusage.ru_stime.tv_usec as u32 * 1000,
64 ),
65 max_rss: rusage.ru_maxrss,
66 page_faults: rusage.ru_majflt,
67 involuntary_context_switches: rusage.ru_nivcsw,
68 voluntary_context_switches: rusage.ru_nvcsw,
69 }
70 }
71}
72
73fn get_default_rusage() -> rusage {
74 rusage {
75 ru_utime: libc::timeval {
76 tv_sec: 0,
77 tv_usec: 0,
78 },
79 ru_stime: libc::timeval {
80 tv_sec: 0,
81 tv_usec: 0,
82 },
83 ru_maxrss: 0,
84 ru_ixrss: 0,
85 ru_idrss: 0,
86 ru_isrss: 0,
87 ru_minflt: 0,
88 ru_majflt: 0,
89 ru_nswap: 0,
90 ru_inblock: 0,
91 ru_oublock: 0,
92 ru_msgsnd: 0,
93 ru_msgrcv: 0,
94 ru_nsignals: 0,
95 ru_nvcsw: 0,
96 ru_nivcsw: 0,
97 }
98}
99
100pub struct Sandbox {
101 scmp_filter: ScmpFilterContext,
102 rlimit_configs: &'static [RlimitConfig],
103 time_out: u32,
105 project_path: PathBuf,
106 command: RawCommand,
107 input_redirect: File,
108 output_redirect: File,
109 error_redirect: File,
110 child_pid: i32,
111 begin_time: Instant,
112}
113
114impl Sandbox {
115 pub fn new(
116 scmp_black_list: &'static [&'static str],
117 rlimit_configs: &'static [RlimitConfig],
118 time_out: u32,
119 project_path: PathBuf,
120 command: RawCommand,
121 input_path: &Path,
122 output_path: &Path,
123 error_path: &Path,
124 ) -> Result<Self> {
125 let mut scmp_filter = ScmpFilterContext::new_filter(ScmpAction::Allow)?;
126 for s in scmp_black_list {
127 let syscall = ScmpSyscall::from_name(s)?;
128 scmp_filter.add_rule_exact(ScmpAction::KillProcess, syscall)?;
129 }
130 let input_redirect = fs::OpenOptions::new().read(true).open(input_path)?;
131 let output_redirect = fs::OpenOptions::new().write(true).open(output_path)?;
132 let error_redirect = fs::OpenOptions::new().write(true).open(error_path)?;
133
134 let child_pid = -1;
135 let begin_time = Instant::now();
136
137 Ok(Self {
138 scmp_filter,
139 rlimit_configs,
140 time_out,
141 project_path,
142 command,
143 input_redirect,
144 output_redirect,
145 error_redirect,
146 child_pid,
147 begin_time,
148 })
149 }
150
151 fn load_io(&self) -> Result<()> {
153 let stdin_raw_fd = io::stdin().as_raw_fd();
154 dup2(self.input_redirect.as_raw_fd(), stdin_raw_fd)?;
155
156 let stdout_raw_fd = io::stdout().as_raw_fd();
157 dup2(self.output_redirect.as_raw_fd(), stdout_raw_fd)?;
158
159 let stderr_raw_fd = io::stderr().as_raw_fd();
160 dup2(self.error_redirect.as_raw_fd(), stderr_raw_fd)?;
161
162 Ok(())
163 }
164
165 pub fn wait(&self) -> Result<RawRunResultInfo> {
166 let mut status: c_int = 0;
167 let mut usage: rusage = get_default_rusage();
168 unsafe {
169 wait4(self.child_pid, &mut status, WSTOPPED, &mut usage);
170 }
171
172 Ok(RawRunResultInfo {
173 exit_status: status,
174 exit_signal: WTERMSIG(status),
175 exit_code: WEXITSTATUS(status),
176 real_time_cost: self.begin_time.elapsed(),
177 resource_usage: Rusage::from(usage),
178 })
179 }
180
181 pub fn spawn(&mut self) -> Result<i32> {
185 let now = Instant::now();
186 unsafe {
187 sigaction(
188 Signal::SIGALRM,
189 &SigAction::new(
190 SigHandler::Handler(signal_handler),
191 SaFlags::empty(),
192 SigSet::empty(),
193 ),
194 )
195 .unwrap();
196 }
197 match unsafe { fork() } {
198 Ok(ForkResult::Parent { child, .. }) => {
199 self.child_pid = child.as_raw();
200 self.begin_time = now;
201 Ok(child.as_raw())
202 }
203 Ok(ForkResult::Child) => {
205 if env::set_current_dir(&self.project_path).is_err() {
206 eprintln!("Failed to load change to project directory");
207 unsafe { libc::_exit(100) };
208 }
209
210 if self.load_io().is_err() {
211 eprintln!("Failed to load I/O");
212 unsafe { libc::_exit(1) };
213 }
214 if apply_rlimit_configs(&self.rlimit_configs).is_err() {
215 eprintln!("Failed to load rlimit configs");
216 unsafe { libc::_exit(1) };
217 }
218 if self.scmp_filter.load().is_err() {
219 eprintln!("Failed to load seccomp filter");
220 unsafe { libc::_exit(1) };
221 }
222
223 alarm::set(self.time_out);
224
225 let RawCommand { binary, args } = self.command;
226
227 if let Err(error) = execvp(binary, args) {
228 eprintln!("{}", error);
229 }
230 unsafe { libc::_exit(0) };
231 }
232 Err(e) => Err(e.into()),
233 }
234 }
235}