Skip to content

fix(daemon): drop listen sockets held by helper children - #1831

Closed
ScriptedAlchemy wants to merge 1 commit into
masterfrom
cursor/daemon-helper-socket-501e
Closed

ScriptedAlchemy wants to merge 1 commit into
masterfrom
cursor/daemon-helper-socket-501e

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

Motivation

Tip 3f71bccb2dba still only waited for a predecessor socket to stop accepting. That wait treats the symptom. fork duplicates the listen descriptor; FD_CLOEXEC drops it at exec, not before. A pid-only SIGKILL of the leader leaves that child accepting. Local census: after the leader is killed, connect still succeeds until the process group is signalled.

Changes

  • crates/tracedecay-runtime-core/src/process_tree.rs: arm_helper_parent_death (PR_SET_PDEATHSIG) and signal_process_group.
  • run_foreground_unix arms parent-death before any helper is forked.
  • Test daemon teardown signals the harness-created process group, then reaps the leader.

Test plan

  • cargo test -p tracedecay-runtime-core --lib process_tree -- --test-threads=1 — 2 passed. The parent-death test fails closed if a pid-only kill leaves the socket up. The group test first shows the socket still accepts after a pid-only kill, then fails that connect after the group signal.
  • cargo clippy -p tracedecay-runtime-core --lib --tests --offline -- -D warnings — exit 0
  • cargo check -p tracedecay --lib — exit 0
  • Full workspace nextest is the push CI lane, not this slice

Checklist

  • CHANGELOG.md updated (process teardown; no command or wire change)
  • No secrets, credentials, or .env files included
  • Breaking changes documented (none)

SHA: ffafc6fcc5f83be6531f40823662d1476d37a345

Open in Web Open in Cursor 

Fork duplicates the daemon's listen socket. FD_CLOEXEC drops it at exec,
not before, so a helper still between those calls keeps accepting after
a pid-only kill. Children now die with that parent, and the test stop
signals the process group the harness created for the daemon.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ffafc6f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review September 19, 2026 06:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffafc6fcc5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +135 to +136
#[test]
fn helper_between_fork_and_exec_releases_the_listen_socket_when_its_parent_dies() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate the parent-death test to Linux

In the macOS core-storage CI partition, this test is compiled because the containing module is gated only on unix, but arm_helper_parent_death() explicitly returns success without registering a handler on non-Linux targets. The PID-only leader.child.kill() therefore leaves the paused helper holding the socket, so the assertion after the two-second wait fails deterministically on macOS. Restrict this test to Linux or give non-Linux platforms a supported parent-death mechanism.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-19T06:43:30.383759Z ffafc6f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Review lane verdict: REJECT as a merge candidate for now. PR_SET_PDEATHSIG armed via pthread_atfork is Linux-only and the macOS CI lane builds the daemon bootstrap; the harness half (group SIGKILL in terminate_and_reap) is landing via #1833 in batch #1848. If the production root-cause half is still wanted, please rebase onto master after #1848 lands, drop the tests/common/mod.rs hunk, and gate the PDEATHSIG path on cfg(target_os = "linux") with a no-op elsewhere.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Closing after a second review with an experiment.

The production half cannot fire. arm_helper_parent_death installs PR_SET_PDEATHSIG from a pthread_atfork child handler, and glibc runs that handler only on the fork path. Rust's Command uses posix_spawn unless pre_exec (or uid, gid, chroot, setsid) is set, and no production spawn in this repo sets any of those. A probe against glibc 2.39, with the handler reporting through a pipe, ran the handler 0 times for a plain spawn and for a process_group(0) spawn, and 1 time only under pre_exec, which is the arrangement the PR's own test manufactures.

The window it targets is already closed. The listen socket is close-on-exec, so any child drops it at exec. The one long-lived production child, the work-attempt provider in work_attempt_exec.rs, deliberately runs in its own process group with kill_on_drop; a working parent-death signal would kill in-flight work attempts whenever the daemon exits. The change also turns a hook failure into a fatal TraceDecayError::Config on the daemon's startup route.

The harness half already landed via #1833 in #1848: terminate_and_reap sends the group SIGKILL and keeps the bounded vacate poll this diff would have removed.

If the predecessor-socket symptom recurs on a loaded runner, fork inheritance is not the mechanism, and it needs its own investigation.

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.

2 participants