[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