diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 106a3440..41aaa1b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,8 +144,14 @@ jobs: env: # Override all rustflags to skip the zig cross-linker from .cargo/config.toml. # Alpine's cc is already musl-native, so no custom linker is needed. - # Must mirror [build].rustflags from .cargo/config.toml. - RUSTFLAGS: --cfg tokio_unstable -D warnings + # Must mirror [build].rustflags and target rustflags from .cargo/config.toml + # (RUSTFLAGS env var overrides both levels). + # -crt-static: vite-task is shipped as a NAPI module in vite+, and musl Node + # with native modules links to musl libc dynamically, so we must do the same. + RUSTFLAGS: --cfg tokio_unstable -D warnings -C target-feature=-crt-static + # On musl, concurrent PTY operations can trigger SIGSEGV in musl internals. + # Run test threads sequentially to avoid the race. + RUST_TEST_THREADS: 1 steps: - name: Install Alpine dependencies shell: sh {0} diff --git a/Cargo.lock b/Cargo.lock index cc0b85be..905cd6b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,6 +2527,7 @@ dependencies = [ "anyhow", "ctor", "ctrlc", + "nix 0.30.1", "ntest", "portable-pty", "signal-hook", diff --git a/Cargo.toml b/Cargo.toml index 624cf584..87013713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ jsonc-parser = { version = "0.29.0", features = ["serde"] } libc = "0.2.172" memmap2 = "0.9.7" monostate = "1.0.2" -nix = { version = "0.30.1", features = ["dir"] } +nix = { version = "0.30.1", features = ["dir", "signal"] } ntapi = "0.4.1" nucleo-matcher = "0.3.1" once_cell = "1.19" diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index d2b02621..297a2a6e 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -1,4 +1,8 @@ -#![cfg(target_os = "linux")] +//! Tests for fspy tracing of statically-linked executables (seccomp path). +//! Skipped on musl: the test binary is an artifact dep targeting musl, and when +//! the CI builds with `-crt-static` the binary becomes dynamically linked, +//! defeating the purpose of these tests. +#![cfg(all(target_os = "linux", not(target_env = "musl")))] use std::{ fs::{self, Permissions}, os::unix::fs::PermissionsExt as _, diff --git a/crates/pty_terminal/Cargo.toml b/crates/pty_terminal/Cargo.toml index 7bec6fc9..52c800e2 100644 --- a/crates/pty_terminal/Cargo.toml +++ b/crates/pty_terminal/Cargo.toml @@ -20,6 +20,7 @@ subprocess_test = { workspace = true, features = ["portable-pty"] } terminal_size = "0.4" [target.'cfg(unix)'.dev-dependencies] +nix = { workspace = true } signal-hook = "0.3" [lints] diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 826e86b6..b77673bb 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -256,6 +256,15 @@ impl Terminal { /// /// Panics if the writer lock is poisoned when the background thread closes it. pub fn spawn(size: ScreenSize, cmd: CommandBuilder) -> anyhow::Result { + // On musl libc (Alpine Linux), concurrent PTY operations trigger + // SIGSEGV/SIGBUS in musl internals (sysconf, fcntl). This affects + // both openpty+fork and FD cleanup (close) from background threads. + // Serialize all PTY lifecycle operations that touch musl internals. + #[cfg(target_env = "musl")] + static PTY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + #[cfg(target_env = "musl")] + let _spawn_guard = PTY_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let pty_pair = portable_pty::native_pty_system().openpty(portable_pty::PtySize { rows: size.rows, cols: size.cols, @@ -286,6 +295,10 @@ impl Terminal { let slave = pty_pair.slave; move || { let _ = exit_status.set(child.wait().map_err(Arc::new)); + // On musl, serialize FD cleanup (close) with PTY spawn to + // prevent racing on musl-internal state. + #[cfg(target_env = "musl")] + let _cleanup_guard = PTY_LOCK.lock().unwrap_or_else(|e| e.into_inner()); // Close writer first, then drop slave to trigger EOF on the reader. *writer.lock().unwrap() = None; drop(slave); diff --git a/crates/pty_terminal/tests/terminal.rs b/crates/pty_terminal/tests/terminal.rs index 44489124..aebbcb5d 100644 --- a/crates/pty_terminal/tests/terminal.rs +++ b/crates/pty_terminal/tests/terminal.rs @@ -16,7 +16,7 @@ fn is_terminal() { println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal()); })); - let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } = + let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); @@ -40,7 +40,7 @@ fn write_basic_echo() { } })); - let Terminal { mut pty_reader, mut pty_writer, child_handle } = + let Terminal { mut pty_reader, mut pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); pty_writer.write_line(b"hello world").unwrap(); @@ -71,7 +71,7 @@ fn write_multiple_lines() { } })); - let Terminal { mut pty_reader, mut pty_writer, child_handle } = + let Terminal { mut pty_reader, mut pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); pty_writer.write_line(b"first").unwrap(); @@ -113,7 +113,7 @@ fn write_after_exit() { print!("exiting"); })); - let Terminal { mut pty_reader, mut pty_writer, child_handle } = + let Terminal { mut pty_reader, mut pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); // Read all output - this blocks until child exits and EOF is reached @@ -149,7 +149,7 @@ fn write_interactive_prompt() { stdout.flush().unwrap(); })); - let Terminal { mut pty_reader, mut pty_writer, child_handle } = + let Terminal { mut pty_reader, mut pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); // Wait for prompt "Name: " (read until the space after colon) @@ -240,7 +240,7 @@ fn resize_terminal() { stdout().flush().unwrap(); })); - let Terminal { mut pty_reader, mut pty_writer, child_handle: _ } = + let Terminal { mut pty_reader, mut pty_writer, child_handle: _, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); // Wait for initial size line (synchronize before resizing) @@ -275,43 +275,74 @@ fn send_ctrl_c_interrupts_process() { let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdout}; - // On Windows, clear the "ignore CTRL_C" flag set by Rust runtime - // so that CTRL_C_EVENT reaches the ctrlc handler. - #[cfg(windows)] + // On Linux, use signalfd to wait for SIGINT without signal handlers or + // background threads. This avoids musl issues where threads spawned during + // .init_array (via ctor) are blocked by musl's internal lock. + #[cfg(target_os = "linux")] { - // SAFETY: Declaring correct signature for SetConsoleCtrlHandler from kernel32. - unsafe extern "system" { - fn SetConsoleCtrlHandler( - handler: Option i32>, - add: i32, - ) -> i32; - } + use nix::sys::{ + signal::{SigSet, Signal}, + signalfd::SignalFd, + }; - // SAFETY: Clearing the "ignore CTRL_C" flag so handlers are invoked. - unsafe { - SetConsoleCtrlHandler(None, 0); // FALSE = remove ignore - } - } + // Block SIGINT so it goes to signalfd instead of the default handler. + let mut mask = SigSet::empty(); + mask.add(Signal::SIGINT); + mask.thread_block().unwrap(); - ctrlc::set_handler(move || { - // Write directly and exit from the handler to avoid races. - use std::io::Write; - let _ = write!(std::io::stdout(), "INTERRUPTED"); - let _ = std::io::stdout().flush(); + let sfd = SignalFd::new(&mask).unwrap(); + + println!("ready"); + stdout().flush().unwrap(); + + // Block until SIGINT arrives via signalfd. + sfd.read_signal().unwrap().unwrap(); + print!("INTERRUPTED"); + stdout().flush().unwrap(); std::process::exit(0); - }) - .unwrap(); + } - println!("ready"); - stdout().flush().unwrap(); + // On macOS/Windows, use ctrlc which works fine (no .init_array/musl issue). + #[cfg(not(target_os = "linux"))] + { + // On Windows, clear the "ignore CTRL_C" flag set by Rust runtime + // so that CTRL_C_EVENT reaches the ctrlc handler. + #[cfg(windows)] + { + // SAFETY: Declaring correct signature for SetConsoleCtrlHandler from kernel32. + unsafe extern "system" { + fn SetConsoleCtrlHandler( + handler: Option i32>, + add: i32, + ) -> i32; + } + + // SAFETY: Clearing the "ignore CTRL_C" flag so handlers are invoked. + unsafe { + SetConsoleCtrlHandler(None, 0); // FALSE = remove ignore + } + } + + ctrlc::set_handler(move || { + // Write directly and exit from the handler to avoid races. + use std::io::Write; + let _ = write!(std::io::stdout(), "INTERRUPTED"); + let _ = std::io::stdout().flush(); + std::process::exit(0); + }) + .unwrap(); - // Block until Ctrl+C handler exits the process. - loop { - std::thread::park(); + println!("ready"); + stdout().flush().unwrap(); + + // Block until Ctrl+C handler exits the process. + loop { + std::thread::park(); + } } })); - let Terminal { mut pty_reader, mut pty_writer, child_handle: _ } = + let Terminal { mut pty_reader, mut pty_writer, child_handle: _, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); // Wait for process to be ready @@ -342,7 +373,7 @@ fn read_to_end_returns_exit_status_success() { println!("success"); })); - let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } = + let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); @@ -358,7 +389,7 @@ fn read_to_end_returns_exit_status_nonzero() { std::process::exit(42); })); - let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } = + let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle, .. } = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); diff --git a/crates/pty_terminal_test/src/lib.rs b/crates/pty_terminal_test/src/lib.rs index 187cbac3..b0cbe0d1 100644 --- a/crates/pty_terminal_test/src/lib.rs +++ b/crates/pty_terminal_test/src/lib.rs @@ -34,7 +34,7 @@ impl TestTerminal { /// /// Returns an error if the PTY cannot be opened or the command fails to spawn. pub fn spawn(size: ScreenSize, cmd: CommandBuilder) -> anyhow::Result { - let Terminal { pty_reader, pty_writer, child_handle } = Terminal::spawn(size, cmd)?; + let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?; Ok(Self { writer: pty_writer, reader: Reader { pty: BufReader::new(pty_reader), child_handle: child_handle.clone() },