diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 106a3440..0e21551c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,8 +144,11 @@ 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 steps: - name: Install Alpine dependencies shell: sh {0} 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/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..1c7372eb 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,10 +275,49 @@ 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. + // On Unix, use signal_hook directly instead of ctrlc. + // ctrlc spawns a background thread to monitor signals, but the subprocess + // closure runs during .init_array (via ctor). On musl, newly-created threads + // cannot execute during init (musl holds a lock), so ctrlc's thread never + // runs and SIGINT is silently swallowed. + // signal_hook::low_level::register installs a raw signal handler with no + // background thread, avoiding the issue entirely. + #[cfg(unix)] + { + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + + let interrupted = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&interrupted); + + // SAFETY: The closure only performs an atomic store, which is signal-safe. + unsafe { + signal_hook::low_level::register(signal_hook::consts::SIGINT, move || { + flag.store(true, Ordering::SeqCst); + }) + .unwrap(); + } + + println!("ready"); + stdout().flush().unwrap(); + + loop { + if interrupted.load(Ordering::SeqCst) { + print!("INTERRUPTED"); + stdout().flush().unwrap(); + std::process::exit(0); + } + std::thread::yield_now(); + } + } + + // On Windows, ctrlc works fine (no .init_array/musl issue). #[cfg(windows)] { + // Clear the "ignore CTRL_C" flag set by Rust runtime + // so that CTRL_C_EVENT reaches the ctrlc handler. // SAFETY: Declaring correct signature for SetConsoleCtrlHandler from kernel32. unsafe extern "system" { fn SetConsoleCtrlHandler( @@ -291,27 +330,27 @@ fn send_ctrl_c_interrupts_process() { 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(); + 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(); - println!("ready"); - stdout().flush().unwrap(); + println!("ready"); + stdout().flush().unwrap(); - // Block until Ctrl+C handler exits the process. - loop { - std::thread::park(); + // 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 +381,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 +397,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() },