Skip to content

Commit

Permalink
refactor(rsjudge-runner): ♻️ refactor to use capctl crate
Browse files Browse the repository at this point in the history
  • Loading branch information
Jisu-Woniu committed Apr 16, 2024
1 parent 5435d24 commit cb3f2d1
Show file tree
Hide file tree
Showing 12 changed files with 63 additions and 58 deletions.
12 changes: 0 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ rsjudge-grpc = { version = "0.1.0", path = "crates/rsjudge-grpc", optional = tru
rsjudge-rest = { version = "0.1.0", path = "crates/rsjudge-rest", optional = true }

anyhow = "1.0.82"
caps = "0.5.5"
clap = { version = "4.5.4", features = ["derive"] }
env_logger = "0.11.3"
log.workspace = true
Expand Down
1 change: 0 additions & 1 deletion crates/rsjudge-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ description = "Command runner for rsjudge"

[dependencies]
capctl = "0.2.4"
caps = "0.5.5"
log.workspace = true
nix = { version = "0.28.0", features = ["user"] }
rsjudge-utils.workspace = true
Expand Down
6 changes: 3 additions & 3 deletions crates/rsjudge-runner/examples/demo.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0

use anyhow::anyhow;
use caps::Capability;
use capctl::Cap;
use rsjudge_runner::{
user::{builder, runner},
CapHandle, RunAs,
Expand All @@ -17,8 +17,8 @@ async fn main() -> anyhow::Result<()> {
.await?;
println!("{}", String::from_utf8_lossy(&self_output.stdout));

CapHandle::new(Capability::CAP_SETUID)?;
CapHandle::new(Capability::CAP_SETGID)?;
CapHandle::new(Cap::SETUID)?;
CapHandle::new(Cap::SETGID)?;

let builder_output = Command::new("id").run_as(builder()?)?.output().await?;
println!("{}", String::from_utf8_lossy(&builder_output.stdout));
Expand Down
7 changes: 2 additions & 5 deletions crates/rsjudge-runner/examples/exploit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::path::PathBuf;

use caps::{read, CapSet};
use capctl::FullCapState;
use rsjudge_runner::{user::builder, RunAs};
use rsjudge_utils::command::check_output;
use tokio::process::Command;
Expand All @@ -20,10 +20,7 @@ use tokio::process::Command;
/// ```
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dbg!(read(None, CapSet::Ambient).unwrap());
dbg!(read(None, CapSet::Effective).unwrap());
dbg!(read(None, CapSet::Inheritable).unwrap());
dbg!(read(None, CapSet::Permitted).unwrap());
dbg!(FullCapState::get_current().unwrap());

// Get the path to the examples.
// This crate is located at crates/rsjudge-runner,
Expand Down
7 changes: 2 additions & 5 deletions crates/rsjudge-runner/examples/exploit_inner.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0

use caps::{read, CapSet};
use capctl::FullCapState;
use nix::unistd::{setuid, Uid};

/// This example should be called by the `exploit` example.
Expand All @@ -11,10 +11,7 @@ use nix::unistd::{setuid, Uid};
/// have no permitted capabilities.
fn main() {
eprintln!("Start exploit_inner binary");
dbg!(read(None, CapSet::Ambient).unwrap());
dbg!(read(None, CapSet::Effective).unwrap());
dbg!(read(None, CapSet::Inheritable).unwrap());
dbg!(read(None, CapSet::Permitted).unwrap());
dbg!(FullCapState::get_current().unwrap());

eprintln!("Starting setuid syscall.");
let result = setuid(Uid::from_raw(0)).expect_err("Should fail to set UID");
Expand Down
7 changes: 2 additions & 5 deletions crates/rsjudge-runner/examples/normal.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
// SPDX-License-Identifier: Apache-2.0

use caps::{read, CapSet};
use capctl::FullCapState;

fn main() {
eprintln!("Start normal binary");
dbg!(read(None, CapSet::Ambient).unwrap());
dbg!(read(None, CapSet::Effective).unwrap());
dbg!(read(None, CapSet::Inheritable).unwrap());
dbg!(read(None, CapSet::Permitted).unwrap());
dbg!(FullCapState::get_current().unwrap());
eprintln!("End normal binary");
}
42 changes: 30 additions & 12 deletions crates/rsjudge-runner/src/cap_handle.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
//! RAII-style Capability handle.
use std::{cell::RefCell, collections::HashMap, rc::Rc};

use caps::{drop as drop_cap, has_cap, raise as raise_cap, Capability};
pub use capctl::Cap;
use capctl::CapState;
use rsjudge_utils::log_if_error;

use crate::{Error, Result};

/// An RAII-style handle for capabilities.
///
/// When constructed, the handle will raise the capability if it is permitted but not effective.
///
/// When dropped, the handle will drop the capability if it is the last reference.
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct CapHandle {
cap: Capability,
cap: Cap,
ref_count: Rc<()>,
}

impl CapHandle {
thread_local! {
/// Local capability reference count.
static LOCAL_CAPS: RefCell<HashMap<Capability,Rc<()>>> = RefCell::new(HashMap::new());
static LOCAL_CAPS: RefCell<HashMap<Cap, Rc<()>>> = RefCell::new(HashMap::new());
}

/// Create a new capability handle.
Expand All @@ -23,7 +30,7 @@ impl CapHandle {
/// # Errors
///
/// Returns an error if the capability is not permitted,
pub fn new(cap: Capability) -> Result<Self> {
pub fn new(cap: Cap) -> Result<Self> {
let ref_count = Self::LOCAL_CAPS
.with_borrow_mut(|local_caps| local_caps.entry(cap).or_default().clone());
try_raise_cap(cap)?;
Expand All @@ -35,21 +42,32 @@ impl Drop for CapHandle {
fn drop(&mut self) {
if Rc::strong_count(&self.ref_count) == 1 {
// Last reference.
let _ = drop_cap(None, caps::CapSet::Effective, self.cap);

let cap = self.cap;

// We cannot throw errors in `drop`, so we just log and ignore it.
let _ = log_if_error!(CapState::get_current().and_then(|mut state| {
state.effective.drop(cap);
state.set_current()
}));

Self::LOCAL_CAPS.with_borrow_mut(|local_caps| {
local_caps.remove(&self.cap);
});
}
}
}

fn try_raise_cap(cap: Capability) -> Result<bool> {
if has_cap(None, caps::CapSet::Effective, cap)? {
fn try_raise_cap(cap: Cap) -> Result<()> {
assert!(cap.is_supported());
let mut state = CapState::get_current()?;
if state.effective.has(cap) {
// Already has cap.
Ok(true)
} else if has_cap(None, caps::CapSet::Permitted, cap)? {
raise_cap(None, caps::CapSet::Effective, cap)?;
Ok(true)
Ok(())
} else if state.permitted.has(cap) {
state.effective.add(cap);
state.set_current()?;
Ok(())
} else {
Err(Error::CapRequired(cap))
}
Expand Down
15 changes: 10 additions & 5 deletions crates/rsjudge-runner/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: Apache-2.0

use caps::{errors::CapsError, Capability};
use std::io;

use capctl::Cap;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
/// Capabilities required but not set.
#[error("{0} required but not set.")]
CapRequired(Capability),
CapRequired(Cap),

/// The requested user is not found.
#[error("User '{username}' not found")]
Expand All @@ -16,10 +18,13 @@ pub enum Error {
/// A wrapper for `std::io::Error`.
#[error(transparent)]
Io(#[from] std::io::Error),
}

/// A wrapper for `caps::errors::CapsError`.
#[error(transparent)]
CapsError(#[from] CapsError),
/// Convert a [`capctl::Error`] to an [`Error::Io`].
impl From<capctl::Error> for Error {
fn from(value: capctl::Error) -> Self {
Self::Io(io::Error::from_raw_os_error(value.code()))
}
}

pub type Result<T, E = Error> = std::result::Result<T, E>;
2 changes: 1 addition & 1 deletion crates/rsjudge-runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::print_stderr))]

pub use crate::{
cap_handle::CapHandle,
cap_handle::{Cap, CapHandle},
error::{Error, Result},
};

Expand Down
4 changes: 2 additions & 2 deletions crates/rsjudge-runner/src/run_as.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io::{self, ErrorKind};

use caps::Capability;
use capctl::Cap;
use nix::unistd::{setgroups, Gid};
use rsjudge_utils::log_if_error;
use tokio::process::Command;
Expand Down Expand Up @@ -44,7 +44,7 @@ impl RunAs for Command {
.collect();

let set_groups = move || {
CapHandle::new(Capability::CAP_SETGID)
CapHandle::new(Cap::SETGID)
.map_err(|e| io::Error::new(ErrorKind::PermissionDenied, e.to_string()))?;
log_if_error!(setgroups(&groups))?;
Ok(())
Expand Down
17 changes: 11 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
use clap::Parser as _;
use env_logger::Env;
use log::{debug, info, warn};
use rsjudge_runner::{user::builder, RunAs as _};
use rsjudge_utils::command::display_cmd;
use rsjudge_runner::{user::builder, Cap, CapHandle, RunAs as _};
use tokio::{fs::read, process::Command};

use crate::cli::Args;
Expand All @@ -34,11 +33,17 @@ pub async fn main_impl() -> anyhow::Result<()> {

let config = read(args.config_dir.join("executors.toml")).await?;

info!("Executing \"id\" as rsjudge-builder");
info!("Executing \"captest\" as rsjudge-builder");

match Command::new("id").run_as(builder()?) {
Ok(it) => {
debug!("{} exited with {}", display_cmd(it), it.status().await?);
CapHandle::new(Cap::SETUID)?;
CapHandle::new(Cap::SETGID)?;

match Command::new("captest")
.run_as(builder()?)
.and_then(|cmd| Ok(cmd.spawn()?))
{
Ok(mut child) => {
debug!("Command exited with {}", child.wait().await?);
}
Err(err) => {
warn!("Failed to run \"id\" as rsjudge-builder: {}", err);
Expand Down

0 comments on commit cb3f2d1

Please sign in to comment.