[Bug 2146860] [NEW] command permanently stop

qianqiu 2146860 at bugs.launchpad.net
Tue Mar 31 03:34:24 UTC 2026


Public bug reported:

## Summary

When a command running under `sudo` with `use_pty` receives SIGTTOU
twice in succession, the parent process fails to send SIGCONT_FG on the
second occurrence. This causes the command to remain permanently stopped
(T state), deadlocking the entire process tree.

The bug is a misplaced `return` in `suspend_pty()` in
`src/exec/use_pty/parent.rs`. The `return Some(SIGCONT_FG)` is nested
inside `if !self.term_raw`, so when `term_raw` is already `true` (set
during the first SIGTTOU handling), the early return is skipped and the
code falls through to the suspend path, which sends SIGTTOU to the
parent process group via `killpg()`.

Original sudo (sudo.ws) does not have this issue — its equivalent `ret =
SIGCONT_FG; break;` is at the `if (ec->foreground)` level, always
executing when the parent is the foreground process.

## Environment

- sudo-rs 0.2.13-0ubuntu1
- `Defaults use_pty` in `/etc/sudoers`
- Linux (tested on 6.x kernel, riscv64)

## Steps to Reproduce

Prerequisites:
- `Defaults use_pty` enabled in `/etc/sudoers`
- A command that triggers SIGTTOU twice (e.g., a dpkg post-install script failure during `apt-get full-upgrade`)

Minimal reproduction using Python:

```python
import subprocess, threading

proc = subprocess.Popen(
    ["sudo", "/bin/sh", "-c", "LC_ALL=C apt-get -y full-upgrade"],
    stdin=None,              # inherit terminal
    stdout=subprocess.PIPE,  # pipe stdout
    stderr=subprocess.STDOUT,
    text=True,
)

def drain():
    for line in proc.stdout:
        print(line, end="")

threading.Thread(target=drain, daemon=True).start()
proc.wait(timeout=120)  # hangs here
```

When a dpkg trigger/script fails, `sh` receives SIGTTOU from the kernel
and stops. The monitor sends SIGCONT (via SIGCONT_FG from parent). `sh`
resumes but receives SIGTTOU again. This time, no SIGCONT is sent — the
command stays stopped permanently.

Workaround: use `stdin=subprocess.DEVNULL` to prevent sudo from
detecting a terminal on stdin.

Switching to original sudo (sudo.ws) via `update-alternatives`
eliminates the issue entirely.

## Root Cause

In `src/exec/use_pty/parent.rs`, the `suspend_pty()` function handles
SIGTTOU/SIGTTIN at lines 576-603:

```rust
if let SIGTTOU | SIGTTIN = signal {
    if !self.foreground && self.check_foreground().is_err() {
        return None;
    }

    if self.foreground {
        dev_info!(...);
        if !self.term_raw {
            if self.tty_pipe
                .left_mut()
                .set_raw_mode(false, self.preserve_oflag)
                .is_ok()
            {
                self.term_raw = true;
            }
            // Resume command in the foreground
            self.tty_pipe.enable_input(registry);
            return Some(SIGCONT_FG);  // BUG: only reached when term_raw was false
        }
        // When term_raw is already true, falls through!
    }
}

// Falls through to suspend path:
// ...
// killpg(self.parent_pgrp, signal)  ← deadlocks the process tree
```

The equivalent code in original sudo (`src/exec_pty.c:236-246`) does not
have this problem:

```c
if (ec->foreground) {
    if (!ec->term_raw) {
        if (sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags))
            ec->term_raw = true;
    }
    ret = SIGCONT_FG;   /* always reached when foreground is true */
    break;
}
```

The `ret = SIGCONT_FG; break;` is at the `if (ec->foreground)` scope, so
it executes regardless of `term_raw` state.

## Sequence of Events

```
stdout=PIPE → foreground=false, term_raw=false at startup

1st SIGTTOU:
  command stops (SIGTTOU from kernel)
  → monitor: on_stop() → sends Stop(SIGTTOU) to parent
  → parent: suspend_pty(SIGTTOU)
    → check_foreground() → foreground=true (sudo IS fg process on user tty)
    → term_raw=false → enters if(!term_raw) block
    → set_raw_mode() → term_raw=true
    → returns SIGCONT_FG ✓
  → monitor: tcsetpgrp(command_pgrp) + kill(SIGCONT)
  → command resumes

2nd SIGTTOU:
  command stops again (SIGTTOU from kernel)
  → monitor: on_stop() → sends Stop(SIGTTOU) to parent
  → parent: suspend_pty(SIGTTOU)
    → foreground=true, term_raw=true
    → if(!term_raw) is FALSE → skips the block with return SIGCONT_FG
    → falls through to suspend path
    → killpg(parent_pgrp, SIGTTOU) → stops entire parent process group
    → DEADLOCK: nobody sends SIGCONT to resume ✗
```

## Suggested Fix

Move `return Some(SIGCONT_FG)` and `enable_input()` outside the `if
!self.term_raw` block to match sudo.ws behavior:

```rust
if self.foreground {
    dev_info!(...);
    if !self.term_raw {
        if self.tty_pipe
            .left_mut()
            .set_raw_mode(false, self.preserve_oflag)
            .is_ok()
        {
            self.term_raw = true;
        }
    }
    // Resume command in the foreground (always, regardless of term_raw)
    self.tty_pipe.enable_input(registry);
    return Some(SIGCONT_FG);
}
```

** Affects: rust-sudo-rs (Ubuntu)
     Importance: Undecided
         Status: New

-- 
You received this bug notification because you are a member of Ubuntu
Foundations Bugs, which is subscribed to rust-sudo-rs in Ubuntu.
https://bugs.launchpad.net/bugs/2146860

