diff --git a/Cargo.toml b/Cargo.toml index b3ccdcf..d2a4af7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["judge-core", "judger"] -resolver = "2" \ No newline at end of file +members = ["judge-core", "judger", "runguard"] +resolver = "2" diff --git a/runguard/Cargo.toml b/runguard/Cargo.toml new file mode 100644 index 0000000..9a94cf5 --- /dev/null +++ b/runguard/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "runguard" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[dependencies] +libc = "0.2" +nix = { version = "0.29", features = ["signal"] } + +clap = { version = "4", features = ["derive"] } +humantime = "2" diff --git a/runguard/README.md b/runguard/README.md new file mode 100644 index 0000000..03d6527 --- /dev/null +++ b/runguard/README.md @@ -0,0 +1,5 @@ +# runguard + +A Rust version of +[Domjudge runguard](https://github.com/DOMjudge/domjudge/blob/main/judge/runguard.cc) +written in C++. diff --git a/runguard/build.rs b/runguard/build.rs new file mode 100644 index 0000000..2368e89 --- /dev/null +++ b/runguard/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-link-lib=cgroup"); +} \ No newline at end of file diff --git a/runguard/src/cgroup.rs b/runguard/src/cgroup.rs new file mode 100644 index 0000000..8046a1e --- /dev/null +++ b/runguard/src/cgroup.rs @@ -0,0 +1,125 @@ +use std::ffi::CString; +use std::fs::File; +use std::io::{self, BufRead}; +use std::os::raw::c_char; + +extern "C" { + fn cgroup_new_cgroup(name: *const c_char) -> *mut libc::c_void; + fn cgroup_strerror(err: i32) -> *const c_char; +} + +pub enum CGroupError { + ECGROUPNOTCOMPILED = 50000, + ECGROUPNOTMOUNTED, + ECGROUPNOTEXIST, + ECGROUPNOTCREATED, + ECGROUPSUBSYSNOTMOUNTED, + ECGROUPNOTOWNER, + /** Controllers bound to different mount points */ + ECGROUPMULTIMOUNTED, + /* This is the stock error. Default error. @todo really? */ + ECGROUPNOTALLOWED, + ECGMAXVALUESEXCEEDED, + ECGCONTROLLEREXISTS, + ECGVALUEEXISTS, + ECGINVAL, + ECGCONTROLLERCREATEFAILED, + ECGFAIL, + ECGROUPNOTINITIALIZED, + ECGROUPVALUENOTEXIST, + /** + * Represents error coming from other libraries like glibc. @c libcgroup + * users need to check cgroup_get_last_errno() upon encountering this + * error. + */ + ECGOTHER, + ECGROUPNOTEQUAL, + ECGCONTROLLERNOTEQUAL, + /** Failed to parse rules configuration file. */ + ECGROUPPARSEFAIL, + /** Rules list does not exist. */ + ECGROUPNORULES, + ECGMOUNTFAIL, + /** + * Not an real error, it just indicates that iterator has come to end + * of sequence and no more items are left. + */ + ECGEOF = 50023, + /** Failed to parse config file (cgconfig.conf). */ + ECGCONFIGPARSEFAIL, + ECGNAMESPACEPATHS, + ECGNAMESPACECONTROLLER, + ECGMOUNTNAMESPACE, + ECGROUPUNSUPP, + ECGCANTSETVALUE, + /** Removing of a group failed because it was not empty. */ + ECGNONEMPTY, +} + +struct CGroup { + cgroup: *mut libc::c_void, +} + +impl CGroup { + fn new(name: &str) -> Self { + let cgroup_name = CString::new(name).expect("CString::new failed"); + unsafe { + let cgroup = cgroup_new_cgroup(cgroup_name.as_ptr()); + if cgroup.is_null() { + eprintln!("Failed to create new cgroup"); + } else { + println!("Successfully created new cgroup {}", name); + } + CGroup { cgroup } + } + } +} + +fn cgroup_is_v2() -> bool { + let file = match File::open("/proc/mounts") { + Ok(file) => file, + Err(_) => { + eprintln!("Error opening /proc/mounts"); + return false; + } + }; + + let reader = io::BufReader::new(file); + for line in reader.lines() { + if let Ok(line) = line { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 && parts[1] == "/sys/fs/cgroup" && parts[2] == "cgroup2" { + return true; + } + } + } + + false +} + +pub fn cgroup_strerror_safe(err: i32) -> String { + unsafe { + let errstr = cgroup_strerror(err); + let errstr = std::ffi::CStr::from_ptr(errstr).to_str().unwrap(); + errstr.to_string() + } +} + +#[test] +fn test_cgroup() { + let _ = CGroup::new("my_cgroup"); + + if cgroup_is_v2() { + println!("cgroup v2 is enabled"); + } else { + println!("cgroup v2 is not enabled"); + } +} + +#[test] +fn test_cgroup_strerror() { + println!( + "{}", + cgroup_strerror_safe(CGroupError::ECGROUPNOTCOMPILED as i32) + ); +} diff --git a/runguard/src/cli.rs b/runguard/src/cli.rs new file mode 100644 index 0000000..8d51de4 --- /dev/null +++ b/runguard/src/cli.rs @@ -0,0 +1,102 @@ +use std::path; + +use clap::Parser; + +#[derive(Parser)] +#[command( + override_usage = "runguard [OPTION]... ...", + about = "Run COMMAND with specified options.", + after_help = "Note that root privileges are needed for the `root' and `user' options. \ +If `user' is set, then `group' defaults to the same to prevent security issues, \ +since otherwise the process would retain group root permissions. \ +The COMMAND path is relative to the changed ROOT directory if specified. \ +TIME may be specified as a float; two floats separated by `:' are treated as soft and hard limits. \ +The runtime written to file is that of the last of wall/cpu time options set, \ +and defaults to CPU time when neither is set. \ +When run setuid without the `user' option, the user ID is set to the real user ID." +)] +pub struct Cli { + /// run COMMAND with root directory set to ROOT + #[arg(short, long)] + root: String, + + /// run COMMAND as user with username or ID USER + #[arg(short, long)] + user: String, + + /// run COMMAND under group with name or ID GROUP + #[arg(short, long)] + group: String, + + /// change to directory DIR after setting root directory + #[arg(short = 'd', long, value_name = "DIR")] + chdir: String, + + /// kill COMMAND after TIME wallclock seconds + #[arg(short = 't', long, value_name = "TIME")] + walltime: humantime::Duration, + + /// set maximum CPU time to TIME seconds + #[arg(short = 'C', long, value_name = "TIME")] + cputime: humantime::Duration, + + /// set total memory limit to SIZE kB + #[arg(short = 'm', long, value_name = "SIZE")] + memsize: u64, + + /// set maximum created filesize to SIZE kB + #[arg(short = 'f', long, value_name = "SIZE")] + filesize: u64, + + /// set maximum no. processes to N + #[arg(short = 'p', long, value_name = "N")] + nproc: u64, + + /// use only processor number ID (or set, e.g. \"0,2-3\") + #[arg(short = 'P', long, value_name = "ID")] + cpuset: String, + + /// disable core dumps + #[arg(short = 'c', long)] + no_core: bool, + + /// redirect COMMAND stdout output to FILE + #[arg(short = 'o', long, value_name = "FILE")] + stdout: path::PathBuf, + + /// redirect COMMAND stderr output to FILE + #[arg(short = 'e', long, value_name = "FILE")] + stderr: path::PathBuf, + + /// truncate COMMAND stdout/stderr streams at SIZE kB + #[arg(short, long, value_name = "SIZE")] + streamsize: u64, + + /// preserve environment variables (default only PATH) + #[arg(short = 'E', long)] + environment: String, + + /// write metadata (runtime, exitcode, etc.) to FILE + #[arg(short = 'M', long, value_name = "FILE")] + metadata: path::PathBuf, + + /// process ID of runpipe to send SIGUSR1 signal when + /// timelimit is reached + #[arg(short = 'U', long, value_name = "PID")] + runpipepid: u32, + + /// display some extra warnings and information + #[arg(short, long)] + verbose: bool, + + /// suppress all warnings and verbose output + #[arg(short, long)] + quiet: bool, + + /// output version information and exit + #[arg(long)] + version: bool, + + #[arg(required = true)] + command: Vec, +} diff --git a/runguard/src/context.rs b/runguard/src/context.rs new file mode 100644 index 0000000..27a8d0e --- /dev/null +++ b/runguard/src/context.rs @@ -0,0 +1,105 @@ +use std::{fmt::Arguments, fs::File, io::Write}; + +use nix::sys::signal::{ + sigprocmask, SigSet, + SigmaskHow::SIG_BLOCK, + Signal::{SIGALRM, SIGTERM}, +}; + +use crate::{ + cgroup::{cgroup_strerror_safe, CGroupError}, + safe_libc::{fclose, strerror}, + PROGNAME, +}; + +struct Context { + outputmeta: bool, + metafile: Option, + metafilename: String, + + errno: i32, + in_error_handling: bool, +} + +impl Context { + fn error(&mut self, errnum: i32, format: Arguments) { + // Silently ignore errors that happen while handling other errors. + if self.in_error_handling { + return; + } + self.in_error_handling = true; + + /* + * Make sure the signal handler for these (terminate()) does not + * interfere, we are exiting now anyway. + */ + let mut sigs: SigSet = SigSet::empty(); + sigs.add(SIGALRM); + sigs.add(SIGTERM); + let _ = sigprocmask(SIG_BLOCK, Some(&sigs), None); + + /* First print to string to be able to reuse the message. */ + let mut errstr: String = PROGNAME.to_string(); + if !format.to_string().is_empty() { + errstr = format!("{}: {}", errstr, strerror(errnum)); + } + if errnum != 0 { + /* Special case libcgroup error codes. */ + if errnum == CGroupError::ECGOTHER as i32 { + errstr = format!("{}: libcgroup", errstr); + } + if errnum == CGroupError::ECGROUPNOTCOMPILED as i32 { + errstr = format!("{}: {}", errstr, cgroup_strerror_safe(errnum)); + } else { + errstr = format!("{}: {}", errstr, strerror(errnum)); + } + } + if format.to_string().is_empty() && errnum == 0 { + errstr = format!("{}: unknown error", errstr); + } + + self.write_meta("internal-error", format_args!("{}", errstr)); + if self.outputmeta && self.metafile.is_some() { + if let Some(file_ref) = &self.metafile { + if fclose(file_ref.try_clone().unwrap()) != 0 { + eprintln!("\nError closing metafile '{}'.\n", self.metafilename); + } + } + } + + eprintln!( + "{}\nTry `{} --help' for more information.", + errstr, PROGNAME + ); + } + + fn write_meta(&mut self, key: &str, format: Arguments) { + if !self.outputmeta { + return; + } + + if let Some(file) = self.metafile.as_mut() { + if writeln!(file, "{}: {}", key, format).is_err() { + self.outputmeta = false; + self.error(0, format_args!("cannot write to file: {}", "metafile.txt")); + } + } else { + self.outputmeta = false; + self.error(0, format_args!("cannot write to file: {}", "metafile.txt")); + } + } +} + +#[test] +fn test_context() { + let mut ctx = Context { + outputmeta: true, + metafile: Some(File::create("metafile.txt").unwrap()), + metafilename: "metafile.txt".to_string(), + errno: 0, + in_error_handling: false, + }; + + ctx.error(0, format_args!("test error")); + ctx.write_meta("test", format_args!("test meta")); +} \ No newline at end of file diff --git a/runguard/src/main.rs b/runguard/src/main.rs new file mode 100644 index 0000000..0768637 --- /dev/null +++ b/runguard/src/main.rs @@ -0,0 +1,12 @@ +use clap::Parser; + +mod cgroup; +mod cli; +mod context; +mod safe_libc; + +const PROGNAME: &str = "runguard"; + +fn main() { + let _ = cli::Cli::parse(); +} diff --git a/runguard/src/safe_libc.rs b/runguard/src/safe_libc.rs new file mode 100644 index 0000000..61493a3 --- /dev/null +++ b/runguard/src/safe_libc.rs @@ -0,0 +1,25 @@ +use std::{fs::File, os::fd::IntoRawFd}; + +pub fn strerror(errnum: i32) -> String { + unsafe { + let errstr = libc::strerror(errnum); + let errstr = std::ffi::CStr::from_ptr(errstr).to_str().unwrap(); + errstr.to_string() + } +} + +pub fn fclose(file: File) -> i32 { + let fd = file.into_raw_fd(); + unsafe { libc::close(fd) } +} + +#[test] +fn test_strerror() { + println!("{}", strerror(libc::EINVAL)); +} + +#[test] +fn test_fclose() { + let file = File::open("/dev/null").unwrap(); + assert_eq!(fclose(file), 0); +} \ No newline at end of file