Signal forwarded to the command is lost when it arrives between fork() and execve() of the command
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 35/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- c, linux
- 领域
- cli, operating-systems
调研方向
The race sits between fork() and execve() in src/exec.c (exec_cmnd), src/signal.c (restore_signals), src/exec_nopty.c (signal_cb_nopty) and, for the pty path, src/exec_monitor.c (deliver_signal). Start by reading how signal handlers and the mask are set before the fork. Reproduce with the sudo_lost_signal.py script in the linked gist. Done means a SIGTERM forwarded during that window terminates the command, and make check still passes.
由索引模型根据 Issue 内容生成。
描述
Summary
When sudo receives a signal (e.g. SIGTERM from a supervisor that decides to stop
the command shortly after starting it), it forwards the signal to the command.
If the forwarded signal reaches the command process before it has called
execve(), the signal is silently dropped: the process still runs sudo's own
signal handler, inherited across fork(), which only records the signal in
memory nobody reads any more. The command then starts normally, runs to
completion and sudo exits with its status (0), so the sender has no way to tell
that the signal was lost.
On a multi-core machine this is not a rare race: a SIGTERM sent to sudo right
after it has forked is lost almost every time (see numbers below). It affects
both the exec_nopty path and the use_pty path (where the monitor forwards
the signal), and both the current main and the latest release.
Versions
Reproduced with sudo built from unmodified upstream sources on x86_64 Linux
(kernel 7.0, Debian bookworm container, 16 CPUs):
| sudo | source | forwarded SIGTERMs lost |
|---|---|---|
1.9.18, main @ 43defa1309 (2026-10-14) |
tarball, ./configure && make |
196 of 200 without a tty, 200 of 200 with a tty (use_pty) |
1.9.17p2, tag v1.9.17p2 |
tarball, ./configure && make |
190 of 200 without a tty |
| 1.9.15p5, Ubuntu package | distro build, run from a terminal (use_pty) |
200 of 200 |
Reproducer
sudo_lost_signal.py (in https://gist.github.com/pepa-cz/b231de7faecb03affd8977ac559e05fe) runs sudo -n /bin/sleep 0.3, waits until sudo
has a child process whose name is still sudo (i.e. forked, not yet exec'd) and
sends SIGTERM to sudo. It then classifies sudo's exit status:
- sudo killed by SIGTERM, or exit 143: the command was terminated as expected;
- exit 0: the command ran to completion, the forwarded signal was lost.
$ sudo -v && ./sudo_lost_signal.py -n 200
Sudo version 1.9.18; kernel 7.0.0-38-generic; CPUs 16; command: /opt/sudo-main/bin/sudo -n /bin/sleep 0.3
terminated: 4, lost: 196, other: 0 of 200 runs; child name when the lost signal was sent: {'sudo': 196}
build_and_test.sh REV (also in the gist, together with pty_test.sh, patch_test.sh, the patch, the full strace outputs and the logs) builds the given upstream revision in a
throw-away Debian container and runs the reproducer against it, with and
without strace.
The race needs the parent to forward the signal while the child is still
between fork() and execve(). On a multi-core machine the two run in
parallel and the forwarding (~100 us) wins against the child's pre-exec work
(~0.5 ms) almost every time. With everything pinned to one CPU the child usually
reaches execve() first and the race is hidden.
strace of a lost signal (main, no tty)
25385 execve("/opt/sudo-main/bin/sudo", ["sudo", "-n", "/bin/sleep", "0.3"], ...) = 0
25385 --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=25378, si_uid=0} ---
25385 rt_sigreturn({mask=[]}) = 0
25385 kill(25386, SIGTERM) = 0 # sudo forwards the signal to the command
25386 --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=25385, si_uid=0} ---
25386 rt_sigreturn({mask=[]}) = 0 # the child handles it with the inherited handler
25386 execve("/bin/sleep", ["/bin/sleep", "0.3"], ...) = 0 # and only now resets the handlers
25386 exit_group(0)
25385 exit_group(0)
strace of a lost signal (main, use_pty)
26044 clone(...) = 26045 # sudo forks the monitor
26044 --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=26036, si_uid=0} ---
26045 clone(...) = 26046 # the monitor forks the command
26045 kill(-26046, SIGTERM) = 0 # the monitor forwards the signal (from the backchannel)
26046 --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=26045, si_uid=0} ---
26046 rt_sigreturn({mask=[]}) = 1 # handled by the handler inherited from the monitor
26046 execve("/bin/sleep", ["/bin/sleep", "0.3"], ...) = 0
26046 exit_group(0)
Cause (line numbers refer to main @ 43defa1309)
src/signal.c:84-104restore_signals(): only the signals flagged
ss->restore(those that were ignored on entry, SIGPIPE, SIGCHLD if ignored)
get their original disposition back. Every other signal keeps the handler
sudo installed (sudo_handlerfrominit_signals(), orsudo_ev_handler
once the event handlers are set up) untilexecve()replaces it.src/exec.c:264exec_cmnd()callsrestore_signals()and then does the
rest of the setup (exec_setup(): uid/gid, rlimits, closefrom, chdir, ...)
beforesudo_execve(). During all of this the child still catches signals
with sudo's handler, which just sets a flag / writes to the parent's signal
pipe. The flag is never read in the child, so the signal is gone.src/exec_nopty.c:616-621: since commit 8781032140 (PR #519) the handlers
are installed and the signal mask is restored beforefork(), so the child
is created with signals unblocked and sudo_ev_handler installed. In the
parent,signal_cb_nopty()(:192) forwards a received signal with
kill(ec->cmnd_pid, signo)as soon as the event loop sees it. Before #519 the
child inheritedsudo_handlerinstead and the signal was lost the same way
(1.9.17p2 in the table above).use_ptypath:src/exec_pty.c:841signal_cb_pty()->:619
schedule_signal()sendsCMD_SIGNOover the backchannel;
src/exec_monitor.c:308mon_backchannel_cb()->:68deliver_signal()
->:118killpg(mc->cmnd_pid, signo). The monitor installs its handlers at
:622and forks the command at:624; the command inherits them and, when
foreground, additionally waits for the tty hand-over before exec
(exec_cmnd_pty(),:377), which makes the window even larger.
What execve() does to caught signals (reset to SIG_DFL) is simply done too
late: a signal that arrives between fork() and execve() should either
terminate the child (default action) or stay pending until the exec'd command
can handle it. Neither happens.
Fix (patch in the gist)
0001-Reset-signal-handlers-in-the-child-before-signals-ca.patch against
main @ 43defa1309:
- the parent keeps signals blocked across the
fork()of the command
(exec_nopty();exec_monitor()now blocks them too), the handlers are
still installed before the fork as #519 intended, restore_signals()restores the saved state of every signal, not only
the ones flaggedrestore; the saved state is whatexecve()would
leave (SIG_IGNorSIG_DFL, sudo itself was exec'd),exec_cmnd()takes the saved signal mask and restores it right after
restore_signals(), so a signal that arrived in the window is delivered
with the default action and terminates the child.
Point 2 also fixes a side effect of #519 visible in current main: because
the event handlers are now installed before the fork over a SIG_IGN
disposition, a signal ignored by sudo's caller was no longer ignored by the
command (sh -c "trap '' TERM; exec sudo sh -c 'kill -TERM \$\$; echo survived'"
prints nothing and exits 143 on main, prints survived on 1.9.17p2 and on
patched main).
Results with patch_test.sh (in the gist; builds the given revision in a Debian
container, runs make check, the reproducer and a few behaviour checks):
| main unpatched | main patched | 1.9.17p2 | |
|---|---|---|---|
make check |
all pass | all pass | all pass |
| forwarded SIGTERM lost, no tty | 194 of 200 | 0 of 200 | 199 of 200 |
forwarded SIGTERM lost, use_pty |
198 of 200 | 0 of 200 | 200 of 200 |
| SIGTERM/SIGINT to sudo while the command runs | 143 / 130 | 143 / 130 | 143 / 130 |
| SIGTERM forwarded to a command that traps it | trapped | trapped | trapped |
| signal ignored by the caller stays ignored for the command | no | yes | yes |
| stdin pipe, exit status 0/1, command under a pty | ok | ok | ok |
I can open a pull request with the patch if that is preferred.
- 主要语言
- C
- 星标
- 1.7k
- 派生
- 306
- 平均合并
- 1 天 8 小时
- 30 天内合并 PR
- 24
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
sudo-project/sudo 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 42/100
sudo-project/sudo#583 ·
维护者通常 1 天内回复
-
难度 3/5 1-2 天 新手友好度 52/100
sudo-project/sudo#553 ·
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 48/100
sudo-project/sudo#552 ·
维护者通常 1 天内回复
-
Documentation text overflows container instead of wrapping可能重新可做 @BittuBarnwal7479 于 130 天前认领,目前没有进行中的 PR。 未关闭
难度 2/5 1-3 小时 新手友好度 58/100
sudo-project/sudo#547 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 46/100
sudo-project/sudo#536 · 1 条评论 ·
维护者通常 1 天内回复
查看 sudo-project/sudo 的全部 Issue
相似的 Issue
-
vim.filetype.match() prefixes a URL buffer name with the cwd before passing it to filetype functions未关闭
难度 2/5 1-3 小时 新手友好度 73/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
mpv-player/mpv#18728 ·
维护者通常 1 天内回复
-
followup Prio 📌
难度 2/5 1-3 小时 新手友好度 66/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
cyrusimap/cyrus-imapd#6640 ·
维护者通常 2 天内回复
-
难度 2/5 1-3 小时 新手友好度 60/100
维护者通常 1 天内回复