3"""Failure-direction cleanup proofs for image supervisor runtime mutations."""
5from __future__
import annotations
11from contextlib
import suppress
12from pathlib
import Path
13from unittest.mock
import patch
15import hil_convergence_safety_runtime_loader
as runtime_loader
16import hil_convergence_safety_runtime_mutations
as runtime_mutations
17from hil_convergence_safety_runtime_mutations
import (
19 INTEGRITY_REFUSAL_STATUS,
25 _close_owned_descriptors,
26 _close_runner_streams,
28 _create_replacement_root,
36 _open_supervisor_sources,
38 _paths_have_no_live_references,
40 _process_group_members,
41 _reap_terminal_runner,
43 _release_owned_descriptor,
46 _replace_root_after_gate,
53GATE_DESCRIPTOR_COUNT = 4
54SOURCE_DESCRIPTOR_COUNT = 3
58 """Record one stream close and optionally inject its failure."""
60 def __init__(self, *, fail: bool =
False) ->
None:
64 def close(self) -> None:
65 """Mark the attempt before raising the configured failure."""
68 message =
"injected stream close failure"
69 raise OSError(message)
72class _StreamProcessProbe:
73 """Expose the three stream fields consumed by the cleanup helper."""
75 def __init__(self) -> None:
76 self.stdin = _CloseProbe(fail=
True)
77 self.stdout = _CloseProbe()
78 self.stderr = _CloseProbe()
81def _terminal_is_retained(process: subprocess.Popen[bytes]) -> bool:
82 """Report whether one terminal direct child remains waitable and unreaped."""
84 result = os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
85 except ChildProcessError:
87 return result
is not None
90def _spawn_descendant_runner() -> subprocess.Popen[bytes]:
91 """Start a terminal leader whose live descendant retains its process group."""
93 "import os, signal, time\n"
96 " signal.signal(signal.SIGTERM, signal.SIG_IGN)\n"
97 " os.execl('/bin/sleep', '/bin/sleep', '30')\n"
99 f
"os._exit({runtime_mutations.DESCENDANT_STATUS})\n"
101 return subprocess.Popen(
102 (sys.executable,
"-B",
"-I",
"-S",
"-c", source),
103 start_new_session=
True,
104 stdout=subprocess.PIPE,
105 stderr=subprocess.PIPE,
109def _runner_ownership_case() -> tuple[str, bool]:
110 """Prove descendant cleanup and census failure retain leader authority."""
111 base = _spawn_descendant_runner()
112 base_status, base_clean, _stderr = _collect_runner(base)
113 mutant = _spawn_descendant_runner()
118 _wait_runner(mutant,
lambda _group:
None)
119 except RuntimeMutationError:
121 retained = _terminal_is_retained(mutant)
122 mutant_status, mutant_clean = _reap_terminal_runner(mutant)
123 _close_runner_streams(mutant)
127 _dispose_runner(mutant)
128 return "runner census failure retains leader through descendant cleanup", (
129 base_status == mutant_status == runtime_mutations.DESCENDANT_STATUS
137def _invalid_source_descriptor(sources: SourceBundle, invalid: str) -> tuple[str, bool]:
138 """Prove one absent authenticated helper FD refuses before root effects."""
139 supervisor, process_source, cases_source = sources
140 root, identity = _create_root()
142 main_path, process_path, cases_path = _write_sources(
143 root, supervisor, process_source, cases_source
145 baseline = {path.name
for path
in root.iterdir()}
146 main_descriptor = os.open(main_path, os.O_RDONLY | os.O_NOFOLLOW)
147 helper_path = cases_path
if invalid ==
"process" else process_path
148 helper_descriptor = os.open(helper_path, os.O_RDONLY | os.O_NOFOLLOW)
149 process_descriptor =
"2147483647" if invalid ==
"process" else str(helper_descriptor)
150 cases_descriptor =
"2147483647" if invalid ==
"cases" else str(helper_descriptor)
152 runner = subprocess.Popen(
158 f
"/proc/self/fd/{main_descriptor}",
163 "--selftest-missing-entry",
165 _identity_text(root),
167 pass_fds=(main_descriptor, helper_descriptor),
168 start_new_session=
True,
169 stdout=subprocess.PIPE,
170 stderr=subprocess.PIPE,
173 os.close(main_descriptor)
174 os.close(helper_descriptor)
175 status, clean, _stderr = _collect_runner(runner)
177 status == runtime_mutations.PUBLIC_REFUSAL_STATUS
179 and {path.name
for path
in root.iterdir()} == baseline
180 and _no_residue((root,))
182 return f
"invalid {invalid} descriptor refuses before root effects", passed
184 _remove_root(root, identity)
187def _descriptor_is_closed(descriptor: int) -> bool:
188 """Return whether one exact descriptor no longer names an open file."""
196def _descriptor_close_failure_case() -> tuple[str, bool]:
197 """Prove ambiguous close failure cannot retain stale numeric authority."""
198 failed, failed_peer = os.pipe2(os.O_CLOEXEC)
199 later, later_peer = os.pipe2(os.O_CLOEXEC)
200 owned = {failed, later}
204 def close_reuse(descriptor: int) ->
None:
207 if descriptor == failed:
208 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
209 if replacement != descriptor:
210 message =
"released descriptor number was not reused"
211 raise RuntimeMutationError(message)
212 message =
"injected ambiguous close failure"
213 raise OSError(message)
216 _close_owned_descriptors(owned, close_reuse)
223 and replacement == failed
224 and os.fstat(replacement)
is not None
225 and _descriptor_is_closed(later)
227 for descriptor
in (replacement, failed_peer, later_peer):
228 with suppress(OSError):
230 return "ambiguous descriptor close never regains reused numeric authority", passed
233def _stream_close_failure_case() -> tuple[str, bool]:
234 """Prove one stream-close error cannot skip the remaining streams."""
235 process = _StreamProcessProbe()
238 _close_runner_streams(process)
241 closed = process.stdin.closed
and process.stdout.closed
and process.stderr.closed
242 return "stream close failure still attempts every owned stream", raised
and closed
245def _launcher_argv_case(sources: SourceBundle) -> tuple[str, bool]:
246 """Bind the exact interpreter argv and three inherited source descriptors."""
247 supervisor, process_source, cases_source = sources
248 root, identity = _create_root()
251 paths = _write_sources(root, supervisor, process_source, cases_source)
253 runtime_mutations.runtime_launcher.subprocess,
255 return_value=sentinel,
257 result = runtime_mutations.runtime_launcher.launch(
262 argv = popen.call_args.args[0]
263 options = popen.call_args.kwargs
264 descriptors = options[
"pass_fds"]
266 result.process
is sentinel
267 and result.start_error
is result.close_error
is None
270 runtime_mutations.runtime_launcher.PYTHON_INTERPRETER,
274 f
"/proc/self/fd/{descriptors[0]}",
277 == (
"--process-fd", str(descriptors[1]),
"--cases-fd", str(descriptors[2]))
278 and argv[9:] == (
"request",)
279 and len(descriptors) == SOURCE_DESCRIPTOR_COUNT
280 and options[
"start_new_session"]
is True
281 and options[
"stdout"]
is subprocess.PIPE
282 and options[
"stderr"]
is subprocess.PIPE
283 and all(_descriptor_is_closed(descriptor)
for descriptor
in descriptors)
285 return "launcher binds exact isolated three-source interpreter argv", exact
287 _remove_root(root, identity)
290def _source_open_failure_case(
291 sources: SourceBundle, failure_index: int, source_name: str
292) -> tuple[str, bool]:
293 """Prove later source-open failure closes every predecessor FD."""
294 supervisor, process_source, cases_source = sources
295 root, identity = _create_root()
296 opened: list[int] = []
298 def fail_later(path: Path, flags: int) -> int:
299 if len(opened) == failure_index:
300 message = f
"injected {source_name}-source open failure"
301 raise OSError(message)
302 descriptor = os.open(path, flags)
303 opened.append(descriptor)
307 main_path, process_path, cases_path = _write_sources(
308 root, supervisor, process_source, cases_source
311 _open_supervisor_sources(main_path, process_path, cases_path, fail_later)
313 passed = len(opened) == failure_index
and all(
314 _descriptor_is_closed(descriptor)
for descriptor
in opened
318 return f
"{source_name}-source open failure closes every predecessor FD", passed
320 _remove_root(root, identity)
323def _first_source_open_failure_case(sources: SourceBundle) -> tuple[str, bool]:
324 """Prove first-source refusal has no descriptor cleanup authority."""
325 root, identity = _create_root()
326 supervisor, process_source, cases_source = sources
327 close_attempts: list[int] = []
329 def fail_first(_path: Path, _flags: int) -> int:
330 message =
"injected main-source open failure"
331 raise OSError(message)
334 main_path, process_path, cases_path = _write_sources(
335 root, supervisor, process_source, cases_source
338 _open_supervisor_sources(
343 close_attempts.append,
345 except OSError
as error:
346 passed = str(error) ==
"injected main-source open failure" and not close_attempts
349 return "main-source open failure owns no descriptor to close", passed
351 _remove_root(root, identity)
354def _source_close_ambiguity_case(sources: SourceBundle) -> tuple[str, bool]:
355 """Prove failed source cleanup never retries one reused descriptor number."""
356 root, identity = _create_root()
357 supervisor, process_source, cases_source = sources
358 opened: list[int] = []
361 def fail_second(path: Path, flags: int) -> int:
363 message =
"injected process-source open failure"
364 raise OSError(message)
365 descriptor = os.open(path, flags)
366 opened.append(descriptor)
369 def close_reuse(descriptor: int) ->
None:
372 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
373 if replacement != descriptor:
374 message =
"source descriptor number was not reused"
375 raise RuntimeMutationError(message)
376 message =
"injected ambiguous source close failure"
377 raise OSError(message)
380 main_path, process_path, cases_path = _write_sources(
381 root, supervisor, process_source, cases_source
384 _open_supervisor_sources(main_path, process_path, cases_path, fail_second, close_reuse)
385 except OSError
as error:
387 str(error) ==
"injected process-source open failure"
388 and replacement == opened[0]
389 and os.fstat(replacement)
is not None
393 return "source-open failure preserves primary error without stale close retry", passed
395 with suppress(OSError):
396 os.close(replacement)
397 _remove_root(root, identity)
400def _popen_failure_case(sources: SourceBundle) -> tuple[str, bool]:
401 """Prove every documented Popen failure closes all authenticated sources."""
402 root, identity = _create_root()
403 supervisor, process_source, cases_source = sources
404 descriptors = Path(
"/proc/self/fd")
406 main_path, process_path, cases_path = _write_sources(
407 root, supervisor, process_source, cases_source
410 OSError(
"injected Popen OS failure"),
411 ValueError(
"injected Popen value failure"),
412 subprocess.SubprocessError(
"injected Popen subprocess failure"),
415 for failure
in failures:
416 before = {entry.name
for entry
in descriptors.iterdir()}
419 runtime_mutations.runtime_launcher.subprocess,
423 _start_supervisor(main_path, process_path, cases_path, ())
424 except (OSError, ValueError, subprocess.SubprocessError)
as error:
425 outcomes.append(type(error)
is type(failure))
426 outcomes.append({entry.name
for entry
in descriptors.iterdir()} == before)
428 outcomes.append(
False)
429 return "every Popen failure closes all source FDs", all(outcomes)
431 _remove_root(root, identity)
434def _popen_close_ambiguity_case(sources: SourceBundle) -> tuple[str, bool]:
435 """Prove Popen failure remains primary when one source close is ambiguous."""
436 root, identity = _create_root()
437 supervisor, process_source, cases_source = sources
441 def close_reuse(descriptor: int) ->
None:
442 nonlocal first, replacement
446 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
447 if replacement != descriptor:
448 message =
"Popen source descriptor number was not reused"
449 raise RuntimeMutationError(message)
450 message =
"injected ambiguous Popen-source close failure"
451 raise OSError(message)
454 main_path, process_path, cases_path = _write_sources(
455 root, supervisor, process_source, cases_source
457 message =
"injected supervisor Popen failure"
460 runtime_mutations.runtime_launcher.subprocess,
462 side_effect=OSError(message),
469 SupervisorStart(descriptor_closer=close_reuse),
471 except OSError
as error:
473 str(error) == message
and replacement >= 0
and os.fstat(replacement)
is not None
477 return "Popen failure preserves primary error without stale close retry", passed
479 with suppress(OSError):
480 os.close(replacement)
481 _remove_root(root, identity)
484def _descriptor_reuse_case() -> tuple[str, bool]:
485 """Prove final cleanup cannot close one released and reused FD number."""
486 root: Path |
None =
None
487 identity: tuple[int, int] |
None =
None
488 owned: set[int] = set()
492 def close_reuse(descriptor: int) ->
None:
495 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
496 if replacement != descriptor:
497 message =
"released descriptor number was not reused"
498 raise RuntimeMutationError(message)
499 message =
"injected ambiguous release failure"
500 raise OSError(message)
503 root, identity = _create_root()
504 retained, released = os.pipe2(os.O_CLOEXEC)
505 owned.update((retained, released))
506 saved = _new_root_path()
508 _release_owned_descriptor(owned, released, close_reuse)
511 _finalize_root_swap(
None, owned, (root, saved), (identity,
None), (
True,
False, set()))
512 passed = raised
and replacement == released
and os.fstat(replacement)
is not None
513 return "released descriptor reuse remains outside cleanup authority", passed
515 for descriptor
in (*owned, replacement):
516 with suppress(OSError):
518 if root
is not None and identity
is not None and root.exists():
519 _remove_root(root, identity)
522def _pipe_failure_case(sources: SourceBundle) -> tuple[str, bool]:
523 """Prove a second-pipe failure closes the already acquired pair."""
524 root, identity = _create_root()
525 opened: list[int] = []
527 def fail_second(flags: int) -> tuple[int, int]:
529 message =
"injected second pipe failure"
530 raise OSError(message)
531 pair = os.pipe2(flags)
537 _prepare_root_gate(sources,
"pre-open", root, hooks=(fail_second, _write_sources))
541 closed = bool(opened)
and all(_descriptor_is_closed(item)
for item
in opened)
542 _remove_root(root, identity)
543 return "second pipe failure closes the first owned pair", raised
and closed
546def _first_pipe_failure_case(sources: SourceBundle) -> tuple[str, bool]:
547 """Prove first-pipe refusal leaves no descriptor or root residue."""
548 root, identity = _create_root()
549 descriptors = Path(
"/proc/self/fd")
550 before = {entry.name
for entry
in descriptors.iterdir()}
552 def fail_first(_flags: int) -> tuple[int, int]:
553 message =
"injected first pipe failure"
554 raise OSError(message)
558 _prepare_root_gate(sources,
"pre-open", root, hooks=(fail_first, _write_sources))
559 except OSError
as error:
561 str(error) ==
"injected first pipe failure"
562 and {entry.name
for entry
in descriptors.iterdir()} == before
566 return "first pipe failure owns no descriptor to close", passed
568 _remove_root(root, identity)
571def _source_failure_case(sources: SourceBundle) -> tuple[str, bool]:
572 """Prove source preparation failure closes all four gate descriptors."""
573 root, identity = _create_root()
574 opened: list[int] = []
576 def tracked_pipe(flags: int) -> tuple[int, int]:
577 pair = os.pipe2(flags)
582 _root: Path, _supervisor: str, _process: str, _cases: str
583 ) -> tuple[Path, Path, Path]:
584 message =
"injected source preparation failure"
585 raise RuntimeMutationError(message)
593 hooks=(tracked_pipe, fail_source),
595 except RuntimeMutationError:
598 closed = len(opened) == GATE_DESCRIPTOR_COUNT
and all(
599 _descriptor_is_closed(item)
for item
in opened
601 _remove_root(root, identity)
602 return "source preparation failure closes every gate descriptor", raised
and closed
605def _root_acquisition_failure_case(sources: SourceBundle) -> tuple[str, bool]:
606 """Prove gate preparation failure cannot leak its newly allocated root."""
607 observed: list[Path] = []
610 _sources: SourceBundle, _phase: str, root: Path
611 ) -> tuple[Path, Path, Path, dict[str, str], tuple[int, int, int, int], set[int]]:
612 observed.append(root)
613 message =
"injected gate preparation failure"
614 raise RuntimeMutationError(message)
618 _replace_root_after_gate(
621 hooks=(fail_gate, _new_root_path),
623 except RuntimeMutationError:
625 return "gate preparation failure removes its allocated root", (
626 raised
and len(observed) == 1
and not observed[0].exists()
630def _root_validation_exception_case() -> tuple[str, bool]:
631 before = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
632 validation_raised =
False
636 "_root_path_is_safe",
637 side_effect=RuntimeMutationError(
"injected root validation exception"),
640 except RuntimeMutationError:
641 validation_raised =
True
642 after_validation = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
643 return "root validation exception removes its just-created root", (
644 validation_raised
and after_validation == before
648def _mkdir_race_case() -> tuple[str, bool]:
649 mkdir_target = _new_root_path()
652 def create_then_fail(*_args: object, **kwargs: object) ->
None:
653 mkdir(mkdir_target, **kwargs)
654 message =
"injected post-mkdir exception"
655 raise RuntimeMutationError(message)
659 patch.object(runtime_mutations,
"_new_root_path", return_value=mkdir_target),
660 patch.object(Path,
"mkdir", side_effect=create_then_fail),
663 except RuntimeMutationError:
665 mkdir_survives = mkdir_target.exists()
and not mkdir_target.is_symlink()
666 if mkdir_target.exists():
668 return "mkdir exception preserves a same-owner competing root", mkdir_survives
671def _identity_replacement_case() -> tuple[str, bool]:
672 identity_target = _new_root_path()
673 identity_backup = identity_target.with_name(f
"identity-original-{identity_target.name}")
674 identity_raised =
False
676 def replace_then_fail(path: Path) ->
None:
677 path.rename(identity_backup)
678 path.mkdir(mode=runtime_mutations.PRIVATE_MODE)
679 message =
"injected root identity replacement exception"
680 raise RuntimeMutationError(message)
684 patch.object(runtime_mutations,
"_new_root_path", return_value=identity_target),
685 patch.object(runtime_mutations,
"_identity", side_effect=replace_then_fail),
688 except RuntimeMutationError:
689 identity_raised =
True
690 identity_survives = identity_target.exists()
and not identity_target.is_symlink()
691 if identity_target.exists():
692 identity_target.rmdir()
693 if identity_backup.exists():
694 identity_backup.rmdir()
695 return "root identity replacement survives without captured identity", (
696 identity_raised
and identity_survives
700def _replacement_identity_case() -> tuple[str, bool]:
701 replacement_path = _new_root_path()
702 replacement_raised =
False
707 side_effect=RuntimeMutationError(
"injected replacement identity exception"),
709 _create_replacement_root(replacement_path)
710 except RuntimeMutationError:
711 replacement_raised =
True
712 replacement_survives = replacement_path.exists()
and not replacement_path.is_symlink()
713 if replacement_path.exists():
714 replacement_path.rmdir()
715 return "replacement identity exception preserves its root", (
716 replacement_raised
and replacement_survives
720def _outer_root_exception_case() -> tuple[str, bool]:
721 before = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
724 with _owned_root_scope():
726 message =
"injected outer runtime exception"
727 raise RuntimeMutationError(message)
728 except RuntimeMutationError:
730 after_outer = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
731 return "outer runtime exception drains only owned roots", outer_raised
and after_outer == before
734def _root_allocation_exception_cases() -> list[tuple[str, bool]]:
735 """Prove every post-mkdir exception leaves the owned-root set unchanged."""
737 _root_validation_exception_case(),
739 _identity_replacement_case(),
740 _replacement_identity_case(),
741 _outer_root_exception_case(),
745def _root_removal_mutation_case() -> tuple[str, bool]:
746 """Prove root cleanup refuses a wrong identity and a final symlink."""
747 root, identity = _create_root()
748 saved = _new_root_path()
749 wrong_identity = identity[0], identity[1] + 1
750 wrong_refused =
False
751 symlink_refused =
False
754 _remove_root(root, wrong_identity)
755 except RuntimeMutationError:
756 wrong_refused = root.is_dir()
and _identity(root) == identity
758 root.symlink_to(saved)
760 _remove_root(root, identity)
761 except RuntimeMutationError:
762 symlink_refused = root.is_symlink()
and _identity(saved) == identity
764 _remove_root(saved, identity)
765 return "root removal refuses wrong identity and symlink mutations", (
766 wrong_refused
and symlink_refused
769 if root.is_symlink():
772 _remove_root(saved, identity)
775def _saved_path_failure_case(sources: SourceBundle) -> tuple[str, bool]:
776 """Prove second-path acquisition failure removes the first suite root."""
777 before = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
779 def fail_saved() -> Path:
780 message =
"injected saved-path acquisition failure"
781 raise RuntimeMutationError(message)
785 _replace_root_after_gate(
788 hooks=(_prepare_root_gate, fail_saved),
790 except RuntimeMutationError:
792 after = {path.name
for path
in CANONICAL_TMP.glob(f
"{ROOT_PREFIX}*")}
793 return "saved-path acquisition failure removes the first suite root", raised
and before == after
796def _replacement_cases(sources: SourceBundle) -> list[tuple[str, bool]]:
797 """Prove pre-open refusal and retained post-open root authority."""
800 "pre-open suite-root replacement refuses with zero effects",
801 _replace_root_after_gate(sources, phase=
"pre-open") == (USAGE_STATUS,
True,
True,
True),
804 "post-open suite-root replacement stays descriptor-bound",
805 _replace_root_after_gate(sources, phase=
"post-open")
806 == (INTEGRITY_REFUSAL_STATUS,
True,
True,
True),
809 "root-swap exception closes runner, descriptors, and paths",
810 _replace_root_after_gate(sources, phase=
"pre-open", inject_after_rename=
True)
811 == (
None,
True,
True,
True),
816def _invalid_root_cases(sources: SourceBundle) -> list[tuple[str, bool]]:
817 """Prove wrong-identity and symlink roots have zero effects."""
818 supervisor, process_source, cases_source = sources
819 target: Path |
None =
None
820 target_identity: tuple[int, int] |
None =
None
821 link: Path |
None =
None
822 link_identity: tuple[int, int] |
None =
None
824 target, target_identity = _create_root()
825 link = _new_root_path()
826 main_path, process_path, cases_path = _write_sources(
827 target, supervisor, process_source, cases_source
829 baseline = {path.name
for path
in target.iterdir()}
830 wrong_status, wrong_clean, _stderr = _run_supervisor(
834 (
"--selftest-missing-entry", str(target),
"0:0"),
837 wrong_status == USAGE_STATUS
839 and {path.name
for path
in target.iterdir()} == baseline
841 link.symlink_to(target)
842 link_identity = _identity(link)
843 link_status, link_clean, _stderr = _run_supervisor(
847 (
"--selftest-missing-entry", str(link), _identity_text(link)),
850 link_status == USAGE_STATUS
852 and {path.name
for path
in target.iterdir()} == baseline
855 (
"wrong suite-root identity refuses before effects", wrong_passed),
856 (
"symlink suite root refuses before effects", link_passed),
858 "invalid-root runtime leaves no process or descriptor residue",
859 _no_residue((target, link)),
863 if link
is not None and link_identity
is not None and link.is_symlink():
864 _remove_link(link, link_identity)
865 if target
is not None and target_identity
is not None and target.exists():
866 _remove_root(target, target_identity)
869def _procfs_unknown_cases() -> list[tuple[str, bool]]:
870 """Prove malformed or unreadable Linux observations retain authority."""
872 with patch.object(Path,
"read_bytes", side_effect=PermissionError(
"injected stat denial")):
873 denied_stat = _process_group_members(group)
is None
874 with patch.object(Path,
"read_bytes", return_value=b
"malformed"):
875 malformed_stat = _process_group_members(group)
is None
876 malformed_command =
not _paths_have_no_live_references((CANONICAL_TMP,))
877 with patch.object(Path,
"readlink", side_effect=PermissionError(
"injected fd denial")):
878 denied_descriptor =
not _paths_have_no_live_references((CANONICAL_TMP,))
880 (
"unreadable process stat retains process-group authority", denied_stat),
881 (
"malformed process stat retains process-group authority", malformed_stat),
882 (
"malformed process command retains path authority", malformed_command),
883 (
"unreadable process descriptor retains path authority", denied_descriptor),
887def _receipt_unknown_cases() -> list[tuple[str, bool]]:
888 """Prove malformed and unreadable receipts cannot erase group authority."""
889 root, identity = _create_root()
890 receipt = root /
"unknown.bound"
892 receipt.write_bytes(b
"not-a-process-group\n")
893 malformed = _receipt_groups((root,))
is None
895 Path,
"read_bytes", side_effect=PermissionError(
"injected receipt denial")
897 denied = _receipt_groups((root,))
is None
899 missing = _receipt_groups((root /
"absent",)) == set()
901 (
"malformed group receipt retains process authority", malformed),
902 (
"unreadable group receipt retains process authority", denied),
903 (
"vanished receipt root is the only skipped observation", missing),
908 _remove_root(root, identity)
911def _receipt_scoped_cleanup_cases() -> list[tuple[str, bool]]:
912 """Prove unrelated procfs denial cannot hide a receipt-bound live group."""
913 root, identity = _create_root()
914 receipt = root /
"live.bound"
915 child: subprocess.Popen[bytes] |
None =
None
917 with patch.object(Path,
"read_bytes", side_effect=PermissionError(
"injected denial")):
918 strict_unknown =
not _paths_have_no_live_references((root,))
919 unrelated_does_not_wedge = _no_residue((root,))
920 child = subprocess.Popen(
921 (
"/bin/sleep",
"30"),
923 start_new_session=
True,
925 receipt.write_text(f
"{child.pid}\n", encoding=
"ascii")
926 live_refuses =
not _no_residue((root,))
927 os.killpg(child.pid, signal.SIGKILL)
928 child.wait(timeout=runtime_mutations.RESIDUE_TIMEOUT_SECONDS)
929 gone_accepts = _no_residue((root,))
932 "unrelated unreadable procfs state does not gate receipt-scoped cleanup",
933 strict_unknown
and unrelated_does_not_wedge,
936 "receipt-bound descendant cwd reference prevents cleanup until group absence",
937 live_refuses
and gone_accepts,
941 if child
is not None and child.poll()
is None:
942 os.killpg(child.pid, signal.SIGKILL)
943 child.wait(timeout=runtime_mutations.RESIDUE_TIMEOUT_SECONDS)
946 _remove_root(root, identity)
949def cases(inputs: dict[str, str]) -> list[tuple[str, bool]]:
950 """Run every cleanup, acquisition, replacement, and invalid-root proof."""
951 supervisor = inputs[
"devcontainer_image_selftest_supervisor"]
952 process_source = inputs[
"devcontainer_image_selftest_process"]
953 supervisor_cases = inputs[
"devcontainer_image_selftest_supervisor_cases"]
954 sources = supervisor, process_source, supervisor_cases
955 with _owned_root_scope():
957 *runtime_loader.cases(inputs),
958 _runner_ownership_case(),
959 _invalid_source_descriptor(sources,
"process"),
960 _invalid_source_descriptor(sources,
"cases"),
961 _descriptor_close_failure_case(),
962 _stream_close_failure_case(),
963 _launcher_argv_case(sources),
964 _first_source_open_failure_case(sources),
965 _source_open_failure_case(sources, 1,
"process"),
966 _source_open_failure_case(sources, 2,
"cases"),
967 _source_close_ambiguity_case(sources),
968 _popen_failure_case(sources),
969 _popen_close_ambiguity_case(sources),
970 _descriptor_reuse_case(),
971 _first_pipe_failure_case(sources),
972 _pipe_failure_case(sources),
973 _source_failure_case(sources),
974 _root_acquisition_failure_case(sources),
975 *_root_allocation_exception_cases(),
976 _root_removal_mutation_case(),
977 _saved_path_failure_case(sources),
978 *_replacement_cases(sources),
979 *_invalid_root_cases(sources),
980 *_procfs_unknown_cases(),
981 *_receipt_unknown_cases(),
982 *_receipt_scoped_cleanup_cases(),