Skip to content

fix(sandbox): kill children that fail init and surface refusal errors fixes #4283 - #4284

Merged
roggervalf merged 15 commits into
masterfrom
fix/fix-issue-4283
Jul 16, 2026
Merged

fix(sandbox): kill children that fail init and surface refusal errors fixes #4283#4284
roggervalf merged 15 commits into
masterfrom
fix/fix-issue-4283

Conversation

@manast

@manast manast commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

A sandboxed child whose processor init() failed was released back into the free pool instead of being killed. Since the pool only removes children on their 'exit' event and such a child never exits, it became an immortal "zombie": every subsequent job popped it, was refused with 'cannot start a not idling child process', and failed almost instantly.

The refusal reason was also lost because ParentCommand.Error carries the payload under the err key while the sandbox message handler only read msg.value, producing failures with an empty error message.

  • child-pool: kill and remove a child when init() rejects instead of release()-ing it, so a fresh child is forked on the next retain.
  • sandbox: read the error payload from msg.value ?? msg.err so the refusal/failure reason reaches failedReason.

Adds a regression test covering a transient one-time init failure and recovery on the following job, for both child processes and worker threads.

Port Impact Checklist

  • Python – does this change need to be ported or documented in the Python library?
  • Elixir – does this change need to be ported or documented in the Elixir library?
  • PHP – does this change need to be ported or documented in the PHP library?
  • Rust – does this change need to be ported or documented in the Rust library?

Why

Enter your explanation here.

How

Enter the implementation details here.

Additional Notes (Optional)

Any extra info here.

…#4283)

A sandboxed child whose processor init() failed was released back into
the free pool instead of being killed. Since the pool only removes
children on their 'exit' event and such a child never exits, it became
an immortal "zombie": every subsequent job popped it, was refused with
'cannot start a not idling child process', and failed almost instantly.

The refusal reason was also lost because ParentCommand.Error carries the
payload under the `err` key while the sandbox message handler only read
`msg.value`, producing failures with an empty error message.

- child-pool: kill and remove a child when init() rejects instead of
  release()-ing it, so a fresh child is forked on the next retain.
- sandbox: read the error payload from `msg.value ?? msg.err` so the
  refusal/failure reason reaches failedReason.

Adds a regression test covering a transient one-time init failure and
recovery on the following job, for both child processes and worker
threads.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a sandbox edge case where a child/worker-thread that fails during init() could be returned to the free pool and then repeatedly reused as a non-idling “zombie”, causing subsequent jobs to be refused immediately with a lost/empty error message.

Changes:

  • Kill + remove sandbox children that fail init() instead of releasing them back to the free pool.
  • Preserve refusal/failure reasons by reading error payloads from msg.value ?? msg.err in the sandbox message handler.
  • Add a regression test that simulates a one-time transient processor module-load failure and verifies recovery on the next job (for both child processes and worker threads).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/sandboxed_process.test.ts Adds a regression test ensuring init-failed children aren’t reused and the next job recovers successfully.
tests/fixtures/fixture_processor_fail_init_once.js New fixture processor that throws only on first import via a flag file, simulating transient init failure.
src/classes/sandbox.ts Fixes lost refusal/failure reasons by reading error payload from value or err.
src/classes/child-pool.ts Ensures init-failed children are killed/removed (not released), preventing “zombie” reuse.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

roggervalf
roggervalf previously approved these changes Jul 14, 2026
…#4283)

A sandboxed child whose processor init() failed was released back into
the free pool instead of being killed. Since the pool only removes
children on their 'exit' event and such a child never exits, it became
an immortal "zombie": every subsequent job popped it, was refused with
'cannot start a not idling child process', and failed almost instantly.

The refusal reason was also lost because ParentCommand.Error carries the
payload under the `err` key while the sandbox message handler only read
`msg.value`, producing failures with an empty error message.

- child-pool: kill and remove a child when init() rejects instead of
  release()-ing it, so a fresh child is forked on the next retain.
- sandbox: read the error payload from `msg.value ?? msg.err` so the
  refusal/failure reason reaches failedReason.

Adds a regression test covering a transient one-time init failure and
recovery on the following job, for both child processes and worker
threads.
@roggervalf
roggervalf force-pushed the fix/fix-issue-4283 branch from b5e5275 to 1ffa288 Compare July 14, 2026 03:10
@miridih-jujang

miridih-jujang commented Jul 14, 2026

Copy link
Copy Markdown

Hi @manast, thank you for the quick turnaround on #4283!

While reviewing the changes, there are a few points we would appreciate you double-checking for side effects before this ships:

  1. this.kill(child, 'SIGKILL').catch(() => {}) silently swallows any kill failure, and the child never exits by itself after sending InitFailed (the open IPC channel keeps its event loop alive). If the parent-side kill ever fails, the process would linger as an invisible orphan outside the pool. A belt-and-braces process.exit() in ChildProcessor.init's catch (after the send resolves) — or at least logging kill failures instead of fully swallowing them — might be worth considering.

  2. The new regression test doesn't seem to exercise the sandbox key fix (msg.value ?? msg.err): once init-failed children are killed, the ParentCommand.Error refusal path never fires, so a regression of that line would go unnoticed by the test suite. A small dedicated test (e.g., sending Start to a non-idle child and asserting the failure message) would lock it in.

  3. It may be worth confirming the new test also runs under useWorkerThreads: true, since kill() takes the worker.terminate() path there.

Everything else looked correct to us: the synchronous remove() inside kill() cleans the pool maps before the throw propagates, hasProcessExited() covers the already-dead case (which also fixes dead children being pushed into the free pool), and SIGKILL is safe here since the child never loaded any user code.

Thank you again for carefully considering the side effects of this change. If there are any side effects we haven't spotted, we would appreciate you giving them additional consideration as well!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/classes/child-processor.ts Outdated
Comment thread rust/CHANGELOG.md Outdated
@manast

manast commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address my comment regarding formatting of the changelog.

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@copilot address my comment regarding formatting of the changelog.

Addressed in commit 87d280e: restored rust/CHANGELOG.md formatting to match the generated changelog format.

roggervalf
roggervalf previously approved these changes Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/classes/child-pool.ts Outdated
@manast
manast requested a review from roggervalf July 15, 2026 20:33
@roggervalf
roggervalf merged commit 7dd064e into master Jul 16, 2026
26 checks passed
@roggervalf
roggervalf deleted the fix/fix-issue-4283 branch July 16, 2026 01:49
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.80.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.25.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.1.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants