From 5b772589e042b2f9df56e9bfeb6aeffab5cccca8 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Narajala Date: Fri, 30 Oct 2026 16:27:28 +0000 Subject: [PATCH] fix(gateway): run the VM compute driver in its own process group Ctrl+C on a gateway running in the foreground sends SIGINT to the terminal's whole foreground process group. The VM compute driver and its VM launchers inherited the gateway's group, so they exited on that signal before the gateway's shutdown sweep asked the driver to stop its sandboxes. The sweep then failed (stopped=0 failed=1), an attached `sandbox create --no-keep` reported "ephemeral cleanup failed", and the sandbox came back in Error after a restart. Spawn the driver with process_group(0). Terminal signals now reach only the gateway, which runs its sweep while the driver is still serving and then stops the driver itself with SIGTERM. The launchers inherit the driver's new group, so they are also out of the terminal's group and stay under the driver's control. A gateway that dies without a graceful shutdown still takes the driver with it: the driver arms a parent-death signal at startup, which does not depend on the process group. Closes #4770 Signed-off-by: Vineeth Sai Narajala --- crates/openshell-gateway/src/vm.rs | 59 +++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/openshell-gateway/src/vm.rs b/crates/openshell-gateway/src/vm.rs index 2f9ea5cc8e..a003fe4a72 100644 --- a/crates/openshell-gateway/src/vm.rs +++ b/crates/openshell-gateway/src/vm.rs @@ -539,11 +539,7 @@ pub async fn spawn( let guest_tls_paths = compute_driver_guest_tls_paths(vm_config)?; prepare_compute_driver_socket_path(vm_config, &socket_path)?; - let mut command = Command::new(&driver_bin); - command.kill_on_drop(true); - command.stdin(Stdio::null()); - command.stdout(Stdio::inherit()); - command.stderr(Stdio::inherit()); + let mut command = compute_driver_command(&driver_bin); command.arg("--bind-socket").arg(&socket_path); command.arg("--admission-config-json").arg( serde_json::to_string(&openshell_core::resource_admission::DriverAdmissionConfig { @@ -599,6 +595,27 @@ pub async fn spawn( )) } +/// Base command for the VM compute-driver subprocess. +/// +/// The driver gets its own process group. Ctrl+C on a foreground gateway +/// sends SIGINT to the terminal's foreground process group; if the driver +/// shared the gateway's group it would exit on that signal before the +/// gateway's shutdown sweep could stop its sandboxes. In its own group the +/// driver keeps serving until the gateway stops it through +/// `ManagedDriverProcess::shutdown` after the sweep. A gateway that dies +/// without that shutdown still takes the driver with it: the driver arms a +/// parent-death signal at startup, which does not depend on the group. +#[cfg(unix)] +fn compute_driver_command(driver_bin: &Path) -> Command { + let mut command = Command::new(driver_bin); + command.kill_on_drop(true); + command.process_group(0); + command.stdin(Stdio::null()); + command.stdout(Stdio::inherit()); + command.stderr(Stdio::inherit()); + command +} + fn validate_vm_sandbox_identity(config: &VmComputeConfig) -> Result<()> { let range = openshell_policy::MIN_SANDBOX_UID..=openshell_policy::MAX_SANDBOX_UID; for (field, value) in [ @@ -797,10 +814,10 @@ async fn connect_compute_driver(socket_path: &Path) -> Result { mod tests { use super::{ VmComputeConfig, append_otlp_args, append_vm_identity_args, append_vm_network_trust_args, - append_vm_proxy_and_spiffe_args, append_vm_rootfs_tar_args, compute_driver_guest_tls_paths, - compute_driver_socket_path, current_euid, prepare_compute_driver_socket_path, - prepare_vm_state_dir, resolve_compute_driver_bin, resolve_driver_search_dirs, - validate_vm_sandbox_identity, + append_vm_proxy_and_spiffe_args, append_vm_rootfs_tar_args, compute_driver_command, + compute_driver_guest_tls_paths, compute_driver_socket_path, current_euid, + prepare_compute_driver_socket_path, prepare_vm_state_dir, resolve_compute_driver_bin, + resolve_driver_search_dirs, validate_vm_sandbox_identity, }; use openshell_core::{NetworkSupervisorTrustBundle, UpstreamProxyConfig}; use openshell_server::config_file::{OcsfOtlpConfig, OtlpConfig, OtlpTracesConfig}; @@ -845,6 +862,30 @@ mod tests { .expect("an explicit bootstrap image is valid"); } + #[tokio::test] + async fn vm_driver_command_runs_driver_in_its_own_process_group() { + use rustix::process::{Pid, getpgid}; + + let mut child = compute_driver_command(std::path::Path::new("sleep")) + .arg("30") + .spawn() + .expect("spawn stand-in driver"); + let pid = Pid::from_raw(i32::try_from(child.id().expect("child pid")).unwrap()) + .expect("child pid is non-zero"); + let child_group = getpgid(Some(pid)).expect("read child process group"); + let gateway_group = getpgid(None).expect("read own process group"); + child.kill().await.expect("stop stand-in driver"); + + assert_eq!( + child_group, pid, + "the driver must lead its own process group" + ); + assert_ne!( + child_group, gateway_group, + "terminal SIGINT to the gateway's group must not reach the driver" + ); + } + #[test] fn vm_driver_command_includes_gateway_otlp_configuration() { let mut command = tokio::process::Command::new("openshell-driver-vm");