From a70a90c3c554ae229de41d0381faadf29da0f050 Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Sat, 15 Aug 2026 15:28:29 +0000 Subject: [PATCH] Start jobs with default signal dispositions Ignored dispositions survive exec, and the shell passes inherited SIGINT/SIGQUIT ignores to its children, so jobs under sled-agent ignored ^C and ^\ at the terminal. Co-Authored-By: Claude Mythos 5 --- Cargo.lock | 1 + server/src/executor.rs | 16 ++++++++++++ tests/Cargo.toml | 1 + tests/src/manager_tests.rs | 53 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8ad1de3..18c1e75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5019,6 +5019,7 @@ dependencies = [ "function_name", "futures", "http-range-header", + "libc", "pwd", "rand_core 0.6.4", "rumors", diff --git a/server/src/executor.rs b/server/src/executor.rs index ab96f27..309a48f 100644 --- a/server/src/executor.rs +++ b/server/src/executor.rs @@ -9,8 +9,10 @@ use std::collections::BTreeMap; use std::io; +use std::mem::MaybeUninit; use std::os::fd::AsRawFd as _; use std::process::Stdio; +use std::ptr::null_mut; use std::sync::{Arc, RwLock}; use chrono::Utc; @@ -298,6 +300,20 @@ async fn job_spawn( }); } + // Reset signal dispositions and the signal mask. + let max_signal = libc::SIGRTMAX(); + unsafe { + cmd.pre_exec(move || { + for signal in 1..=max_signal { + libc::signal(signal, libc::SIG_DFL); + } + let mut none = MaybeUninit::::uninit(); + libc::sigemptyset(none.as_mut_ptr()); + libc::sigprocmask(libc::SIG_SETMASK, none.as_ptr(), null_mut()); + Ok(()) + }); + } + let job = if interactive { // Create a pseudoterminal and wire the child up to it. let (pty, writer, pts, pts_path) = diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 5faa62f..f528e61 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,6 +16,7 @@ ed25519-dalek.workspace = true function_name.workspace = true futures.workspace = true http-range-header.workspace = true +libc.workspace = true pwd.workspace = true rand_core.workspace = true rumors.workspace = true diff --git a/tests/src/manager_tests.rs b/tests/src/manager_tests.rs index 8fb18fa..6418129 100644 --- a/tests/src/manager_tests.rs +++ b/tests/src/manager_tests.rs @@ -4,8 +4,10 @@ //! Job manager tests. +use std::mem::MaybeUninit; use std::os::unix::fs::PermissionsExt as _; use std::path::Path; +use std::ptr::null_mut; use std::slice::from_ref; use std::time::Duration; @@ -2057,3 +2059,54 @@ async fn iam() { .is_err() ); } + +/// Jobs must not inherit ignored signal dispositions. The embedding +/// process may ignore SIGINT, and a shell cannot trap a signal that +/// was ignored at entry, which turns ^C into a no-op in every job. +#[named] +#[tokio::test] +async fn job_signal_dispositions() { + // Ignore and block SIGINT, as an embedding daemon might. + unsafe { + libc::signal(libc::SIGINT, libc::SIG_IGN); + let mut set = MaybeUninit::::uninit(); + libc::sigemptyset(set.as_mut_ptr()); + libc::sigaddset(set.as_mut_ptr(), libc::SIGINT); + libc::pthread_sigmask(libc::SIG_BLOCK, set.as_ptr(), null_mut()); + } + let log = test_logger(function_name!()); + let (mgr, mut root, _dir, _shutdown) = manager_and_test_root(log).await; + let baseboard_id = mgr.own_baseboard(); + let authn = fake_identity(&mut root).await; + let session_id = SessionId::random(); + let mut session = Session::new(session_id); + mgr.session_start(&authn, session_id, true).await.unwrap(); + + let job_id = session.next_job_id(); + let job = root + .sign_job_request( + &job_id, + "trap 'echo caught' INT; kill -INT $$; echo after", + false, + ) + .await; + mgr.job_start( + &authn, + job.clone().into_signed(), + JobStartParams { + wait: JobWait::Stop, + ..Default::default() + }, + ) + .await + .unwrap(); + session.job_started(job.clone().into_signed()); + let stdout = mgr + .job_output(&authn, &job_id, baseboard_id, Stdout, None) + .await + .unwrap() + .into_bytes() + .await; + let stdout = String::from_utf8(stdout.to_vec()).unwrap(); + assert!(stdout.contains("caught"), "stdout: {stdout:?}"); +}