Skip to content

fix: avoid tty stuck forever#348

Open
soraxas wants to merge 3 commits into
doy:mainfrom
soraxas:fix/avoid-tty-stuck-forever
Open

fix: avoid tty stuck forever#348
soraxas wants to merge 3 commits into
doy:mainfrom
soraxas:fix/avoid-tty-stuck-forever

Conversation

@soraxas

@soraxas soraxas commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes hidden pinentry processes spawned by rbw-agent that pile up at ~100% CPU when the vault is locked and an SSH-agent request comes in.

This is what i see on htop:

PID USER       PRI  NI  VIRT   RES S  CPU%▽MEM%   TIME+  Command                                                                                                                                                                                
 37696 foobar      20   0  415G  4528 R  99.0  0.0 10h12:02 pinentry --timeout 0 --ttyname /dev/ttys049
  7137 foobar      17   0  415G  4512 R  99.0  0.0 10h12:01 pinentry --timeout 0 --ttyname /dev/ttys049
 29886 foobar      24   0  415G  4656 R  99.0  0.0  1h07:28 pinentry --timeout 0 --ttyname /dev/ttys025                                                                                                                                            
 25019 foobar      20   0  415G  4544 R  99.0  0.0 10h35:07 pinentry --timeout 0 --ttyname /dev/ttys018
 45356 foobar      20   0  415G  4656 R  99.0  0.0  1h07:29 pinentry --timeout 0 --ttyname /dev/ttys025
 56047 foobar      20   0  415G  4528 R  99.0  0.0 10h33:03 pinentry --timeout 0 --ttyname /dev/ttys018
  9374 foobar      17   0 1859G  339M S  12.5  0.7  1:21.50 /Applications/Brave Browser.app/Contents/Frameworks/Brave Browser Framework.framework/Versions/148.1.90.128/Helpers/Brave Browser Helper (Renderer).app/Contents/MacOS/Brave Browser He
  1007 foobar      17   0  466G  177M S  10.9  0.4  1h27:48 /Applications/Brave Browser.app/Contents/Fram
...

The processes seen in ps (pinentry --timeout 0 --ttyname /dev/ttysNNN) are launched by rbw-agent, not gpg-agent — that argv (--timeout 0 --ttyname on the command line) is built in src/pinentry.rs; gpg passes the tty over the Assuan pipe instead.

Root cause

SSH-agent requests carry no environment of their own, so the agent falls back to "the last environment the main agent saw" (State::last_environment). By the time a background process triggers an SSH request, that TTY is usually stale. With --timeout 0, the terminal pinentry attaches to a dead TTY, can't be seen or answered, and busy-loops at 100% CPU forever. Every subsequent locked request spawns another one — hence the accumulation across different stale TTYs.

Changes

Don't launch invisible pinentry on stale TTYs

  • Only pass --ttyname when the recorded TTY still opens as a terminal (Environment::has_usable_tty() — non-blocking O_NOCTTY open + isatty). Live interactive terminals are unaffected; only dead TTYs are dropped, letting pinentry fall back to its own detection or a GUI prompt.
  • Fail fast on SSH-agent requests (get_ssh_public_keys and find_ssh_private_key) when the vault is locked and there's no usable terminal or display, instead of spawning a hidden prompt. Returns an error telling the user to run rbw unlock first.

Configurable prompt timeout

  • New pinentry_timeout config option (default 120, replaces the old hardcoded --timeout 0). Set to 0 to disable and restore the old hang-forever behavior. Wired through rbw config set/unset and documented in the README.

Caveats

  • TTY device numbers get reused, so a stale /dev/ttysNNN could now belong to a different live session — in that narrow case has_usable_tty() returns true and we'd still pass it. The fail-fast guard is what fully covers the SSH path regardless.
  • pinentry_timeout = 0 is intentionally still allowed (documented as "disable"), unlike lock_timeout which rejects 0.

