From aef664b736fd9636cef94d58f19ade088aebd388 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 | 14 +++ runguard/README.md | 5 + runguard/build.rs | 3 + runguard/src/cgroup.rs | 129 +++++++++++++++++++ runguard/src/cli.rs | 103 ++++++++++++++++ runguard/src/context.rs | 206 +++++++++++++++++++++++++++++++ runguard/src/main.rs | 89 +++++++++++++ runguard/src/safe_libc.rs | 36 ++++++ runguard/src/types.rs | 51 ++++++++ runguard/src/utils.rs | 40 ++++++ scripts/env_setup.bash | 2 +- 14 files changed, 685 insertions(+), 5 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..31e07ec --- /dev/null +++ b/runguard/Cargo.toml @@ -0,0 +1,14 @@ +[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" +regex = "1" \ No newline at end of file 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..d5b6438 --- /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::default(); + 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..01aed8e --- /dev/null +++ b/runguard/src/cli.rs @@ -0,0 +1,103 @@ +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: Option, + + // /// 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..3bdd183 --- /dev/null +++ b/runguard/src/context.rs @@ -0,0 +1,206 @@ +use std::{fmt::Arguments, fs::File, io::Write, thread::sleep}; + +use chrono::{format, DateTime, Local}; +use libc::{tms, _SC_CLK_TCK}; +use nix::{ + errno::Errno, + 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 { + pub use_walltime: bool, + pub use_user: bool, + pub use_group: bool, + + pub progstarttime: DateTime, + pub endtime: DateTime, + pub starttime: DateTime, + + pub startticks: tms, + pub endticks: tms, + + pub received_signal: i32, // default -1 + + pub outputmeta: bool, + pub metafile: Option, + pub metafilename: String, + + pub in_error_handling: bool, + + pub be_quiet: bool, + pub be_verbose: bool, +} + +impl Default for Context { + fn default() -> Self { + Self { + use_walltime: false, + use_user: false, + use_group: 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(), + in_error_handling: false, + be_quiet: false, + be_verbose: true, + } + } +} + +impl Context { + 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 = Errno::last_raw(); + } + 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 { + ..Default::default() + }; + + 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..8e04b8f --- /dev/null +++ b/runguard/src/main.rs @@ -0,0 +1,89 @@ +use clap::Parser; +use libc::{rlimit, setrlimit}; +use nix::errno::Errno; +use nix::unistd::getpid; + +mod cgroup; +mod cli; +mod context; +mod safe_libc; +mod types; +mod utils; + +const PROGNAME: &str = "runguard"; + +fn main() { + let userregex = regex::Regex::new(r"^[A-Za-z][A-Za-z0-9\\._-]*$").unwrap(); + + let mut ctx = context::Context::default(); + let cli = cli::Cli::parse(); + + if let Some(user) = cli.user { + ctx.use_user = true; + } + + ctx.verbose(format_args!("starting in verbose mode, PID = {}", getpid())); + + /* Make sure that we change from group root if we change to an + unprivileged user to prevent unintended permissions. */ +} + +fn setrestrictions(ctx: &mut context::Context) { + let mut lim = rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + macro_rules! setlim { + ($type:ident) => { + let resource = match stringify!($type) { + "AS" => libc::RLIMIT_AS, + "CPU" => libc::RLIMIT_CPU, + "DATA" => libc::RLIMIT_DATA, + "FSIZE" => libc::RLIMIT_FSIZE, + "NPROC" => libc::RLIMIT_NPROC, + "STACK" => libc::RLIMIT_STACK, + _ => unreachable!(), + }; + + if unsafe { setrlimit(resource, &lim) } != 0 { + if Errno::last() == Errno::EPERM { + ctx.warning(format_args!( + "no permission to set resource RLIMIT_{}", + stringify!($type) + )); + } else { + ctx.error( + Errno::last_raw(), + format_args!("setting resource RLIMIT_{}", stringify!($type)), + ); + } + } else { + ctx.verbose(format_args!( + "set RLIMIT_{} with cur = {}, max = {}", + stringify!($type), + lim.rlim_cur, + lim.rlim_max + )); + } + }; + } + + lim.rlim_cur = libc::RLIM_INFINITY; + lim.rlim_max = libc::RLIM_INFINITY; + setlim!(AS); + setlim!(DATA); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_setrestrictions() { + let mut ctx = context::Context::default(); + ctx.be_verbose = true; + ctx.verbose(format_args!("test_setrestrictions")); + setrestrictions(&mut ctx); + } +} 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..098dca8 --- /dev/null +++ b/runguard/src/utils.rs @@ -0,0 +1,40 @@ +use nix::errno::Errno; + +pub fn userid(name: &str) -> i32 { + Errno::set_raw(0); + + let passwd = unsafe { libc::getpwnam(name.as_ptr() as *const i8) }; + if passwd.is_null() || Errno::last_raw() != 0 { + return -1; + } + + unsafe { (*passwd).pw_uid as i32 } +} + +pub fn groupid(name: &str) -> i32 { + Errno::set_raw(0); + + let group = unsafe { libc::getgrnam(name.as_ptr() as *const i8) }; + if group.is_null() || Errno::last_raw() != 0 { + return -1; + } + + unsafe { (*group).gr_gid as i32 } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_userid() { + assert_eq!(userid("root"), 0); + assert_eq!(userid("nonexistent"), -1); + } + + #[test] + fn test_groupid() { + assert_eq!(groupid("root"), 0); + assert_eq!(groupid("nonexistent"), -1); + } +} diff --git a/scripts/env_setup.bash b/scripts/env_setup.bash index 41fcb7b..56c2126 100755 --- a/scripts/env_setup.bash +++ b/scripts/env_setup.bash @@ -8,7 +8,7 @@ if [ -x "$(command -v apt)" ]; then echo 'Updating apt...' sudo apt update fi - sudo apt install -y libseccomp-dev gcc curl pkg-config libssl-dev cmake gdb + sudo apt install -y libseccomp-dev gcc curl pkg-config libssl-dev cmake gdb libcgroup-dev fi if [ ! -d "scripts/thirdparty" ]; then