Process Tree Cleanup Is Ownership and Evidence, Not a Kill Call
Aug 05, 2026
When a long-running worker times out, cancels, or fails, the obvious response is to kill the process you started. That often looks like progress and leaves the real hazard intact: shell wrappers, package managers, builds, and pipelines keep running — or get reparented under something that no longer owns them. Cleaning up a process tree is an ownership, evidence, escalation, and post-condition problem. A kill signal without those pieces is not enough.
The problem
Controllers that launch external work almost always hold a direct child process identifier. On timeout or cancellation, they signal that child and move on.
That design assumes the direct child is the work. In practice the child is often only the first link — a shell that spawns a package manager that spawns a build that spawns more tools. Killing the link you can see does not terminate the group you intended to stop. Descendants can survive, get reparented, or escape the relationship your cleanup logic was watching.
The defect is structural. Improving the timeout branch — a stricter timer, a second signal, a longer wait — does not create an ownership boundary that never existed. If ownership was never established at spawn time, post-failure cleanup is guessing about who belongs to whom.
Several hard constraints sit underneath that gap:
- Ownership must not expand to unrelated processes on the host.
- Cleanup evidence must survive timeout, cancellation, controller death, and persistence failure.
- Platform process metadata is not portable: fields that work on one operating system can be missing or unusable on another.
- Observing escapees must not become a second, conflicting definition of “still alive.”
What actually happened
The durable fix was not a patch to one timeout handler. It was an ownership and evidence design:
- Establish an ownership boundary when the worker is spawned — a shared-adapter seam with detached supervisor / self-group signaling — so cleanup targets a defined group rather than a remembered child PID.
- Observe survivors with snapshot-based, monotonic scans: once a process has been seen as escaped, later ancestry changes must not quietly erase that finding.
- Keep durable authority records for what was owned and what cleanup was authorized to do.
- Harden the control channel so cleanup decisions do not depend on fragile side channels.
- Fail closed on dispatch when ownership or evidence is ambiguous: do not launch more work into an unclear tree.
Build and test work surfaced hazards that design review alone often misses:
- Process enumeration can include the enumerator’s own helper and falsely treat it as a surviving group member.
- After a signal, ancestry no longer proves what escaped earlier — so escape observation has to be monotonic across scans, not re-derived from the latest parent chain.
- Closed IPC channels may emit an error event rather than throwing, so “no exception” is not “still open.”
- Forced kill was initially reachable even when the durable pre-sweep authority record could not be written.
That last defect mattered most. The correct repair preferred safety over availability: if durable authority cannot be written, do not issue the forced signal. Preserve the tree as evidence, mark the ambiguity, and block further dispatch until a human or a clearer policy resolves it. A kill without a surviving record of why it was authorized is worse than an incomplete cleanup you can still inspect.
Platform and test work sharpened the same points. One target operating system could not usefully expose a session field that another platform treats as strong. Real-process tests raced: a controller-death case could fire before the worker tree existed; heartbeat staleness checks could flake on millisecond clocks. Those were not product redesigns — they were evidence that synchronization must wait for the condition under test, not for a “started” event.
The delivered change is complete for its declared boundary and explicit about what remains residual: some escape shapes after the final scan; platform paths that are only exercised in CI, not locally; same-credential tampering with the external authority store outside the enforceable threat model; and platforms that stay fail-closed rather than supported. Naming those limits early is part of the design, not a failure to finish.
The lesson
Design ownership at spawn, not cleanup at failure. The group you can clean up later is the group you deliberately formed when you started the work. Inferring membership from a direct child PID after failure is guessing.
Forced escalation needs write-ahead durable authority. Before a destructive forced signal, a durable record of that authorization must exist. If the write fails, do not kill — preserve evidence and fail closed. Safety over availability is the right trade when the alternative is an unaccountable forced kill.
Observation changes after you signal. Escape detection needs scan-order semantics that remember earlier findings. Post-signal ancestry is not a reliable reconstruction of prior escape.
Do not invent a second liveness classifier. Survivor observation and the system’s sole liveness rules must stay aligned. Retained-PID or parallel “still alive?” models that contradict the architecture’s liveness boundary recreate the defect in a different costume.
Treat platform metadata as capability, not assumption. Probe early on every target platform you claim to support. Record unusable fields and unsupported paths as explicit residuals instead of encoding a portable fantasy into production behavior.
Real-process tests are product-safety evidence. Wait for verified tree and authority conditions. Cover descendants and pipelines, controller death, durable-store failure, cancellation races, and platform-specific observation — and expect scheduler and clock races unless synchronization is condition-based.
The broader principle
“Terminate the tree” is really four problems wearing one phrase: ownership, evidence, escalation, and post-condition.
Framing it as a timeout problem leads to a better kill branch on the child you started. Framing it as an ownership-and-evidence problem leads to questions that actually determine safety: What bound this group at spawn? What durable authority exists before a forced signal? What does a survivor mean after signaling? What happens when authority cannot be written, the controller dies, or the platform cannot see the metadata the design assumed?
Governed delivery systems that force those questions through progressive review — behavioral contract, implementation plan, build audit, empirical platform probes, and distinct failed / pending / pass dispositions on verification — catch defects that a single timeout-handler review will miss. The cost is workflow friction. The return is not launching more work into an ambiguous process tree, and not issuing a forced kill you cannot later prove you were authorized to send.
How to apply it
- Before finalizing ownership design, run a small process-topology probe on every target platform you claim to support.
- Specify “durable write before forced signal” as a non-negotiable acceptance criterion from the first specification revision — not as a late hardening note.
- Give every security or control-boundary acceptance criterion an executable suite owner in the plan (file, phase, gate), not only an aggregate coverage percentage.
- When residual wording or escape surface changes, re-confirm risk acceptance against the new surface; do not silently reuse an old decision.
- Prefer fail-closed dispatch and explicit residuals over implying universal cleanup capability you have not proven.
- Keep identity and dirt classification for governed work on the authoritative classifier; raw “nonempty working tree” heuristics are not a substitute when governed evidence is deliberately excluded from dirt.
- Treat closeout and verification records as observational: preserve failed, pending, and pass as distinct dispositions, and write terminal state once it is actually terminal.