Testing

  • cargo build / cargo clippy clean (no new warnings)
  • cargo test --lib pinentry passes
  • Manual repro: lock the vault, make a now-closed terminal's tty the last-seen environment, then trigger a detached SSH signature (e.g. setsid ssh -T ...) — previously spawned a runaway hidden pinentry; now it either drops the stale
    --ttyname or fails fast.

soraxas added 3 commits June 4, 2026 12:13
ssh-agent requests carry no environment, so the agent reuses the last
environment the main agent saw. by the time a background process triggers
an ssh request, that tty is often stale; pinentry then attaches to a dead
tty, can't be seen or answered, and busy-loops at 100% cpu. with the
previous --timeout 0 these accumulated indefinitely.

- only pass --ttyname when the recorded tty still opens as a terminal
- fail fast on ssh-agent requests when locked with no way to prompt,
  instead of spawning a hidden pinentry
- bound pinentry lifetime with --timeout 120 instead of 0
add a `pinentry_timeout` config option controlling how long an unanswered
pinentry prompt lingers before pinentry dismisses it. defaults to 120
seconds (previously hardcoded); set to 0 to disable the timeout and restore
the old hang-forever behaviour.
Signed-off-by: Tin Lai <tin@tinyiu.com>
Copilot AI review requested due to automatic review settings June 4, 2026 12:16

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

Note

Copilot was unable to run its full agentic suite in this review.

Adds safeguards to prevent invisible pinentry prompts (notably for stale TTYs in ssh-agent flows) by introducing a configurable pinentry timeout and checking whether prompting is actually possible.

Changes:

  • Add pinentry_timeout config option and thread it through all pinentry invocations.
  • Add Environment helpers to detect a usable TTY / ability to prompt; prevent ssh-agent unlock attempts when no prompt can be displayed.
  • Minor refactors/modernizations (e.g., map_or, formatting updates, setrlimit call).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/protocol.rs Adds TTY usability checks and a “can we prompt” helper used by agent flows.
src/pinentry.rs Makes pinentry timeout configurable and avoids passing stale --ttyname.
src/config.rs Introduces pinentry_timeout with a default value.
src/bin/rbw/commands.rs Supports setting/unsetting pinentry_timeout; small map_or refactor.
src/bin/rbw-agent/debugger.rs Updates error formatting and setrlimit pointer usage.
src/bin/rbw-agent/actions.rs Plumbs timeout into pinentry calls and bails early for ssh-agent when prompting isn’t possible.
src/api.rs Adjusts login error classification match arm (currently introduces a syntax issue).
README.md Documents the new pinentry_timeout option.

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

Comment thread src/api.rs
Comment on lines +1741 to 1747
""
// bitwarden_rs returns an empty error and error_description for
// this case, for some reason
if error_desc.is_none() || error_desc == Some("") {
if (error_desc.is_none() || error_desc == Some("")) => {
if let Some(error_model) = error_res.error_model.as_ref() {
let message = error_model.message.as_str().to_string();
match message.as_str() {
Comment on lines +827 to +830
async fn config_pinentry_timeout() -> anyhow::Result<u64> {
let config = rbw::config::Config::load_async().await?;
Ok(config.pinentry_timeout)
}
@deade1e

deade1e commented Jun 7, 2026

Copy link
Copy Markdown

Hello, this project has not received maintenance in the last 6 months. I forked it and you can find it under https://github.com/deade1e/rbw/tree/refactor.

The branch is receiving frequent commits right now.

I changed the pinentry.rs file entirely (and a lot of other code). Could you please check if this bug keeps happening with my refactored version? Thank you

@soraxas

soraxas commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Hello, this project has not received maintenance in the last 6 months. I forked it and you can find it under https://github.com/deade1e/rbw/tree/refactor.

The branch is receiving frequent commits right now.

I changed the pinentry.rs file entirely (and a lot of other code). Could you please check if this bug keeps happening with my refactored version? Thank you

I don't think i feel comfortable to directly use a fork for my password manager from a random fork, and it's quite impossible to review the 262 "refactor commits" on top of the original repo

@deade1e

deade1e commented Jun 10, 2026

Copy link
Copy Markdown

#338

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.

3 participants