Experiment formal browser group ownership amendment proposal 01 - kmosoti/FabricO11y GitHub Wiki

Prospective browser group ownership amendment

Status: private proposal only; no trust tests, finalizer, launch path, native clock/helper or packaged adapter changed under this proposal. No capability or runtime control ran. Original browser cleanup failures remain preserved at the actual C080 registration result, wiki 724a74a, page SHA-256 75aa8f63089a724963b34a13a59f2915c219ee840e35b51ec41d131cb1350c63. Pip scope ACK is required before the separate trust registration.

Concrete boundary

At browser head b1e7e83c74ba029c3b377ee047220db93853d578, tools/ui/live_browser_check.py:385-393 retains only (label, Popen); its finalizer at 1238-1256 has neither a spawn-bound pidfd nor original process identity. The literal fixture holds leader/child pidfds privately, but its namespace at tools/ui/test_role_matrix.py:357-363 supplies only Popen pairs. An already waited leader cannot be safely rebound from its numeric PGID. Keep these pairs for the existing sampler; add a private owned_processes mapping keyed by the exact retained Popen object. A public JSON receipt, an arbitrary PGID or a freshly opened pidfd after leader wait is never authority.

Verified source and remaining inference

Linux exposes PIDFD_SIGNAL_PROCESS_GROUP since 6.9; the local header /usr/include/linux/pidfd.h:17 gives value 1 << 2. Linux pidfd_send_signal documentation describes group-leader scope. Linux v6.17 kernel/signal.c, 3753-3803 and 3842-3859, obtains the original struct pid from the descriptor and dispatches flag 4 through PIDTYPE_PGID; it does not first require a live leader task. Lines 1341-1357 iterate that group's tasks and return ESRCH for an empty group. Therefore a retained leader descriptor is a source-supported candidate for after-wait group signaling; actual installed-kernel behavior is still unverified. Numeric PID reuse cannot redirect this kernel-object lookup. CPython 3.13.7 _signal source, 1217-1229, forwards the supplied flags directly to the syscall. Missing Python support, EINVAL, ENOSYS, EBADF or EPERM must refuse; do not infer support from a header or version string and do not fall back to killpg.

Small production mechanism proposed after actual trust red

  1. Before releasing a command, create a private session using Popen and a tiny isolated stdlib exec gate. Only its anonymous gate-reader FD is inherited; it waits at most five seconds, bounded by the existing inner deadline, and exits on EOF, timeout or an invalid byte. It does not fork or exec the target before release. Parent pidfds are close-on-exec, remain private and are never passed to the command. The exact argv/environment reaches the target through same-PID exec; no shell or user-supplied executable is used as the gate. This closes the spawn-to-binding race without relying on SIGCHLD disposition or a leader staying alive voluntarily.
  2. Capture the exact Popen handle, original UID/PID/start_ticks/PGID/session ID, leader pidfd and its fstat device/inode while the gate waits. Require own UID, parent PID, and PID=PGID=session ID. Re-read identity after pidfd open. Register one private ownership object, retain it until confirmed stop, and validate its descriptor identity before every signaling boundary. The mapping and object must agree on the same Popen; caller booleans cannot grant ownership. A real flag-4 signal-zero capability call must succeed before gate release. Failed capture/capability closes the gate and stops only the originally bound individual pidfd, then performs a bounded wait; no target or descendant was released. An unbound gate is released only to EOF/its finite expiry, never signaled by guessed identity.
  3. Group probes, TERM and KILL use only signal.pidfd_send_signal(original_pidfd, signal, None, 4) after ownership validation, including when Popen.poll/wait reports leader exit. Signal-zero ESRCH is accepted as empty only through the still-valid original handle; all other errors retain uncertainty. Attempt every registered group even when another stop fails. Keep 15-second TERM, five-second KILL and two-second sampler bounds. Native successful fixture setup/request/stop must still fit the one global 240-second deadline; an exhausted deadline is failure, not a reset. Emergency force-stop under the outer containment preserves that failed verdict rather than claiming timely acceptance.
  4. Save/enable the controller's subreaper state before launching children and restore it after cleanup. Reap the exact original leader through Popen and adopted descendants through independently captured, revalidated child pidfds and nonblocking waitid(P_PIDFD, WEXITED | WNOHANG). Candidate child capture requires own UID, controller parent, original group membership and unchanged start identity, with original flag-4 group probes around capture; no concurrent new launches or unrelated waits occur during closeout. Linux waitid documentation defines descriptor-selected child waiting; the subreaper documentation defines adoption by the nearest living subreaper. Missing reaping capability refuses admission. Do not use arbitrary waitpid(-1) or guessed-PGID signals. Failure to bind/reap or confirm original group zero retains scratch.
  5. Stop the sampler and archive bounded public stop/identity/error observations before deciding removal. Remove owned scratch only after every original group and sampler is confirmed stopped. A refusal retains scratch and cannot emit the existing stopped-groups cleanup claim. Ownership FDs remain open until their final stop observation; no descriptor number is treated as a persistent identity after close.

Separate trust controls to register first

Extend the existing fixture/helper and tables; no new parallel browser class. The helper passes duplicated originally retained leader pidfds and immutable ownership identities into the explicit mapping before any exited-leader wait. Independent test cleanup continues using its separate original pidfds and is unaffected by finalizer fault injection. Existing finite 45-second processes, PIDFD identity checks, restored subreaper, retained failure evidence and outer cgroup containment stay mandatory.

Literal control Fixed required observation
Missing ownership mapping/entry Refuse before either pidfd or numeric signal; preserve scratch and attempt other valid entries
Wrong exact Popen/leader/start/PGID or another owned group's pidfd Refuse before any signal to the mismatched entry; never touch the other original group
Closed original FD, same integer reused for ordinary FD, or replaced by another live pidfd Original descriptor identity check refuses before signal; independent fixture cleanup retains custody
Unsupported flag-4 EINVAL/ENOSYS or absent Python/child-wait capability Target exec marker absent; finite gate safely stops; failure and scratch retained; no numeric fallback
Real retained leader pidfd after leader wait, with TERM-resistant same-group child Actual flag-4 TERM/KILL reaches the original child, exact child reaped and original flag-4 zero probe yields ESRCH
One real group stop deliberately refused at pidfd boundary Every other owned group attempted/stopped; refused group remains observable; scratch retained and no false cleanup claim
Gate capture/release timeout and target immediate exit Finite stop/reap, no unbound signals or descriptors/processes left, exact successful exec retains original PID

The five existing actual-finalizer counterexamples and empty-finalizer positive keep their outcomes. Amend the deliberate-stop fault boundary to cover both the historical numeric call and actual pidfd call, without suppressing independent cleanup. Preserve the published original red unchanged; rerun the admitted namespace controls on old production before claiming a repaired finalizer. Distinguish absent ownership mechanism assertions from executed unsafe-signal counterexamples. Actual native flag-4/group-zero and gated-exec evidence is a prerequisite; mocked capability refusal alone cannot admit the browser.

Affected production is confined to tools/ui/live_browser_check.py launch and closeout, with the smallest local ownership helper; prospective estimate 150-190 net lines, to be measured before implementation and reported if larger. Trust changes are only tools/ui/test_role_matrix.py, the clock protocol and CURRENT status. No clock/shim/frame/ledger/row/status changes, generic process framework, new deadline, or browser run is admitted by this proposal.

Limit: private group ownership covers the expected browser/server descendants, not hostile descendants deliberately moving into other sessions. The unchanged outer resource-group tree remains the final independent stop boundary; it does not substitute for the required per-fixture zero-population evidence.