ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_runtime_cleanup.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Failure-direction cleanup proofs for image supervisor runtime mutations."""
4
5from __future__ import annotations
6
7import os
8import signal
9import subprocess
10import sys
11from contextlib import suppress
12from pathlib import Path
13from unittest.mock import patch
14
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 (
18 CANONICAL_TMP,
19 INTEGRITY_REFUSAL_STATUS,
20 ROOT_PREFIX,
21 USAGE_STATUS,
22 RuntimeMutationError,
23 SourceBundle,
24 SupervisorStart,
25 _close_owned_descriptors,
26 _close_runner_streams,
27 _collect_runner,
28 _create_replacement_root,
29 _create_root,
30 _dispose_runner,
31 _finalize_root_swap,
32 _identity,
33 _identity_text,
34 _new_root_path,
35 _no_residue,
36 _open_supervisor_sources,
37 _owned_root_scope,
38 _paths_have_no_live_references,
39 _prepare_root_gate,
40 _process_group_members,
41 _reap_terminal_runner,
42 _receipt_groups,
43 _release_owned_descriptor,
44 _remove_link,
45 _remove_root,
46 _replace_root_after_gate,
47 _run_supervisor,
48 _start_supervisor,
49 _wait_runner,
50 _write_sources,
51)
52
53GATE_DESCRIPTOR_COUNT = 4
54SOURCE_DESCRIPTOR_COUNT = 3
55
56
57class _CloseProbe:
58 """Record one stream close and optionally inject its failure."""
59
60 def __init__(self, *, fail: bool = False) -> None:
61 self.closed = False
62 self.fail = fail
63
64 def close(self) -> None:
65 """Mark the attempt before raising the configured failure."""
66 self.closed = True
67 if self.fail:
68 message = "injected stream close failure"
69 raise OSError(message)
70
71
72class _StreamProcessProbe:
73 """Expose the three stream fields consumed by the cleanup helper."""
74
75 def __init__(self) -> None:
76 self.stdin = _CloseProbe(fail=True)
77 self.stdout = _CloseProbe()
78 self.stderr = _CloseProbe()
79
80
81def _terminal_is_retained(process: subprocess.Popen[bytes]) -> bool:
82 """Report whether one terminal direct child remains waitable and unreaped."""
83 try:
84 result = os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
85 except ChildProcessError:
86 return False
87 return result is not None
88
89
90def _spawn_descendant_runner() -> subprocess.Popen[bytes]:
91 """Start a terminal leader whose live descendant retains its process group."""
92 source = (
93 "import os, signal, time\n"
94 "child = os.fork()\n"
95 "if child == 0:\n"
96 " signal.signal(signal.SIGTERM, signal.SIG_IGN)\n"
97 " os.execl('/bin/sleep', '/bin/sleep', '30')\n"
98 "time.sleep(0.1)\n"
99 f"os._exit({runtime_mutations.DESCENDANT_STATUS})\n"
100 )
101 return subprocess.Popen( # noqa: S603 -- current interpreter and fixed fixture source
102 (sys.executable, "-B", "-I", "-S", "-c", source),
103 start_new_session=True,
104 stdout=subprocess.PIPE,
105 stderr=subprocess.PIPE,
106 )
107
108
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()
114 resolved = False
115 refused = False
116 try:
117 try:
118 _wait_runner(mutant, lambda _group: None)
119 except RuntimeMutationError:
120 refused = True
121 retained = _terminal_is_retained(mutant)
122 mutant_status, mutant_clean = _reap_terminal_runner(mutant)
123 _close_runner_streams(mutant)
124 resolved = True
125 finally:
126 if not resolved:
127 _dispose_runner(mutant)
128 return "runner census failure retains leader through descendant cleanup", (
129 base_status == mutant_status == runtime_mutations.DESCENDANT_STATUS
130 and base_clean
131 and refused
132 and retained
133 and mutant_clean
134 )
135
136
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()
141 try:
142 main_path, process_path, cases_path = _write_sources(
143 root, supervisor, process_source, cases_source
144 )
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)
151 try:
152 runner = subprocess.Popen( # noqa: S603 -- current interpreter and bound source FDs
153 (
154 sys.executable,
155 "-B",
156 "-I",
157 "-S",
158 f"/proc/self/fd/{main_descriptor}",
159 "--process-fd",
160 process_descriptor,
161 "--cases-fd",
162 cases_descriptor,
163 "--selftest-missing-entry",
164 str(root),
165 _identity_text(root),
166 ),
167 pass_fds=(main_descriptor, helper_descriptor),
168 start_new_session=True,
169 stdout=subprocess.PIPE,
170 stderr=subprocess.PIPE,
171 )
172 finally:
173 os.close(main_descriptor)
174 os.close(helper_descriptor)
175 status, clean, _stderr = _collect_runner(runner)
176 passed = (
177 status == runtime_mutations.PUBLIC_REFUSAL_STATUS
178 and clean
179 and {path.name for path in root.iterdir()} == baseline
180 and _no_residue((root,))
181 )
182 return f"invalid {invalid} descriptor refuses before root effects", passed
183 finally:
184 _remove_root(root, identity)
185
186
187def _descriptor_is_closed(descriptor: int) -> bool:
188 """Return whether one exact descriptor no longer names an open file."""
189 try:
190 os.fstat(descriptor)
191 except OSError:
192 return True
193 return False
194
195
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}
201 replacement = -1
202 raised = False
203
204 def close_reuse(descriptor: int) -> None:
205 nonlocal replacement
206 os.close(descriptor)
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)
214
215 try:
216 _close_owned_descriptors(owned, close_reuse)
217 except OSError:
218 raised = True
219 finally:
220 passed = (
221 raised
222 and not owned
223 and replacement == failed
224 and os.fstat(replacement) is not None
225 and _descriptor_is_closed(later)
226 )
227 for descriptor in (replacement, failed_peer, later_peer):
228 with suppress(OSError):
229 os.close(descriptor)
230 return "ambiguous descriptor close never regains reused numeric authority", passed
231
232
233def _stream_close_failure_case() -> tuple[str, bool]:
234 """Prove one stream-close error cannot skip the remaining streams."""
235 process = _StreamProcessProbe()
236 raised = False
237 try:
238 _close_runner_streams(process)
239 except OSError:
240 raised = True
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
243
244
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()
249 sentinel = object()
250 try:
251 paths = _write_sources(root, supervisor, process_source, cases_source)
252 with patch.object(
253 runtime_mutations.runtime_launcher.subprocess,
254 "Popen",
255 return_value=sentinel,
256 ) as popen:
257 result = runtime_mutations.runtime_launcher.launch(
258 paths,
259 ("request",),
260 SupervisorStart(),
261 )
262 argv = popen.call_args.args[0]
263 options = popen.call_args.kwargs
264 descriptors = options["pass_fds"]
265 exact = (
266 result.process is sentinel
267 and result.start_error is result.close_error is None
268 and argv[:5]
269 == (
270 runtime_mutations.runtime_launcher.PYTHON_INTERPRETER,
271 "-B",
272 "-I",
273 "-S",
274 f"/proc/self/fd/{descriptors[0]}",
275 )
276 and argv[5:9]
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)
284 )
285 return "launcher binds exact isolated three-source interpreter argv", exact
286 finally:
287 _remove_root(root, identity)
288
289
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] = []
297
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)
304 return descriptor
305
306 try:
307 main_path, process_path, cases_path = _write_sources(
308 root, supervisor, process_source, cases_source
309 )
310 try:
311 _open_supervisor_sources(main_path, process_path, cases_path, fail_later)
312 except OSError:
313 passed = len(opened) == failure_index and all(
314 _descriptor_is_closed(descriptor) for descriptor in opened
315 )
316 else:
317 passed = False
318 return f"{source_name}-source open failure closes every predecessor FD", passed
319 finally:
320 _remove_root(root, identity)
321
322
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] = []
328
329 def fail_first(_path: Path, _flags: int) -> int:
330 message = "injected main-source open failure"
331 raise OSError(message)
332
333 try:
334 main_path, process_path, cases_path = _write_sources(
335 root, supervisor, process_source, cases_source
336 )
337 try:
338 _open_supervisor_sources(
339 main_path,
340 process_path,
341 cases_path,
342 fail_first,
343 close_attempts.append,
344 )
345 except OSError as error:
346 passed = str(error) == "injected main-source open failure" and not close_attempts
347 else:
348 passed = False
349 return "main-source open failure owns no descriptor to close", passed
350 finally:
351 _remove_root(root, identity)
352
353
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] = []
359 replacement = -1
360
361 def fail_second(path: Path, flags: int) -> int:
362 if opened:
363 message = "injected process-source open failure"
364 raise OSError(message)
365 descriptor = os.open(path, flags)
366 opened.append(descriptor)
367 return descriptor
368
369 def close_reuse(descriptor: int) -> None:
370 nonlocal replacement
371 os.close(descriptor)
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)
378
379 try:
380 main_path, process_path, cases_path = _write_sources(
381 root, supervisor, process_source, cases_source
382 )
383 try:
384 _open_supervisor_sources(main_path, process_path, cases_path, fail_second, close_reuse)
385 except OSError as error:
386 passed = (
387 str(error) == "injected process-source open failure"
388 and replacement == opened[0]
389 and os.fstat(replacement) is not None
390 )
391 else:
392 passed = False
393 return "source-open failure preserves primary error without stale close retry", passed
394 finally:
395 with suppress(OSError):
396 os.close(replacement)
397 _remove_root(root, identity)
398
399
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")
405 try:
406 main_path, process_path, cases_path = _write_sources(
407 root, supervisor, process_source, cases_source
408 )
409 failures = (
410 OSError("injected Popen OS failure"),
411 ValueError("injected Popen value failure"),
412 subprocess.SubprocessError("injected Popen subprocess failure"),
413 )
414 outcomes = []
415 for failure in failures:
416 before = {entry.name for entry in descriptors.iterdir()}
417 try:
418 with patch.object(
419 runtime_mutations.runtime_launcher.subprocess,
420 "Popen",
421 side_effect=failure,
422 ):
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)
427 else:
428 outcomes.append(False)
429 return "every Popen failure closes all source FDs", all(outcomes)
430 finally:
431 _remove_root(root, identity)
432
433
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
438 replacement = -1
439 first = True
440
441 def close_reuse(descriptor: int) -> None:
442 nonlocal first, replacement
443 os.close(descriptor)
444 if first:
445 first = False
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)
452
453 try:
454 main_path, process_path, cases_path = _write_sources(
455 root, supervisor, process_source, cases_source
456 )
457 message = "injected supervisor Popen failure"
458 try:
459 with patch.object(
460 runtime_mutations.runtime_launcher.subprocess,
461 "Popen",
462 side_effect=OSError(message),
463 ):
464 _start_supervisor(
465 main_path,
466 process_path,
467 cases_path,
468 (),
469 SupervisorStart(descriptor_closer=close_reuse),
470 )
471 except OSError as error:
472 passed = (
473 str(error) == message and replacement >= 0 and os.fstat(replacement) is not None
474 )
475 else:
476 passed = False
477 return "Popen failure preserves primary error without stale close retry", passed
478 finally:
479 with suppress(OSError):
480 os.close(replacement)
481 _remove_root(root, identity)
482
483
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()
489 replacement = -1
490 raised = False
491
492 def close_reuse(descriptor: int) -> None:
493 nonlocal replacement
494 os.close(descriptor)
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)
501
502 try:
503 root, identity = _create_root()
504 retained, released = os.pipe2(os.O_CLOEXEC)
505 owned.update((retained, released))
506 saved = _new_root_path()
507 try:
508 _release_owned_descriptor(owned, released, close_reuse)
509 except OSError:
510 raised = True
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
514 finally:
515 for descriptor in (*owned, replacement):
516 with suppress(OSError):
517 os.close(descriptor)
518 if root is not None and identity is not None and root.exists():
519 _remove_root(root, identity)
520
521
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] = []
526
527 def fail_second(flags: int) -> tuple[int, int]:
528 if opened:
529 message = "injected second pipe failure"
530 raise OSError(message)
531 pair = os.pipe2(flags)
532 opened.extend(pair)
533 return pair
534
535 raised = False
536 try:
537 _prepare_root_gate(sources, "pre-open", root, hooks=(fail_second, _write_sources))
538 except OSError:
539 raised = True
540 finally:
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
544
545
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()}
551
552 def fail_first(_flags: int) -> tuple[int, int]:
553 message = "injected first pipe failure"
554 raise OSError(message)
555
556 try:
557 try:
558 _prepare_root_gate(sources, "pre-open", root, hooks=(fail_first, _write_sources))
559 except OSError as error:
560 passed = (
561 str(error) == "injected first pipe failure"
562 and {entry.name for entry in descriptors.iterdir()} == before
563 )
564 else:
565 passed = False
566 return "first pipe failure owns no descriptor to close", passed
567 finally:
568 _remove_root(root, identity)
569
570
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] = []
575
576 def tracked_pipe(flags: int) -> tuple[int, int]:
577 pair = os.pipe2(flags)
578 opened.extend(pair)
579 return pair
580
581 def fail_source(
582 _root: Path, _supervisor: str, _process: str, _cases: str
583 ) -> tuple[Path, Path, Path]:
584 message = "injected source preparation failure"
585 raise RuntimeMutationError(message)
586
587 raised = False
588 try:
589 _prepare_root_gate(
590 sources,
591 "pre-open",
592 root,
593 hooks=(tracked_pipe, fail_source),
594 )
595 except RuntimeMutationError:
596 raised = True
597 finally:
598 closed = len(opened) == GATE_DESCRIPTOR_COUNT and all(
599 _descriptor_is_closed(item) for item in opened
600 )
601 _remove_root(root, identity)
602 return "source preparation failure closes every gate descriptor", raised and closed
603
604
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] = []
608
609 def fail_gate(
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)
615
616 raised = False
617 try:
618 _replace_root_after_gate(
619 sources,
620 phase="pre-open",
621 hooks=(fail_gate, _new_root_path),
622 )
623 except RuntimeMutationError:
624 raised = True
625 return "gate preparation failure removes its allocated root", (
626 raised and len(observed) == 1 and not observed[0].exists()
627 )
628
629
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
633 try:
634 with patch.object(
635 runtime_mutations,
636 "_root_path_is_safe",
637 side_effect=RuntimeMutationError("injected root validation exception"),
638 ):
639 _create_root()
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
645 )
646
647
648def _mkdir_race_case() -> tuple[str, bool]:
649 mkdir_target = _new_root_path()
650 mkdir = Path.mkdir
651
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)
656
657 try:
658 with (
659 patch.object(runtime_mutations, "_new_root_path", return_value=mkdir_target),
660 patch.object(Path, "mkdir", side_effect=create_then_fail),
661 ):
662 _create_root()
663 except RuntimeMutationError:
664 pass
665 mkdir_survives = mkdir_target.exists() and not mkdir_target.is_symlink()
666 if mkdir_target.exists():
667 mkdir_target.rmdir()
668 return "mkdir exception preserves a same-owner competing root", mkdir_survives
669
670
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
675
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)
681
682 try:
683 with (
684 patch.object(runtime_mutations, "_new_root_path", return_value=identity_target),
685 patch.object(runtime_mutations, "_identity", side_effect=replace_then_fail),
686 ):
687 _create_root()
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
697 )
698
699
700def _replacement_identity_case() -> tuple[str, bool]:
701 replacement_path = _new_root_path()
702 replacement_raised = False
703 try:
704 with patch.object(
705 runtime_mutations,
706 "_identity",
707 side_effect=RuntimeMutationError("injected replacement identity exception"),
708 ):
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
717 )
718
719
720def _outer_root_exception_case() -> tuple[str, bool]:
721 before = {path.name for path in CANONICAL_TMP.glob(f"{ROOT_PREFIX}*")}
722 outer_raised = False
723 try:
724 with _owned_root_scope():
725 _create_root()
726 message = "injected outer runtime exception"
727 raise RuntimeMutationError(message) # noqa: TRY301 -- injected outer-boundary failure.
728 except RuntimeMutationError:
729 outer_raised = True
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
732
733
734def _root_allocation_exception_cases() -> list[tuple[str, bool]]:
735 """Prove every post-mkdir exception leaves the owned-root set unchanged."""
736 return [
737 _root_validation_exception_case(),
738 _mkdir_race_case(),
739 _identity_replacement_case(),
740 _replacement_identity_case(),
741 _outer_root_exception_case(),
742 ]
743
744
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
752 try:
753 try:
754 _remove_root(root, wrong_identity)
755 except RuntimeMutationError:
756 wrong_refused = root.is_dir() and _identity(root) == identity
757 root.rename(saved)
758 root.symlink_to(saved)
759 try:
760 _remove_root(root, identity)
761 except RuntimeMutationError:
762 symlink_refused = root.is_symlink() and _identity(saved) == identity
763 root.unlink()
764 _remove_root(saved, identity)
765 return "root removal refuses wrong identity and symlink mutations", (
766 wrong_refused and symlink_refused
767 )
768 finally:
769 if root.is_symlink():
770 root.unlink()
771 if saved.exists():
772 _remove_root(saved, identity)
773
774
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}*")}
778
779 def fail_saved() -> Path:
780 message = "injected saved-path acquisition failure"
781 raise RuntimeMutationError(message)
782
783 raised = False
784 try:
785 _replace_root_after_gate(
786 sources,
787 phase="pre-open",
788 hooks=(_prepare_root_gate, fail_saved),
789 )
790 except RuntimeMutationError:
791 raised = True
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
794
795
796def _replacement_cases(sources: SourceBundle) -> list[tuple[str, bool]]:
797 """Prove pre-open refusal and retained post-open root authority."""
798 return [
799 (
800 "pre-open suite-root replacement refuses with zero effects",
801 _replace_root_after_gate(sources, phase="pre-open") == (USAGE_STATUS, True, True, True),
802 ),
803 (
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),
807 ),
808 (
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),
812 ),
813 ]
814
815
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
823 try:
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
828 )
829 baseline = {path.name for path in target.iterdir()}
830 wrong_status, wrong_clean, _stderr = _run_supervisor(
831 main_path,
832 process_path,
833 cases_path,
834 ("--selftest-missing-entry", str(target), "0:0"),
835 )
836 wrong_passed = (
837 wrong_status == USAGE_STATUS
838 and wrong_clean
839 and {path.name for path in target.iterdir()} == baseline
840 )
841 link.symlink_to(target)
842 link_identity = _identity(link)
843 link_status, link_clean, _stderr = _run_supervisor(
844 main_path,
845 process_path,
846 cases_path,
847 ("--selftest-missing-entry", str(link), _identity_text(link)),
848 )
849 link_passed = (
850 link_status == USAGE_STATUS
851 and link_clean
852 and {path.name for path in target.iterdir()} == baseline
853 )
854 return [
855 ("wrong suite-root identity refuses before effects", wrong_passed),
856 ("symlink suite root refuses before effects", link_passed),
857 (
858 "invalid-root runtime leaves no process or descriptor residue",
859 _no_residue((target, link)),
860 ),
861 ]
862 finally:
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)
867
868
869def _procfs_unknown_cases() -> list[tuple[str, bool]]:
870 """Prove malformed or unreadable Linux observations retain authority."""
871 group = os.getpgrp()
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,))
879 return [
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),
884 ]
885
886
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"
891 try:
892 receipt.write_bytes(b"not-a-process-group\n")
893 malformed = _receipt_groups((root,)) is None
894 with patch.object(
895 Path, "read_bytes", side_effect=PermissionError("injected receipt denial")
896 ):
897 denied = _receipt_groups((root,)) is None
898 receipt.unlink()
899 missing = _receipt_groups((root / "absent",)) == set()
900 return [
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),
904 ]
905 finally:
906 if receipt.exists():
907 receipt.unlink()
908 _remove_root(root, identity)
909
910
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
916 try:
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"),
922 cwd=root,
923 start_new_session=True,
924 )
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,))
930 return [
931 (
932 "unrelated unreadable procfs state does not gate receipt-scoped cleanup",
933 strict_unknown and unrelated_does_not_wedge,
934 ),
935 (
936 "receipt-bound descendant cwd reference prevents cleanup until group absence",
937 live_refuses and gone_accepts,
938 ),
939 ]
940 finally:
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)
944 if receipt.exists():
945 receipt.unlink()
946 _remove_root(root, identity)
947
948
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():
956 return [
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(),
983 ]