Spawn Manager lifecycle version 1

This document defines the process ownership, cancellation and containment contract of Spawn Manager version 1. The public API is declared in Spawn.Pool; the request and result encoding is defined by protocol version 1.

Pool and leases

Spawn.Pool.Init requires the absolute path of the matching spawn_manager binary. The library never searches PATH, and it passes the active frame bound and socket address as separate arguments rather than reparsing a command string.

Each pool creates its endpoints below one randomized mode-0700 directory. Relative socket addresses remain short on the wire to preserve the Unix-socket path budget; the pool separately stores absolute cleanup paths. Cleanup also accepts an already absent private directory, for example when a caller removes its temporary parent through the final manager request.

Each Execute call owns one exclusive manager lease. An optional Pid_Setup callback runs after the lease is selected and before the request is sent. An optional Pid_Reset callback runs after a valid result, while that same lease is still exclusive and before the manager becomes reusable.

Abuild uses this pair to move a long-lived manager into one worker cgroup for a request and return it to the Abuild process cgroup afterwards. A caller that supplies setup without reset deliberately retains manager-wide placement.

If setup, transport, reset or caller task execution fails, the complete pool is poisoned. No manager whose ownership is uncertain becomes available for a later request.

Process groups and completion

Every request owns a new process group. Timeouts terminate and reap the group. Normal leader exit also terminates and reaps remaining group members before the exit or signal result is returned.

The leader remains unreaped until all request-group signals have been sent, so its numeric process-group identity cannot be recycled into an unrelated target. The manager stops publishing that identity to its signal handler before the final reap.

If group signaling or leader reaping fails before the identity is retired, the manager makes one final group-kill attempt and fail-stops instead of resuming without ownership. A descendant-reap failure after retirement also fail-stops, but does not signal the now reusable numeric identity again. The pool observes the lost connection as a protocol failure and poisons every manager.

Leader and descendant reaping after termination have fixed grace-period deadlines. An uninterruptible child therefore fail-stops its manager instead of blocking pool cancellation indefinitely.

Version 1 has no independent single-request cancellation operation. Spawn.Pool.Cleanup stops new leases, interrupts all managers, waits for active callers to leave and cancels the complete pool.

Privilege and descendant containment

Every request sets Linux no_new_privs before arming its parent-death signal. Set-user-ID, set-group-ID and file-capability metadata cannot grant the child new privileges or clear that signal during execve. The restriction is inherited by shell children and structured-exec descendants.

This is a deliberate version-1 compatibility boundary with no opt-out. A future exception requires a concrete consumer, an explicit request contract and cgroup containment because a privileged exec can clear the direct leader's parent-death signal.

The parent-death signal covers only the direct request leader; Linux clears that setting in children created by the target. Normal completion and catchable manager termination signal and reap the observed process group. A process group cannot make target-side fork and group signaling atomic, however. A target that creates a new descendant concurrently with the final group signal requires separately selected cgroup containment.

Cgroup containment is also required for descendants left by uncatchable manager loss such as SIGKILL or OOM, and for a descendant that deliberately escapes with setpgid() or setsid(). Version 1 does not claim those cases from its process-group fallback alone.

Signal and fork boundary

GNAT implements the manager's attached SIGINT and SIGTERM handlers with interrupt-server tasks. The manager therefore has one request executor but is not a single-threaded process. A process-local ownership mutex synchronizes the main task with those runtime tasks across fork publication and exact leader reaping.

Before forking, the manager normalizes SIGCHLD so every request leader stays waitable even when the manager inherited SIG_IGN or SA_NOCLDWAIT. The child starts with termination signals blocked, restores default SIGINT and SIGTERM dispositions, and then clears the mask.

A small C module is the only post-fork boundary. The child enters no Ada runtime code before execve or _exit. It performs child-only directory and descriptor setup, reports pre-exec errno through the error pipe and calls execve. The parent owns exact status collection, timeout cleanup and request group supervision.

The manager binds its descriptor ceiling to /proc/self/fd only after a complete scan. An open, read or close error with a finite soft descriptor limit selects that limit before fork. With RLIM_INFINITY, the implementation consults sysconf(_SC_OPEN_MAX) instead; version 1 does not claim portable support for an effectively unbounded result. If no usable finite ceiling exists and close_range is unavailable, the child reports the pre-exec Close_Descriptors stage instead of proceeding to execve. The Work Queue retains the missing unlimited-limit fault injection and boundedness evidence.

The attached manager signal handler invokes only the synchronized C containment hook and immediate OS exit. The pool owns socket-path cleanup after the manager has terminated.

Failure classification

Version 1 reports internal and child-spawn failures through one result kind. Stages that can represent manager-side supervision poison the complete pool, including ambiguous error-pipe and process-group failures. This also includes manager-side signal-mask restoration failures reported as Reset_Signals.

Stages that can only represent child-side pre-exec failures remain reusable. The exact stage encoding is part of the protocol result format.