Skip to content

Serialize TTY access with flock during password prompting#1649

Closed
MoyashiWithDevice wants to merge 3 commits into
trifectatechfoundation:mainfrom
MoyashiWithDevice:main
Closed

Serialize TTY access with flock during password prompting#1649
MoyashiWithDevice wants to merge 3 commits into
trifectatechfoundation:mainfrom
MoyashiWithDevice:main

Conversation

@MoyashiWithDevice

Copy link
Copy Markdown

Commit message

 Serialize TTY access with flock during password prompting

 When multiple sudo processes are piped together (e.g. `sudo a | sudo b`),
 both processes open /dev/tty and call poll()+read() concurrently. Because
 the terminal delivers each byte to only one reader, keystrokes from the
 user get split between the two processes, resulting in both receiving an
 incomplete password and authentication failure.

 Fix this by acquiring an exclusive flock(LOCK_EX) on the TTY fd before
 reading the password and releasing it when done. A process that reads
 from the same controlling terminal will block until the first finishes,
 ensuring only one process reads user input at a time.

 If flock(2) is unavailable or fails (e.g. on filesystems that do not
 support it), the process proceeds without the lock -- a graceful
 degradation rather than a hard failure.

PR description

 When two (or more) `sudo` processes are connected via a shell pipeline
 (e.g. `sudo echo "aaa" | sudo tee test.txt`), both processes open
 `/dev/tty` and call `poll()` + `read()` concurrently to prompt for the
 password. Because each keystroke from the terminal is consumed by
 whichever `read()` wins the race, the input gets split between the
 processes. Neither receives the complete password, and both fail
 authentication.

 This PR adds an exclusive `flock(LOCK_EX)` on the TTY file descriptor
 during password prompting. Only one process can hold the lock at a time;
 the second process blocks until the first finishes reading. This
 serializes TTY access and prevents interleaved reads.

 The lock is acquired inside the retry loop of `prompt_password()` in
 `src/pam/rpassword.rs`, so it is correctly released and re-acquired
 across retry attempts (e.g. after SIGTSTP/SIGTTIN/SIGTTOU). If `flock`
 is not available on the platform, the lock acquisition silently returns
 `None` and the process proceeds without locking (graceful degradation).

 ### Changes
 - `src/pam/rpassword.rs`: Added `FlockGuard` struct that acquires
   `flock(LOCK_EX)` on construction and releases it on `Drop`. Used in
   `prompt_password()` to serialize TTY reads across concurrent sudo
   processes.

When multiple sudo processes are piped together (e.g. `sudo a | sudo b`),
both processes open /dev/tty and call poll()+read() concurrently. Because
the terminal delivers each byte to only one reader, keystrokes from the
user get split between the two processes, resulting in both receiving an
incomplete password and authentication failure.

Fix this by acquiring an exclusive flock(LOCK_EX) on the TTY fd before
reading the password and releasing it when done. A process that reads
from the same controlling terminal will block until the first finishes,
ensuring only one process reads user input at a time.

If flock(2) is unavailable or fails (e.g. on filesystems that do not
support it), the process proceeds without the lock -- a graceful
degradation rather than a hard failure.
Comment thread src/pam/rpassword.rs Outdated
/// (e.g. the filesystem does not support `flock`).
fn new(fd: BorrowedFd<'_>) -> Option<Self> {
// SAFETY: `fd` is a valid open file descriptor.
if unsafe { libc::flock(fd.as_raw_fd(), libc::LOCK_EX) } == 0 {

@squell squell Jul 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: You can use crate::cutils::flock here, avoiding the introduction of unsafe {}:

fn flock(fd: RawFd, action: LockOp, nonblocking: bool) -> Result<()> {

We prefer libc calls to be wrapped in the system/ cutils/ modules.

Comment thread src/pam/rpassword.rs Outdated
impl Drop for FlockGuard {
fn drop(&mut self) {
// SAFETY: `self.0` is a valid file descriptor and `LOCK_UN` always succeeds.
unsafe { libc::flock(self.0, libc::LOCK_UN) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(same comment as above)

@squell

squell commented Jul 7, 2026

Copy link
Copy Markdown
Member

Thanks for the PR! Per our contributing guidelines this is a substantial enough change to warrant an issue. (The description is already listed above, more or less, and can be simply copy-pasted there).

I will create an issue for that based on the above description. (In particular, I have a question about this behaviour that doesn't necessarily need to turn into a change in this particular PR immediately.:)

Edit: I have opened #1650

User added 2 commits July 8, 2026 09:56
When sudo processes are piped together (e.g. sudo a | sudo b), both
processes prompt for a password on the same TTY simultaneously,
corrupting input. Acquire an exclusive flock on the TTY before PAM
authentication so only one process prompts at a time.

After acquiring the lock, re-check the timestamp file via the existing
touch() mechanism. If another process already authenticated while we
were waiting, skip PAM entirely — the existing 'bypass if already
authenticated' logic in determine_auth_status() handles this naturally.

Removes the earlier AuthAlreadyDone / recheck closure approach from
the PAM conversation layer as it is no longer needed.
@MoyashiWithDevice

Copy link
Copy Markdown
Author

@squell Thank you for the review and for opening #1650. Based on your suggestion, I have revised the implementation.

The TTY lock has been moved higher up the call chain into auth_and_update_record_file, and the timestamp is re-checked after the lock is acquired. When sudo processes are piped together, the first process to acquire the lock proceeds with PAM authentication and creates a session record; the second process, upon acquiring the lock, finds the valid timestamp and skips PAM authentication entirely. This means the second sudo invocation reuses the cached credential, just like traditional sudo does when used in a pipeline.

@squell

squell commented Jul 8, 2026

Copy link
Copy Markdown
Member

I can see from an earlier GitHub comment and the general nature of these changes that these changes are being performed using an LLM.

That's something we do not accept per our contributing guidelines. Issue #1650 will be kept open and I am working on a PR of my own.

@squell

squell commented Jul 8, 2026

Copy link
Copy Markdown
Member

Since some messages have been deleted I will re-post one here:

I implemented your suggestion from #1650. Thank you for pointing out the simpler approach — moving the TTY lock higher up in the call chain and re-checking the timestamp after acquiring it, rather than adding a separate AuthAlreadyDone error variant.

The current implementation:

What changed in the latest commit

  • The exclusive flock on the TTY is now acquired in pipeline.rs::auth_and_update_record_file(), before PAM authentication, instead of inside rpassword.rs::prompt_password().
  • After acquiring the lock, the code re-opens the timestamp file and calls touch() again. If another process in the pipe already authenticated and created a valid record while we were waiting for the flock, PAM authentication is skipped entirely.
  • This reuses the existing "bypass if already authenticated" logic (determine_auth_status → must_authenticate) — no new error variants or callback mechanisms needed.

Result

  • sudo a | sudo b: process A acquires the TTY lock, authenticates, creates a timestamp, and releases the lock. Process B then acquires the lock, re-checks the timestamp, finds it valid, and skips PAM — the user only types the password once, just like traditional sudo.
  • Graceful degradation: if flock(2) is unavailable, the lock acquisition silently returns None and the process proceeds without locking.

The AuthAlreadyDone / recheck closure approach from the earlier revision has been removed as it is no longer needed.

@trifectatechfoundation trifectatechfoundation locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A pipeline of two sudo invocations causes TTY contention

2 participants