From d64da0671b0ed602649e966f5a5835df80b9c1ec Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sun, 20 Sep 2026 22:37:11 +0200 Subject: [PATCH] fix: --kernel =PATH syntax, exit-code cleanup, bind warning in VM mode - --kernel took an optional value greedily, swallowing the DISTRO positional: 'ecr --kernel alpine' failed with a missing-argument error and 'ecr --kernel alpine -- cmd' pulled a garbage OCI ref. require =PATH syntax for the kernel path instead. - setup_namespaces returns the child's exit code (or 128+signal) instead of calling process::exit, so the extracted rootfs tempdir is cleaned up before exiting instead of leaking into /tmp. - warn that --bind/--bind-rw are ignored when booting with --kernel. --- src/cli.rs | 16 ++++++++++++---- src/main.rs | 26 +++++++++++++++++++++----- src/namespace.rs | 16 ++++++++-------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 488054e..40a4806 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,12 +33,20 @@ pub struct Args { #[arg(short = 'v', long)] pub verbose: bool, - /// Boot with QEMU system emulation (optionally specify kernel path, or omit to download default) + /// Boot with QEMU system emulation (optionally specify kernel path with =PATH, or omit to download default) /// /// Examples: - /// --kernel Download and use the default Alpine linux-virt kernel - /// --kernel ./vmlinuz Use a specific kernel file - #[arg(long, value_name = "KERNEL_PATH", num_args = 0..=1)] + /// --kernel Download and use the default Alpine linux-virt kernel + /// --kernel=./vmlinuz Use a specific kernel file + /// + /// The kernel path must use `=` syntax: with `--kernel ./vmlinuz` the + /// path would be parsed as the DISTRO argument. + #[arg( + long, + value_name = "KERNEL_PATH", + num_args = 0..=1, + require_equals = true + )] pub kernel: Option>, /// Memory size for QEMU VM (only used with --kernel, e.g., 512M, 2G) diff --git a/src/main.rs b/src/main.rs index 60283b2..7968c62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -130,11 +130,20 @@ fn main() -> Result<()> { extract_tarball(&cache_path, &rootfs)?; // Branch based on --kernel flag - // Option>: + // Option> (require_equals: the value must use =PATH syntax + // so it can never swallow the DISTRO positional): // None -> --kernel not specified, use namespace mode // Some(None) -> --kernel without path, download default kernel - // Some(Some(path)) -> --kernel /path/to/vmlinuz, use provided kernel + // Some(Some(path)) -> --kernel=/path/to/vmlinuz, use provided kernel if let Some(kernel_opt) = &args.kernel { + // VM mode boots an initramfs: host bind mounts are never applied + if !args.bind.is_empty() || !args.bind_rw.is_empty() { + eprintln!( + "Warning: --bind/--bind-rw are ignored with --kernel \ + (the VM boots from an initramfs, no host directories are mounted)" + ); + } + // QEMU system mode let kernel_path = match kernel_opt { Some(path) => { @@ -169,12 +178,19 @@ fn main() -> Result<()> { result } else { // Namespace/chroot mode - namespace_mode(args, rootfs, config) + let exit_code = namespace_mode(args, rootfs, config)?; + // Propagate the command's exit code, cleaning up the extracted rootfs + // first: process::exit does not run destructors. + drop(temp_dir); + if exit_code != 0 { + std::process::exit(exit_code); + } + Ok(()) } } -/// Run in namespace/chroot mode -fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result<()> { +/// Run in namespace/chroot mode, returning the command's exit code +fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result { // Check user namespace availability namespace::check_user_namespace()?; diff --git a/src/namespace.rs b/src/namespace.rs index 9a6d618..5c20665 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -58,8 +58,10 @@ pub fn check_user_namespace() -> Result<()> { Ok(()) } -/// Setup namespaces and run the provided function inside them -pub fn setup_namespaces(f: F) -> Result<()> +/// Setup namespaces and run the provided function inside them. +/// Returns the child's exit code (0 on success, 128+signal when killed by a +/// signal). Setup failures are returned as Err. +pub fn setup_namespaces(f: F) -> Result where F: FnOnce() -> Result<()> + Send + 'static, { @@ -222,20 +224,18 @@ where let status = nix::sys::wait::waitpid(pid, None)?; match status { - nix::sys::wait::WaitStatus::Exited(_, 0) => Ok(()), + nix::sys::wait::WaitStatus::Exited(_, 0) => Ok(0), nix::sys::wait::WaitStatus::Exited(_, code) => { // If the child reported an error (e.g., setup failure), return it. // Otherwise, just forward the exit code without an error message. if let Some(msg) = child_error { Err(anyhow!("{}", msg)) } else { - std::process::exit(code); + Ok(code) } } - nix::sys::wait::WaitStatus::Signaled(_, sig, _) => { - Err(anyhow!("Child process killed by signal {:?}", sig)) - } - _ => Ok(()), + nix::sys::wait::WaitStatus::Signaled(_, sig, _) => Ok(128 + sig as i32), + _ => Ok(0), } }