Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

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

13 changes: 12 additions & 1 deletion crates/agentd/lib/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ mod linux {
"/dev/pts",
Some("devpts"),
noexec_nosuid,
None::<&str>,
Some("ptmxmode=0666,mode=0620"),
)?;
ensure_dev_ptmx()?;

// /dev/shm — tmpfs
mkdir_ignore_exists("/dev/shm")?;
Expand Down Expand Up @@ -244,6 +245,16 @@ mod linux {
mount_ignore_busy(Some("sysfs"), "/sys", Some("sysfs"), flags, None::<&str>)
}

fn ensure_dev_ptmx() -> AgentdResult<()> {
let ptmx = Path::new("/dev/ptmx");
if fs::symlink_metadata(ptmx).is_ok() {
return Ok(());
}

unix_fs::symlink("pts/ptmx", ptmx)
.map_err(|e| AgentdError::Init(format!("failed to symlink /dev/ptmx: {e}")))
}

/// Mounts the virtiofs runtime filesystem at the canonical mount point.
pub fn mount_runtime() -> AgentdResult<()> {
mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
Expand Down
9 changes: 9 additions & 0 deletions crates/agentd/lib/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ fn signal_process_group_only(pid: i32, signum: i32) -> AgentdResult<()> {
fn exit_code(status: i32) -> i32 {
if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else if libc::WIFSIGNALED(status) {
128 + libc::WTERMSIG(status)
} else {
-1
}
Expand All @@ -554,6 +556,13 @@ mod tests {
const HELPER_SENTINEL: &str = "process-manager-helper-passed";
const TEST_NAME: &str = "process::tests::reaping_is_batched_and_tracks_exit_codes";

#[test]
fn maps_normal_and_signal_wait_statuses_to_shell_exit_codes() {
assert_eq!(exit_code(42 << 8), 42);
assert_eq!(exit_code(libc::SIGTERM), 128 + libc::SIGTERM);
assert_eq!(exit_code(libc::SIGKILL), 128 + libc::SIGKILL);
}

#[test]
fn reaping_is_batched_and_tracks_exit_codes() {
if std::env::var_os(HELPER_ENV).is_some() {
Expand Down
19 changes: 15 additions & 4 deletions crates/agentd/lib/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,9 @@ impl ExecSession {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
apply_exec_security_profile(security_profile).map_err(agentd_to_io_error)?;
apply_exec_security_profile(security_profile).map_err(agentd_to_pre_exec_error)?;
if let Some(ref user) = resolved_user {
apply_resolved_user(user).map_err(agentd_to_io_error)?;
apply_resolved_user(user).map_err(agentd_to_pre_exec_error)?;
}
for (resource, limit) in &parsed_rlimits {
if libc::setrlimit(*resource as _, limit) != 0 {
Expand Down Expand Up @@ -996,6 +996,9 @@ fn lookup_passwd_by_uid(uid: libc::uid_t) -> AgentdResult<ResolvedUserLookup> {
)
};
if rc != 0 {
if passwd_lookup_errno_means_missing(rc) {
return Ok(ResolvedUserLookup::Numeric(uid));
}
return Err(AgentdError::ExecSession(format!(
"failed to resolve guest uid {uid}: {}",
std::io::Error::from_raw_os_error(rc)
Expand Down Expand Up @@ -1054,6 +1057,10 @@ fn lookup_buffer_len() -> usize {
if size > 0 { size as usize } else { 16 * 1024 }
}

fn passwd_lookup_errno_means_missing(errno: libc::c_int) -> bool {
matches!(errno, libc::ENOENT | libc::ESRCH)
}

fn apply_resolved_user(user: &ResolvedUser) -> AgentdResult<()> {
if let Some(ref name) = user.initgroups_user {
if unsafe { libc::initgroups(name.as_ptr(), user.gid) } != 0 {
Expand Down Expand Up @@ -1097,8 +1104,12 @@ fn env_contains_key(env: &[String], key: &str) -> bool {
})
}

fn agentd_to_io_error(err: AgentdError) -> std::io::Error {
std::io::Error::other(err.to_string())
fn agentd_to_pre_exec_error(err: AgentdError) -> std::io::Error {
match err {
AgentdError::Io(err) => err,
AgentdError::Nix(err) => std::io::Error::from_raw_os_error(err as i32),
_ => std::io::Error::from_raw_os_error(libc::EINVAL),
}
}

/// Writes data to a raw fd using a blocking task, handling short writes.
Expand Down
1 change: 1 addition & 0 deletions crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ microsandbox-utils.workspace = true
microsandbox-vsock.workspace = true
msb_krun = { workspace = true, features = ["blk"] }
nix = { workspace = true, features = ["process", "signal"] }
oci-spec.workspace = true
rand.workspace = true
rustls = { workspace = true }
sea-orm.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/runtime/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod launch;
pub mod logging;
pub mod maintenance;
pub mod metrics;
pub mod oci;
pub mod policy;
pub mod relay;
mod startup;
Expand Down
220 changes: 220 additions & 0 deletions crates/runtime/lib/oci/bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//! OCI bundle parsing used by Microsandbox runtime integration.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use oci_spec::runtime::{Mount, Process, Spec};

use super::{OciResult, OciRuntimeError, io_error};

//--------------------------------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------------------------------

const CONFIG_JSON: &str = "config.json";

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// OCI bundle loaded from a host directory.
#[derive(Debug, Clone)]
pub struct OciBundle {
/// Absolute path to the bundle directory.
pub path: PathBuf,

/// Parsed OCI `config.json`.
pub spec: OciSpec,
}

/// OCI runtime specification parsed from `config.json`.
pub type OciSpec = Spec;

/// OCI process descriptor parsed from `config.json` or `process.json`.
pub type OciProcess = Process;

/// OCI mount descriptor parsed from `config.json`.
pub type OciMount = Mount;

//--------------------------------------------------------------------------------------------------
// Methods
//--------------------------------------------------------------------------------------------------

impl OciBundle {
/// Load and validate an OCI bundle from a directory.
pub fn load(path: impl AsRef<Path>) -> OciResult<Self> {
let path = absolutize_existing(path.as_ref())?;
let config_path = path.join(CONFIG_JSON);
let spec = OciSpec::load(&config_path).map_err(|e| OciRuntimeError::InvalidBundle {
bundle: path.clone(),
reason: format!(
"failed to load `{}` with oci-spec: {e}",
config_path.display()
),
})?;

let bundle = Self { path, spec };
bundle.validate()?;
Ok(bundle)
}

/// Resolve the OCI rootfs path to an absolute host path.
pub fn rootfs_path(&self) -> PathBuf {
let root = self
.spec
.root()
.as_ref()
.expect("validated OCI bundle must have a root filesystem");
if root.path().is_absolute() {
root.path().clone()
} else {
self.path.join(root.path())
}
}

/// Return the OCI process configured for `start`, if present.
pub fn process(&self) -> Option<&OciProcess> {
self.spec.process().as_ref()
}

/// Return additional OCI mounts.
pub fn mounts(&self) -> &[OciMount] {
self.spec.mounts().as_deref().unwrap_or_default()
}

/// Return annotations as the deterministic map used by persisted OCI state.
pub fn annotations(&self) -> BTreeMap<String, String> {
self.spec
.annotations()
.as_ref()
.map(|annotations| {
annotations
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
})
.unwrap_or_default()
}

/// Validate the bundle shape needed by Microsandbox's OCI layer.
pub fn validate(&self) -> OciResult<()> {
if self.spec.version().trim().is_empty() {
return Err(OciRuntimeError::InvalidBundle {
bundle: self.path.clone(),
reason: "ociVersion must not be empty".to_string(),
});
}
if self.spec.root().is_none() {
return Err(OciRuntimeError::InvalidBundle {
bundle: self.path.clone(),
reason: "root must be present".to_string(),
});
}
if let Some(process) = self.process() {
validate_process(process, &self.path)?;
}
let rootfs = self.rootfs_path();
if !rootfs.is_dir() {
return Err(OciRuntimeError::InvalidBundle {
bundle: self.path.clone(),
reason: format!("rootfs `{}` is not a directory", rootfs.display()),
});
}
Ok(())
}
}

//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------

/// Validate process fields needed by Microsandbox's OCI execution layer.
pub fn validate_process(process: &OciProcess, bundle: &Path) -> OciResult<()> {
if !process.cwd().is_absolute() {
return Err(OciRuntimeError::InvalidBundle {
bundle: bundle.to_path_buf(),
reason: format!("process.cwd must be absolute: {}", process.cwd().display()),
});
}
if process.args().as_deref().unwrap_or_default().is_empty() {
return Err(OciRuntimeError::InvalidBundle {
bundle: bundle.to_path_buf(),
reason: "process.args must contain at least one entry".to_string(),
});
}
Ok(())
}

fn absolutize_existing(path: &Path) -> OciResult<PathBuf> {
std::fs::canonicalize(path).map_err(|e| io_error("canonicalize", path, e))
}

//--------------------------------------------------------------------------------------------------
// Tests
//--------------------------------------------------------------------------------------------------

#[cfg(test)]
mod tests {
use std::fs;

use tempfile::TempDir;

use super::*;

#[test]
fn load_resolves_relative_rootfs_against_bundle() {
let temp = TempDir::new().expect("tempdir");
let rootfs = temp.path().join("rootfs");
fs::create_dir(&rootfs).expect("rootfs");
fs::write(
temp.path().join(CONFIG_JSON),
r#"{
"ociVersion": "1.2.0",
"root": { "path": "rootfs" },
"process": {
"user": { "uid": 0, "gid": 0 },
"cwd": "/",
"args": ["/bin/sh"],
"env": ["PATH=/bin"]
},
"annotations": {
"io.containerd.runtime.v2.task": "default/demo"
}
}"#,
)
.expect("config");

let bundle = OciBundle::load(temp.path()).expect("load bundle");
let rootfs = fs::canonicalize(rootfs).expect("canonical rootfs");

assert_eq!(bundle.rootfs_path(), rootfs);
assert_eq!(bundle.spec.version(), "1.2.0");
assert_eq!(
bundle.annotations()["io.containerd.runtime.v2.task"],
"default/demo"
);
}

#[test]
fn load_rejects_relative_process_cwd() {
let temp = TempDir::new().expect("tempdir");
fs::create_dir(temp.path().join("rootfs")).expect("rootfs");
fs::write(
temp.path().join(CONFIG_JSON),
r#"{
"ociVersion": "1.2.0",
"root": { "path": "rootfs" },
"process": {
"user": { "uid": 0, "gid": 0 },
"cwd": "app",
"args": ["/bin/sh"]
}
}"#,
)
.expect("config");

let err = OciBundle::load(temp.path()).expect_err("invalid cwd");

assert!(err.to_string().contains("process.cwd must be absolute"));
}
}
Loading