From ae60437d45795d14258d74d2f66eba6274a771e0 Mon Sep 17 00:00:00 2001 From: slhmy Date: Thu, 24 Oct 2024 22:57:06 +0800 Subject: [PATCH] Prepare runguard --- .github/workflows/rust-check.yml | 7 +- Cargo.toml | 4 +- runguard/.gitignore | 1 + runguard/Cargo.toml | 13 ++ runguard/README.md | 5 + runguard/build.rs | 3 + runguard/src/cgroup.rs | 129 ++++++++++++++++++ runguard/src/cli.rs | 104 +++++++++++++++ runguard/src/context.rs | 220 +++++++++++++++++++++++++++++++ runguard/src/main.rs | 14 ++ runguard/src/safe_libc.rs | 36 +++++ runguard/src/types.rs | 51 +++++++ runguard/src/utils.rs | 1 + 13 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 runguard/.gitignore create mode 100644 runguard/Cargo.toml create mode 100644 runguard/README.md create mode 100644 runguard/build.rs create mode 100644 runguard/src/cgroup.rs create mode 100644 runguard/src/cli.rs create mode 100644 runguard/src/context.rs create mode 100644 runguard/src/main.rs create mode 100644 runguard/src/safe_libc.rs create mode 100644 runguard/src/types.rs create mode 100644 runguard/src/utils.rs diff --git a/.github/workflows/rust-check.yml b/.github/workflows/rust-check.yml index 01cb3a2..80dc111 100644 --- a/.github/workflows/rust-check.yml +++ b/.github/workflows/rust-check.yml @@ -18,9 +18,12 @@ jobs: - name: Check format run: cargo fmt --all -- --check - name: Run clippy - run: cargo clippy --all-targets --all-features -- -D warnings + # run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy --all-targets --all-features - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y libseccomp-dev protobuf-compiler + run: | + sudo apt-get update + sudo apt-get install -y libseccomp-dev protobuf-compiler libcgroup-dev - name: Check ENV run: echo $(rustup --version && g++ -v) - name: Build test dist 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/.gitignore b/runguard/.gitignore new file mode 100644 index 0000000..5b64890 --- /dev/null +++ b/runguard/.gitignore @@ -0,0 +1 @@ +metafile.txt \ No newline at end of file diff --git a/runguard/Cargo.toml b/runguard/Cargo.toml new file mode 100644 index 0000000..a05b022 --- /dev/null +++ b/runguard/Cargo.toml @@ -0,0 +1,13 @@ +[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"] } +chrono = "0.4" +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..65efa36 --- /dev/null +++ b/runguard/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-link-lib=cgroup"); +} diff --git a/runguard/src/cgroup.rs b/runguard/src/cgroup.rs new file mode 100644 index 0000000..8379d8d --- /dev/null +++ b/runguard/src/cgroup.rs @@ -0,0 +1,129 @@ +use std::ffi::CString; +use std::fs::File; +use std::io::{self, BufRead}; +use std::os::raw::c_char; + +use crate::context::Context; + +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 { + ctx: Context, + cgroup: *mut libc::c_void, +} + +impl CGroup { + fn new(mut ctx: Context, 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() { + ctx.error(0, format_args!("cgroup_new_cgroup")); + } else { + ctx.verbose(format_args!("cgroup_new_cgroup: {}", name)); + } + CGroup { ctx, 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 ctx = Context::new(); + let _ = CGroup::new(ctx, "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..8f6f1e1 --- /dev/null +++ b/runguard/src/cli.rs @@ -0,0 +1,104 @@ +use std::path; + +use clap::Parser; + +use crate::types::SoftHardTime; + +#[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)] + pub root: String, + + // /// run COMMAND as user with username or ID USER + // #[arg(short, long)] + // pub user: String, + + // /// run COMMAND under group with name or ID GROUP + // #[arg(short, long)] + // pub group: String, + + // /// change to directory DIR after setting root directory + // #[arg(short = 'd', long, value_name = "DIR")] + // pub chdir: String, + + // For `TIME` values, the format is `soft:hard`. + /// kill COMMAND after TIME wallclock seconds + #[arg(short = 't', long, value_name = "TIME")] + pub walltime: SoftHardTime, + + /// set maximum CPU time to TIME seconds + #[arg(short = 'C', long, value_name = "TIME")] + pub cputime: SoftHardTime, + // /// set total memory limit to SIZE kB + // #[arg(short = 'm', long, value_name = "SIZE")] + // pub memsize: u64, + + // /// set maximum created filesize to SIZE kB + // #[arg(short = 'f', long, value_name = "SIZE")] + // pub filesize: u64, + + // /// set maximum no. processes to N + // #[arg(short = 'p', long, value_name = "N")] + // pub nproc: u64, + + // /// use only processor number ID (or set, e.g. \"0,2-3\") + // #[arg(short = 'P', long, value_name = "ID")] + // pub cpuset: String, + + // /// disable core dumps + // #[arg(short = 'c', long)] + // pub no_core: bool, + + // /// redirect COMMAND stdout output to FILE + // #[arg(short = 'o', long, value_name = "FILE")] + // pub stdout: path::PathBuf, + + // /// redirect COMMAND stderr output to FILE + // #[arg(short = 'e', long, value_name = "FILE")] + // pub stderr: path::PathBuf, + + // /// truncate COMMAND stdout/stderr streams at SIZE kB + // #[arg(short, long, value_name = "SIZE")] + // pub streamsize: u64, + + // /// preserve environment variables (default only PATH) + // #[arg(short = 'E', long)] + // pub environment: String, + + // /// write metadata (runtime, exitcode, etc.) to FILE + // #[arg(short = 'M', long, value_name = "FILE")] + // pub metadata: path::PathBuf, + + // /// process ID of runpipe to send SIGUSR1 signal when + // /// timelimit is reached + // #[arg(short = 'U', long, value_name = "PID")] + // pub runpipepid: u32, + + // /// display some extra warnings and information + // #[arg(short, long)] + // pub verbose: bool, + + // /// suppress all warnings and verbose output + // #[arg(short, long)] + // pub quiet: bool, + + // /// output version information and exit + // #[arg(long)] + // pub version: bool, + + // #[arg(required = true)] + // pub command: Vec, +} diff --git a/runguard/src/context.rs b/runguard/src/context.rs new file mode 100644 index 0000000..b19cb8f --- /dev/null +++ b/runguard/src/context.rs @@ -0,0 +1,220 @@ +use std::{fmt::Arguments, fs::File, io::Write, thread::sleep}; + +use chrono::{format, DateTime, Local}; +use libc::{tms, _SC_CLK_TCK}; +use nix::sys::signal::{ + sigprocmask, SigSet, + SigmaskHow::SIG_BLOCK, + Signal::{SIGALRM, SIGTERM}, +}; + +use crate::{ + cgroup::{cgroup_strerror_safe, CGroupError}, + safe_libc::{fclose, strerror, sysconf}, + PROGNAME, +}; + +pub struct Context { + use_walltime: bool, + + progstarttime: DateTime, + endtime: DateTime, + starttime: DateTime, + + startticks: tms, + endticks: tms, + + received_signal: i32, // default -1 + + outputmeta: bool, + metafile: Option, + metafilename: String, + + errno: i32, + in_error_handling: bool, + + be_quiet: bool, + be_verbose: bool, +} + +impl Context { + pub fn new() -> Self { + Self { + use_walltime: false, + progstarttime: chrono::Local::now(), + endtime: chrono::Local::now(), + starttime: chrono::Local::now(), + startticks: unsafe { + let mut ticks = std::mem::zeroed(); + libc::times(&mut ticks); + ticks + }, + endticks: unsafe { + let mut ticks = std::mem::zeroed(); + libc::times(&mut ticks); + ticks + }, + received_signal: -1, + outputmeta: true, + metafile: Some(File::create("metafile.txt").unwrap()), + metafilename: "metafile.txt".to_string(), + errno: 0, + in_error_handling: false, + be_quiet: false, + be_verbose: true, + } + } + + pub fn warning(&self, format: Arguments) { + if !self.be_quiet { + eprintln!("{}: warning: {}", PROGNAME, format); + } + } + + pub fn verbose(&self, format: Arguments) { + if !self.be_quiet && self.be_verbose { + let currtime = chrono::Local::now(); + let runtime = + (currtime - self.progstarttime).num_microseconds().unwrap() as f64 / 1_000_000.0; + eprintln!( + "{} [{} @ {:10.6}]: verbose: {}", + PROGNAME, + std::process::id(), + runtime, + format + ); + } + } + + pub fn error(&mut self, mut 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); + errnum = self.errno; + } + 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 + ); + } + + pub 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")); + } + } + + pub fn output_exit_time(&mut self, exitcode: i32, cpudiff: f64) { + self.verbose(format_args!("command exited with exitcode {}", exitcode)); + self.write_meta("exitcode", format_args!("{}", exitcode)); + + if self.received_signal != -1 { + let received_signal = self.received_signal; + self.write_meta("signal", format_args!("{}", received_signal)); + } + + let walldiff = + (self.endtime - self.starttime).num_microseconds().unwrap() as f64 / 1_000_000.0; + + let ticks_per_second = sysconf(_SC_CLK_TCK); + let userdiff = (self.endticks.tms_cutime as f64 - self.startticks.tms_cutime as f64) + / ticks_per_second as f64; + let systemdiff = (self.endticks.tms_cstime as f64 - self.startticks.tms_cstime as f64) + / ticks_per_second as f64; + + self.write_meta("wall-time", format_args!("{:.3}", walldiff)); + self.write_meta("user-time", format_args!("{:.3}", userdiff)); + self.write_meta("sys-time", format_args!("{:.3}", systemdiff)); + self.write_meta("cpu-time", format_args!("{:.3}", cpudiff)); + + self.verbose(format_args!( + "runtime is {:.3} seconds real, {:.3} seconds user, {:.3} seconds sys", + walldiff, userdiff, systemdiff + )); + } +} + +#[test] +fn test_context() { + let mut ctx = Context { + use_walltime: false, + progstarttime: chrono::Local::now(), + endtime: chrono::Local::now(), + starttime: chrono::Local::now(), + startticks: unsafe { + let mut ticks = std::mem::zeroed(); + libc::times(&mut ticks); + ticks + }, + endticks: unsafe { + let mut ticks = std::mem::zeroed(); + libc::times(&mut ticks); + ticks + }, + received_signal: -1, + outputmeta: true, + metafile: Some(File::create("metafile.txt").unwrap()), + metafilename: "metafile.txt".to_string(), + errno: 0, + in_error_handling: false, + be_quiet: false, + be_verbose: true, + }; + + ctx.error(0, format_args!("test error")); + ctx.write_meta("test", format_args!("test meta")); + ctx.warning(format_args!("test warning")); + ctx.verbose(format_args!("test verbose")); + sleep(std::time::Duration::from_secs(1)); + ctx.verbose(format_args!("test verbose")); +} diff --git a/runguard/src/main.rs b/runguard/src/main.rs new file mode 100644 index 0000000..a476641 --- /dev/null +++ b/runguard/src/main.rs @@ -0,0 +1,14 @@ +use clap::Parser; + +mod cgroup; +mod cli; +mod context; +mod safe_libc; +mod types; +mod utils; + +const PROGNAME: &str = "runguard"; + +fn main() { + let cli = cli::Cli::parse(); +} diff --git a/runguard/src/safe_libc.rs b/runguard/src/safe_libc.rs new file mode 100644 index 0000000..89394f5 --- /dev/null +++ b/runguard/src/safe_libc.rs @@ -0,0 +1,36 @@ +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) } +} + +pub fn sysconf(name: i32) -> i64 { + unsafe { libc::sysconf(name) } +} + +#[test] +fn test_strerror() { + println!("{}", strerror(libc::EINVAL)); +} + +#[test] +fn test_fclose() { + let file = File::open("/dev/null").unwrap(); + assert_eq!(fclose(file), 0); +} + +#[test] +fn test_sysconf() { + let ticks_per_second = sysconf(libc::_SC_CLK_TCK); + println!("{}", ticks_per_second); + assert!(ticks_per_second > 0); +} diff --git a/runguard/src/types.rs b/runguard/src/types.rs new file mode 100644 index 0000000..85a1abe --- /dev/null +++ b/runguard/src/types.rs @@ -0,0 +1,51 @@ +use std::fmt; +use std::str::FromStr; + +#[derive(Debug, Clone, Copy)] +pub struct SoftHardTime(f64, f64); + +#[derive(Debug)] +pub struct ParseSoftHardTimeError { + details: String, +} + +impl ParseSoftHardTimeError { + fn new(msg: &str) -> ParseSoftHardTimeError { + ParseSoftHardTimeError { + details: msg.to_string(), + } + } +} + +impl fmt::Display for ParseSoftHardTimeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.details) + } +} + +impl std::error::Error for ParseSoftHardTimeError { + fn description(&self) -> &str { + &self.details + } +} + +impl FromStr for SoftHardTime { + type Err = ParseSoftHardTimeError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + + let soft = parts[0] + .parse::() + .map_err(|_| ParseSoftHardTimeError::new("Failed to parse soft time"))?; + let hard = if parts.len() > 1 { + parts[1] + .parse::() + .map_err(|_| ParseSoftHardTimeError::new("Failed to parse hard time"))? + } else { + soft + }; + + Ok(SoftHardTime(soft, hard)) + } +} diff --git a/runguard/src/utils.rs b/runguard/src/utils.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/runguard/src/utils.rs @@ -0,0 +1 @@ +