judge_framework/backend/
sandbox.rs

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 in second
104    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    /// Currently close all `stderr` and close `stdin`/`stdout` if redirect is not set
152    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    /// WARNING:   
182    /// Unsafe to use `println!()` (or `unwrap()`) in child process.
183    /// See more in `fork()` document.
184    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            // child process should not return to do things outside `spawn()`
204            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}