Skip to content

bash deadline kill: a root process started through sudo survives in the task group, and AFT reports a clean timed_out with live_descendants null #830

Open

Description

@iceteaSA

Summary

When a bash task reaches its deadline, AFT sends SIGTERM to the task's process group, waits up to 2 s, then sends SIGKILL to the group (crates/aft/src/bash_background/process.rs:376-395). An unprivileged AFT cannot signal group members owned by another user, so a root process started through sudo survives in the task's own process group, reparented to PID 1, still running. AFT then reports the task as timed_out (exit 124) with live_descendants: null and says nothing about the survivor.

Repro (0.62.0 release binary, Linux, sudo 1.9.16p2)

In a debian:stable-slim container: user u with u ALL=(root) NOPASSWD: ALL, aft run standalone as u over NDJSON with scratch storage. Then configure, then a tool_call for bash with timeout: 3000 and this command:

sudo -n bash -c "trap 'echo TRAP_RAN >> /tmp/marker' EXIT; sleep 30"

About 4 s after the deadline, bash_status and ps show:

bash_status: status=timed_out exit_code=124 status_reason="killed by the `timeout` of 3s you passed (exit 124)" live_descendants=null
             text="Task bash-…: timed_out (exit 124) 5s"
/tmp/marker: TRAP_RAN
ps:   PID  PPID  PGID   SID USER  CMD
     3084     1  3080  3080 root  sleep 30      <- still running, in the task's group

Control in the same container, unprivileged: bash -c "trap '' TERM; sleep 30" with the same timeout. The TERM-ignoring sleep is killed by the group SIGKILL, and bash_status reports live_descendants: [].

Mechanism

  • killpg(pgid, SIGTERM) reaches sudo (same real UID), and sudo relays SIGTERM to its command only. The root bash runs its EXIT trap and exits without killing its foreground child.
  • killpg(pgid, SIGKILL) gets EPERM for the root-owned members, so the root sleep survives.
  • terminate_pgid_with does not check the group after the SIGKILL. In this case the descendant sample is null (not taken), not a list.

Main (c719cd4) has the same kill path: the only change to process.rs since v0.62.0 is in tests.

Expected

AFT cannot kill another user's process, but it should not report a clean kill. After the group SIGKILL, enumerate the group again; linux_process_group_members already treats EPERM as "group exists". If members remain, report them in live_descendants and in the text, for example: "1 process survived the kill and is still running: pid 3084, user root, sleep 30. AFT cannot signal processes owned by another user." The same applies to the completion notice.

A command can protect itself on the root side. Both of these left no survivor in the same container:

  • sudo -n timeout -k 1 2 <cmd>, with a timeout shorter than AFT's;
  • sudo -n bash -c "trap 'kill -KILL 0' TERM; <cmd> & wait".

A line in the bash docs about this limit would help too.

Activity

  1. added 20 commits that reference this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions