Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/buildomat/jobs/ci-helios.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
#: name = "Build Helios client / CI"
#: variety = "basic"
#: target = "helios-2.0"
#: target = "helios-3.0"
#: rust_toolchain = true
#: output_rules = ["=/work/sush"]
#: access_repos = ["oxidecomputer/permission-slip"]
Expand All @@ -15,6 +15,7 @@ set -o errexit
set -o pipefail
set -o xtrace

uname -v
cargo --version
rustc --version

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ sprockets-tls-test-utils = { git = "https://github.com/oxidecomputer/sprockets.g
ssh-key = { version = "0.6", features = ["ed25519", "p256", "serde"] }
tempfile = "3"
thiserror = "1"
tokio = { version = "1", features = ["macros", "process", "rt", "sync", "time"] }
tokio = { version = "1", features = ["macros", "process", "rt", "signal", "sync", "time"] }
tokio-rustls = "0.26"
tokio-stream = "0.1"
tokio-fd = "0.3"
Expand Down
48 changes: 19 additions & 29 deletions common/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,37 +740,24 @@ pub struct JobLimits {
}

impl JobLimits {
/// Clamp each limit to the default ceiling. Limits arrive as
/// unsigned request parameters, which may narrow the server's
/// limits but never widen them.
pub fn clamp(self) -> Self {
/// Clamp each limit to the default ceiling and to this process's
/// hard limit (since a job inherits these). Limits are unsigned
/// request parameters, so they may only narrow the server's limits.
pub fn clamp(self) -> Result<Self, IoError> {
let ceiling = Self::default();
Self {
max_cpu: self.max_cpu.min(ceiling.max_cpu),
max_mem: self.max_mem.min(ceiling.max_mem),
max_fsize: self.max_fsize.min(ceiling.max_fsize),
}
}

pub fn apply(&self) -> Result<(), IoError> {
let Self {
max_cpu,
max_mem,
max_fsize,
} = *self;
self.set_limit(Resource::CPU, max_cpu)?;
self.set_limit(Resource::AS, max_mem)?;
self.set_limit(Resource::FSIZE, max_fsize)?;
Ok(())
}

fn set_limit(&self, resource: Resource, value: u64) -> Result<(), IoError> {
let (_soft, hard) = resource.get()?;
resource.set(value.min(hard), hard)
let hard = |resource: Resource| resource.get().map(|(_soft, hard)| hard);
Ok(Self {
max_cpu: self.max_cpu.min(ceiling.max_cpu).min(hard(Resource::CPU)?),
max_mem: self.max_mem.min(ceiling.max_mem).min(hard(Resource::AS)?),
max_fsize: self
.max_fsize
.min(ceiling.max_fsize)
.min(hard(Resource::FSIZE)?),
})
}
}

/// The default limits are also the ceilings: see [`JobLimits::clamp`].
/// The default limits are also the ceilings; see [`JobLimits::clamp`].
impl Default for JobLimits {
fn default() -> Self {
JobLimits {
Expand Down Expand Up @@ -1045,12 +1032,15 @@ mod test {
max_mem: GB,
max_fsize: GB,
};
assert_eq!(narrowed.clone().clamp(), narrowed);
assert_eq!(narrowed.clone().clamp().unwrap(), narrowed);
let widened = JobLimits {
max_cpu: u64::MAX,
max_mem: u64::MAX,
max_fsize: u64::MAX,
};
assert_eq!(widened.clamp(), JobLimits::default());
assert_eq!(
widened.clamp().unwrap(),
JobLimits::default().clamp().unwrap()
);
}
}
59 changes: 33 additions & 26 deletions server/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ use std::io;
use std::os::fd::AsRawFd as _;

use libc::ctid_t;
use rustix::process::Signal;

use sush_common::jobs::JobId;

use crate::error::check_errno;

#[allow(non_camel_case_types)]
type ct_stathdl_t = *mut libc::c_void;

Expand All @@ -40,6 +43,10 @@ unsafe extern "C" {
fn ct_ctl_abandon(fd: c_int) -> c_int;
}

unsafe extern "C" {
fn sigsend(idtype: libc::idtype_t, id: libc::id_t, sig: c_int) -> c_int;
}

/// Process had an uncorrectable hardware error.
const CT_PR_EV_HWERR: c_uint = 0x20;

Expand All @@ -51,28 +58,28 @@ const CTD_COMMON: c_int = 0;

const PROCESS: &str = "/system/contract/process";

/// libcontract returns an error number rather than setting `errno`.
fn check(what: &str, errno: c_int) -> io::Result<()> {
if errno == 0 {
Ok(())
} else {
let error = io::Error::from_raw_os_error(errno);
Err(io::Error::new(error.kind(), format!("{what}: {error}")))
}
}

/// Open a ctfs file, naming it in any error.
fn open(options: &OpenOptions, path: String) -> io::Result<File> {
options
.open(&path)
.map_err(|error| io::Error::new(error.kind(), format!("opening {path}: {error}")))
}

/// Send `signal` to every process in contract `ctid`.
pub fn signal_contract(ctid: ctid_t, signal: Signal) -> io::Result<()> {
if unsafe { sigsend(libc::P_CTID, ctid, signal.as_raw()) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

/// Run `spawn` with a job's contract template active, then abandon the
/// contract its `fork` created. The template is per thread, so `spawn`
/// must fork on the calling thread; being synchronous, it cannot move
/// to another. If `spawn` fails after forking or abandoning fails,
/// the empty contract lingers until we exit.
/// new contract that holds the child. The template is per thread, so
/// `spawn` must create the child on the calling thread; being
/// synchronous, it cannot move to another. If `spawn` fails after
/// creating a child, or abandoning fails, the empty contract lingers
/// until we exit.
pub fn spawn_in_contract<T>(
job_id: &JobId,
spawn: impl FnOnce() -> io::Result<T>,
Expand All @@ -83,8 +90,8 @@ pub fn spawn_in_contract<T>(
Ok((child, ctid))
}

/// An active process contract template. While it lives, every `fork`
/// on the calling thread creates a new contract.
/// An active process contract template. While it lives, every child
/// the calling thread creates starts in a new contract.
struct Template(File);

impl Template {
Expand All @@ -99,29 +106,29 @@ impl Template {
let fd = file.as_raw_fd();
let aux = CString::new(format!("sush job {job_id}")).map_err(io::Error::other)?;
unsafe {
check(
check_errno(
"setting fatal events",
ct_pr_tmpl_set_fatal(fd, CT_PR_EV_HWERR),
)?;
check("setting critical events", ct_tmpl_set_critical(fd, 0))?;
check("setting informative events", ct_tmpl_set_informative(fd, 0))?;
check(
check_errno("setting critical events", ct_tmpl_set_critical(fd, 0))?;
check_errno("setting informative events", ct_tmpl_set_informative(fd, 0))?;
check_errno(
"setting parameters",
ct_pr_tmpl_set_param(fd, CT_PR_PGRPONLY),
)?;
check("setting label", ct_pr_tmpl_set_svc_aux(fd, aux.as_ptr()))?;
check("activating template", ct_tmpl_activate(fd))?;
check_errno("setting label", ct_pr_tmpl_set_svc_aux(fd, aux.as_ptr()))?;
check_errno("activating template", ct_tmpl_activate(fd))?;
}
Ok(Self(file))
}

/// Abandon the contract created by the last `fork` on this thread,
/// and return its ID.
/// Abandon the newest contract this thread created, and return its
/// ID.
fn abandon_latest(self) -> io::Result<ctid_t> {
let latest = open(OpenOptions::new().read(true), format!("{PROCESS}/latest"))?;
let ctid = unsafe {
let mut status: ct_stathdl_t = std::ptr::null_mut();
check(
check_errno(
"reading latest contract",
ct_status_read(latest.as_raw_fd(), CTD_COMMON, &mut status),
)?;
Expand All @@ -133,7 +140,7 @@ impl Template {
OpenOptions::new().write(true),
format!("{PROCESS}/{ctid}/ctl"),
)?;
check("abandoning contract", unsafe {
check_errno("abandoning contract", unsafe {
ct_ctl_abandon(ctl.as_raw_fd())
})?;
Ok(ctid)
Expand Down
18 changes: 18 additions & 0 deletions server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

//! Job server errors.

use std::ffi::c_int;
use std::fmt::Display;
use std::io;
use std::path::{Path, PathBuf};

use dropshot::{ClientErrorStatusCode, HttpError};
Expand All @@ -14,6 +17,21 @@ use sush_common::interactive::InteractiveJobError;
use sush_common::jobs::{ExecutionError, JobId, JobOutputHash, SessionId};
use sush_common::keys::{KeyError, KeyId};

/// Prefix an I/O error with `what` we were doing.
pub fn io_context(what: impl Display, error: io::Error) -> io::Error {
io::Error::new(error.kind(), format!("{what}: {error}"))
}

/// Check the result of a C function that returns an error number rather
/// than setting the global `errno`, as libcontract and posix_spawn(3C) do.
pub fn check_errno(what: impl Display, errno: c_int) -> io::Result<()> {
if errno == 0 {
Ok(())
} else {
Err(io_context(what, io::Error::from_raw_os_error(errno)))
}
}

/// What went wrong processing a client job request.
#[derive(Debug, Error)]
pub enum JobError {
Expand Down
Loading