Skip to content

[nextest-runner] ignore SIGTTIN/SIGTTOU while input handling is active#2884

Merged
sunshowers merged 1 commit into
mainfrom
sigttou
Jan 3, 2026
Merged

[nextest-runner] ignore SIGTTIN/SIGTTOU while input handling is active#2884
sunshowers merged 1 commit into
mainfrom
sigttou

Conversation

@sunshowers

@sunshowers sunshowers commented Jan 3, 2026

Copy link
Copy Markdown
Member

When a test spawns an interactive shell (e.g., zsh -ic), the shell opens /dev/tty directly and calls tcsetpgrp to become the foreground process group. This causes nextest to become a background process. If nextest then tries to read from stdin (SIGTTIN) or restore terminal state via tcsetattr (SIGTTOU), it gets stopped.

The fix is to ignore SIGTTIN and SIGTTOU for the process lifetime once input handling is enabled (which only happens after we've checked that we're in the foreground in the first place). This is the same approach zsh uses for job control: https://github.com/zsh-users/zsh/blob/3e72a52/Src/init.c#L1439

Less invasive approaches don't work:

  1. Blocking SIGTTOU only around tcsetattr calls: This doesn't handle SIGTTIN, which is sent when crossterm's background thread tries to read from stdin while we're a background process.

  2. Restoring SIG_DFL after restore(): The crossterm input thread may still be running and could trigger SIGTTIN after we restore default signal handling but before the thread exits.

  3. Dropping EventStream before restoring SIG_DFL: Dropping the stream doesn't synchronously wait for the crossterm thread to exit, so there's still a race.

The signals remain ignored for the process lifetime. This is fine for a test runner since we don't need SIGTTIN/SIGTTOU behavior.

Also add a test helper binary (grab-foreground) that simulates what interactive shells do, and an ignored integration test to verify the fix. (We'll un-ignore it once a version of nextest is out with this fix.)

cargo nextest run -p integration-tests sigttou --run-ignored all

Note that this fixes the signal, though not the larger issue that input handling is broken after a test does something weird like that. At some point we'll probably want to let users specify that some tests should be run under a pty, though that's not today.

Fixes: #2878

@codecov

codecov Bot commented Jan 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.60%. Comparing base (a623d09) to head (c4fed5c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
integration-tests/test-helpers/grab-foreground.rs 0.00% 31 Missing ⚠️
nextest-runner/src/input.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2884      +/-   ##
==========================================
- Coverage   80.70%   80.60%   -0.10%     
==========================================
  Files         118      119       +1     
  Lines       28228    28263      +35     
==========================================
  Hits        22781    22781              
- Misses       5447     5482      +35     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a suspension issue where nextest would receive SIGTTOU and get suspended when tests spawn interactive shells that take over the foreground process group. The fix blocks SIGTTOU during tcsetattr calls, following the pattern used by established terminal-aware programs like less and Node.js.

Key changes:

  • Blocks SIGTTOU around tcsetattr calls in the input handler using sigprocmask
  • Adds a test helper binary (grab-foreground) that simulates interactive shell behavior
  • Includes an integration test (currently ignored) to verify the fix

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
nextest-runner/src/input.rs Adds SIGTTOU blocking around tcsetattr calls to prevent suspension when in background
integration-tests/tests/integration/sigttou.rs New integration test verifying nextest doesn't suspend when subprocess grabs foreground (currently ignored)
integration-tests/test-helpers/grab-foreground.rs Test helper binary that simulates interactive shell behavior by grabbing foreground process group
integration-tests/Cargo.toml Registers the grab-foreground test helper binary
integration-tests/tests/integration/main.rs Imports the new sigttou test module with unix cfg guard

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

When a test spawns an interactive shell (e.g., `zsh -ic`), the shell
opens `/dev/tty` directly and calls `tcsetpgrp` to become the foreground
process group. This causes nextest to become a background process. If
nextest then tries to read from stdin (SIGTTIN) or restore terminal
state via `tcsetattr` (SIGTTOU), it gets stopped.

The fix is to ignore SIGTTIN and SIGTTOU for the process lifetime once
input handling is enabled. This is the same approach zsh uses for job
control:
https://github.com/zsh-users/zsh/blob/3e72a52/Src/init.c#L1439

Less invasive approaches don't work:

1. Blocking SIGTTOU only around `tcsetattr` calls: This doesn't handle
   SIGTTIN, which is sent when crossterm's background thread tries to
   read from stdin while we're a background process.

2. Restoring SIG_DFL after `restore()`: The crossterm input thread may
   still be running and could trigger SIGTTIN after we restore default
   signal handling but before the thread exits.

3. Dropping EventStream before restoring SIG_DFL: Dropping the stream
   doesn't synchronously wait for the crossterm thread to exit, so
   there's still a race.

The signals remain ignored for the process lifetime. This is fine for a
test runner since we don't need SIGTTIN/SIGTTOU behavior.

Also adds a test helper binary (`grab-foreground`) that simulates what
interactive shells do, and an ignored integration test to verify the
fix. The test requires an interactive terminal and must be run manually.

Fixes: #2878
@sunshowers sunshowers changed the title [nextest-runner] block SIGTTOU during tcsetattr in InputHandler [nextest-runner] ignore SIGTTIN/SIGTTOU while input handling is active Jan 3, 2026
@trunk-io

trunk-io Bot commented Jan 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@trunk-io

trunk-io Bot commented Jan 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@sunshowers sunshowers merged commit 044ee44 into main Jan 3, 2026
23 checks passed
@sunshowers sunshowers deleted the sigttou branch January 3, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SIGTTOU when test spawns interactive shell

2 participants