Skip to content

Commit

Permalink
Prepare runguard
Browse files Browse the repository at this point in the history
  • Loading branch information
slhmy committed Oct 26, 2024
1 parent 262f0c8 commit d5d1677
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[workspace]
members = ["judge-core", "judger"]
resolver = "2"
members = ["judge-core", "judger", "runguard"]
resolver = "2"
12 changes: 12 additions & 0 deletions runguard/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions runguard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# runguard

A Rust version of
[Domjudge runguard](https://github.com/DOMjudge/domjudge/blob/main/judge/runguard.cc)
written in C++.
3 changes: 3 additions & 0 deletions runguard/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-link-lib=cgroup");
}
125 changes: 125 additions & 0 deletions runguard/src/cgroup.rs
Original file line number Diff line number Diff line change
@@ -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)
);
}
102 changes: 102 additions & 0 deletions runguard/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::path;

use clap::Parser;

#[derive(Parser)]
#[command(
override_usage = "runguard [OPTION]... <COMMAND>...",
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<String>,
}
105 changes: 105 additions & 0 deletions runguard/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<File>,
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"));
}
12 changes: 12 additions & 0 deletions runguard/src/main.rs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit d5d1677

Please sign in to comment.