diff --git a/research/h25-group-retirement-audit/ASSESSMENT.json b/research/h25-group-retirement-audit/ASSESSMENT.json new file mode 100644 index 0000000000..0db10d8a09 --- /dev/null +++ b/research/h25-group-retirement-audit/ASSESSMENT.json @@ -0,0 +1,30 @@ +{ + "schema": "scoped-group-retirement-source-audit-v1", + "status": "SOURCE_COUNTEREXAMPLE_REVIEWED_PROPOSED_CORRECTION_UNIMPLEMENTED", + "established_scope": [ + "The selected owner enters consuming wait before an unconditional final numeric group signal.", + "Linux v6.12 can release numeric ID lookup while old reference-counted PID objects remain alive.", + "A permitted reused-number scheduling trace refutes the required numeric signalling ownership invariant." + ], + "accepted_for_future_implementation": [ + "Retain the direct child through non-consuming terminal observation, signal before consuming wait, and reconcile actual exclusive wait ownership.", + "Integrate one declared common-runner successor through the authentic caller, both complete source recipes and detached bindings." + ], + "not_established": [ + "Proposed code implemented or executed", + "Regressions executed", + "Independent human or proof-kernel acceptance", + "Complete helper/context source qualification", + "Current native/compiler/environment qualification", + "Original baseline/candidate equality", + "Successful retention/CLOSE", + "Complete physical cost or resource fit", + "Measured performance gain" + ], + "new_executed_project_controls": 0, + "new_target_operations": 0, + "new_budget": false, + "physical_and_outside_costs": "OPEN_NOT_ZERO", + "scope_changed": "The constructive transfer inquiry produced a negative result and an explicit repair handoff. Implementation dispatch stopped at the fixed thirty-minute reassessment before the complete response arrived.", + "ai_review": "Pro supplied construction; Codex checked the critical source implications. This is bounded AI source review, not independent human validation." +} diff --git a/research/h25-group-retirement-audit/COUNTEREXAMPLE.md b/research/h25-group-retirement-audit/COUNTEREXAMPLE.md new file mode 100644 index 0000000000..4a9ea69f2c --- /dev/null +++ b/research/h25-group-retirement-audit/COUNTEREXAMPLE.md @@ -0,0 +1,27 @@ +# A post-reap numeric group signal can address a new group + +The affected owner is `run_guarded_v3.py` in PR #6864 at `c8f7bd582ffc1301b851dde2364c60f1636aac8b`. Its exact source is retained as `run_guarded_v3.py.txt`. Lines 354–374 launch the direct child with `start_new_session=True`, monitor it with `Popen.poll()`, enter `process.wait()`, and then unconditionally call `killpg(process.pid, SIGKILL)` from `finally` before another wait. + +Let q be that child's PID, session ID and initial group ID. The required invariant is that every numeric group signal performed by this owner addresses its original owned group, or establishes that group is absent. The following execution is compatible with an ordinary completed original case or the original optimized refusal; no scientific API or body is added. + +1. The waiting entry and inner scientific path finish their local work and the direct GNU-time child exits. Its group has no remaining live members. +2. The runner consumes the child's wait. Its monitoring poll can do this earlier; the explicit wait before `finally` independently supplies a consuming boundary. +3. Once all task associations for the old PID object are detached, Linux removes q from namespace lookup. A held pidfd may keep the old object alive without preserving the numeric lookup reservation. +4. The runner is descheduled. An unrelated process with signal-permitting credentials is allocated q and successfully establishes its own session/group. Cyclic allocation permits number reuse; the selected source profile supplies no host-wide exclusion over all PID allocation. +5. The runner resumes and calls numeric `killpg(q, SIGKILL)`. Numeric lookup resolves the new group, so the signal can kill an unrelated process. The original cgroup guardian cannot undo this external signal. + +The trace is existential, not a claim that every run recycles the number or that a particular workload did so. Even a rare permitted trace refutes the universal signalling-ownership invariant. No live reproduction, PID-reuse stress test or observed incident is claimed. + +## Primary-source implications + +In Linux v6.12, `kernel/pid.c:133–163` removes each numeric ID with `idr_remove` before deferred reference release; `346–367` reaches that removal after the last task association disappears. Allocation at `215–242` uses cyclic namespace allocation. `kernel/signal.c:1603–1617` resolves negative numeric group signalling with `find_vpid`. These source facts support steps 3–5. Exact raw-source hashes and URLs are in `SOURCES.json`. + +The `WNOWAIT` path in `kernel/exit.c:1143–1169` reports the zombie's terminal information without the consuming state transition. This supports the proposed repair invariant, provided the actual parent retains exclusive wait ownership and auto-reaping is excluded by the selected launch chain. It does not by itself qualify the proposed Python implementation or prove every error path. + +Official CPython v3.12.3 `Lib/subprocess.py:1973–2005` confirms that polling uses consuming `waitpid(..., WNOHANG)`. This is corroborating CPython 3.12 source, not a newly asserted current-host or exact selected patch-release association. The explicit wait-before-final-signal trace does not require selecting that patch release. + +## What stays conditional + +Keep the current native entry's `start_new_session=False`: changing it blindly would move scientific work out of the common runner's sampled and signalled group. The inspected cold-build control compares the child's inherited group with the actual parent's captured group, not with a fixed numeric group. Its failure protocol still requires outer termination and join. That narrow relational result does not establish a complete theorem over every helper/context body, group-changing path or instantaneous resource peak. + +The current selected source therefore remains unsuitable for the required numeric signalling invariant. The finding is complete within this negative scope. Whole caller/source qualification and actual execution remain unestablished. diff --git a/research/h25-group-retirement-audit/MANIFEST.json b/research/h25-group-retirement-audit/MANIFEST.json new file mode 100644 index 0000000000..443c09d54a --- /dev/null +++ b/research/h25-group-retirement-audit/MANIFEST.json @@ -0,0 +1,53 @@ +{ + "schema": "source-audit-raw-byte-manifest-v1", + "package": "research/h25-group-retirement-audit", + "files": { + "ASSESSMENT.json": { + "bytes": 1759, + "sha256": "e5d20055126db9c400e533a4e5e9bc26a9ffc7cf44d93397e5a6eb5e83f0b17c", + "git_blob_sha1": "0db10d8a09014c1101dc8c80fefb61bcb0286505" + }, + "COUNTEREXAMPLE.md": { + "bytes": 4058, + "sha256": "916faa1929912e77dbf1cbb10e2a1c6d5546a63348a2a1570bb92ae47788470d", + "git_blob_sha1": "4a9ea69f2c00ea8ca819beabcf59bf18d4630c38" + }, + "NOTICE.md": { + "bytes": 1093, + "sha256": "eae9b3f3d6e5574e3d16f300aa9324d1d27df8325dbd93b1ee116baf0cf3238b", + "git_blob_sha1": "d03416a8018f9907ade1c94d56ba59c190253e59" + }, + "PRO-CONSTRUCTION.md": { + "bytes": 24584, + "sha256": "0eef3ba712a4875ad1fed1431adbb4af26b327219ad6cb75ae9b9e49a49238d7", + "git_blob_sha1": "3720564a7beee0ae1eb0d0238ac62b71dfc7fe39" + }, + "README.md": { + "bytes": 3801, + "sha256": "d44655cc7513ad8223e15a1736172d2825f9b2f5dd252c8c5d5a13f215ced781", + "git_blob_sha1": "438270404709f4559840d90873f0566ae20f8ca5" + }, + "REPAIR-PLAN.md": { + "bytes": 4107, + "sha256": "b34655af282a2690bc1df1976c024ff5c74eedd109ef4d9451a1ec2fe026e06b", + "git_blob_sha1": "e5f91e6a9dfb69d4d441a14197fbb9fbe1ee545a" + }, + "SEARCH-COVERAGE.json": { + "bytes": 2348, + "sha256": "05df4b1e3014b7d8af87565d34877eb6cdd88550f2d8e60938a46af41c391bae", + "git_blob_sha1": "f5fad229809581504fe9f643f961644399ed5489" + }, + "SOURCES.json": { + "bytes": 3462, + "sha256": "2161ab0389ed191c97f1403ee0623941e83beba2e85cae8d2154b3ac5eade0b7", + "git_blob_sha1": "3a38082f517e12fbadbc0e43c60aa68466d98962" + }, + "run_guarded_v3.py.txt": { + "bytes": 21222, + "sha256": "17380a3839c6ba83b23613c2af7fd96485625de9d306e355fbc96bf88d2f99a8", + "git_blob_sha1": "296db95da230b0400c06931f21685118d1f1c8c2" + } + }, + "self_excluded": true, + "execution": "No replay hook and no executable successor is supplied." +} diff --git a/research/h25-group-retirement-audit/NOTICE.md b/research/h25-group-retirement-audit/NOTICE.md new file mode 100644 index 0000000000..d03416a801 --- /dev/null +++ b/research/h25-group-retirement-audit/NOTICE.md @@ -0,0 +1,7 @@ +New source-audit text and the proposed construction were prepared for Tekalign with substantial OpenAI Codex and ChatGPT Pro assistance, under Apache-2.0. + +The affected runner and source lineage come from the existing CrocSwap/integer-mult-bounds verification contributions, including PRs #6321, #6830, #6837, #6847 and #6862–#6864. Their original attribution and notices are preserved. The predecessor wait-state owner remains credited for the compatible ownership principle. + +The primary Linux sources are referenced at Torvalds Linux v6.12 and retain their GPL-2.0-only notices; their source bodies are not relicensed or copied into this package. Official CPython v3.12.3 is referenced under its existing license. Other contributors' inspected Windows portability work, including Mihir Srivastava's PR #4686 and its cited predecessors, is credited as separate work and not claimed as this repair. + +The complete Pro response is retained as proposed reasoning and unexecuted source, with its explicit gaps. AI assistance is not human review, proof-kernel acceptance or executed validation. diff --git a/research/h25-group-retirement-audit/PRO-CONSTRUCTION.md b/research/h25-group-retirement-audit/PRO-CONSTRUCTION.md new file mode 100644 index 0000000000..3720564a7b --- /dev/null +++ b/research/h25-group-retirement-audit/PRO-CONSTRUCTION.md @@ -0,0 +1,286 @@ +## Decision + +**Keep `start_new_session=False`, but repair the common runner’s retirement order. The published process-group invariant is false because the runner can signal a recycled process-group number after reaping its original group leader.** + +This is an admissible counterexample involving the actual selected runner and an original completed or optimized-refusal case. It requires neither a new scientific API call nor compiler misbehaviour. The smallest correction is to observe the direct child’s terminal state without reaping it, perform the final group signal while that child still reserves its numeric identity, and then consume its wait once. + +The correction needs a declared common supervisory successor and updated bindings. It does **not** require changing either scientific image, the factory/cwd repairs, the native isolation model, or the original scientific refusal. It also does not complete the whole source-binding activity. + +### Input and coverage audit + +I inspected the current README, source map, interface argument, common runner, waiting entry, supervisor, guardian, native view, receiver, registry, relevant carrier sources, and the earlier runner. I also inspected the original cold-build owner and its group-sensitive controls, and the relevant Linux v6.12 PID, signal, and wait implementations. No project code, control, native operation, or workload was executed. + +The directly readable sources were sufficient to establish the counterexample. They were not sufficient for a fresh, complete no-group-change proof over all 52 bodies and 65 contexts. In particular, the [#6830 source map](https://github.com/Clear1nsight/integer-mult-bounds/blob/39fcd67c991bd72918080b1d2c02feb3d679206d/research/h25-caller-proof-completion/SOURCE-MAP.json) identifies, but does not expose as readable bodies, the exact selected `cache_contract_v8.py` and `cold_authority_v7.py` variants. Their normal hashes are `06911ec0…fa64ac` and `b32155cd…abee5`; the optimized variants are `d317c986…01a2` and `f279ab86…aec10`. I have not inferred their behaviour from those hashes or decoded their archive. + +## 1. What the topology actually changes + +Let `R` denote the runner, `T₀` the new outer GNU-time process, `E` the waiting entry, and `T₁` the native-entry process that subsequently execs inner GNU time. Each recorded generation is the actual PID plus start ticks, corroborated where required by a held pidfd. + +| Birth or boundary | Earlier #6830 | Current #6864 | Actual ownership | +|---|---|---|---| +| G → M → K → P/C2 | Fresh cgroup D; M, K, and P establish the selected sessions. P execs C2 without changing its PID. | Unchanged. | G owns D and native lifecycle records; each carrier owns its immediate wait and captures. | +| C2 → R | R starts a new session/group. | Same. | C2 registry, `OwnedDomain`, actual R pidfd, and G Root registration. C2 owns R’s consuming wait. | +| R → immediate native/observer child | Native entry starts a new session, then execs GNU time. | R starts `T₀` in a new session, with SID = PGID = PID `q`. | R owns the immediate `Popen` and wait. C2 also holds observed generation pidfds. | +| `T₀` → E | Absent. | E inherits group `q`, publishes ARRIVED, and stops before inner birth. | GNU time waits for E; the authenticated C2 parent owns the one-use pidfd release after qualification and known intent closure. | +| E → native entry → `T₁` | The equivalent native/time process was R’s separately grouped child. | `start_new_session=False` preserves group `q`; native setup and exec do not change it. | E owns the immediate pidfd, wait, inner relay, and local closure record. | +| `T₁` → scientific Root and compiler children | Ordinary children inherit the inner time group. | Ordinary children inherit `q`. | GNU time, original scientific wrappers, and compiler owners retain their distinct waits and failure protocols. | +| C2 → property query | Separate query session/group, before scientific launch. | Separate query session/group while R/T₀/E are protected and E is stopped. | Query `OwnedDomain` borrows the exact protected-generation exclusion; it does not own their pidfds or release. | +| Seal and final retirement | Source registry plus genuine G closure. | Same responsibilities. | Local waits and EOF precede registry closure; G’s frozen D census and final retirement remain mandatory. | + +The relevant births and ownership checks are in [fresh_supervisor.py](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/fresh_supervisor.py.txt#L547-L788), [candidate_entry.py](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/candidate_entry.py.txt#L296-L402), and [retention_guardian.c](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/native/retention_guardian.c.txt#L373-L426). + +Blindly restoring `True` in E would place science outside the group sampled and killed by common R. The outer domain could still observe or retire it, but that would not preserve the common runner’s declared group measurement and control boundary. + +## 2. The concrete failure trace + +The current common runner does the following. + +1. Line 354 launches `T₀` with `start_new_session=True`. +2. Line 356 calls consuming `process.poll()`. +3. Line 368 calls `process.wait()`. +4. Its `finally` then unconditionally calls `killpg(process.pid, SIGKILL)` at line 371, followed by another wait. + +See [the actual monitoring block](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/run_guarded_v3.py.txt#L350-L374). + +A permitted trace is therefore: + +- The original optimized case produces its genuine refusal, E completes its local retirement, and `T₀` exits. +- R’s `poll()` reaps `T₀`. Its session/group has no remaining members. +- R is descheduled before the final group signal. +- An unrelated same-UID host process receives recycled number `q` and establishes a new session/group. +- R resumes and sends `SIGKILL` to that unrelated group. + +The existing premises do not grant a host-wide exclusion over PID allocation. This example changes no scientific body and introduces no scientific group operation. + +**The held pidfd does not prevent this.** Linux v6.12 removes the numeric PID from the namespace lookup in `free_pid`, independently of the later destruction of the reference-counted PID object. Numeric group signalling subsequently resolves the number through `find_vpid`. A pidfd remains a reference to the old identity, but `killpg(q)` does not use that reference. See [`pid.c` allocation retirement](https://github.com/torvalds/linux/blob/v6.12/kernel/pid.c#L133-L163), [`__change_pid`](https://github.com/torvalds/linux/blob/v6.12/kernel/pid.c#L346-L367), and [numeric signal lookup](https://github.com/torvalds/linux/blob/v6.12/kernel/signal.c#L1603-L1617). + +The earlier #6830 runner avoided this particular final signal after a committed wait. Its final group kill was conditional on `READY` or `SAFE_NONTERMINAL`. See [the predecessor retirement block](https://github.com/Clear1nsight/integer-mult-bounds/blob/39fcd67c991bd72918080b1d2c02feb3d679206d/research/h25-caller-proof-completion/readable/implementation/source/fresh_runner.py.txt#L683-L728). + +G cannot repair the erroneous signal afterwards. Its cgroup ownership protects retirement of D; it does not undo harm to a process outside D. + +## 3. Preservation results that can be established + +### The original group observation is relational + +The inspected cold-build controls capture `original_group = os.getpgrp()`, require the parent group to remain unchanged, and compare the child’s JSON against that captured group. They do not require a particular numeric PGID or PID = PGID. The cold builder launches with `start_new_session=False`, and its limit callback changes FSIZE rather than group membership. Therefore the inherited-group predicate is preserved in the current topology, provided the original operation follows that selected path. The actual numeric observations must remain in the retained evidence without rewriting. See the [cold-build owner](https://github.com/CrocSwap/integer-mult-bounds/blob/ff7df8391c3bb3f5518ce43682ba4c1e41380e3e/research/h25-fresh-sc-constructor/successor-v26/implementation/payload/body-pool/aa0a315110e031e19291970c9611fe5e0f91465fa20eaec51e81186099563f78.blob#L169-L240) and [original controls](https://github.com/CrocSwap/integer-mult-bounds/blob/ff7df8391c3bb3f5518ce43682ba4c1e41380e3e/research/h25-fresh-sc-constructor/successor-v26/implementation/payload/body-pool/37db9881193f0ecf111f8ce94b6ef84cd3b22d1123b3266ced25390aa48d5a52.blob#L93-L179). + +Those same controls expressly leave failed-build finality to the outer owner’s whole-group termination and join. The inheritance lemma does not discharge retirement. + +### Session changes do not escape the genuine cgroup + +G creates D before M’s `CLONE_INTO_CGROUP` birth. Under the unchanged finite effect premises, descendants inherit D and have no migration/export channel. `setsid` and `setpgid` do not change cgroup membership. Indeed, the selected science policy permits those calls, so it would be incorrect to derive group invariance merely from seccomp. + +G’s seal freezes D and rejects live producers or unregistered descendants. Failure retirement separately requires `cgroup.kill`, recursive population emptiness, native reaping, and final wait reconciliation. Short-lived children, changed groups, and reparenting do not make these obligations disappear. G loss itself still has no automatic-success theorem. These implications follow from [the native view](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/native/science_view_entry.c.txt#L698-L820) and [guardian retirement](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/native/retention_guardian.c.txt#L537-L597). + +This is a conditional cgroup/lifetime result, not proof that group sampling captures every instantaneous memory peak or every permitted group-changing path. + +### Local closure remains distinct from domain closure + +E deliberately reports `process_group_empty=False`. The receiver separately requires completed outer cleanup, no remaining or adopted-after-root processes, complete relay EOF, and known descriptor closure. If a cap kill also kills E before its relay drains, missing or incomplete closure remains failure/uncertainty. It must not become “complete retained output”. See [entry closure](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/candidate_entry.py.txt#L372-L402) and [terminal validation](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/full_result_receiver.py.txt#L272-L338). + +## 4. Proposed owning correction + +### The retained-child lemma + +Replace consuming monitoring polls with `waitid(P_PID, pid, WEXITED | WNOHANG | WNOWAIT)`. Linux’s `WNOWAIT` path obtains terminal information without moving the zombie into the consumed state. Perform the last numeric group signal before the sole consuming wait. See [`wait_task_zombie`](https://github.com/torvalds/linux/blob/v6.12/kernel/exit.c#L1143-L1169). + +The no-auto-reap premise has a source derivation for the inspected candidate chain. Actual M entry requires default SIGCHLD; the selected intervening carrier/controller sources do not replace it; fresh exec clears signal-action flags, including `SA_NOCLDWAIT`. Thus R retains its direct child until its own consuming wait. This uses the existing authentic-entry and selected-native semantics, not a new global exclusion. See [M’s actual launch checks](https://github.com/Clear1nsight/integer-mult-bounds/blob/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/carrier/inner_meter.py.txt#L222-L231) and [Linux signal-action reset](https://github.com/torvalds/linux/blob/v6.12/kernel/signal.c#L533-L545). + +Consequently, while numeric group signalling is possible, `q` still belongs to the original live or unreaped child. Once the consuming wait is entered, this function performs no further numeric signal. A failed or unrecognised observation does not license a guessed cleanup target. + +### Patch-ready implementation + +Add the following owner to `run_guarded_v3.py`. It also replaces the surrounding capture `with` statement, because a later file-close exception must not mask the working primary. This is proposed source, not executed or independently accepted code. + +```python +_GROUP_CUSTODY = [None] + + +def run_group_capture(timed, stage, env, folder, runner_start, cap, spent): + if _GROUP_CUSTODY[0] is not None: + raise RuntimeError('one common-runner group owner') + + state = { + 'process': None, 'spawn': 'NOT_ENTERED', + 'peek': 'NOT_ENTERED', 'raw_peek': None, + 'owns_unreaped_child': False, + 'signal': 'NOT_ENTERED', 'signal_absent': None, + 'wait': 'NOT_ENTERED', 'raw_wait': None, + 'streams': [[None, 'RESERVED'], [None, 'RESERVED']], + # Body, signal, wait, stderr-close, stdout-close. + 'errors': [[None, None] for _ in range(5)], + 'unknown': False, + } + _GROUP_CUSTODY[0] = state + peak, stop, code = 0, None, None + + try: + for index, name in enumerate(('stdout.txt', 'stderr.txt')): + row = state['streams'][index] + row[1] = 'OPEN_ENTERED' + row[0] = (folder / name).open('wb') + row[1] = 'OWNED' + + state['spawn'] = 'ENTERED' + process = subprocess.Popen( + timed, cwd=stage, env=env, + stdout=state['streams'][0][0], + stderr=state['streams'][1][0], + start_new_session=True) + state['process'] = process + state['spawn'] = 'RETURNED' + + while True: + state['owns_unreaped_child'] = False + state['peek'] = 'ENTERED' + observed = os.waitid( + os.P_PID, process.pid, + os.WEXITED | os.WNOHANG | os.WNOWAIT) + state['raw_peek'] = observed + if observed is not None: + if (type(observed) is not os.waitid_result or + observed.si_pid != process.pid or + observed.si_code not in + (os.CLD_EXITED, os.CLD_KILLED, os.CLD_DUMPED)): + raise RuntimeError('actual owned terminal observation') + state['peek'] = 'KNOWN' + state['owns_unreaped_child'] = True + if observed is not None: + break + + peak = max(peak, group_rss(process.pid)) + if peak > RSS_CAP: + stop = 'sampled_group_rss_cap' + elif time.monotonic() - runner_start > min(TIMEOUT, cap - spent): + stop = 'wall_or_aggregate_cap' + elif max((folder / 'stdout.txt').stat().st_size, + (folder / 'stderr.txt').stat().st_size) > OUTPUT_CAP: + stop = 'output_cap' + if stop: + break + time.sleep(0.05) + + except BaseException as error: + state['errors'][0][0] = error + state['errors'][0][1] = ( + BaseException.__dict__['__traceback__'].__get__( + error, BaseException)) + if state['spawn'] == 'ENTERED': + state['spawn'] = 'UNKNOWN' + state['unknown'] = True + + finally: + process = state['process'] + if process is not None: + if state['owns_unreaped_child']: + state['signal'] = 'ENTERED' + try: + os.killpg(process.pid, signal.SIGKILL) + state['signal'] = 'KNOWN' + except ProcessLookupError as error: + state['signal_absent'] = error + state['signal'] = 'KNOWN_ABSENT' + except BaseException as error: + state['errors'][1][0] = error + state['errors'][1][1] = ( + BaseException.__dict__['__traceback__'].__get__( + error, BaseException)) + state['signal'] = 'UNKNOWN' + state['unknown'] = True + + state['wait'] = 'ENTERED' + try: + code = process.wait() + state['raw_wait'] = code + if type(code) is not int: + raise RuntimeError('actual terminal wait result') + state['wait'] = 'COMMITTED' + except BaseException as error: + state['errors'][2][0] = error + state['errors'][2][1] = ( + BaseException.__dict__['__traceback__'].__get__( + error, BaseException)) + state['wait'] = 'UNKNOWN' + state['unknown'] = True + else: + state['unknown'] = True + + for error_slot, stream_index in ((3, 1), (4, 0)): + row = state['streams'][stream_index] + if row[1] == 'OPEN_ENTERED': + row[1] = 'UNKNOWN' + state['unknown'] = True + if row[1] != 'OWNED': + continue + row[1] = 'CLOSE_ENTERED' + try: + row[0].close() + except BaseException as error: + state['errors'][error_slot][0] = error + state['errors'][error_slot][1] = ( + BaseException.__dict__['__traceback__'].__get__( + error, BaseException)) + row[1] = 'UNKNOWN' + state['unknown'] = True + else: + row[1] = 'CLOSED' + + for error, traceback in state['errors']: + if error is not None: + raise BaseException.with_traceback(error, traceback) + if state['unknown'] or state['wait'] != 'COMMITTED': + raise RuntimeError('common-runner retirement remains unknown') + return code, peak, stop +``` + +Replace the entire original capture/launch/monitor block at lines 353–374 with: + +```python + code, peak, stop = run_group_capture( + timed, stage, env, folder, runner_start, cap, spent) +``` + +Keep the preceding `start = time.monotonic()` and subsequent elapsed-time, log finalisation, usage, source-currentness, and receipt code in their existing order. + +This retains the original threshold order, sampling function, 50 ms delay, stop labels, and return-code interpretation. The primary error occupies the first fixed slot before retirement. Signal, wait, and capture-close errors retain their actual objects and tracebacks in operation order. Uncertain acquisition or retirement cannot return a success tuple. There is no numerical fallback signal after lost ownership, no retry after an uncertain wait, and no new artifact or service. + +“Once” here means one explicit entry into the existing `Popen.wait()` or file-close interface. It does not invent a guarantee about the number of internal kernel calls in the selected CPython implementation. + +### Binding and protocol consequences + +This is a declared retirement-order change under the authorised common supervisory amendment. It is **not** covered by the ancestor’s two entry/path substitutions alone. + +Freeze the changed common R, update the supervisor’s immutable runner pin, both stage manifests, command/profile and packet bindings, and the common-runner inverse description. Preserve the current waiting-entry bytes, factory/cwd repair, native/compiler subjects, and scientific member identities. When the genuine primary baseline is adopted, it must select the same common R and establish the same actual wait-ownership launch condition at its real parent. + +The parent-command wall and CPU-through-validation endpoints remain unchanged. The new observation work and retained custody are charged inside that interval. Nested GNU-time fields remain diagnostics and are not added again to enclosing totals. Group RSS remains a sampled metric, not a proof of instantaneous peak or complete numerical fit. + +## 5. Decisive changed controls + +These should use the actual new function with clearly labelled inert filesystem, process, wait, signal, and clock ports. None requires a genuine owner or child launch. + +| Control | Required discriminator | +|---|---| +| Recycled PGID | In a deterministic kernel-state model, the published consuming poll frees `q`, allowing unrelated U to acquire it before final `killpg`. The corrected function must signal before its consuming wait and never signal after reuse. | +| Normal and original refusal | Terminal cases returning 0 and 1 retain those codes and unchanged stop labels. The real receiver still distinguishes original optimized refusal from envelope failure. | +| Working P, signal E, wait S, and close failures | Raise distinct objects at each boundary. P remains raised; E, S, stderr-close, and stdout-close remain in that order with their traceback objects. Each retirement interface is entered once. | +| Lost wait ownership | `ChildProcessError`, a mismatched PID, or an invalid terminal observation causes no numeric signal or consuming wait and yields no successful result. | +| Partial opening or uncertain launch | Failure opening stderr closes known stdout once. Uncertain `Popen` return produces no reconstructed PID. Earlier errors survive later close errors. | +| Cancellation and binding | The original cap decision leads to one group signal before wait. Both selected stages and the authentic caller select the exact changed R; scientific members, factory/cwd, timer, and release bindings remain unchanged. | + +The counterexample is established by source reasoning now. These are proposed regressions, not reported passes. + +## 6. Whole-activity disposition and next exit + +The process-group question has a concrete answer. **Inherited membership is compatible with the inspected original relational control, but the selected common runner’s post-reap numeric signal is unsafe. Repair that owner rather than toggling the native launch flag.** + +The next implementation should contain this retirement correction, its caller integration, its exact binding updates, and the discriminating controls in one immutable revision. Independent review should then accept or reject the retained-child signalling lemma against the actual changed functions. No unchanged timer suite needs to be rerun simply to add evidence counts. + +The following boundaries remain separate and cannot be credited by this repair. + +- **Complete original coverage.** Reconcile the remaining exact helper/context bodies, including the identified unread cache/authority variants, against their actual selected callers. The inspected relational group control is not a universal 52-body/65-context theorem. +- **Native and compiler qualification.** Reuse the unchanged conditional proof only for its selected Linux, security, filesystem, compiler, and interval subject. Actual image/configuration association remains unissued. +- **Genuine primary baseline and comparison.** Adopt #6837/#6847 through the real common parent, preserving the genuine PR #6029 baseline, typed independent transfer, original anchors, and complete comparison protocol. +- **Retention and successful CLOSE.** Preserve the existing genuine registry/G seal and retention path. Adopt #6762 through the actual caller/role/source tuple before claiming successful formal CLOSE. +- **Funding, fit, and execution.** Complete current cost coverage and admission remain S10.04/S10.05 gates. This source argument supplies neither a current balance nor a wall/CPU/RSS bound. + +**The current published subject remains NO-GO for the required signalling invariant. The connected correction above is a source-construction route, with a falsifiable acceptance check. Neither it nor this consultation completes S07.04, qualifies execution, or demonstrates an optimisation.** \ No newline at end of file diff --git a/research/h25-group-retirement-audit/README.md b/research/h25-group-retirement-audit/README.md new file mode 100644 index 0000000000..4382704047 --- /dev/null +++ b/research/h25-group-retirement-audit/README.md @@ -0,0 +1,29 @@ +# Retain the group leader until the final numeric signal + +This source audit identifies a concrete retirement-order defect in the common runner selected by [PR #6864](https://github.com/CrocSwap/integer-mult-bounds/pull/6864), at `c8f7bd582ffc1301b851dde2364c60f1636aac8b`. The runner can reap its direct child and then call `killpg` with the child's old number. If the number is reused while the runner is descheduled, the final signal can target an unrelated process group. + +The result is a source-level counterexample under the selected Linux v6.12 model. No project code, control, kernel probe, native operation or scientific workload was executed. The proposed correction is recorded for implementation; it is not installed or qualified by this package. + +- [Counterexample and its precise premises](COUNTEREXAMPLE.md) +- [Connected repair and decisive acceptance checks](REPAIR-PLAN.md) +- [Complete Pro construction, with proposed code clearly unexecuted](PRO-CONSTRUCTION.md) +- [Assessment and claim boundaries](ASSESSMENT.json) +- [Immutable external references and exact source hashes](SOURCES.json) +- [Bounded upstream search and reuse decisions](SEARCH-COVERAGE.json) +- [Exact affected runner snapshot](run_guarded_v3.py.txt) + +The original investigation is [PR #6321](https://github.com/CrocSwap/integer-mult-bounds/pull/6321); its [maintained plain-English handoff](https://github.com/CrocSwap/integer-mult-bounds/pull/6321#issuecomment-6713710045) explains the wider verification goal. This finding preserves #6864's factory/cwd corrections, #6863's timer/error correction and the original scientific bodies. It does not change the selected mathematical frontier or supply a speedup. + +## Source and reproduction boundary + +`MANIFEST.json` binds every other package file by raw UTF-8 byte count, SHA256 and Git blob identity. The runner snapshot is inert text and equals the published affected file. External inputs remain at explicit immutable GitHub URLs in `SOURCES.json`; they are intentionally not asserted to be ancestors of this publication. Read the referenced line ranges to audit the argument. No network-free full source history or executable replay is claimed. + +This package has no registered replay hook. Hosted planning or onboarding, if successful, verifies repository integration only; skipped replay does not test the proposed correction. No regression passes, environment eligibility, full helper/context coverage, successful comparison, numerical resource fit or final retention result is claimed. Recorded execution allowance is unchanged; complete outside and physical costs remain `OPEN_NOT_ZERO`. + +## Open next questions + +Implement one connected common-runner successor using non-consuming terminal observation, final signalling before reaping, and explicit first/cleanup-error custody. Update the actual caller's runner pin, both source recipes and detached bindings in that same revision. Adapt the earlier runner's wait-state discipline rather than adding another disconnected helper. Run the new discriminators listed in [REPAIR-PLAN.md](REPAIR-PLAN.md) under an admitted bounded operation and obtain exact-source review. The current implementation round stopped dispatch at its thirty-minute reassessment; no new execution or research window is granted here. + +Complete scientific dependency coverage, genuine predecessor-baseline adoption through the same common parent, successful retention/CLOSE, current runtime issuance and complete comparison funding remain separate obligations. Their resolution cannot be inferred from this counterexample or from publication. + +Tekalign, with substantial OpenAI Codex and ChatGPT Pro assistance. Apache-2.0 for new contribution text and code; predecessor and external sources retain their notices. See [NOTICE.md](NOTICE.md). diff --git a/research/h25-group-retirement-audit/REPAIR-PLAN.md b/research/h25-group-retirement-audit/REPAIR-PLAN.md new file mode 100644 index 0000000000..e5f91e6a9d --- /dev/null +++ b/research/h25-group-retirement-audit/REPAIR-PLAN.md @@ -0,0 +1,26 @@ +# One connected retirement correction + +This is an unimplemented construction and acceptance plan. Proposed source is retained in `PRO-CONSTRUCTION.md`; no listed control is reported as executed. + +The owner must retain its direct child as live or unreaped while numeric group signalling is possible. Observe terminal state with `waitid(P_PID, pid, WEXITED | WNOHANG | WNOWAIT)`, preserve the actual returned observation, and reject missing ownership or an unexpected terminal identity. Perform the final group signal before entering the consuming wait. Enter the existing wait interface once and perform no later numeric signal. An unresolved wait or acquisition must remain uncertainty, without guessing a PID or claiming successful retirement. + +Derive default SIGCHLD/no-auto-reap and exclusive consuming-wait ownership through the actual parent/carrier/exec chain. Linux's reset of signal-action flags and the inspected authentic-entry checks are supporting premises; a synthetic replacement owner cannot substitute for that real launch condition. Keep first working errors and actual signal, wait and reverse-order capture-close exceptions with their tracebacks in operation order. Preserve the original cap precedence, labels, sampling interval, scientific return/refusal distinction and enclosing wall/CPU endpoints. + +## Reuse and binding + +The earlier #6830 runner at `39fcd67c991bd72918080b1d2c02feb3d679206d` has an explicit wait-state discipline: its final numeric kill is limited to READY or SAFE_NONTERMINAL and excluded after COMMITTED/CONSUMED. Adapt that ownership principle. Its relay, funding-intent and command interfaces differ from the common runner, so copying the function alone would not establish compatibility. The successor must also retain #6864's exact factory/cwd repairs and #6863's corrected timer/error custody. + +Declare the common supervisory successor explicitly. Update the actual supervisor's immutable runner pin, both complete stage manifests, command/profile and packet bindings, and the common-runner inverse description in one revision. Keep waiting-entry and scientific member bytes unchanged unless a separately justified necessary correction is demonstrated. The genuine primary baseline must later select this same common runner through its actual parent; a diagnostic baseline is not a replacement. + +## Decisive new checks + +| Boundary | Required observation | +|---|---| +| Recycled group number | A deterministic inert kernel-state discriminator shows the old runner frees q before its last signal; the actual successor signals while identity is retained, then waits, and never signals after reuse. | +| Normal and original refusal | The actual successor preserves exit codes 0 and 1 and original stop labels. Receiver qualification remains a distinct check. | +| Working and cleanup failures | Distinct actual objects at body, signal, wait, stderr-close and stdout-close retain the first primary and ordered secondary tracebacks; each retirement interface is entered once. | +| Lost ownership | ChildProcessError, mismatched PID or invalid terminal state produces neither guessed numeric signal nor successful result; consuming wait is not retried. | +| Partial opening or uncertain launch | Known acquired captures close once, original errors survive close errors, and no PID is reconstructed after an uncertain launch return. | +| Cancellation and connected binding | Original cap decisions lead to signal-before-wait; both recipes and the actual caller select the changed runner while scientific, factory/cwd, timer and one-use-release bindings remain intact. | + +Use the actual changed function with explicitly labelled inert process, wait, signal, clock and filesystem ports for these finite regressions. Their passing would not demonstrate a live kernel race, complete native/guardian qualification or a full scientific run. Independent exact-source review must assess the retained-child lemma and the whole connected revision. Do not rerun unchanged timer or factory suites merely to add counts; transfer their unchanged pins and applicable failure obligations. diff --git a/research/h25-group-retirement-audit/SEARCH-COVERAGE.json b/research/h25-group-retirement-audit/SEARCH-COVERAGE.json new file mode 100644 index 0000000000..f5fad22980 --- /dev/null +++ b/research/h25-group-retirement-audit/SEARCH-COVERAGE.json @@ -0,0 +1,62 @@ +{ + "schema": "bounded-upstream-reuse-audit-v1", + "date_utc": "2026-11-08", + "scope": "All-author/all-state GitHub PR-body behavior searches for start_new_session, process-group with guardian, killpg, WNOWAIT and post-reap; implicated heads refreshed. Results below are contract dispositions, not global novelty.", + "queries": [ + { + "terms": "start_new_session", + "results": 1 + }, + { + "terms": "process-group guardian", + "results": 1 + }, + { + "terms": "killpg", + "results": 8 + }, + { + "terms": "WNOWAIT OR post-reap", + "results": 1 + } + ], + "dispositions": [ + { + "PR": 6864, + "head": "c8f7bd582ffc1301b851dde2364c60f1636aac8b", + "decision": "reuse", + "reason": "Affected exact source; preserve its actual factory/cwd repairs." + }, + { + "PR": 6863, + "head": "c009a7514790e865b664ab8cd4688d37787d7eaa", + "decision": "reuse", + "reason": "Preserve its actual timer/error-custody correction; source graph remains a prerequisite." + }, + { + "PR": 6830, + "head": "39fcd67c991bd72918080b1d2c02feb3d679206d", + "decision": "adapt", + "reason": "Its final kill excludes consumed/committed wait states. Different relay/funding/command owner prevents a drop-in replacement; adapt its concrete wait-ownership discipline." + }, + { + "PR": 4686, + "head": "de93ebd044d0b00a8647a9f47f86939a6b18b9aa", + "decision": "not relevant", + "reason": "Inspected body and actual patch repair Windows Git closed-stdin EINVAL and preserve retry gates. No common Linux group-identity retirement correction." + }, + { + "PR": 2498, + "head": "100b46506a55e7fceb36918a8e127a5dbc45d815", + "decision": "not relevant", + "reason": "Windows taskkill/read-only-snapshot cleanup; different native owner and contract." + }, + { + "PR": 1298, + "head": "e76026571dbfa7299bf474b941c9abeb5bd556b3", + "decision": "not relevant", + "reason": "Windows onboarding/locale portability, different launcher and lifetime contract." + } + ], + "limitations": "Searches cover bounded public PR text and inspected candidates, not arbitrary unpublished work or every diff in the repository. Closed/merged results and other contributors were included. No new code justified by a mere empty search." +} diff --git a/research/h25-group-retirement-audit/SOURCES.json b/research/h25-group-retirement-audit/SOURCES.json new file mode 100644 index 0000000000..3a38082f51 --- /dev/null +++ b/research/h25-group-retirement-audit/SOURCES.json @@ -0,0 +1,119 @@ +{ + "schema": "external-pinned-source-audit-references-v1", + "base_commit": "7c49fe32c55de02c2c873655c804b16e4c3fef0a", + "affected_PR": 6864, + "affected_head": "c8f7bd582ffc1301b851dde2364c60f1636aac8b", + "sources": [ + { + "source": "linux-pid", + "url": "https://raw.githubusercontent.com/torvalds/linux/v6.12/kernel/pid.c", + "bytes": 19199, + "sha256": "3a9065c03bf74805a58ce3a372a69f911b3382db769237414aa2034fe218e60c", + "reviewed_lines": [ + [ + 118, + 163 + ], + [ + 215, + 242 + ], + [ + 346, + 367 + ] + ] + }, + { + "source": "linux-exit", + "url": "https://raw.githubusercontent.com/torvalds/linux/v6.12/kernel/exit.c", + "bytes": 49803, + "sha256": "7c28bfc644117470b9cbe6d46c84580de98225755bbb227e61d638ea7104025d", + "reviewed_lines": [ + [ + 1138, + 1174 + ] + ] + }, + { + "source": "linux-signal", + "url": "https://raw.githubusercontent.com/torvalds/linux/v6.12/kernel/signal.c", + "bytes": 129959, + "sha256": "e93c014e2354c8806dfe3504c4f274cab250df0251745e1fd9af9c60616e9326", + "reviewed_lines": [ + [ + 533, + 545 + ], + [ + 1603, + 1617 + ] + ] + }, + { + "source": "cpython-subprocess", + "url": "https://raw.githubusercontent.com/python/cpython/v3.12.3/Lib/subprocess.py", + "bytes": 88725, + "sha256": "baa9f9138d8d20df6284f67e7d2e790f847f65e2c5370de322d54cccd737f2d9", + "reviewed_lines": [ + [ + 1973, + 2005 + ] + ] + }, + { + "source": "affected common runner", + "url": "https://raw.githubusercontent.com/Clear1nsight/integer-mult-bounds/c8f7bd582ffc1301b851dde2364c60f1636aac8b/research/h25-caller-binding-repairs/readable/implementation/source/run_guarded_v3.py.txt", + "bytes": 21222, + "sha256": "17380a3839c6ba83b23613c2af7fd96485625de9d306e355fbc96bf88d2f99a8", + "retained_as": "run_guarded_v3.py.txt", + "reviewed_lines": [ + [ + 350, + 384 + ] + ] + } + ], + "external_acquisition": "Read the exact immutable raw GitHub URLs and compare raw SHA256 before using the cited line ranges. No import, compiler, helper or target operation is required for the source argument. External commits are not asserted to be ancestors of this publication.", + "related_source_references": [ + { + "PR": 6830, + "head": "39fcd67c991bd72918080b1d2c02feb3d679206d", + "path": "research/h25-caller-proof-completion/readable/implementation/source/fresh_runner.py.txt", + "reviewed_lines": [ + [ + 683, + 728 + ], + [ + 882, + 925 + ] + ] + }, + { + "PR": 6863, + "head": "c009a7514790e865b664ab8cd4688d37787d7eaa", + "relation": "preserve prior timer/error source repair" + }, + { + "PR": 6837, + "head": "92bd2213be787cc4451a3b61b00793dad26bebdd", + "relation": "later genuine predecessor-baseline adoption" + }, + { + "PR": 6847, + "head": "efb116491184bd16c5d1ada60d71efdefee61691", + "relation": "common supervisory amendment reference; successor still needs explicit bindings" + }, + { + "PR": 6762, + "head": "3bcf99910a95248452e955d382c59bd23792a39b", + "relation": "separate complete-outcome CLOSE adoption" + } + ] +} diff --git a/research/h25-group-retirement-audit/run_guarded_v3.py.txt b/research/h25-group-retirement-audit/run_guarded_v3.py.txt new file mode 100644 index 0000000000..296db95da2 --- /dev/null +++ b/research/h25-group-retirement-audit/run_guarded_v3.py.txt @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +"""Parent-only immutable-stage runner; explicit clean no-site launch, fixed hash seed.""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import platform +import resource +import signal +import stat +import subprocess +import sys +import time + +HERE = Path('/home/d3lerium/.codex/.chatgpt-projects/g-p-6ad5817fd84881918865926b6fd51453/outputs/caller-qualification-reconciliation-20261107T2312Z/successor/implementation/bindings') +# Reviewed path substitution. This remains the complete registered attempt union. +SOURCE_ROOT = Path('/home/d3lerium/.codex/.chatgpt-projects/g-p-6ad5817fd84881918865926b6fd51453/outputs/caller-qualification-reconciliation-20261107T2312Z/successor/runtime/completed-source/IndependentPrefixDATAComposedV1') +TIMEOUT = 180 +RSS_CAP = 1024 ** 3 +OUTPUT_CAP = 2 * 1024 ** 2 +TOTAL_CAP = 2400 +LOG_CHUNK_BYTES = 64 * 1024 +USAGE_METADATA_CAP = 16 * 1024 + +# This is a trusted parent launch contract, not Python self-authentication or -I. +# -P suppresses unsafe path insertion; -S/-s prevent site and user-site loading. +# Unlike -I, this preserves the explicitly whitelisted fixed hash seed. +LAUNCH_FLAGS = ('-S', '-s', '-P', '-B') +LAUNCH_ENV = {'PATH': '/usr/bin:/bin', 'PYTHONHASHSEED': '0', + 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC', + 'PYTHONDONTWRITEBYTECODE': '1'} + + +def require_trusted_launch(): + if not (sys.flags.no_site == 1 and sys.flags.no_user_site == 1 and + sys.flags.safe_path and sys.flags.dont_write_bytecode == 1 and + sys.flags.ignore_environment == 0 and sys.flags.isolated == 0 and + sys.flags.hash_randomization == 0): + raise ValueError('Explicit no-site/safe-path parent launch required') + if dict(os.environ) != LAUNCH_ENV: + raise ValueError('Exact clean parent environment required') + if sys.executable != '/usr/bin/python3.12': + raise ValueError('Absolute admitted interpreter required') + if tuple(sys.path) != ('/usr/lib/python312.zip', '/usr/lib/python3.12', + '/usr/lib/python3.12/lib-dynload'): + raise ValueError('Unadmitted ordered initial Python search path') + return {'contract': 'trusted-clean-nosite-safe-path-fixedseed-v1', + 'no_site': sys.flags.no_site, 'no_user_site': sys.flags.no_user_site, + 'safe_path': bool(sys.flags.safe_path), + 'ignore_environment': sys.flags.ignore_environment, + 'isolated': sys.flags.isolated, 'hash_seed_environment': '0', + 'hash_randomization': sys.flags.hash_randomization, + 'initial_sys_path': list(sys.path), 'environment': dict(os.environ), + 'trust_boundary': 'Authenticated parent launcher, OS, interpreter and system stdlib before this module; no Python self-authentication claim'} + + +def digest(raw): + return hashlib.sha256(raw).hexdigest() + + +def inventory(stage): + rows = {} + for path in sorted(stage.rglob('*')): + if path.is_symlink(): + raise ValueError('Symlink in immutable stage') + if path.is_file() and path.name != 'STAGE.json': + rows[str(path.relative_to(stage))] = { + 'sha256': digest(path.read_bytes()), + 'mode': path.stat().st_mode & 0o777, + } + return rows + + +def group_rss(group): + total = 0 + for path in Path('/proc').iterdir(): + if not path.name.isdigit(): + continue + try: + raw = (path / 'stat').read_text() + fields = raw[raw.rfind(')') + 2:].split() + if int(fields[2]) == group: + total += int(fields[21]) * os.sysconf('SC_PAGE_SIZE') + except (FileNotFoundError, ProcessLookupError, PermissionError): + pass + return total + + +def log_identity(value): + """Exclude atime: our own read may change it.""" + return (value.st_dev, value.st_ino, value.st_mode, value.st_size, + value.st_mtime_ns, value.st_ctime_ns) + + +def stream_log(path): + """Hash a finite snapshot in bounded memory, failing closed on file drift. + + A stable record covers every byte of the retained file. An unstable record + describes only the bytes actually hashed and cannot support a PASS. Reading + at most the initial length also prevents a growing file prolonging hashing. + """ + path_before = path.stat(follow_symlinks=False) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb', buffering=0) as stream: + before = os.fstat(stream.fileno()) + if not stat.S_ISREG(before.st_mode): + raise ValueError('Retained log must be a regular file') + hasher = hashlib.sha256() + count = 0 + while count < before.st_size: + block = stream.read(min(LOG_CHUNK_BYTES, before.st_size - count)) + if not block: + break + hasher.update(block) + count += len(block) + after = os.fstat(stream.fileno()) + path_after = path.stat(follow_symlinks=False) + stable = (log_identity(path_before) == log_identity(before) == + log_identity(after) == log_identity(path_after) and + count == before.st_size) + return {'sha256': hasher.hexdigest(), 'bytes': count, + 'snapshot_bytes': before.st_size, 'final_bytes': path_after.st_size, + 'stable': stable} + + +def finalize_logs(folder, stop): + """Check sizes before any log read, stream complete stable evidence, recheck.""" + paths = [folder / 'stdout.txt', folder / 'stderr.txt'] + before = [path.stat(follow_symlinks=False) for path in paths] + cap_seen = 'process_monitor' if stop == 'output_cap' else None + if max(value.st_size for value in before) > OUTPUT_CAP: + stop = stop or 'output_cap' + cap_seen = cap_seen or 'before_stream_hash' + records = [stream_log(path) for path in paths] + final = [path.stat(follow_symlinks=False) for path in paths] + if max(value.st_size for value in final) > OUTPUT_CAP: + stop = stop or 'output_cap' + cap_seen = cap_seen or 'after_stream_hash' + for initial, record, ending in zip(before, records, final): + record['final_bytes'] = ending.st_size + record['stable'] = (record['stable'] and + log_identity(initial) == log_identity(ending) and + record['bytes'] == ending.st_size) + if not all(record['stable'] for record in records): + stop = stop or 'output_changed_during_hash' + return records[0], records[1], stop, cap_seen + + +def read_usage(path): + """The time tool's small diagnostic must not reintroduce an unbounded read.""" + if not path.exists(): + return None, False + size = path.stat().st_size + if size > USAGE_METADATA_CAP: + return {'raw_omitted': True, 'bytes': size}, True + with path.open('rb') as stream: + raw = stream.read(USAGE_METADATA_CAP + 1) + if len(raw) > USAGE_METADATA_CAP or path.stat().st_size > USAGE_METADATA_CAP: + return {'raw_omitted': True, 'bytes': path.stat().st_size}, True + try: + return json.loads(raw), False + except (json.JSONDecodeError, UnicodeDecodeError): + return {'raw': raw.decode('utf-8', errors='replace')}, False + + +def read_waiting_configuration(path, expected_sha): + """Read parent-created DATA, never restore its capability or release a child.""" + path = Path(path) + if (not path.is_absolute() or path.resolve(strict=True) != path or + type(expected_sha) is not str or len(expected_sha) != 64 or + any(c not in '0123456789abcdef' for c in expected_sha)): + raise ValueError('Exact canonical waiting configuration and digest required') + identity = lambda q: (q.st_dev,q.st_ino,q.st_mode,q.st_uid,q.st_nlink,q.st_size,q.st_mtime_ns,q.st_ctime_ns) + before = path.lstat() + if (not stat.S_ISREG(before.st_mode) or stat.S_IMODE(before.st_mode) != 0o444 or + before.st_uid != os.getuid() or before.st_nlink != 1 or not 0 < before.st_size <= 16384): + raise ValueError('Owned immutable bounded waiting configuration required') + fd = None + primary = secondary = None + raw = None + try: + fd = os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK|os.O_CLOEXEC) + if identity(os.fstat(fd)) != identity(before): + raise ValueError('Waiting configuration opened identity') + blocks, count = [], 0 + while True: + block = os.read(fd,min(65536,before.st_size+1-count)) + if not block: + break + blocks.append(block); count += len(block) + if count > before.st_size: + raise ValueError('Waiting configuration extra byte') + raw = b''.join(blocks) + if (len(raw) != before.st_size or digest(raw) != expected_sha or + identity(os.fstat(fd)) != identity(before) or identity(path.lstat()) != identity(before)): + raise ValueError('Waiting configuration exact stable readback') + except BaseException as error: + primary = error + finally: + if fd is not None: + owned, fd = fd, None + try: + os.close(owned) + except BaseException as error: + secondary = error + if primary is not None: + if secondary is not None: + raise primary from secondary + raise primary + if secondary is not None: + raise secondary + def pairs(items): + result = {} + for key,value in items: + if key in result: + raise ValueError('Duplicate waiting configuration key') + result[key] = value + return result + def no_number(value): + raise ValueError('Waiting configuration forbids floating/nonfinite numbers') + value = json.loads(raw.decode('ascii'),object_pairs_hook=pairs,parse_float=no_number,parse_constant=no_number) + if type(value) is not dict or value.get('schema') != 'h25-waiting-entry-config-v1': + raise ValueError('Exact waiting configuration schema') + return value + + +def main(): + launch = require_trusted_launch() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('stage') + parser.add_argument('job') + parser.add_argument('label') + parser.add_argument('--waiting-config') + parser.add_argument('--waiting-sha256') + args = parser.parse_args() + for value in (args.stage, args.job, args.label): + if not value or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' for c in value): + raise ValueError('Simple immutable stage/job/run names required') + runner_start = time.monotonic() + runner_usage_start = resource.getrusage(resource.RUSAGE_SELF) + (HERE / 'runtime').mkdir(exist_ok=True) + # A single parent owns this round; never wait behind another scientific run. + with (HERE / 'runtime/science.lock').open('a') as lock: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + stage = HERE / 'stages' / args.stage + stage_raw = (stage / 'STAGE.json').read_bytes() + declared = json.loads(stage_raw) + before = inventory(stage) + if before != declared['files']: + raise ValueError('Stage bytes/modes differ from declared closure') + jobs = json.loads((stage / 'JOBS.json').read_bytes()) + job = jobs[args.job] + phase_raw = (HERE / 'control/EXECUTION-PHASE.json').read_bytes() + phase = json.loads(phase_raw) + cap = phase['aggregate_cap_seconds'] + if type(cap) is not int or cap not in (600, TOTAL_CAP): + raise ValueError('Unrecognized parent budget phase') + candidate_entry = job.get('entry') == 'integration/candidate_entry.py' + waiting = None + if candidate_entry: + if not args.waiting_config or not args.waiting_sha256: + raise ValueError('Candidate waiting configuration required') + waiting = read_waiting_configuration(args.waiting_config,args.waiting_sha256) + expected_case = SOURCE_ROOT / args.label + expected_layout = waiting.get('completed_layout') + if (waiting.get('case_id') != args.label or waiting.get('job') != args.job or + waiting.get('stage_path') != str(stage) or waiting.get('stage_sha256') != digest(stage_raw) or + waiting.get('runner_path') != str(Path(__file__).resolve()) or + waiting.get('runner_sha256') != declared['runner_sha256'] or + waiting.get('entry_sha256') != declared['files']['integration/candidate_entry.py']['sha256'] or + waiting.get('outer_output') != str(expected_case/'runner/artifact/science') or + type(expected_layout) is not dict or expected_layout.get('case_container') != str(expected_case) or + expected_layout.get('runner_root') != str(expected_case/'runner') or + Path(args.waiting_config) != expected_case/'supervision/WAIT.json'): + raise ValueError('Exact registered candidate waiting inverse') + elif args.waiting_config is not None or args.waiting_sha256 is not None: + raise ValueError('Waiting configuration belongs only to the candidate entry') + runs = SOURCE_ROOT + runs.mkdir(exist_ok=True) + spent = 0.0 + for run in runs.iterdir(): + if not run.is_dir(): + continue + # The current case is genuinely reserved before birth. Its exact + # WAIT binding permits only this one pre-launch absence. + if candidate_entry and run == SOURCE_ROOT/args.label: + if (run/'runner').exists() or (run/'runner').is_symlink(): + raise ValueError('Current registered runner must still be absent') + continue + receipt_path = run / 'runner/receipt.json' + if not receipt_path.exists(): + raise ValueError('Unclosed prior attempt: preserve and reconcile before another run') + prior = json.loads(receipt_path.read_bytes()) + spent += prior.get('runner_wall_seconds', prior['wall_seconds']) + if spent >= cap: + raise ValueError('Aggregate budget consumed') + if 'entry' in job: + entry = job['entry'] + if (type(entry) is not str or entry not in ('integration/predecessor_entry.py','integration/candidate_entry.py') or + 'bootstrap' in job or entry not in declared['files'] or + (stage / entry).is_symlink() or + (stage / entry).resolve() != stage.resolve() / entry): + raise ValueError('Entry must be the declared canonical predecessor file without bootstrap') + folder = runs / args.label / 'runner' + folder.mkdir() + runner_before = Path(__file__).read_bytes() + if digest(runner_before) != declared['runner_sha256']: + raise ValueError('Actual runner differs from frozen runner source') + fraction_path = Path('/usr/lib/python3.12/fractions.py') + fractions_before = fraction_path.read_bytes() + env = dict(LAUNCH_ENV) + command = ['/usr/bin/python3.12', *LAUNCH_FLAGS] + if job.get('optimized', False): + command.append('-O') + if 'entry' in job: + command.append(str(stage / entry)) + else: + command.append(str(stage / 'integration/full_driver.py')) + if 'bootstrap' in job: + bootstrap = job['bootstrap'] + if type(bootstrap) is not dict or set(bootstrap) != {'provider', 'driver'}: + raise ValueError('Exact bootstrap map required') + for relative in bootstrap.values(): + if (type(relative) is not str or relative not in declared['files'] or + Path(relative).is_absolute() or '..' in Path(relative).parts or + Path(relative).parent != Path('integration')): + raise ValueError('Bootstrap source must be a declared integration file') + command.extend(['--root-sha256', declared['files']['integration/full_driver.py']['sha256'], + '--provider', str(stage / bootstrap['provider']), + declared['files'][bootstrap['provider']]['sha256'], + '--driver', str(stage / bootstrap['driver']), + declared['files'][bootstrap['driver']]['sha256'], + '--integration', str(stage / 'integration'), '--']) + command.extend(['--output', str(folder / 'artifact/science'), '--lanes', job['lanes']]) + if candidate_entry: + if job.get('finite_only',False) or job.get('lanes') != 'candidate': + raise ValueError('Complete candidate entry cannot select finite science') + command.extend(['--waiting-config',args.waiting_config,'--waiting-sha256',args.waiting_sha256]) + if job.get('finite_only', False): + command.append('--finite-only') + usage = folder / 'usage.json' + timed = ['/usr/bin/time', '-f', '{"user_seconds":%U,"system_seconds":%S,"kernel_max_rss_kib":%M,"exit":%x}', '-o', str(usage), '--', *command] + started = {'command': command, 'parent_launch': launch, 'stage_sha256': digest(stage_raw), + 'runner_sha256': digest(runner_before), 'fractions_sha256': digest(fractions_before), + 'budget_phase_sha256': digest(phase_raw), 'spent_before_seconds': spent, + 'phase_cap_seconds': cap, 'job': args.job, 'label': args.label} + (folder / 'START.json').write_text(json.dumps(started, indent=2, sort_keys=True) + '\n') + start = time.monotonic() + stop = None + peak = 0 + with (folder / 'stdout.txt').open('wb') as out, (folder / 'stderr.txt').open('wb') as err: + process = subprocess.Popen(timed, cwd=stage, env=env, stdout=out, stderr=err, start_new_session=True) + try: + while process.poll() is None: + peak = max(peak, group_rss(process.pid)) + if peak > RSS_CAP: + stop = 'sampled_group_rss_cap' + elif time.monotonic() - runner_start > min(TIMEOUT, cap - spent): + stop = 'wall_or_aggregate_cap' + elif max((folder / 'stdout.txt').stat().st_size, (folder / 'stderr.txt').stat().st_size) > OUTPUT_CAP: + stop = 'output_cap' + if stop: + os.killpg(process.pid, signal.SIGKILL) + break + time.sleep(0.05) + code = process.wait() + finally: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + elapsed = time.monotonic() - start + stdout, stderr, stop, cap_seen = finalize_logs(folder, stop) + measurement, oversized_usage = read_usage(usage) + if oversized_usage: + stop = stop or 'usage_metadata_cap' + unchanged = (inventory(stage) == before and (stage / 'STAGE.json').read_bytes() == stage_raw and + Path(__file__).read_bytes() == runner_before and fraction_path.read_bytes() == fractions_before) + runner_elapsed = time.monotonic() - runner_start + runner_usage_end = resource.getrusage(resource.RUSAGE_SELF) + if runner_elapsed > min(TIMEOUT, cap - spent): + stop = stop or 'wall_or_aggregate_cap' + if runner_usage_end.ru_maxrss * 1024 > RSS_CAP: + stop = stop or 'runner_rss_cap' + receipt = dict(started, exit=code, wall_seconds=elapsed, usage=measurement, + termination=stop, sampled_process_group_rss_bytes=peak, + source_bytes_and_modes_unchanged=unchanged, + stdout_sha256=stdout['sha256'], stdout_bytes=stdout['bytes'], + stderr_sha256=stderr['sha256'], stderr_bytes=stderr['bytes'], + stdout_final_bytes=stdout['final_bytes'], stderr_final_bytes=stderr['final_bytes'], + retained_log_bytes_stable=stdout['stable'] and stderr['stable'], + output_cap_observed_at=cap_seen, log_read_chunk_bytes=LOG_CHUNK_BYTES, + runner_wall_seconds=runner_elapsed, + runner_user_seconds=runner_usage_end.ru_utime - runner_usage_start.ru_utime, + runner_system_seconds=runner_usage_end.ru_stime - runner_usage_start.ru_stime, + runner_lifetime_max_rss_kib=runner_usage_end.ru_maxrss, + python=sys.version, platform=platform.platform(), + status='PASS' if code == 0 and stop is None and stderr['bytes'] == 0 and unchanged else 'STOP') + (folder / 'receipt.json').write_text(json.dumps(receipt, indent=2, sort_keys=True) + '\n') + print(json.dumps(receipt, sort_keys=True), flush=True) + if receipt['status'] != 'PASS': + raise SystemExit(1) + + +if __name__ == '__main__': + main()