Title:
  command permanently stop

Status in rust-sudo-rs package in Ubuntu:
  New

Bug description:
  ## Summary

  When a command running under `sudo` with `use_pty` receives SIGTTOU
  twice in succession, the parent process fails to send SIGCONT_FG on
  the second occurrence. This causes the command to remain permanently
  stopped (T state), deadlocking the entire process tree.

  The bug is a misplaced `return` in `suspend_pty()` in
  `src/exec/use_pty/parent.rs`. The `return Some(SIGCONT_FG)` is nested
  inside `if !self.term_raw`, so when `term_raw` is already `true` (set
  during the first SIGTTOU handling), the early return is skipped and
  the code falls through to the suspend path, which sends SIGTTOU to the
  parent process group via `killpg()`.

  Original sudo (sudo.ws) does not have this issue — its equivalent `ret
  = SIGCONT_FG; break;` is at the `if (ec->foreground)` level, always
  executing when the parent is the foreground process.

  ## Environment

  - sudo-rs 0.2.13-0ubuntu1
  - `Defaults use_pty` in `/etc/sudoers`
  - Linux (tested on 6.x kernel, riscv64)

  ## Steps to Reproduce

  Prerequisites:
  - `Defaults use_pty` enabled in `/etc/sudoers`
  - A command that triggers SIGTTOU twice (e.g., a dpkg post-install script failure during `apt-get full-upgrade`)

  Minimal reproduction using Python:

  ```python
  import subprocess, threading

  proc = subprocess.Popen(
      ["sudo", "/bin/sh", "-c", "LC_ALL=C apt-get -y full-upgrade"],
      stdin=None,              # inherit terminal
      stdout=subprocess.PIPE,  # pipe stdout
      stderr=subprocess.STDOUT,
      text=True,
  )

  def drain():
      for line in proc.stdout:
          print(line, end="")

  threading.Thread(target=drain, daemon=True).start()
  proc.wait(timeout=120)  # hangs here
  ```

  When a dpkg trigger/script fails, `sh` receives SIGTTOU from the
  kernel and stops. The monitor sends SIGCONT (via SIGCONT_FG from
  parent). `sh` resumes but receives SIGTTOU again. This time, no
  SIGCONT is sent — the command stays stopped permanently.

  Workaround: use `stdin=subprocess.DEVNULL` to prevent sudo from
  detecting a terminal on stdin.

  Switching to original sudo (sudo.ws) via `update-alternatives`
  eliminates the issue entirely.

  ## Root Cause

  In `src/exec/use_pty/parent.rs`, the `suspend_pty()` function handles
  SIGTTOU/SIGTTIN at lines 576-603:

  ```rust
  if let SIGTTOU | SIGTTIN = signal {
      if !self.foreground && self.check_foreground().is_err() {
          return None;
      }

      if self.foreground {
          dev_info!(...);
          if !self.term_raw {
              if self.tty_pipe
                  .left_mut()
                  .set_raw_mode(false, self.preserve_oflag)
                  .is_ok()
              {
                  self.term_raw = true;
              }
              // Resume command in the foreground
              self.tty_pipe.enable_input(registry);
              return Some(SIGCONT_FG);  // BUG: only reached when term_raw was false
          }
          // When term_raw is already true, falls through!
      }
  }

  // Falls through to suspend path:
  // ...
  // killpg(self.parent_pgrp, signal)  ← deadlocks the process tree
  ```

  The equivalent code in original sudo (`src/exec_pty.c:236-246`) does
  not have this problem:

  ```c
  if (ec->foreground) {
      if (!ec->term_raw) {
          if (sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags))
              ec->term_raw = true;
      }
      ret = SIGCONT_FG;   /* always reached when foreground is true */
      break;
  }
  ```

  The `ret = SIGCONT_FG; break;` is at the `if (ec->foreground)` scope,
  so it executes regardless of `term_raw` state.

  ## Sequence of Events

  ```
  stdout=PIPE → foreground=false, term_raw=false at startup

  1st SIGTTOU:
    command stops (SIGTTOU from kernel)
    → monitor: on_stop() → sends Stop(SIGTTOU) to parent
    → parent: suspend_pty(SIGTTOU)
      → check_foreground() → foreground=true (sudo IS fg process on user tty)
      → term_raw=false → enters if(!term_raw) block
      → set_raw_mode() → term_raw=true
      → returns SIGCONT_FG ✓
    → monitor: tcsetpgrp(command_pgrp) + kill(SIGCONT)
    → command resumes

  2nd SIGTTOU:
    command stops again (SIGTTOU from kernel)
    → monitor: on_stop() → sends Stop(SIGTTOU) to parent
    → parent: suspend_pty(SIGTTOU)
      → foreground=true, term_raw=true
      → if(!term_raw) is FALSE → skips the block with return SIGCONT_FG
      → falls through to suspend path
      → killpg(parent_pgrp, SIGTTOU) → stops entire parent process group
      → DEADLOCK: nobody sends SIGCONT to resume ✗
  ```

  ## Suggested Fix

  Move `return Some(SIGCONT_FG)` and `enable_input()` outside the `if
  !self.term_raw` block to match sudo.ws behavior:

  ```rust
  if self.foreground {
      dev_info!(...);
      if !self.term_raw {
          if self.tty_pipe
              .left_mut()
              .set_raw_mode(false, self.preserve_oflag)
              .is_ok()
          {
              self.term_raw = true;
          }
      }
      // Resume command in the foreground (always, regardless of term_raw)
      self.tty_pipe.enable_input(registry);
      return Some(SIGCONT_FG);
  }
  ```

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/rust-sudo-rs/+bug/2146860/+subscriptions




More information about the foundations-bugs mailing list