Skip to content

fix(process): kill the whole process group when exec times out - #454

Open
TanishGudise wants to merge 1 commit into
kernel:mainfrom
TanishGudise:fix/process-exec-timeout-group
Open

TanishGudise wants to merge 1 commit into
kernel:mainfrom
TanishGudise:fix/process-exec-timeout-group

Conversation

@TanishGudise

@TanishGudise TanishGudise commented Oct 12, 2026 •

Copy link
Copy Markdown

Checklist


Problem

On timeout, POST /process/exec killed only the direct child and then waited on cmd.Wait(), which also waits for stdout and stderr to close. Children of the command inherit those pipes, so sh -c "sleep 8; true" with timeout_sec: 1 returned after 8 s, and the children kept running (#452).

Changes (server/cmd/api/api/process.go, +9/−1)

  • Start the exec'd command in its own process group (Setpgid, set on the SysProcAttr that buildCmd may already have filled in for as_user/as_root).
  • On timeout, kill the group with the existing signalChildProcessGroup(cmd, killSignal), as the Browser REPL and Playwright executors already do.

I didn't use configureChildProcessCmd, because it replaces SysProcAttr (which would drop the as_user/as_root credential) and also sets Pdeathsig, which exec doesn't need for this fix. Spawn is unchanged.

Scope and limits:

  • The same kill runs when the request is cancelled (for example, the client disconnects), so a cancelled exec now stops the group too, instead of killing only the direct child and then waiting for its children.
  • Children that start their own session or process group (setsid, daemons) aren't in the group, so they aren't killed and can still hold the response open if they keep stdout or stderr. One way to bound that is cmd.WaitDelay, but it also changes calls that don't time out, so I left it out.
  • Exec calls that never time out now run in their own process group too, so a signal sent to the API server's group (for example Ctrl-C when running the server in a terminal) no longer reaches them. kernel-images-api.conf doesn't set stopasgroup, and I didn't find anything in the image that signals the API server's group.

Tests

TestProcessExec_TimeoutKillsDescendants (Linux only, like the REPL process-group tests) runs sh -c 'sleep 30 & echo $! > "$1"; wait' with timeout_sec: 1. It checks that the call returns within 10 s and that the background sleep is gone.

main own group, kill only the child group kill without own group this PR
Test result (Linux, golang:1.26.4 container) fails, returned after 30.0 s fails, 30.0 s fails, 30.0 s passes, 1.0 s

Over HTTP, against kernel-images-api built for linux/arm64 on an internal Docker network, sh -c "sleep 8; true" with timeout_sec: 1 returns 500 {"message":"process timed out"} after 8.01 s on main and after 1.01 s with this PR.

go vet ./... and go test -race $(go list ./... | grep -v /e2e$) on Linux give the same results as main. The only failure on both is TestSetFilteredFailsWhenItCannotPersist, which can't pass as root in my container. I didn't run make test-runtime (the Node daemon is unchanged) or the e2e suite: the published images are amd64-only and my Docker VM is arm64.

The 500 status for a timeout is unchanged here. It's part of #453.

🤖 Generated with Claude Code


Note

Medium Risk
Changes synchronous process execution and signal behavior on timeout/cancel; low blast radius but affects resource cleanup and timing for all exec calls (which now run in isolated process groups).

Overview
POST /process/exec now tears down the whole process group on timeout or request cancellation, instead of killing only the direct child and blocking until grandchildren release inherited stdout/stderr.

Exec starts the command with Setpgid (preserving existing SysProcAttr for as_user/as_root). When the deadline or client context ends, it calls signalChildProcessGroup—the same helper used by the Browser REPL—rather than Process.Kill().

A Linux-only test runs a shell that backgrounds a long sleep and waits; it asserts the API returns within ~1s and the background PID is gone.

Reviewed by Cursor Bugbot for commit 32be48a. Bugbot is set up for automated code reviews on this repo. Configure here.

POST /process/exec killed only the direct child on timeout, then waited
on cmd.Wait, which also waits for stdout and stderr to close. Children
of the command inherit those pipes, so `sh -c "sleep 8; true"` with
timeout_sec=1 returned after 8 seconds.

Run the command in its own process group and kill the group on timeout
with the existing signalChildProcessGroup helper, as the Browser REPL
and Playwright executors already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] /process/exec timeout_sec doesn't stop commands that start child processes

1 participant