Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8625ef5
test: add milestone stress test to reproduce flaky SIGSEGV on musl
claude Mar 20, 2026
9eafaaf
ci: temporarily focus on musl pty_terminal_test only
claude Mar 20, 2026
3ff3630
test: increase stress test iterations and add binary loop
claude Mar 20, 2026
f2e76a2
test: add SIGSEGV signal handler for debugging on musl
claude Mar 20, 2026
861cf00
test: add SIGSEGV handler to milestone test + run it 500x in CI
claude Mar 20, 2026
3d96b5e
test: improve SIGSEGV handler to capture fault addr + RIP from ucontext
claude Mar 20, 2026
6aa30b6
test: add frame-pointer backtrace + addr2line resolution for crash
claude Mar 20, 2026
9d53b04
fix: resolve function-cast-as-integer lint in signal handler
claude Mar 20, 2026
61c25b0
fix: use busybox-compatible grep in CI addr2line script
claude Mar 20, 2026
eff6969
test: increase stress iterations to 2000, add objdump + env info
claude Mar 20, 2026
41ef8ff
fix: serialize PTY spawn on musl to avoid libc race condition
claude Mar 20, 2026
7852975
chore: remove debugging artifacts, restore full CI
claude Mar 20, 2026
c21122b
fix: hold PTY lock for entire Terminal lifetime on musl
claude Mar 20, 2026
adacb13
fix: add `..` to Terminal destructuring for musl _pty_guard field
claude Mar 20, 2026
4f5e40b
ci: add temporary musl stability workflow (10 parallel runs)
claude Mar 20, 2026
7eb4061
fix: only serialize PTY spawn on musl, not entire lifetime
claude Mar 20, 2026
d42d442
chore: remove temporary musl stability workflow
claude Mar 20, 2026
e0a8860
fix: serialize PTY FD cleanup with spawn on musl
claude Mar 20, 2026
7651fe9
ci(test-musl): switch to dynamic musl libc linking
claude Mar 20, 2026
dbcf2f8
ci(musl): move -crt-static to .cargo/config.toml
claude Mar 20, 2026
8965904
revert: undo -crt-static changes in .cargo/config.toml
claude Mar 20, 2026
f8090f8
ci(musl): clarify why -crt-static is needed
claude Mar 20, 2026
b29cf88
fix(fspy_test_bin): force static linking for seccomp test binary
claude Mar 20, 2026
03e9908
fix(fspy): allow dynamically-linked test binary in seccomp tests
claude Mar 20, 2026
7d31d20
fix(fspy): skip static_executable tests on musl
claude Mar 20, 2026
abd1385
fix(pty_terminal): use signal_hook instead of ctrlc for musl compat
branchseer Mar 20, 2026
5d20db5
ci: add temporary musl stability workflow (10 parallel runs)
claude Mar 20, 2026
3d27894
chore: remove temporary musl stability workflow
claude Mar 20, 2026
d3244e7
ci: verify musl stability (run 1/5)
claude Mar 20, 2026
bd7aee9
fix: guard PTY FD drops with musl lock to prevent SIGSEGV
claude Mar 20, 2026
05f9323
fix: serialize milestone tests on musl to prevent SIGSEGV
claude Mar 20, 2026
0e6db16
fix: use permit-based gate to serialize entire PTY lifetime on musl
claude Mar 20, 2026
daa00ac
ci: set RUST_TEST_THREADS=1 on musl to prevent concurrent PTY SIGSEGV
claude Mar 20, 2026
5af51bf
revert: remove PTY gate/permit code from terminal.rs
claude Mar 20, 2026
4443308
chore: remove temporary musl stability workflow
claude Mar 20, 2026
387013c
fix(pty_terminal): use signalfd for SIGINT handling on Linux
claude Mar 20, 2026
3319440
chore: update Cargo.lock for nix signal feature
claude Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion crates/fspy/tests/static_executable.rs
Original file line number Diff line number Diff line change
@@ -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 _,
Expand Down
1 change: 1 addition & 0 deletions crates/pty_terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 13 additions & 0 deletions crates/pty_terminal/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
// 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,
Expand Down Expand Up @@ -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);
Expand Down
103 changes: 67 additions & 36 deletions crates/pty_terminal/tests/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<unsafe extern "system" fn(u32) -> 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<unsafe extern "system" fn(u32) -> 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
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion crates/pty_terminal_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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() },
Expand Down
Loading