#[cfg(unix)]
use std::env;
#[cfg(target_os = "linux")]
use std::fs;
#[cfg(target_os = "linux")]
use std::os::fd::AsRawFd;
#[cfg(target_os = "linux")]
use std::os::fd::OwnedFd;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::time::{Duration, Instant};
use crate::models::*;
#[cfg(any(
target_os = "linux",
all(feature = "libkrun", not(target_os = "linux"))
))]
pub const VM_SESSION_DONE_CMD: &str = "__epkg_vm_session_done__";
#[cfg(all(feature = "libkrun", not(target_os = "linux")))]
fn try_connect_and_execute_vm(env_root: &Path, run_options: &RunOptions) -> Result<Option<i32>> {
let mut cmd_parts = vec![run_options.command.clone()];
cmd_parts.extend(run_options.args.clone());
log::debug!("run: checking for existing VM session for {}", env_root.display());
let cwd_str;
let cwd = if run_options.chdir_to_env_root {
Some("/")
} else {
cwd_str = std::env::current_dir().ok().map(|p| p.to_string_lossy().to_string());
cwd_str.as_deref()
};
crate::libkrun::execute_via_existing_vm(
env_root,
&cmd_parts,
run_options.io_mode,
Some(&run_options.env_vars),
cwd,
run_options.stdin.as_deref(),
)
}
#[cfg(target_os = "linux")]
use crate::namespace::{determine_process_config, build_unified_context, create_process_with_namespaces};
use crate::lfs;
#[cfg(target_os = "linux")]
use crate::utils::is_suid;
use color_eyre::eyre;
use color_eyre::Result;
use log::debug;
#[cfg(unix)]
use log::trace;
#[cfg(target_os = "linux")]
use log::{info, warn};
#[cfg(target_os = "linux")]
use nix::errno::Errno;
#[cfg(target_os = "linux")]
use nix::sys::signal::{self, Signal};
#[cfg(target_os = "linux")]
use nix::unistd::{close, pipe, write};
#[cfg(target_os = "linux")]
use nix::unistd::setuid;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
#[cfg(target_os = "linux")]
use users::get_current_uid;
#[derive(Debug, Clone, Default)]
pub struct RunOptions {
pub user: Option<String>,
#[allow(dead_code)]
pub group: Option<String>,
pub command: String,
pub args: Vec<String>,
pub env_vars: std::collections::HashMap<String, String>,
pub stdin: Option<Vec<u8>>,
pub no_exit: bool,
pub chdir_to_env_root: bool,
pub skip_namespace_isolation: bool,
pub timeout: u64,
pub background: bool,
pub redirect_stdio: bool,
pub io_mode: crate::models::IoMode,
pub kernel: Option<String>,
pub kernel_args: Option<String>,
pub initrd: Option<String>,
pub vm_cpus: Option<u8>,
pub vm_memory_mib: Option<u32>,
pub vsock_socket_path: Option<std::path::PathBuf>,
pub sandbox: crate::models::SandboxOptions,
pub effective_sandbox: crate::models::SandboxOptions,
pub vmm_order: Vec<String>,
pub vm_keep_timeout: Option<u32>,
pub translate_uid: Vec<String>,
pub translate_gid: Vec<String>,
pub host_uid: Option<u32>,
pub host_gid: Option<u32>,
pub working_dir: Option<std::path::PathBuf>,
pub env_name: String,
pub vm_daemon: bool,
}
#[cfg(target_os = "linux")]
pub(crate) fn with_sigpipe_handler<F, R>(handler: usize, f: F) -> R
where
F: FnOnce() -> R,
{
unsafe {
let old_handler = libc::signal(libc::SIGPIPE, handler);
let result = f();
libc::signal(libc::SIGPIPE, old_handler);
result
}
}
#[cfg(any(target_os = "linux", feature = "libkrun"))]
pub fn resolve_vm_cpus(run_options: &RunOptions) -> u8 {
if let Some(cpus) = run_options.vm_cpus {
return cpus;
}
std::env::var("EPKG_VM_CPUS")
.ok()
.and_then(|s| s.parse::<u8>().ok())
.unwrap_or(1)
}
#[cfg(any(target_os = "linux", feature = "libkrun"))]
pub fn resolve_vm_memory_mib(run_options: &RunOptions) -> u32 {
if let Some(mib) = run_options.vm_memory_mib {
return mib;
}
std::env::var("EPKG_VM_MEMORY")
.ok()
.and_then(|s| {
if let Some(bytes) = crate::utils::parse_size_bytes_opt(&s) {
Some((bytes / (1024 * 1024)) as u32)
} else {
s.parse::<u32>().ok()
}
})
.unwrap_or(4096)
}
#[cfg(target_os = "linux")]
pub fn ensure_linux_kvm_ready_for_vm() -> Result<()> {
use std::fs::OpenOptions;
const KVM_DEVICE: &str = "/dev/kvm";
if !Path::new(KVM_DEVICE).exists() {
return Err(eyre::eyre!(
"KVM is not available: {} is missing (VM backends need the host KVM device).\n\
Typical fix (as root): `modprobe kvm` and `modprobe kvm_intel` or `modprobe kvm_amd` for your CPU.\n\
If the file is still missing, enable virtualization in firmware/BIOS; \
if this machine is already a VM, enable nested virtualization.",
KVM_DEVICE
));
}
match OpenOptions::new().read(true).write(true).open(KVM_DEVICE) {
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => Err(eyre::eyre!(
"Cannot access {}: permission denied. Add your user to the `kvm` group, then re-login or `newgrp kvm`.",
KVM_DEVICE
)),
Err(e) => Err(eyre::eyre!(
"Cannot open {}: {}. VM backends require read/write access to KVM.",
KVM_DEVICE,
e
)),
}
}
#[cfg(all(not(target_os = "linux"), feature = "libkrun"))]
#[allow(dead_code)]
pub fn ensure_linux_kvm_ready_for_vm() -> Result<()> {
Ok(())
}
#[allow(dead_code)]
const LIBKRUN_KERNEL_LOAD_MIB: u32 = 512;
#[allow(dead_code)]
const LIBKRUN_MEMORY_SLACK_MIB: u32 = 64;
#[allow(dead_code)]
pub fn round_up_vm_memory_for_libkrun(requested_mib: u32, kernel_path: &str) -> u32 {
let kernel_size_mib = lfs::metadata_on_host(kernel_path)
.ok()
.map(|m| (m.len() as u32 + (1024 * 1024) - 1) / (1024 * 1024))
.unwrap_or(128);
let min_mib = LIBKRUN_KERNEL_LOAD_MIB
.saturating_add(kernel_size_mib)
.saturating_add(LIBKRUN_MEMORY_SLACK_MIB);
std::cmp::max(requested_mib, min_mib)
}
#[cfg(target_os = "linux")]
#[allow(dead_code)]
pub fn privdrop_on_suid() {
if is_suid() {
setuid(Uid::from_raw(get_current_uid())).expect("Failed to drop privileges");
}
}
#[cfg(target_os = "linux")]
fn kill_child_on_timeout(child: nix::unistd::Pid, cmd_path: &Path, timeout: u64) -> Result<()> {
warn!("Command '{}' timed out after {} seconds, killing child process", cmd_path.display(), timeout);
if let Err(e) = signal::kill(child, Signal::SIGTERM) {
warn!("Failed to send SIGTERM to child {}: {}", child, e);
}
std::thread::sleep(Duration::from_millis(100));
match nix::sys::wait::waitpid(child, Some(nix::sys::wait::WaitPidFlag::WNOHANG)) {
Ok(nix::sys::wait::WaitStatus::StillAlive) => {
if let Err(e) = signal::kill(child, Signal::SIGKILL) {
warn!("Failed to send SIGKILL to child {}: {}", child, e);
}
}
_ => {
}
}
let _ = nix::sys::wait::waitpid(child, None);
Err(eyre::eyre!("Command '{}' timed out after {} seconds", cmd_path.display(), timeout))
}
#[cfg(target_os = "linux")]
fn handle_wait_status(wait_status: nix::sys::wait::WaitStatus, cmd_path: &Path, run_options: &RunOptions) -> Result<()> {
use nix::sys::wait::WaitStatus;
match wait_status {
WaitStatus::Exited(_, exit_code) => {
if exit_code != 0 {
if run_options.no_exit {
eprintln!("Command '{}' exited with code {} (no_exit=true, continuing)", cmd_path.display(), exit_code);
} else {
warn!("Child process exited with code {} (cmd: {})", exit_code, cmd_path.display());
std::process::exit(exit_code);
}
}
Ok(())
}
WaitStatus::Signaled(_, signal, _) => {
if signal == Signal::SIGPIPE {
debug!("Child process terminated by SIGPIPE (broken pipe) - treating as normal exit (cmd: {})", cmd_path.display());
Ok(())
} else {
debug!("Child process killed by signal {:?} (cmd: {})", signal, cmd_path.display());
Err(eyre::eyre!("Command killed by signal {:?}", signal))
}
}
_ => {
debug!("Child process ended with status: {:?} (cmd: {})", wait_status, cmd_path.display());
Err(eyre::eyre!("Command ended with unexpected status: {:?}", wait_status))
}
}
}
#[cfg(target_os = "linux")]
fn wait_for_child_with_timeout_polling(child: nix::unistd::Pid, cmd_path: &Path, run_options: &RunOptions, timeout_duration: Duration) -> Result<()> {
let start_time = Instant::now();
loop {
if start_time.elapsed() >= timeout_duration {
return kill_child_on_timeout(child, cmd_path, run_options.timeout);
}
match nix::sys::wait::waitpid(child, Some(nix::sys::wait::WaitPidFlag::WNOHANG)) {
Ok(wait_status) => {
use nix::sys::wait::WaitStatus;
match wait_status {
WaitStatus::StillAlive => {
std::thread::sleep(Duration::from_millis(100));
continue;
}
_ => {
return handle_wait_status(wait_status, cmd_path, run_options);
}
}
}
Err(nix::errno::Errno::ECHILD) => {
break;
}
Err(e) => {
return Err(eyre::eyre!("Failed to wait for child process (cmd: {}): {}", cmd_path.display(), e));
}
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn wait_for_child_with_timeout(child: nix::unistd::Pid, cmd_path: &Path, run_options: &RunOptions) -> Result<()> {
trace!("Parent process waiting for child {} (cmd: {})", child, cmd_path.display());
if run_options.timeout > 0 {
let timeout_duration = Duration::from_secs(run_options.timeout);
wait_for_child_with_timeout_polling(child, cmd_path, run_options, timeout_duration)
} else {
match nix::sys::wait::waitpid(child, None) {
Ok(wait_status) => {
handle_wait_status(wait_status, cmd_path, run_options)
}
Err(e) => {
Err(eyre::eyre!("Failed to wait for child process (cmd: {}): {}", cmd_path.display(), e))
}
}
}
}
#[cfg(target_os = "linux")]
fn resolve_command_path(env_root: &Path, run_options: &RunOptions) -> Result<PathBuf> {
let cmd_path = PathBuf::from(&run_options.command);
if cmd_path.starts_with(env_root) {
debug!("Command {} is already under env_root, using directly", cmd_path.display());
return Ok(cmd_path);
}
if cmd_path.is_absolute() {
let relative_path = run_options.command.trim_start_matches('/');
let env_cmd_path = env_root.join(relative_path);
if lfs::exists_in_env(&env_cmd_path) {
debug!("Resolved absolute path {} to {}", cmd_path.display(), env_cmd_path.display());
return Ok(env_cmd_path);
}
}
if run_options.command.contains('/') && !cmd_path.is_absolute() {
return Ok(cmd_path);
}
find_command_in_env_path(&run_options.command, env_root)
}
#[cfg(not(target_os = "linux"))]
fn resolve_command_path(env_root: &Path, run_options: &RunOptions) -> Result<PathBuf> {
let is_vm_mode = run_options.effective_sandbox.isolate_mode == Some(IsolateMode::Vm);
let is_unix_absolute = run_options.command.starts_with('/');
if is_unix_absolute {
let cmd_path = PathBuf::from(&run_options.command);
if cmd_path.starts_with(env_root) {
debug!("Command {} is already under env_root, using directly", cmd_path.display());
return Ok(cmd_path);
}
if cmd_path.exists() {
debug!("Command {} exists on host, using directly", cmd_path.display());
return Ok(cmd_path);
}
let relative_path = run_options.command.trim_start_matches('/');
let cmd_path = env_root.join(relative_path);
#[cfg(windows)]
{
if !cmd_path.exists() && !run_options.command.ends_with(".exe") {
let cmd_with_exe = env_root.join(format!("{}.exe", relative_path));
if cmd_with_exe.exists() {
return Ok(cmd_with_exe);
}
}
}
let exists = if is_vm_mode {
lfs::exists_in_env(&cmd_path)
} else {
cmd_path.exists()
};
if exists {
return Ok(cmd_path);
}
if is_vm_mode {
debug!("VM mode: accepting Unix path {} (guest will resolve)", cmd_path.display());
return Ok(cmd_path);
}
return Err(eyre::eyre!(
"Command '{}' not found at {}",
run_options.command,
cmd_path.display()
));
}
if Path::new(&run_options.command).is_absolute() {
return Ok(PathBuf::from(&run_options.command));
}
if run_options.command.contains('/') {
let cmd_path = env_root.join(&run_options.command);
#[cfg(windows)]
{
if !cmd_path.exists() && !run_options.command.ends_with(".exe") {
let cmd_with_exe = env_root.join(format!("{}.exe", &run_options.command));
if cmd_with_exe.exists() {
return Ok(cmd_with_exe);
}
}
}
let exists = if is_vm_mode {
lfs::exists_in_env(&cmd_path)
} else {
cmd_path.exists()
};
if exists {
return Ok(cmd_path);
}
if is_vm_mode {
debug!("VM mode: accepting relative path {} (guest will resolve)", cmd_path.display());
return Ok(cmd_path);
}
}
#[cfg(windows)]
let cmd_with_exe: String;
#[cfg(windows)]
let cmd_names: Vec<&str> = if run_options.command.ends_with(".exe") {
vec![&run_options.command]
} else {
cmd_with_exe = format!("{}.exe", run_options.command);
vec![&run_options.command, &cmd_with_exe]
};
#[cfg(not(windows))]
let cmd_names: Vec<&str> = vec![&run_options.command];
for cmd_name in cmd_names {
let cmd_in_bin = env_root.join("bin").join(cmd_name);
if lfs::exists_in_env(&cmd_in_bin) {
return Ok(cmd_in_bin);
}
let cmd_in_usr_bin = crate::dirs::path_join(env_root, &["usr", "bin"]).join(cmd_name);
if lfs::exists_in_env(&cmd_in_usr_bin) {
return Ok(cmd_in_usr_bin);
}
let cmd_in_scripts = env_root.join("Scripts").join(cmd_name);
if cmd_in_scripts.exists() {
return Ok(cmd_in_scripts);
}
let cmd_in_library_bin = crate::dirs::path_join(env_root, &["Library", "bin"]).join(cmd_name);
if cmd_in_library_bin.exists() {
return Ok(cmd_in_library_bin);
}
let cmd_in_mingw = crate::dirs::path_join(env_root, &["Library", "mingw-w64", "bin"]).join(cmd_name);
if cmd_in_mingw.exists() {
return Ok(cmd_in_mingw);
}
let cmd_in_root = env_root.join(cmd_name);
if cmd_in_root.exists() {
return Ok(cmd_in_root);
}
for msys2_prefix in &["ucrt64", "mingw64", "mingw32", "clang64", "clang32", "clangarm64"] {
let cmd_in_msys2 = env_root.join(msys2_prefix).join("bin").join(cmd_name);
if cmd_in_msys2.exists() {
return Ok(cmd_in_msys2);
}
}
}
if is_vm_mode {
return Err(eyre::eyre!(
"Command '{}' not found in {} (checked: bin/, usr/bin/)",
run_options.command,
env_root.display()
));
}
if lfs::exists_on_host(Path::new(&run_options.command)) {
Ok(PathBuf::from(&run_options.command))
} else {
Err(eyre::eyre!("Command '{}' not found in {}", run_options.command, env_root.display()))
}
}
#[cfg(target_os = "linux")]
fn prepare_and_create_process(
env_root: &Path,
run_options: &RunOptions,
stdin_read_fd: Option<i32>,
vm_daemon_ready_fd: Option<std::os::fd::OwnedFd>,
) -> Result<(nix::unistd::Pid, PathBuf, ProcessCreationConfig)> {
let cmd_path = resolve_command_path(env_root, run_options)?;
let config = determine_process_config(env_root, run_options);
let context = build_unified_context(
env_root,
run_options,
&config,
cmd_path.clone(),
run_options.args.clone(),
stdin_read_fd,
vm_daemon_ready_fd,
)?;
let child_pid = create_process_with_namespaces(&config, context)?;
Ok((child_pid, cmd_path, config))
}
pub fn fork_and_execute(env_root: &Path, run_options: &RunOptions) -> Result<Option<i32>> {
let mut prepared_opts = run_options.clone();
prepare_run_options_for_command(env_root, &mut prepared_opts);
if !prepared_opts.env_vars.contains_key("EPKG_CACHE") {
let dirs = crate::models::dirs();
prepared_opts.env_vars.insert("EPKG_CACHE".to_string(), dirs.home_cache.to_string_lossy().to_string());
}
let isolate_mode = prepared_opts.effective_sandbox.isolate_mode
.unwrap_or(IsolateMode::Env);
debug!("fork_and_execute: env_root={}, isolate_mode={:?}, skip_namespace={}",
env_root.display(), isolate_mode, prepared_opts.skip_namespace_isolation);
match isolate_mode {
IsolateMode::Vm => {
crate::debug_epkg!("fork_and_execute: starting for VM mode");
#[cfg(target_os = "linux")]
if crate::busybox::is_inside_vm() {
return Err(eyre::eyre!(
"Nested VM not supported. Already running inside a VM or container.\n\
VM sandbox (--isolate=vm) cannot be used inside WSL2, VirtualBox,\n\
cloud VMs, or other virtualized environments."
));
}
#[cfg(target_os = "linux")]
{
let resolved_cmd_path = resolve_command_path(env_root, &prepared_opts)?;
let guest_cmd_path = if resolved_cmd_path.starts_with(env_root) {
let stripped = resolved_cmd_path.strip_prefix(env_root).unwrap_or(&resolved_cmd_path);
std::path::Path::new("/").join(stripped)
} else {
resolved_cmd_path.clone()
};
let mut cmd_parts = vec![guest_cmd_path.to_string_lossy().to_string()];
cmd_parts.extend(prepared_opts.args.clone());
let cwd_str;
let cwd = if prepared_opts.chdir_to_env_root {
Some("/")
} else {
cwd_str = std::env::current_dir().ok().map(|p| p.to_string_lossy().to_string());
cwd_str.as_deref()
};
if let Some(exit_code) = crate::vm::client::try_execute_via_existing_vm_session(
&cmd_parts,
prepared_opts.io_mode,
Some(&prepared_opts.env_vars),
cwd,
)? {
log::info!("run: reused existing VM session, exit_code={}", exit_code);
if exit_code == 0 {
return Ok(None);
} else {
return Err(eyre::eyre!("Command exited with code {} in reused VM session", exit_code));
}
}
}
#[cfg(all(feature = "libkrun", not(target_os = "linux")))]
if let Some(exit_code) = try_connect_and_execute_vm(env_root, &prepared_opts)? {
log::info!("run: reused existing VM session, exit_code={}", exit_code);
if exit_code == 0 {
return Ok(None);
} else {
return Err(eyre::eyre!("Command exited with code {} in reused VM session", exit_code));
}
}
#[cfg(target_os = "linux")]
{
crate::debug_epkg!("fork_and_execute: VM mode using namespace path");
return fork_and_execute_raw(env_root, &prepared_opts);
}
#[cfg(all(feature = "libkrun", not(target_os = "linux")))]
{
crate::debug_epkg!("fork_and_execute: VM mode using direct libkrun path");
let guest_cmd_path = PathBuf::from(&prepared_opts.command);
crate::libkrun::run_command_in_krun(env_root, &prepared_opts, &guest_cmd_path)?;
return Ok(None);
}
#[cfg(not(any(target_os = "linux", feature = "libkrun")))]
{
return Err(eyre::eyre!(
"VM sandbox requires libkrun feature. \
Recompile epkg with libkrun support for --isolate=vm"
));
}
}
IsolateMode::Env | IsolateMode::Fs => {
if prepared_opts.skip_namespace_isolation {
fork_and_execute_direct(env_root, &prepared_opts)
} else {
fork_and_execute_raw(env_root, &prepared_opts)
}
}
}
}
#[cfg(all(not(target_os = "linux"), windows))]
fn conda_windows_path_env(env_root: &Path) -> String {
let library_bin = env_root.join("Library").join("bin");
let mingw_bin = env_root.join("Library").join("mingw-w64").join("bin");
let scripts_bin = env_root.join("Scripts");
let usr_bin = env_root.join("usr").join("bin");
let bin_dir = env_root.join("bin");
let mut path_dirs = vec![
env_root.display().to_string(),
bin_dir.display().to_string(),
usr_bin.display().to_string(),
scripts_bin.display().to_string(),
library_bin.display().to_string(),
mingw_bin.display().to_string(),
"C:\\Windows\\System32".to_string(),
"C:\\Windows".to_string(),
];
path_dirs.push(std::env::var("PATH").unwrap_or_default());
path_dirs.join(";")
}
#[cfg(all(not(target_os = "linux"), windows))]
fn msys2_pacman_path_env(env_root: &Path) -> String {
let bin_dir = env_root.join("bin");
let usr_bin = env_root.join("usr").join("bin");
let original_path = std::env::var("PATH").unwrap_or_default();
[
bin_dir.display().to_string(),
usr_bin.display().to_string(),
original_path,
]
.join(";")
}
fn fork_and_execute_direct(env_root: &Path, run_options: &RunOptions) -> Result<Option<i32>> {
use std::process::{Command, Stdio};
let cmd_path = resolve_command_path(env_root, run_options)?;
let cmd_path = if cmd_path.is_absolute() && !cmd_path.starts_with(env_root) && !cmd_path.exists() {
let host_path = env_root.join(cmd_path.strip_prefix("/").unwrap_or(&cmd_path));
debug!("Converting guest path {} to host path {}", cmd_path.display(), host_path.display());
host_path
} else {
cmd_path
};
debug!("Running command directly on host: {}", cmd_path.display());
debug!("Args: {:?}", run_options.args);
let mut cmd = Command::new(&cmd_path);
cmd.args(&run_options.args);
let mut env_vars = run_options.env_vars.clone();
let channel_configs = crate::io::deserialize_channel_config_from_root(&env_root.to_path_buf())
.unwrap_or_default();
let ch = channel_configs.first();
let channel_format = ch.map(|c| c.format).unwrap_or(crate::models::PackageFormat::Apk);
#[cfg(windows)]
let distro = ch.map(|c| c.distro.clone()).unwrap_or_default();
if channel_format == crate::models::PackageFormat::Conda {
env_vars.insert("CONDA_PREFIX".to_string(), env_root.display().to_string());
#[cfg(windows)]
env_vars.insert("PATH".to_string(), conda_windows_path_env(env_root));
}
#[cfg(windows)]
if channel_format == crate::models::PackageFormat::Pacman && distro == "msys2" {
env_vars.insert("PATH".to_string(), msys2_pacman_path_env(env_root));
}
if channel_format == crate::models::PackageFormat::Brew {
let usr_local_bin_path = env_root.join("usr/local/bin");
let bin_path = env_root.join("bin");
let ebin_path = env_root.join("ebin");
let current_path = std::env::var("PATH").unwrap_or_default();
let mut new_path = String::new();
if usr_local_bin_path.exists() {
new_path.push_str(&format!("{}:", usr_local_bin_path.display()));
debug!("Added usr/local/bin to PATH: {}", usr_local_bin_path.display());
}
if bin_path.exists() {
new_path.push_str(&format!("{}:", bin_path.display()));
debug!("Added bin to PATH: {}", bin_path.display());
}
if ebin_path.exists() {
new_path.push_str(&format!("{}:", ebin_path.display()));
debug!("Added ebin to PATH: {}", ebin_path.display());
}
new_path.push_str(¤t_path);
env_vars.insert("PATH".to_string(), new_path);
}
for (key, value) in &env_vars {
cmd.env(key, value);
}
if run_options.chdir_to_env_root {
cmd.current_dir(env_root);
}
if run_options.stdin.is_some() {
cmd.stdin(Stdio::piped());
} else {
cmd.stdin(Stdio::inherit());
}
if run_options.redirect_stdio {
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
} else {
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
}
let mut child = cmd.spawn()
.map_err(|e| eyre::eyre!("Failed to spawn command '{}': {}", cmd_path.display(), e))?;
if let Some(stdin_data) = &run_options.stdin {
use std::io::Write;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(stdin_data)
.map_err(|e| eyre::eyre!("Failed to write to stdin: {}", e))?;
}
}
if run_options.background {
let pid = child.id() as i32;
debug!("Background process started with PID: {}", pid);
Ok(Some(pid))
} else {
let result = if run_options.timeout > 0 {
let timeout_duration = std::time::Duration::from_secs(run_options.timeout);
let start_time = std::time::Instant::now();
let mut status_opt = None;
while start_time.elapsed() < timeout_duration {
match child.try_wait() {
Ok(Some(status)) => {
status_opt = Some(status);
break;
}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) => {
return Err(eyre::eyre!("Failed to check child status: {}", e));
}
}
}
if let Some(status) = status_opt {
status
} else {
let _ = child.kill();
return Err(eyre::eyre!(
"Command '{}' timed out after {} seconds",
cmd_path.display(),
run_options.timeout
));
}
} else {
child.wait()
.map_err(|e| eyre::eyre!("Failed to wait for child: {}", e))?
};
if let Some(code) = result.code() {
if code != 0 && !run_options.no_exit {
std::process::exit(code);
}
}
Ok(None)
}
}
#[cfg(target_os = "linux")]
fn fork_and_execute_raw(env_root: &Path, run_options: &RunOptions) -> Result<Option<i32>> {
let stdin_bytes = run_options.stdin.as_ref().map(|v| v.as_slice());
let (mut stdin_read_fd_opt, stdin_write_fd_opt) = create_stdin_pipe_if_needed(run_options)?;
let (vm_daemon_ready_read_fd, vm_daemon_ready_write_fd) = if run_options.vm_daemon {
let (read_fd, write_fd) = nix::unistd::pipe()?;
log::debug!("fork_and_execute_raw: vm_daemon mode - created ready pipe read={}, write={}",
read_fd.as_raw_fd(), write_fd.as_raw_fd());
(Some(read_fd), Some(write_fd))
} else {
(None, None)
};
let (child_pid, cmd_path, _config) = prepare_and_create_process(
env_root,
run_options,
stdin_read_fd_opt.as_ref().map(|fd| fd.as_raw_fd()),
vm_daemon_ready_write_fd,
)?;
if let (Some(bytes), Some(write_fd)) = (stdin_bytes, stdin_write_fd_opt) {
if let Some(read_fd) = stdin_read_fd_opt.take() {
if let Err(e) = close(read_fd) {
trace!("Failed to close child stdin read fd in parent: {}", e);
}
}
with_sigpipe_handler(libc::SIG_IGN, move || {
let mut written = 0;
while written < bytes.len() {
match write(&write_fd, &bytes[written..]) {
Ok(0) => break,
Ok(n) => written += n,
Err(e) => {
if e == Errno::EPIPE {
break;
}
let _ = close(write_fd);
return Err(eyre::eyre!("Failed to write to child stdin: {}", e));
}
}
}
let _ = close(write_fd);
Ok(())
})?;
}
if let Some(read_fd) = vm_daemon_ready_read_fd {
log::info!("fork_and_execute_raw: vm_daemon mode - parent PID {} waiting for VM ready signal, child_pid={}",
std::process::id(), child_pid);
let mut buf = [0u8; 6];
match nix::unistd::read(&read_fd, &mut buf) {
Ok(n) if n >= 5 && &buf[0..5] == b"READY" => {
log::info!("fork_and_execute_raw: vm_daemon mode - parent PID {} received VM ready signal",
std::process::id());
}
Ok(0) => {
log::error!("fork_and_execute_raw: vm_daemon mode - child exited before VM ready");
match nix::sys::wait::waitpid(child_pid, Some(nix::sys::wait::WaitPidFlag::WNOHANG)) {
Ok(nix::sys::wait::WaitStatus::Exited(_, exit_code)) => {
return Err(eyre::eyre!("VM daemon child exited with code {} before VM was ready", exit_code));
}
Ok(nix::sys::wait::WaitStatus::Signaled(_, signal, _)) => {
return Err(eyre::eyre!("VM daemon child killed by signal {} before VM was ready", signal));
}
Ok(_) => {
return Err(eyre::eyre!("VM daemon child closed pipe without sending READY"));
}
Err(_) => {
return Err(eyre::eyre!("VM daemon child failed and pipe closed"));
}
}
}
Ok(n) => {
log::warn!("fork_and_execute_raw: vm_daemon mode - received unexpected signal: {} bytes", n);
return Err(eyre::eyre!("VM daemon child sent unexpected response: {} bytes", n));
}
Err(e) => {
log::error!("fork_and_execute_raw: vm_daemon mode - failed to read ready signal: {}", e);
return Err(eyre::eyre!("Failed to read VM ready signal: {}", e));
}
}
}
if run_options.background {
Ok(Some(child_pid.as_raw() as i32))
} else {
wait_for_child_with_timeout(child_pid, &cmd_path, run_options)?;
Ok(None)
}
}
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)]
fn fork_and_execute_raw(_env_root: &Path, _run_options: &RunOptions) -> Result<Option<i32>> {
use color_eyre::eyre;
Err(eyre::eyre!("fork_and_execute_raw not implemented for this platform"))
}
#[cfg(unix)]
pub fn is_executable(path: &Path) -> Result<bool> {
trace!("is_executable checking: {}", path.display());
let metadata = lfs::symlink_metadata(path)
.map_err(|e| {
trace!("is_executable metadata error for {}: {}", path.display(), e);
eyre::eyre!("Failed to get metadata for {}: {}", path.display(), e)
})?;
let permissions = metadata.permissions();
let executable = permissions.mode() & 0o111 != 0;
trace!("is_executable result for {}: {}", path.display(), executable);
Ok(executable)
}
#[cfg(unix)]
fn is_symlink_to_multicall_binary(path: &Path) -> bool {
if !lfs::is_symlink(path) {
return false;
}
let link_target = std::fs::read_link(path).unwrap_or_default();
let target_name = link_target
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
["epkg", "coreutils", "busybox", "toybox"].contains(&target_name)
}
#[cfg(unix)]
fn is_executable_within_env(path: &Path, env_root: &Path) -> Result<Option<PathBuf>> {
trace!("is_executable_within_env checking: {}", path.display());
match lfs::resolve_symlink_in_env(path, env_root) {
Some(resolved) => {
trace!("Resolved {} -> {}", path.display(), resolved.display());
if is_executable(&resolved)? {
if is_symlink_to_multicall_binary(path) {
Ok(Some(path.to_path_buf()))
} else {
Ok(Some(resolved))
}
} else {
Ok(None)
}
}
None => {
trace!("Path {} cannot be resolved within environment root", path.display());
Ok(None)
}
}
}
#[cfg(unix)]
pub fn find_command_in_env_path(cmd_name: &str, env_root: &Path) -> Result<PathBuf> {
let is_brew_env = is_brew_environment(env_root);
let path_str = env::var("PATH").unwrap_or_default();
let mut dirs: Vec<&str> = path_str.split(':').filter(|d| !d.is_empty()).collect();
if dirs.is_empty() {
dirs.extend(["/usr/bin", "/bin", "/usr/sbin", "/sbin"]);
}
let brew_paths = if is_brew_env {
vec!["bin", "libexec/bin"]
} else {
vec![]
};
for subdir in &brew_paths {
let cmd_path = env_root.join(subdir).join(cmd_name);
trace!("find_command_in_env_path: checking brew path {:?}", cmd_path);
if let Some(resolved_path) = is_executable_within_env(&cmd_path, env_root)? {
if resolved_path.starts_with(env_root) {
let guest_rel = resolved_path.strip_prefix(env_root).unwrap_or(&resolved_path);
let guest_rel_normalized = crate::lfs::normalize_path_components(guest_rel);
let homebrew_prefix = crate::brew_pkg::prefix::preferred_path();
let guest_path = homebrew_prefix.join(guest_rel_normalized);
return Ok(guest_path);
}
}
}
for path_dir in dirs {
trace!("find_command_in_env_path: checking path_dir={}", path_dir);
if path_dir.ends_with("/ebin") {
continue;
}
let (cmd_path, _is_guest_path) = if is_brew_env {
let homebrew_prefix = crate::brew_pkg::prefix::preferred_path();
let homebrew_prefix_str = homebrew_prefix.to_string_lossy();
if path_dir.starts_with(&*homebrew_prefix_str) {
let rel = path_dir.strip_prefix(&*homebrew_prefix_str).unwrap_or(path_dir);
let host_path = env_root.join(rel);
trace!("find_command_in_env_path: converted guest path {} to host path {}", path_dir, host_path.display());
(host_path.join(cmd_name), true)
} else {
let rel_path = path_dir.strip_prefix("/").unwrap_or(path_dir);
(env_root.join(rel_path).join(cmd_name), false)
}
} else {
let rel_path = path_dir.strip_prefix("/").unwrap_or(path_dir);
(env_root.join(rel_path).join(cmd_name), false)
};
trace!("find_command_in_env_path: cmd_path={:?}", cmd_path);
if let Some(resolved_path) = is_executable_within_env(&cmd_path, env_root)? {
if resolved_path.starts_with(env_root) {
let guest_rel = resolved_path.strip_prefix(env_root).unwrap_or(&resolved_path);
let guest_rel_normalized = crate::lfs::normalize_path_components(guest_rel);
if is_brew_env {
let homebrew_prefix = crate::brew_pkg::prefix::preferred_path();
return Ok(homebrew_prefix.join(guest_rel_normalized));
} else {
return Ok(PathBuf::from("/").join(guest_rel_normalized));
}
} else {
let guest_rel = cmd_path.strip_prefix(env_root).unwrap_or(&cmd_path);
let guest_rel_normalized = crate::lfs::normalize_path_components(guest_rel);
if is_brew_env {
let homebrew_prefix = crate::brew_pkg::prefix::preferred_path();
return Ok(homebrew_prefix.join(guest_rel_normalized));
} else {
return Ok(PathBuf::from("/").join(guest_rel_normalized));
}
}
}
}
Err(eyre::eyre!("Command '{}' not found in environment PATH under {}", cmd_name, env_root.display()))
}
#[cfg(unix)]
pub fn is_brew_environment(env_root: &Path) -> bool {
let channel_configs = crate::io::deserialize_channel_config_from_root(&env_root.to_path_buf())
.unwrap_or_default();
channel_configs.first()
.map(|c| c.format == crate::models::PackageFormat::Brew)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
pub fn host_uses_traditional_layout() -> bool {
let lib_path = Path::new("/lib");
if let Ok(metadata) = fs::symlink_metadata(lib_path) {
return metadata.file_type().is_dir();
}
true
}
fn merge_sandbox_options(sources: &[&crate::models::SandboxOptions]) -> crate::models::SandboxOptions {
let mut result = crate::models::SandboxOptions::default();
for source in sources {
if let Some(mode) = source.isolate_mode {
result.isolate_mode = Some(mode);
}
if let Some(strategy) = source.namespace_strategy {
result.namespace_strategy = Some(strategy);
}
result.mount_specs
.extend(source.mount_specs.iter().cloned());
}
result
}
fn prepare_run_options_for_command(env_root: &Path, run_options: &mut RunOptions) {
let config_guard = config();
let sources = vec![
&config_guard.sandbox,
&env_config().sandbox,
&run_options.sandbox,
];
run_options.effective_sandbox = merge_sandbox_options(&sources);
if run_options.effective_sandbox.isolate_mode.is_none() {
run_options.effective_sandbox.isolate_mode = Some(crate::models::IsolateMode::Env);
}
if run_options.effective_sandbox.isolate_mode == Some(crate::models::IsolateMode::Env)
&& !crate::init::host_lib64_symlink_ok()
{
debug!("Host /lib64 symlink missing, falling back from Env mode to Fs mode");
run_options.effective_sandbox.isolate_mode = Some(crate::models::IsolateMode::Fs);
}
let channel_configs = crate::io::deserialize_channel_config_from_root(&env_root.to_path_buf())
.unwrap_or_default();
let ch = channel_configs.first();
let (channel_format, distro) = ch.map(|c| (c.format, c.distro.clone()))
.unwrap_or((crate::models::PackageFormat::Apk, "alpine".to_string()));
let is_conda = channel_format == crate::models::PackageFormat::Conda;
let _is_brew = channel_format == crate::models::PackageFormat::Brew;
let is_msys2 = channel_format == crate::models::PackageFormat::Pacman && distro == "msys2";
let is_linux_format = is_linux_package_format(channel_format, &distro);
#[cfg(target_os = "macos")]
if _is_brew {
run_options.skip_namespace_isolation = true;
log::debug!("Brew packages on macOS: skipping namespace isolation, running directly");
}
if is_conda || is_msys2 {
run_options.skip_namespace_isolation = true;
}
#[cfg(not(target_os = "linux"))]
if is_linux_format && run_options.sandbox.isolate_mode.is_none() {
debug!("Auto-enabling VM sandbox for Linux package format: {:?}/{}",
channel_format, distro);
run_options.effective_sandbox.isolate_mode = Some(IsolateMode::Vm);
}
#[cfg(target_os = "linux")]
let _ = is_linux_format;
if config_guard.common.in_env_root {
if env_root.as_os_str() == "/" {
debug!("Running from current in_env_root environment, skipping namespace isolation");
run_options.skip_namespace_isolation = true;
} else {
debug!("Running from different environment (in_env_root=true, env_root={}), enabling namespace isolation",
env_root.display());
}
} else if env_root.as_os_str() == "/" {
run_options.skip_namespace_isolation = true;
}
if std::env::var("EPKG_SKIP_NAMESPACE").is_ok() {
run_options.skip_namespace_isolation = true;
}
}
fn is_linux_package_format(format: crate::models::PackageFormat, distro: &str) -> bool {
use crate::models::PackageFormat;
match format {
PackageFormat::Deb |
PackageFormat::Rpm |
PackageFormat::Apk => true,
PackageFormat::Pacman => {
distro != "msys2"
}
PackageFormat::Epkg |
PackageFormat::Conda |
PackageFormat::Brew |
PackageFormat::Python => false,
}
}
#[cfg(target_os = "linux")]
fn create_stdin_pipe_if_needed(run_options: &RunOptions) -> Result<(Option<OwnedFd>, Option<OwnedFd>)> {
if let Some(_) = &run_options.stdin {
let (read_fd, write_fd) = pipe()
.map_err(|e| eyre::eyre!("Failed to create stdin pipe: {}", e))?;
Ok((Some(read_fd), Some(write_fd)))
} else {
Ok((None, None))
}
}
#[cfg(target_os = "linux")]
pub fn command_run(_sub_matches: &clap::ArgMatches) -> Result<()> {
let run_options = config().run.clone();
debug!("Running command: {} with args: {:?}", run_options.command, run_options.args);
debug!("Sandbox input: {:?}, User: {:?}", run_options.sandbox, run_options.user);
let env_root = crate::dirs::get_default_env_root()?;
info!("Using environment root: {}", env_root.display());
fork_and_execute(&env_root, &run_options)?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn command_run(_sub_matches: &clap::ArgMatches) -> Result<()> {
let run_options = config().run.clone();
debug!("Running command: {} with args: {:?}", run_options.command, run_options.args);
debug!("Sandbox input: {:?}", run_options.sandbox);
let env_root = crate::dirs::get_default_env_root()?;
debug!("Using environment root: {}", env_root.display());
fork_and_execute(&env_root, &run_options)?;
Ok(())
}
pub fn command_busybox(sub_matches: &clap::ArgMatches) -> Result<()> {
if sub_matches.get_flag("list") {
println!("{}", crate::busybox::sorted_applet_names().join("\n"));
return Ok(());
}
* - Some((cmd_name, cmd_matches)): A subcommand was specified
* - None: No subcommand specified (error case)
*/
match sub_matches.subcommand() {
Some((cmd_name, cmd_matches)) => {
let known = crate::busybox::busybox_subcommands()
.iter()
.any(|c| c.get_name() == cmd_name);
if known {
debug!("Running built-in command: {}", cmd_name);
let applet_cmd = crate::busybox::busybox_subcommands()
.into_iter()
.find(|c| c.get_name() == cmd_name)
.expect("Applet command should exist");
if let Some(raw_args) = cmd_matches.get_raw("") {
* - Arguments arrive as raw OsString values (key "" in matches)
* - We need to re-parse them using the applet's command parser
* - This avoids option name conflicts with global epkg options
*/
let args_vec: Vec<std::ffi::OsString> = raw_args.map(|s| s.to_os_string()).collect();
debug!("Parsing external args for {}: {:?}", cmd_name, args_vec);
let mut all_args = vec![std::ffi::OsString::from("epkg")];
all_args.extend(args_vec.clone());
match applet_cmd.clone().try_get_matches_from(all_args) {
Ok(parsed_matches) => {
crate::busybox::exec_builtin_command(cmd_name, &parsed_matches)
}
Err(e) => {
let args_display: Vec<String> = args_vec.iter().map(|a| a.to_string_lossy().into_owned()).collect();
let cmdline = if args_display.is_empty() {
format!("epkg busybox {}", cmd_name)
} else {
format!("epkg busybox {} {}", cmd_name, args_display.join(" "))
};
crate::utils::handle_clap_error_with_cmdline(e, cmdline);
}
}
} else {
* - Applet subcommand is registered directly under busybox
* - Arguments are already parsed by clap
* - This mode would cause option name conflicts if used
*/
crate::busybox::exec_builtin_command(cmd_name, cmd_matches)
}
} else {
* - Command name doesn't match any registered applet
* - Print error and exit with busybox-style exit code (127)
*/
eprintln!("{}: applet not found", cmd_name);
std::process::exit(127);
}
}
None => {
* - User ran `epkg busybox` without an applet name
* - Return error (clap should have prevented this with arg_required_else_help)
*/
Err(eyre::eyre!("No command specified"))
}
}
}
pub fn parse_options_run(options: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
let mut mount_specs = Vec::new();
if let Some(values) = sub_matches.get_many::<String>("mount") {
mount_specs.extend(values.cloned());
}
let user = sub_matches.get_one::<String>("user").cloned();
let command = sub_matches.get_one::<String>("command")
.ok_or_else(|| eyre::eyre!("Command is required"))?
.clone();
if command.starts_with('-') {
return Err(eyre::eyre!(
"Command looks like an option ('{}'). Put the command after options, e.g. \
epkg run --isolate=vm --vmm=qemu --io=stream -- whoami \
or epkg run whoami --isolate=vm --vmm=qemu",
command
));
}
let args: Vec<String> = if let Some(args_iter) = sub_matches.get_many::<String>("args") {
args_iter.cloned().collect()
} else {
Vec::new()
};
let timeout = if let Some(timeout_str) = sub_matches.get_one::<String>("timeout") {
timeout_str.parse::<u64>()
.map_err(|e| eyre::eyre!("Invalid timeout value '{}': {}", timeout_str, e))?
} else {
0
};
let kernel = sub_matches
.get_one::<String>("kernel")
.cloned();
let vm_cpus = if let Some(cpus_str) = sub_matches.get_one::<String>("cpus") {
Some(
cpus_str
.parse::<u8>()
.map_err(|e| eyre::eyre!("Invalid --cpus value '{}': {}", cpus_str, e))?,
)
} else {
None
};
let vm_memory_mib = if let Some(mem_str) = sub_matches.get_one::<String>("memory") {
let mib = if let Some(bytes) = crate::utils::parse_size_bytes_opt(mem_str) {
(bytes / (1024 * 1024)) as u32
} else {
mem_str.parse::<u32>().map_err(|e| {
eyre::eyre!(
"Invalid --memory value '{}': {} (expected size like 4096M or MiB integer)",
mem_str,
e
)
})?
};
Some(mib)
} else {
None
};
let kernel_args = sub_matches
.get_one::<String>("kernel-args")
.cloned();
let initrd = sub_matches
.get_one::<String>("initrd")
.cloned();
let vmm_order = sub_matches
.get_one::<String>("vmm")
.map(|s| {
s.split(',')
.map(|part| part.trim().to_lowercase())
.filter(|part| !part.is_empty())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let isolate_mode = sub_matches
.get_one::<String>("isolate")
.map(|s| s.parse::<crate::models::IsolateMode>().expect("clap validates env|fs|vm"));
let namespace_strategy = sub_matches
.get_one::<String>("namespace-strategy")
.map(|s| match s.as_str() {
"clone" => crate::models::NamespaceStrategy::Clone,
"unshare" => crate::models::NamespaceStrategy::Unshare,
_ => unreachable!("clap validates clone|unshare"),
});
let io_mode = sub_matches
.get_one::<String>("io")
.map(|s| s.parse::<crate::models::IoMode>().expect("clap validates auto|tty|stream|batch"))
.unwrap_or_default();
let vm_keep_timeout = sub_matches
.get_one::<u32>("vm-keep-timeout")
.copied();
if vm_keep_timeout.is_some() {
let vm_mode = isolate_mode == Some(crate::models::IsolateMode::Vm);
if !vm_mode {
return Err(eyre::eyre!(
"--vm-keep-timeout requires --isolate=vm"
));
}
}
let translate_uid: Vec<String> = sub_matches
.get_many::<String>("translate-uid")
.map(|v| v.cloned().collect())
.unwrap_or_default();
let translate_gid: Vec<String> = sub_matches
.get_many::<String>("translate-gid")
.map(|v| v.cloned().collect())
.unwrap_or_default();
if (!translate_uid.is_empty() || !translate_gid.is_empty()) && isolate_mode != Some(crate::models::IsolateMode::Vm) {
return Err(eyre::eyre!(
"--translate-uid and --translate-gid require --isolate=vm"
));
}
let sandbox = crate::models::SandboxOptions {
isolate_mode,
namespace_strategy,
mount_specs,
};
options.run = RunOptions {
user,
command,
args,
timeout,
kernel,
vm_cpus,
vm_memory_mib,
kernel_args,
initrd,
io_mode,
sandbox,
vmm_order,
vm_keep_timeout,
translate_uid,
translate_gid,
..Default::default()
};
Ok(())
}