From 503578d64818844d6bdf8946ecc74a3151ca51bb Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 17 Jun 2026 14:03:04 +0200 Subject: [PATCH] feat: terminate when init process exits (--kernel) Add an init script to the initramfs that traps shell exit and triggers poweroff via /proc/sysrq-trigger. This ensures QEMU terminates cleanly when the user exits the shell, rather than hanging indefinitely. Changes: - Add /init script to initramfs that runs as PID 1 - Use -no-reboot QEMU flag to exit on guest poweroff - Simplify kernel command line using environment variables - Init script handles hostname, device creation, and poweroff on exit --- src/qemu_vm.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/src/qemu_vm.rs b/src/qemu_vm.rs index 80f4a8a..07152de 100644 --- a/src/qemu_vm.rs +++ b/src/qemu_vm.rs @@ -69,20 +69,17 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> { // For initramfs boot, use rdinit= instead of init= // No root= needed as initramfs becomes the rootfs // 'quiet' suppresses kernel log messages for a cleaner console - // Use setsid with redirected stdio to get proper job control - // /dev/ttyS0 is created in the initramfs for serial console - // Set hostname before spawning shell + // The init script (added to initramfs) handles hostname, shell, and poweroff let kernel_append = if let Some(ref cmd) = config.command { let cmd_str = cmd.join(" "); format!( - "console=ttyS0 quiet rdinit=/bin/sh -- -c \"echo {} >/etc/hostname; hostname {}; setsid sh -c 'exec {} /dev/ttyS0 2>&1 -c {}'\"", - hostname, hostname, shell, cmd_str + "console=ttyS0 quiet ECR_SHELL={} ECR_CMD=\"{}\" ECR_HOSTNAME={}", + shell, cmd_str, hostname ) } else { - // Default to interactive shell with proper job control format!( - "console=ttyS0 quiet rdinit=/bin/sh -- -c \"echo {} >/etc/hostname; hostname {}; setsid sh -c 'exec {} /dev/ttyS0 2>&1'\"", - hostname, hostname, shell + "console=ttyS0 quiet ECR_SHELL={} ECR_HOSTNAME={}", + shell, hostname ) }; @@ -95,6 +92,7 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> { // Build QEMU arguments // -display none suppresses VGA/BIOS output // -serial mon:stdio connects serial console to terminal with QEMU monitor muxed + // -no-reboot makes QEMU exit when the guest requests poweroff/reboot let args = vec![ "-kernel".to_string(), config.kernel_path.to_string_lossy().to_string(), @@ -108,6 +106,7 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> { "none".to_string(), "-serial".to_string(), "mon:stdio".to_string(), + "-no-reboot".to_string(), "-netdev".to_string(), "user,id=net0".to_string(), "-device".to_string(), @@ -285,6 +284,64 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result> { .context("Failed to finish device node entry")?; } + // Add the /init script that will be run as PID 1 + // This script handles hostname setup, shell execution, and poweroff on exit + // Uses /proc/sysrq-trigger for poweroff since poweroff command may not be available + let init_script = r#"#!/bin/sh +# ECR init script - runs as PID 1 + +# Mount essential filesystems +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev 2>/dev/null || true + +# Set hostname from kernel cmdline +if [ -n "$ECR_HOSTNAME" ]; then + echo "$ECR_HOSTNAME" > /etc/hostname + hostname "$ECR_HOSTNAME" +fi + +# Create console device if missing +mknod -m 600 /dev/console c 5 1 2>/dev/null || true +mknod -m 666 /dev/ttyS0 c 4 64 2>/dev/null || true + +# Function to poweroff - use sysrq-trigger which works without external binaries +do_poweroff() { + # Silence kernel printk to suppress shutdown messages + echo 0 > /proc/sys/kernel/printk + # 'o' means power off, see Documentation/admin-guide/sysrq.rst + echo o > /proc/sysrq-trigger + # Fallback: infinite loop to prevent kernel panic + while true; do sleep 1; done +} + +# Trap exit to ensure poweroff runs +trap do_poweroff EXIT + +# Run the shell or command +if [ -n "$ECR_CMD" ]; then + # Run command with the specified shell + setsid sh -c "exec $ECR_SHELL /dev/ttyS0 2>&1 -c '$ECR_CMD'" +else + # Run interactive shell + setsid sh -c "exec $ECR_SHELL /dev/ttyS0 2>&1" +fi +"#; + + let init_data = init_script.as_bytes(); + let init_builder = NewcBuilder::new("init") + .mode(0o100755) // executable + .uid(0) + .gid(0) + .nlink(1) + .mtime(0); + + let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32); + init_writer.write_all(init_data) + .context("Failed to write init script to cpio archive")?; + init_writer.finish() + .context("Failed to finish init script entry")?; + // Write the trailer (takes ownership and returns the writer) archive = newc::trailer(archive) .context("Failed to write cpio trailer")?;