diff --git a/shuttle/Cargo.toml b/shuttle/Cargo.toml index 06a69c363..d791ca1e1 100644 --- a/shuttle/Cargo.toml +++ b/shuttle/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true [dependencies] anyhow = "1" base64 = "0.22" +libc = "0.2" nix = { version = "0.31", features = ["fs", "process", "reboot", "signal", "term", "user"] } prost = "0.14" prost-reflect = "0.16" diff --git a/shuttle/src/command.rs b/shuttle/src/command.rs index 3001ba104..652834058 100644 --- a/shuttle/src/command.rs +++ b/shuttle/src/command.rs @@ -210,6 +210,18 @@ pub fn spawn_streaming(mut spec: Spec) -> Result { }) } +// deps that leak fds without FD_CLOEXEC (tokio-vsock sets O_CLOEXEC via F_SETFL, +// a no-op) leave orphaned grandchildren holding the exec stdio vsock conn open, +// and the host waits for EOF forever. 436 = SYS_close_range, unlisted on x86_64. +fn close_inherited_fds() -> io::Result<()> { + let rc = unsafe { libc::syscall(436, 3u32, u32::MAX, libc::CLOSE_RANGE_CLOEXEC) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + fn spawn(spec: &mut Spec) -> Result { let mut cmd = Command::new(&spec.program); cmd.args(&spec.args) @@ -224,6 +236,11 @@ fn spawn(spec: &mut Spec) -> Result { cmd.current_dir(cwd); } + // SAFETY: close_inherited_fds only makes an async-signal-safe syscall + unsafe { + cmd.pre_exec(close_inherited_fds); + } + // don't use rust's .uid() / .gid() methods here because they clear // supplemantary groups, which means for example adding a user to "docker" // group won't actually let it access the sock. @@ -289,6 +306,7 @@ pub fn spawn_pty(spec: Spec, rows: u16, cols: u16) -> Result<(OwnedReadPty, Owne // below is async-signal-safe and touches no shared state. cmd = unsafe { cmd.pre_exec(move || { + close_inherited_fds()?; setgroups(&groups).map_err(io::Error::from)?; setgid(Gid::from_raw(gid)).map_err(io::Error::from)?; setuid(Uid::from_raw(uid)).map_err(io::Error::from)?;