executor: a hash-less foreground record falls back to liveness on Windows only (C6 R7 consequence)

_host_pid_matches_record fell back to pid_is_alive whenever a record carried no
host_command_sha256. The fallback exists for Windows, where no command line can be
captured, but it was reachable on POSIX by an owner-shaped forged record -- and
since round 5.4 EPERM reads alive (the process exists), so kill_all_foreground
signalled pid 1 for such a record. The serial lane caught it
(tests/test_workspace_executor_cleanup.py::test_executor_cleanup_ignores_owner_shaped_forged_host_pid_records,
red at 9faccf31). On POSIX a registered foreground process always carries its
command hash: an empty one is not ours to kill -- liveness is not ownership.
Every other pid_is_alive consumer defers on an alive pid; none signals on
liveness alone (ledger section «From the C6 integration battery»).
This commit is contained in:
Ouroboros 2026-09-02 15:18:20 +00:00
parent 9faccf31fd
commit 504bb20c6d
2 changed files with 15 additions and 9 deletions

View file

@ -7667,3 +7667,7 @@ Three read-only lenses on the 5.4 delta: NEEDS_FIXES × 3, no HIGH (3 MEDIUM, 7
3. **R3 stamp-less symlink levels (LOW) — fixed**, pin deferred (suite at its 1600-line cap; mutation-verified by hand). **R5 witness inode tie + `_Abort` on a vanished ledger (LOW) — fixed**, no pin (disclosed). **R6 heartbeat-ownership pin (LOW) — disclosed, not fixed.** **R1 uncached unprobeable directory, R7 shared liveness primitive, R8d epoch-floor duration, R8e socket shape (LOW) — docs corrected** in DESIGN §8/§10/§12.5, packet §5.9/§5.10/§9/§10, ARCHITECTURE row.
Gates at the close-out tip: `tests/test_lockfile_helpers.py` + `tests/test_usage_compaction.py` + `tests/test_usage_*` + `tests/test_persistence_inventory.py` rc 0 (107 passed, 1 skipped Windows-only); `ruff --select F` rc 0; `scripts/check_domains.py` rc 0; `scripts/regenerate_size_ratchet.py --check` rc 0 (`platform_layer.py` 1500, `tests/test_usage_compaction.py` 1600 — both AT their ceilings); `git diff --check` rc 0. The CI-shape battery and `-m serial` run on the integration tree after the lane merges.
## From the C6 integration battery (serial lane on 9faccf31)
The serial lane found the one consumer of `pid_is_alive` the round-5.4 R7 flip (EPERM → alive) turned from "defer" into "signal": `ouroboros/workspace_executor._host_pid_matches_record` fell back to liveness whenever a foreground record had no `host_command_sha256` — a fallback written for Windows (no command line there) but reachable on POSIX by an owner-shaped FORGED record — so `kill_all_foreground` signalled pid 1 (`tests/test_workspace_executor_cleanup.py::test_executor_cleanup_ignores_owner_shaped_forged_host_pid_records`, red at 9faccf31: «owner-shaped forged record should be ignored»). The fallback is now Windows-only: on POSIX a registered process always carries its command hash, so an empty hash is not ours to kill — liveness is not ownership. Other consumers re-checked: process_custody, launcher_server_reaper, delegate_recovery, cancel_intents, extension_import_staging, review_owner_custody, skill_review_runner, agent_startup_checks all DEFER (keep/skip) on an alive pid; none signals on liveness alone. Executor suites (serial + non-serial), process custody and zombie prevention green after the fix; the packet §9 R7 disclosure stands with this consumer named.

View file

@ -603,15 +603,17 @@ def _host_pid_matches_record(record: dict[str, Any]) -> bool:
return False
expected = str(record.get("host_command_sha256") or "").strip()
if not expected:
# No command line could be captured at register time. This is always the
# case on Windows, where platform_layer.process_command() is POSIX-only
# and returns "". Without this fallback the record would be permanently
# unvalidatable, so kill_all_foreground/_services would never dispatch
# taskkill for it (the worktree/service cleanup leak). Fall back to a
# liveness check; owner/schema/id are already verified by the caller
# (_valid_process_record). The PID-reuse hardening via command-hash
# comparison still applies on POSIX, where a command line is available.
return pid_is_alive(host_pid)
# No command line could be captured at register time. On Windows that is
# ALWAYS the case (platform_layer.process_command() is POSIX-only), and
# without a fallback the record would be permanently unvalidatable, so
# kill_all_foreground/_services would never dispatch taskkill for it (the
# worktree/service cleanup leak): there, fall back to liveness — owner/
# schema/id are already verified by the caller (_valid_process_record).
# On POSIX a registered process always has a command line, so an empty
# hash is not ours to kill: liveness is NOT proof of ownership — EPERM
# reads alive (C6 round 5.4), so an owner-shaped forged record naming a
# foreign pid would otherwise be signalled. Fail safe: no hash, no kill.
return IS_WINDOWS and pid_is_alive(host_pid)
return _process_command_sha256(host_pid) == expected