use super::landlock::LandlockManager;
use crate::error::{NucleusError, Result};
use crate::oci::OciBundle;
use nix::unistd::Uid;
use sha2::{Digest, Sha256};
use std::ffi::CString;
use std::fs::{self, DirBuilder, OpenOptions};
use std::io;
use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use tracing::{debug, info, warn};
#[cfg(test)]
const NIX_STORE_EXEC_ROOT: &str = "/nix/store";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GVisorNetworkMode {
None,
Sandbox,
Host,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
pub enum GVisorPlatform {
#[default]
Systrap,
Kvm,
Ptrace,
}
impl GVisorPlatform {
pub fn as_flag(self) -> &'static str {
match self {
Self::Systrap => "systrap",
Self::Kvm => "kvm",
Self::Ptrace => "ptrace",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GVisorOciRunOptions {
pub network_mode: GVisorNetworkMode,
pub ignore_cgroups: bool,
pub runsc_rootless: bool,
pub require_supervisor_exec_policy: bool,
pub platform: GVisorPlatform,
}
impl Default for GVisorOciRunOptions {
fn default() -> Self {
Self {
network_mode: GVisorNetworkMode::None,
ignore_cgroups: false,
runsc_rootless: false,
require_supervisor_exec_policy: false,
platform: GVisorPlatform::default(),
}
}
}
impl GVisorOciRunOptions {
fn network_flag(self) -> &'static str {
match self.network_mode {
GVisorNetworkMode::None => "none",
GVisorNetworkMode::Sandbox => "sandbox",
GVisorNetworkMode::Host => "host",
}
}
}
pub struct GVisorRuntime {
runsc_path: String,
}
impl GVisorRuntime {
pub fn new() -> Result<Self> {
let runsc_path = Self::find_runsc()?;
info!("Found runsc at: {}", runsc_path);
Ok(Self { runsc_path })
}
pub fn with_path(runsc_path: String) -> Self {
Self { runsc_path }
}
pub fn resolve_path() -> Result<String> {
Self::find_runsc()
}
fn find_runsc() -> Result<String> {
let paths = vec![
"/usr/local/bin/runsc",
"/usr/bin/runsc",
"/opt/gvisor/runsc",
];
for path in &paths {
if let Some(validated) = Self::validate_runsc_path(Path::new(path))? {
return Ok(validated);
}
}
if Uid::effective().is_root() {
return Err(NucleusError::GVisorError(
"runsc binary not found in trusted system paths".to_string(),
));
}
if let Some(path_var) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join("runsc");
if let Some(validated) = Self::validate_runsc_path(&candidate)? {
return Ok(validated);
}
}
}
Err(NucleusError::GVisorError(
"runsc binary not found. Please install gVisor.".to_string(),
))
}
fn validate_runsc_path(path: &Path) -> Result<Option<String>> {
if !path.exists() {
return Ok(None);
}
if !path.is_file() {
return Ok(None);
}
let canonical = std::fs::canonicalize(path).map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to canonicalize runsc path {:?}: {}",
path, e
))
})?;
let resolved = Self::unwrap_nix_wrapper(&canonical).unwrap_or_else(|| canonical.clone());
let metadata = std::fs::metadata(&resolved).map_err(|e| {
NucleusError::GVisorError(format!("Failed to stat runsc path {:?}: {}", resolved, e))
})?;
let mode = metadata.permissions().mode();
if mode & 0o022 != 0 {
return Err(NucleusError::GVisorError(format!(
"Refusing insecure runsc binary permissions at {:?} (mode {:o})",
resolved, mode
)));
}
if mode & 0o111 == 0 {
return Ok(None);
}
use std::os::unix::fs::MetadataExt;
let owner = metadata.uid();
let current_uid = nix::unistd::Uid::effective().as_raw();
if !Self::is_trusted_runsc_owner(&resolved, owner, current_uid) {
return Err(NucleusError::GVisorError(format!(
"Refusing runsc binary at {:?} owned by uid {} (expected root, current user {}, or immutable /nix/store artifact)",
resolved, owner, current_uid
)));
}
Ok(Some(resolved.to_string_lossy().to_string()))
}
fn is_trusted_runsc_owner(path: &Path, owner: u32, current_uid: u32) -> bool {
if owner == 0 || owner == current_uid {
return true;
}
if path.starts_with("/nix/store") {
if let Ok(meta) = std::fs::metadata(path) {
let mode = meta.permissions().mode();
if mode & 0o200 != 0 {
return false;
}
} else {
return false;
}
if let Some(parent) = path.parent() {
if let Ok(parent_meta) = std::fs::metadata(parent) {
let parent_mode = parent_meta.permissions().mode();
if parent_mode & 0o222 != 0 {
return false;
}
} else {
return false;
}
}
return true;
}
false
}
fn unwrap_nix_wrapper(path: &Path) -> Option<std::path::PathBuf> {
let content = std::fs::read_to_string(path).ok()?;
if content.len() > 4096 || !content.starts_with("#!") {
return None;
}
for line in content.lines().rev() {
let trimmed = line.trim();
if trimmed.starts_with("exec ") {
for token in trimmed.split_whitespace() {
let unquoted = token.trim_matches('"');
if unquoted.starts_with('/') && unquoted.contains("runsc") {
let candidate = std::path::PathBuf::from(unquoted);
if candidate.exists() && candidate.is_file() {
debug!("Resolved Nix wrapper {:?} → {:?}", path, candidate);
return Some(candidate);
}
}
}
}
}
None
}
pub fn exec_with_oci_bundle(&self, container_id: &str, bundle: &OciBundle) -> Result<()> {
self.exec_with_oci_bundle_options(container_id, bundle, GVisorOciRunOptions::default())
}
pub fn exec_with_oci_bundle_options(
&self,
container_id: &str,
bundle: &OciBundle,
options: GVisorOciRunOptions,
) -> Result<()> {
info!(
"Executing with gVisor using OCI bundle at {:?} (network: {:?}, platform: {:?})",
bundle.bundle_path(),
options.network_mode,
options.platform,
);
let runsc_root = Self::secure_runsc_root(container_id)?;
let runsc_runtime_dir = runsc_root.join("runtime");
Self::ensure_secure_runsc_dir(&runsc_runtime_dir, "runsc runtime directory")?;
let (program_path, exec_allow_roots) =
self.prepare_supervisor_runsc_program(&runsc_root)?;
let mut args = self.build_oci_run_args(container_id, bundle, &runsc_root, options);
args[0] = program_path.to_string_lossy().to_string();
debug!("runsc OCI args: {:?}", args);
let program = CString::new(program_path.to_string_lossy().as_ref())
.map_err(|e| NucleusError::GVisorError(format!("Invalid runsc path: {}", e)))?;
let c_args: Result<Vec<CString>> = args
.iter()
.map(|arg| {
CString::new(arg.as_str())
.map_err(|e| NucleusError::GVisorError(format!("Invalid argument: {}", e)))
})
.collect();
let c_args = c_args?;
let c_env = self.exec_environment(&runsc_runtime_dir)?;
if options.runsc_rootless {
self.apply_supervisor_exec_policy(
&exec_allow_roots,
options.require_supervisor_exec_policy,
)?;
}
nix::unistd::execve::<std::ffi::CString, std::ffi::CString>(&program, &c_args, &c_env)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn exec_with_oci_bundle_network(
&self,
container_id: &str,
bundle: &OciBundle,
network_mode: GVisorNetworkMode,
ignore_cgroups: bool,
runsc_rootless: bool,
require_supervisor_exec_policy: bool,
platform: GVisorPlatform,
) -> Result<()> {
self.exec_with_oci_bundle_options(
container_id,
bundle,
GVisorOciRunOptions {
network_mode,
ignore_cgroups,
runsc_rootless,
require_supervisor_exec_policy,
platform,
},
)
}
pub fn is_available() -> bool {
Self::find_runsc().is_ok()
}
pub fn version(&self) -> Result<String> {
let output = Command::new(&self.runsc_path)
.arg("--version")
.output()
.map_err(|e| NucleusError::GVisorError(format!("Failed to get version: {}", e)))?;
if !output.status.success() {
return Err(NucleusError::GVisorError(
"Failed to get runsc version".to_string(),
));
}
let version = String::from_utf8_lossy(&output.stdout).to_string();
Ok(version.trim().to_string())
}
fn exec_environment(&self, runtime_dir: &Path) -> Result<Vec<CString>> {
let mut env = Vec::new();
let mut push = |key: &str, value: String| -> Result<()> {
env.push(
CString::new(format!("{}={}", key, value))
.map_err(|e| NucleusError::GVisorError(format!("Invalid {}: {}", key, e)))?,
);
Ok(())
};
push(
"PATH",
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
)?;
let runtime_dir = runtime_dir.to_string_lossy().to_string();
push("TMPDIR", runtime_dir.clone())?;
push("XDG_RUNTIME_DIR", runtime_dir)?;
push("HOME", "/root".to_string())?;
push("USER", "root".to_string())?;
push("LOGNAME", "root".to_string())?;
Ok(env)
}
fn prepare_supervisor_runsc_program(
&self,
runsc_root: &Path,
) -> Result<(PathBuf, Vec<PathBuf>)> {
let canonical = fs::canonicalize(&self.runsc_path).map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to canonicalize runsc path {:?}: {}",
self.runsc_path, e
))
})?;
Self::ensure_secure_runsc_dir(runsc_root, "runsc root directory")?;
let private_dir = runsc_root.join("exec-allow");
Self::ensure_secure_runsc_dir(&private_dir, "private runsc exec directory")?;
let stage_dir = Self::create_unique_runsc_stage_dir(&private_dir)?;
let staged = stage_dir.join("runsc");
Self::copy_runsc_nofollow(&canonical, &staged)?;
Ok((staged, Self::supervisor_exec_allow_roots(private_dir)))
}
fn supervisor_exec_allow_roots(program_root: PathBuf) -> Vec<PathBuf> {
vec![program_root]
}
fn secure_runsc_root(container_id: &str) -> Result<PathBuf> {
let artifact_base = Self::gvisor_artifact_base()?;
let artifact_dir = artifact_base.join(Self::runsc_state_component(container_id));
if Self::host_root_requires_trusted_runsc_ancestry() {
Self::ensure_trusted_host_root_runsc_ancestry(
&artifact_base,
"gVisor runsc artifact base",
)?;
}
Self::ensure_secure_runsc_dir(&artifact_base, "gVisor runsc artifact base")?;
Self::ensure_secure_runsc_dir(&artifact_dir, "gVisor runsc artifact directory")?;
let runsc_root = artifact_dir.join("runsc-root");
Self::ensure_secure_runsc_dir(&runsc_root, "runsc root directory")?;
Ok(runsc_root)
}
fn gvisor_artifact_base() -> Result<PathBuf> {
if let Some(path) =
std::env::var_os("NUCLEUS_GVISOR_ARTIFACT_BASE").filter(|path| !path.is_empty())
{
return Self::absolute_path(Path::new(&path), "gVisor artifact base");
}
if !Uid::effective().is_root() || Self::root_uid_maps_to_unprivileged_host_uid_from_proc() {
if let Some(dir) = dirs::runtime_dir() {
return Ok(dir.join("nucleus-gvisor"));
}
}
if Uid::effective().is_root() {
Ok(PathBuf::from("/run/nucleus-gvisor"))
} else {
Ok(std::env::temp_dir().join(format!("nucleus-gvisor-{}", Uid::effective().as_raw())))
}
}
fn absolute_path(path: &Path, label: &str) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
std::env::current_dir()
.map(|cwd| cwd.join(path))
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to resolve current directory for {} {:?}: {}",
label, path, e
))
})
}
fn runsc_state_component(container_id: &str) -> String {
if container_id.len() == 32 && container_id.chars().all(|c| c.is_ascii_hexdigit()) {
return container_id.to_string();
}
let digest = Sha256::digest(container_id.as_bytes());
format!("id-{}", hex::encode(&digest[..16]))
}
fn root_uid_maps_to_unprivileged_host_uid_from_proc() -> bool {
fs::read_to_string("/proc/self/uid_map")
.map(|uid_map| Self::root_uid_maps_to_unprivileged_host_uid(&uid_map))
.unwrap_or(false)
}
fn root_uid_maps_to_unprivileged_host_uid(uid_map: &str) -> bool {
for line in uid_map.lines() {
let mut fields = line.split_whitespace();
let Some(namespace_start) = fields.next() else {
continue;
};
let Some(host_start) = fields.next() else {
continue;
};
let Some(length) = fields.next() else {
continue;
};
if fields.next().is_some() {
continue;
}
let Ok(namespace_start) = namespace_start.parse::<u64>() else {
continue;
};
let Ok(host_start) = host_start.parse::<u64>() else {
continue;
};
let Ok(length) = length.parse::<u64>() else {
continue;
};
if namespace_start == 0 && length > 0 {
return host_start != 0;
}
}
false
}
fn host_root_requires_trusted_runsc_ancestry() -> bool {
Uid::effective().is_root() && !Self::root_uid_maps_to_unprivileged_host_uid_from_proc()
}
fn ensure_trusted_host_root_runsc_ancestry(path: &Path, label: &str) -> Result<()> {
let path = Self::absolute_path(path, label)?;
let mut current = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(prefix) => current.push(prefix.as_os_str()),
Component::RootDir => current.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
return Err(NucleusError::GVisorError(format!(
"{} {:?} contains a parent-directory component",
label, path
)));
}
Component::Normal(name) => {
current.push(name);
match fs::symlink_metadata(¤t) {
Ok(metadata) => Self::ensure_trusted_host_root_runsc_ancestor_component(
¤t, metadata, label,
)?,
Err(e) if e.kind() == io::ErrorKind::NotFound => break,
Err(e) => {
return Err(NucleusError::GVisorError(format!(
"Failed to stat {} ancestor {:?}: {}",
label, current, e
)));
}
}
}
}
}
Ok(())
}
fn ensure_trusted_host_root_runsc_ancestor_component(
path: &Path,
metadata: fs::Metadata,
label: &str,
) -> Result<()> {
if metadata.file_type().is_symlink() {
return Err(NucleusError::GVisorError(format!(
"Refusing symlink {} ancestor {:?}",
label, path
)));
}
if !metadata.file_type().is_dir() {
return Err(NucleusError::GVisorError(format!(
"{} ancestor {:?} is not a directory",
label, path
)));
}
let owner = metadata.uid();
if owner != 0 {
return Err(NucleusError::GVisorError(format!(
"{} ancestor {:?} is owned by uid {} (expected root)",
label, path, owner
)));
}
let mode = metadata.permissions().mode();
if mode & 0o022 != 0 && mode & 0o1000 == 0 {
return Err(NucleusError::GVisorError(format!(
"{} ancestor {:?} has unsafe permissions {:o}",
label,
path,
mode & 0o7777
)));
}
Ok(())
}
fn ensure_secure_runsc_dir(path: &Path, label: &str) -> Result<()> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
Self::ensure_trusted_runsc_parent(parent, label)?;
}
let mut created = false;
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(NucleusError::GVisorError(format!(
"Refusing symlink {} {:?}",
label, path
)));
}
Ok(metadata) if !metadata.file_type().is_dir() => {
return Err(NucleusError::GVisorError(format!(
"{} {:?} is not a directory",
label, path
)));
}
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
match DirBuilder::new().mode(0o700).create(path) {
Ok(()) => {
created = true;
}
Err(create_err) if create_err.kind() == io::ErrorKind::AlreadyExists => {}
Err(create_err) => {
return Err(NucleusError::GVisorError(format!(
"Failed to create {} {:?}: {}",
label, path, create_err
)));
}
}
}
Err(e) => {
return Err(NucleusError::GVisorError(format!(
"Failed to stat {} {:?}: {}",
label, path, e
)));
}
}
if created {
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to secure newly-created {} permissions {:?}: {}",
label, path, e
))
})?;
}
let dir = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_DIRECTORY)
.open(path)
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to open {} {:?} without following symlinks: {}",
label, path, e
))
})?;
let metadata = dir.metadata().map_err(|e| {
NucleusError::GVisorError(format!("Failed to stat {} {:?}: {}", label, path, e))
})?;
if !metadata.file_type().is_dir() {
return Err(NucleusError::GVisorError(format!(
"{} {:?} is not a directory",
label, path
)));
}
let owner = metadata.uid();
let expected = Uid::effective().as_raw();
if owner != expected {
return Err(NucleusError::GVisorError(format!(
"{} {:?} is owned by uid {} (expected {})",
label, path, owner, expected
)));
}
let mode = metadata.permissions().mode() & 0o777;
if mode != 0o700 {
dir.set_permissions(fs::Permissions::from_mode(0o700))
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to secure {} permissions {:?}: {}",
label, path, e
))
})?;
}
Ok(())
}
fn ensure_trusted_runsc_parent(parent: &Path, label: &str) -> Result<()> {
let metadata = fs::symlink_metadata(parent).map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to stat parent for {} {:?}: {}",
label, parent, e
))
})?;
if metadata.file_type().is_symlink() {
return Err(NucleusError::GVisorError(format!(
"Refusing symlink parent for {} {:?}",
label, parent
)));
}
if !metadata.file_type().is_dir() {
return Err(NucleusError::GVisorError(format!(
"Parent for {} {:?} is not a directory",
label, parent
)));
}
let owner = metadata.uid();
let current = Uid::effective().as_raw();
let owner_trusted = owner == current || owner == 0;
let mode = metadata.permissions().mode();
let unsafe_writable = mode & 0o022 != 0 && mode & 0o1000 == 0;
if !owner_trusted || unsafe_writable {
return Err(NucleusError::GVisorError(format!(
"Parent for {} {:?} is not trusted (owner uid {}, mode {:o})",
label,
parent,
owner,
mode & 0o7777
)));
}
Ok(())
}
fn create_unique_runsc_stage_dir(private_dir: &Path) -> Result<PathBuf> {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
for attempt in 0..100u32 {
let stage_dir = private_dir.join(format!(
"stage-{}-{}-{}",
std::process::id(),
nonce,
attempt
));
match DirBuilder::new().mode(0o700).create(&stage_dir) {
Ok(()) => {
Self::ensure_secure_runsc_dir(&stage_dir, "runsc stage directory")?;
return Ok(stage_dir);
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => {
return Err(NucleusError::GVisorError(format!(
"Failed to create runsc stage directory {:?}: {}",
stage_dir, e
)));
}
}
}
Err(NucleusError::GVisorError(format!(
"Failed to create unique runsc stage directory under {:?}",
private_dir
)))
}
fn copy_runsc_nofollow(source: &Path, staged: &Path) -> Result<()> {
let mut source_file = OpenOptions::new()
.read(true)
.custom_flags(libc::O_CLOEXEC)
.open(source)
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to open runsc source {:?}: {}",
source, e
))
})?;
let source_meta = source_file.metadata().map_err(|e| {
NucleusError::GVisorError(format!("Failed to stat runsc source {:?}: {}", source, e))
})?;
if !source_meta.file_type().is_file() {
return Err(NucleusError::GVisorError(format!(
"runsc source {:?} is not a regular file",
source
)));
}
let mut staged_file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o500)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(staged)
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to create staged runsc binary {:?}: {}",
staged, e
))
})?;
io::copy(&mut source_file, &mut staged_file).map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to stage runsc binary from {:?} to {:?}: {}",
source, staged, e
))
})?;
staged_file
.set_permissions(fs::Permissions::from_mode(0o500))
.map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to secure staged runsc binary {:?}: {}",
staged, e
))
})?;
staged_file.sync_all().map_err(|e| {
NucleusError::GVisorError(format!(
"Failed to sync staged runsc binary {:?}: {}",
staged, e
))
})?;
Ok(())
}
fn apply_supervisor_exec_policy(
&self,
allowed_roots: &[PathBuf],
required: bool,
) -> Result<()> {
let mut landlock = LandlockManager::new();
let applied = landlock.apply_execute_allowlist_policy(allowed_roots, !required)?;
if applied {
info!(
allowed_roots = ?allowed_roots,
"Applied gVisor supervisor execute allowlist"
);
} else if required {
return Err(NucleusError::LandlockError(
"Required gVisor supervisor execute allowlist was not applied".to_string(),
));
} else {
warn!(
allowed_roots = ?allowed_roots,
"gVisor supervisor execute allowlist unavailable"
);
}
Ok(())
}
fn build_oci_run_args(
&self,
container_id: &str,
bundle: &OciBundle,
runsc_root: &Path,
options: GVisorOciRunOptions,
) -> Vec<String> {
let mut args = vec![
self.runsc_path.clone(),
"--root".to_string(),
runsc_root.to_string_lossy().to_string(),
];
if options.runsc_rootless {
args.push("--rootless".to_string());
}
if options.ignore_cgroups {
args.push("--ignore-cgroups".to_string());
}
args.extend([
"--network".to_string(),
options.network_flag().to_string(),
"--platform".to_string(),
options.platform.as_flag().to_string(),
"run".to_string(),
"--bundle".to_string(),
bundle.bundle_path().to_string_lossy().to_string(),
container_id.to_string(),
]);
args
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oci::OciConfig;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvLock {
_guard: MutexGuard<'static, ()>,
}
impl EnvLock {
fn acquire() -> Self {
Self {
_guard: ENV_LOCK.lock().unwrap(),
}
}
}
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
let previous = std::env::var_os(key);
std::env::set_var(key, value);
Self { key, previous }
}
fn remove(key: &'static str) -> Self {
let previous = std::env::var_os(key);
std::env::remove_var(key);
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
#[test]
fn test_gvisor_availability() {
let available = GVisorRuntime::is_available();
println!("gVisor available: {}", available);
}
#[test]
fn test_gvisor_new() {
let runtime = GVisorRuntime::new();
if let Ok(rt) = runtime {
println!("Found runsc at: {}", rt.runsc_path);
if let Ok(version) = rt.version() {
println!("runsc version: {}", version);
}
}
}
#[test]
fn test_find_runsc() {
match GVisorRuntime::find_runsc() {
Ok(path) => {
println!("Found runsc at: {}", path);
assert!(!path.is_empty());
}
Err(e) => {
println!("runsc not found (expected if gVisor not installed): {}", e);
}
}
}
#[test]
fn test_validate_runsc_rejects_world_writable() {
let dir = tempfile::tempdir().unwrap();
let fake_runsc = dir.path().join("runsc");
std::fs::write(&fake_runsc, "#!/bin/sh\necho fake").unwrap();
std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o777)).unwrap();
let result = GVisorRuntime::validate_runsc_path(&fake_runsc);
assert!(
result.is_err(),
"validate_runsc_path must reject world-writable binaries"
);
}
#[test]
fn test_validate_runsc_rejects_group_writable() {
let dir = tempfile::tempdir().unwrap();
let fake_runsc = dir.path().join("runsc");
std::fs::write(&fake_runsc, "#!/bin/sh\necho fake").unwrap();
std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o775)).unwrap();
let result = GVisorRuntime::validate_runsc_path(&fake_runsc);
assert!(
result.is_err(),
"validate_runsc_path must reject group-writable binaries"
);
}
#[test]
fn test_runsc_owner_accepts_nix_store_artifact_owner() {
let nix_binary = std::fs::read_dir("/nix/store")
.ok()
.and_then(|mut entries| {
entries.find_map(|e| {
let dir = e.ok()?.path();
let candidate = dir.join("bin/runsc");
if candidate.exists() {
Some(candidate)
} else {
None
}
})
});
let path = match nix_binary {
Some(p) => p,
None => {
eprintln!("skipping: no runsc binary found in /nix/store");
return;
}
};
assert!(GVisorRuntime::is_trusted_runsc_owner(&path, 65534, 1000));
}
#[test]
fn test_exec_environment_uses_hardcoded_path() {
std::env::set_var("PATH", "/tmp/evil-inject/bin:/opt/attacker/sbin");
let rt = GVisorRuntime::with_path("/fake/runsc".to_string());
let tmp = tempfile::tempdir().unwrap();
let env = rt.exec_environment(tmp.path()).unwrap();
let path_entry = env
.iter()
.find(|e| e.to_str().is_ok_and(|s| s.starts_with("PATH=")))
.expect("exec_environment must set PATH");
let path_val = path_entry.to_str().unwrap();
assert!(
!path_val.contains("evil-inject") && !path_val.contains("attacker"),
"exec_environment must use hardcoded PATH, not host PATH. Got: {}",
path_val
);
assert_eq!(
path_val, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"exec_environment PATH must be the standard hardcoded value"
);
}
#[test]
fn test_precreated_rootless_args_pass_runsc_rootless() {
let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
let tmp = tempfile::tempdir().unwrap();
let bundle = OciBundle::new(
tmp.path().join("bundle"),
OciConfig::new(vec!["/bin/true".to_string()], None),
);
let args = rt.build_oci_run_args(
"container-id",
&bundle,
tmp.path(),
GVisorOciRunOptions {
network_mode: GVisorNetworkMode::Host,
ignore_cgroups: true,
runsc_rootless: true,
require_supervisor_exec_policy: false,
platform: GVisorPlatform::Systrap,
},
);
assert!(args.iter().any(|arg| arg == "--rootless"));
assert!(args.iter().any(|arg| arg == "--ignore-cgroups"));
}
#[test]
fn test_rootless_oci_args_do_not_pass_runsc_rootless() {
let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
let tmp = tempfile::tempdir().unwrap();
let bundle = OciBundle::new(
tmp.path().join("bundle"),
OciConfig::new(vec!["/bin/true".to_string()], None),
);
let args = rt.build_oci_run_args(
"container-id",
&bundle,
tmp.path(),
GVisorOciRunOptions {
network_mode: GVisorNetworkMode::Host,
ignore_cgroups: true,
runsc_rootless: false,
require_supervisor_exec_policy: false,
platform: GVisorPlatform::Systrap,
},
);
assert!(!args.iter().any(|arg| arg == "--rootless"));
assert!(args.iter().any(|arg| arg == "--ignore-cgroups"));
}
#[test]
fn test_non_nix_runsc_is_staged_for_supervisor_exec_policy() {
let tmp = tempfile::tempdir().unwrap();
let fake_runsc = tmp.path().join("runsc-source");
std::fs::write(&fake_runsc, b"fake-runsc").unwrap();
std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o500)).unwrap();
let rt = GVisorRuntime::with_path(fake_runsc.to_string_lossy().to_string());
let runsc_root = tmp.path().join("runsc-root");
let (program, allow_roots) = rt.prepare_supervisor_runsc_program(&runsc_root).unwrap();
assert!(program.starts_with(runsc_root.join("exec-allow")));
assert_eq!(allow_roots, vec![runsc_root.join("exec-allow")]);
assert_eq!(std::fs::read(&program).unwrap(), b"fake-runsc");
let mode = std::fs::metadata(&program).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o500);
}
#[test]
fn test_supervisor_exec_allow_roots_do_not_include_procfs() {
let roots = GVisorRuntime::supervisor_exec_allow_roots(PathBuf::from(NIX_STORE_EXEC_ROOT));
assert_eq!(roots, vec![PathBuf::from(NIX_STORE_EXEC_ROOT)]);
assert!(
!roots.iter().any(|root| root == Path::new("/proc")),
"the supervisor policy must not allow recursive procfs execution"
);
}
#[test]
fn test_runsc_root_uses_hardened_artifact_dir_not_bundle_parent() {
let _env_lock = EnvLock::acquire();
let tmp = tempfile::tempdir().unwrap();
let artifact_base = tmp.path().join("gvisor-artifacts");
let _artifact_base = EnvVarGuard::set("NUCLEUS_GVISOR_ARTIFACT_BASE", &artifact_base);
let _runtime = EnvVarGuard::remove("XDG_RUNTIME_DIR");
let bundle_parent = tmp.path().join("shared");
std::fs::create_dir_all(&bundle_parent).unwrap();
std::fs::set_permissions(&bundle_parent, std::fs::Permissions::from_mode(0o777)).unwrap();
let bundle = OciBundle::new(
bundle_parent.join("bundle"),
OciConfig::new(vec!["/bin/true".to_string()], None),
);
let runsc_root = GVisorRuntime::secure_runsc_root("container-id").unwrap();
assert!(runsc_root
.starts_with(artifact_base.join(GVisorRuntime::runsc_state_component("container-id"))));
assert!(
!runsc_root.starts_with(bundle.bundle_path().parent().unwrap()),
"runsc root must not be derived from a custom bundle parent"
);
}
#[test]
fn test_runsc_staging_rejects_symlink_exec_allow_dir() {
let tmp = tempfile::tempdir().unwrap();
let fake_runsc = tmp.path().join("runsc-source");
std::fs::write(&fake_runsc, b"fake-runsc").unwrap();
std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o500)).unwrap();
let runsc_root = tmp.path().join("runsc-root");
std::fs::create_dir(&runsc_root).unwrap();
std::fs::set_permissions(&runsc_root, std::fs::Permissions::from_mode(0o700)).unwrap();
let victim_dir = tmp.path().join("victim");
std::fs::create_dir(&victim_dir).unwrap();
std::os::unix::fs::symlink(&victim_dir, runsc_root.join("exec-allow")).unwrap();
let rt = GVisorRuntime::with_path(fake_runsc.to_string_lossy().to_string());
let err = rt
.prepare_supervisor_runsc_program(&runsc_root)
.unwrap_err()
.to_string();
assert!(
err.contains("Refusing symlink private runsc exec directory"),
"unexpected error: {}",
err
);
assert!(
!victim_dir.join("runsc").exists(),
"staging must not follow the exec-allow symlink"
);
}
#[test]
fn test_runsc_owner_rejects_untrusted_non_store_owner() {
assert!(!GVisorRuntime::is_trusted_runsc_owner(
Path::new("/tmp/runsc"),
4242,
1000
));
}
}