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.
This commit is contained in:
2026-09-20 22:37:11 +02:00
parent ef00776414
commit d64da0671b
3 changed files with 41 additions and 17 deletions
+12 -4
View File
@@ -33,12 +33,20 @@ pub struct Args {
#[arg(short = 'v', long)] #[arg(short = 'v', long)]
pub verbose: bool, 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: /// Examples:
/// --kernel Download and use the default Alpine linux-virt kernel /// --kernel Download and use the default Alpine linux-virt kernel
/// --kernel ./vmlinuz Use a specific kernel file /// --kernel=./vmlinuz Use a specific kernel file
#[arg(long, value_name = "KERNEL_PATH", num_args = 0..=1)] ///
/// 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<Option<PathBuf>>, pub kernel: Option<Option<PathBuf>>,
/// Memory size for QEMU VM (only used with --kernel, e.g., 512M, 2G) /// Memory size for QEMU VM (only used with --kernel, e.g., 512M, 2G)
+21 -5
View File
@@ -130,11 +130,20 @@ fn main() -> Result<()> {
extract_tarball(&cache_path, &rootfs)?; extract_tarball(&cache_path, &rootfs)?;
// Branch based on --kernel flag // Branch based on --kernel flag
// Option<Option<PathBuf>>: // Option<Option<PathBuf>> (require_equals: the value must use =PATH syntax
// so it can never swallow the DISTRO positional):
// None -> --kernel not specified, use namespace mode // None -> --kernel not specified, use namespace mode
// Some(None) -> --kernel without path, download default kernel // 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 { 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 // QEMU system mode
let kernel_path = match kernel_opt { let kernel_path = match kernel_opt {
Some(path) => { Some(path) => {
@@ -169,12 +178,19 @@ fn main() -> Result<()> {
result result
} else { } else {
// Namespace/chroot mode // 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 /// Run in namespace/chroot mode, returning the command's exit code
fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result<()> { fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result<i32> {
// Check user namespace availability // Check user namespace availability
namespace::check_user_namespace()?; namespace::check_user_namespace()?;
+8 -8
View File
@@ -58,8 +58,10 @@ pub fn check_user_namespace() -> Result<()> {
Ok(()) Ok(())
} }
/// Setup namespaces and run the provided function inside them /// Setup namespaces and run the provided function inside them.
pub fn setup_namespaces<F>(f: F) -> Result<()> /// 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: F) -> Result<i32>
where where
F: FnOnce() -> Result<()> + Send + 'static, F: FnOnce() -> Result<()> + Send + 'static,
{ {
@@ -222,20 +224,18 @@ where
let status = nix::sys::wait::waitpid(pid, None)?; let status = nix::sys::wait::waitpid(pid, None)?;
match status { match status {
nix::sys::wait::WaitStatus::Exited(_, 0) => Ok(()), nix::sys::wait::WaitStatus::Exited(_, 0) => Ok(0),
nix::sys::wait::WaitStatus::Exited(_, code) => { nix::sys::wait::WaitStatus::Exited(_, code) => {
// If the child reported an error (e.g., setup failure), return it. // If the child reported an error (e.g., setup failure), return it.
// Otherwise, just forward the exit code without an error message. // Otherwise, just forward the exit code without an error message.
if let Some(msg) = child_error { if let Some(msg) = child_error {
Err(anyhow!("{}", msg)) Err(anyhow!("{}", msg))
} else { } else {
std::process::exit(code); Ok(code)
} }
} }
nix::sys::wait::WaitStatus::Signaled(_, sig, _) => { nix::sys::wait::WaitStatus::Signaled(_, sig, _) => Ok(128 + sig as i32),
Err(anyhow!("Child process killed by signal {:?}", sig)) _ => Ok(0),
}
_ => Ok(()),
} }
} }