Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Signal forwarded to the command is lost when it arrives between fork() and execve() of the command

未关闭
#566 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
c, linux

调研方向

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-104 restore_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_handler from init_signals(), or sudo_ev_handler
    once the event handlers are set up) until execve() replaces it.
  • src/exec.c:264 exec_cmnd() calls restore_signals() and then does the
    rest of the setup (exec_setup(): uid/gid, rlimits, closefrom, chdir, ...)
    before sudo_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 before fork(), 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 inherited sudo_handler instead and the signal was lost the same way
    (1.9.17p2 in the table above).
  • use_pty path: src/exec_pty.c:841 signal_cb_pty() -> :619
    schedule_signal() sends CMD_SIGNO over the backchannel;
    src/exec_monitor.c:308 mon_backchannel_cb() -> :68 deliver_signal()
    -> :118 killpg(mc->cmnd_pid, signo). The monitor installs its handlers at
    :622 and 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:

  1. 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,
  2. restore_signals() restores the saved state of every signal, not only
    the ones flagged restore; the saved state is what execve() would
    leave (SIG_IGN or SIG_DFL, sudo itself was exec'd),
  3. 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,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

sudo-project/sudo 的其他 Issue

查看 sudo-project/sudo 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。