ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_runtime_mutations.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Runtime mutation proofs for the descriptor-bound image supervisor."""
4
5from __future__ import annotations
6
7import os
8import secrets
9import shutil
10import signal
11import stat
12import subprocess
13import time
14from collections.abc import Callable, Iterator
15from contextlib import contextmanager, suppress
16from pathlib import Path
17
18import hil_convergence_safety_runtime_launcher as runtime_launcher
19import hil_convergence_safety_runtime_root_swap as runtime_root_swap
20import hil_convergence_safety_runtime_sources as runtime_sources
21
22Mutation = tuple[str, str, str, str]
23Census = Callable[[int], set[int] | None]
24PipeFactory = Callable[[int], tuple[int, int]]
25SourceWriter = Callable[[Path, str, str, str], tuple[Path, Path, Path]]
26SourceBundle = tuple[str, str, str]
27RUNTIME_TIMEOUT_SECONDS, RESIDUE_TIMEOUT_SECONDS = 25.0, 5.0
28POLL_SECONDS = 0.01
29PRIVATE_MODE = 0o700
30ROOT_PREFIX = "ra8-devcontainer-image-selftest."
31ROOT_SUFFIX_LENGTH, PROCESS_GROUP_FIELD = 32, 2
32PROCESS_UID_FIELD_COUNT = 4
33INTEGRITY_REFUSAL_STATUS = 126
34PUBLIC_REFUSAL_STATUS = 125
35USAGE_STATUS = 64
36DESCENDANT_STATUS = 23
37ONE_SHOT_MUTATION_STATUS = 5
38CANONICAL_TMP = Path(os.path.sep, "tmp").resolve(strict=True)
39RuntimeMutationError = runtime_sources.RuntimeSourceError
40_write_sources = runtime_sources.publish
41SupervisorStart = runtime_launcher.SupervisorStart
42_close_owned_descriptors = runtime_launcher.close_owned_descriptors
43_open_supervisor_sources = runtime_launcher.open_supervisor_sources
44
45
46GatePreparer = Callable[
47 [SourceBundle, str, Path],
48 tuple[Path, Path, Path, dict[str, str], tuple[int, int, int, int], set[int]],
49]
50RootSwapHooks = tuple[GatePreparer, Callable[[], Path]]
51
52
53def _identity(path: Path) -> tuple[int, int]:
54 """Return one filesystem identity without following a final link."""
55 metadata = path.lstat()
56 return metadata.st_dev, metadata.st_ino
57
58
59def _identity_text(path: Path) -> str:
60 """Return the supervisor's stable device/inode spelling."""
61 return ":".join(str(value) for value in _identity(path))
62
63
64def _register_owned_root(root: Path, identity: tuple[int, int]) -> None:
65 """Bind one created root to the currently active selftest run."""
66 registry = getattr(_create_root, "_owned_root_registry", None)
67 if registry is not None:
68 registry.append((root, identity))
69
70
71def _forget_owned_root(root: Path, identity: tuple[int, int]) -> None:
72 """Forget one root only after its exact bound inode was removed."""
73 registry = getattr(_create_root, "_owned_root_registry", None)
74 if registry is None:
75 return
76 for index in range(len(registry) - 1, -1, -1):
77 if registry[index] == (root, identity):
78 del registry[index]
79 return
80
81
82def _created_root_cleanup(root: Path, identity: tuple[int, int]) -> RuntimeMutationError | None:
83 """Remove a just-created root using independent containment and identity checks."""
84 try:
85 metadata = root.lstat()
86 resolved = root.resolve(strict=True)
87 except OSError:
88 return RuntimeMutationError("created supervisor runtime root became unobservable")
89 name = root.name
90 suffix = name.removeprefix(ROOT_PREFIX)
91 safe = (
92 root.is_absolute()
93 and root.parent == CANONICAL_TMP
94 and resolved == root
95 and name.startswith(ROOT_PREFIX)
96 and len(suffix) == ROOT_SUFFIX_LENGTH
97 and all(character in "0123456789abcdef" for character in suffix)
98 and stat.S_ISDIR(metadata.st_mode)
99 and metadata.st_uid == os.getuid()
100 and metadata.st_gid == os.getgid()
101 and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE
102 and (metadata.st_dev, metadata.st_ino) == identity
103 )
104 if not safe:
105 return RuntimeMutationError("refusing unbound supervisor runtime root cleanup")
106 if not _no_residue((root,)):
107 return RuntimeMutationError("refusing live supervisor runtime cleanup")
108 try:
109 shutil.rmtree(root)
110 except OSError:
111 return RuntimeMutationError("created supervisor runtime root cleanup failed")
112 if root.exists() or root.is_symlink():
113 return RuntimeMutationError("supervisor runtime root survived cleanup")
114 return None
115
116
117def _drain_owned_roots(registry: list[tuple[Path, tuple[int, int]]]) -> RuntimeMutationError | None:
118 """Clean only roots recorded by this run, preserving the first refusal."""
119 first_error: RuntimeMutationError | None = None
120 for root, identity in reversed(tuple(registry)):
121 if not root.exists() and not root.is_symlink():
122 _forget_owned_root(root, identity)
123 continue
124 try:
125 _remove_root(root, identity)
126 except RuntimeMutationError as error:
127 if first_error is None:
128 first_error = error
129 return first_error
130
131
132@contextmanager
133def _owned_root_scope() -> Iterator[None]:
134 """Own every root allocated in one run until its outer cleanup completes."""
135 state = vars(_create_root)
136 previous = state.get("_owned_root_registry")
137 registry: list[tuple[Path, tuple[int, int]]] = []
138 state["_owned_root_registry"] = registry
139 try:
140 yield
141 except BaseException as primary:
142 cleanup_error = _drain_owned_roots(registry)
143 if cleanup_error is not None:
144 raise primary from cleanup_error
145 raise
146 else:
147 cleanup_error = _drain_owned_roots(registry)
148 if cleanup_error is not None:
149 raise cleanup_error
150 finally:
151 if previous is None:
152 del state["_owned_root_registry"]
153 else:
154 state["_owned_root_registry"] = previous
155
156
157def _root_path_is_safe(path: Path, identity: tuple[int, int]) -> bool:
158 """Prove one exact private direct child of the physical temporary root."""
159 try:
160 metadata = path.lstat()
161 resolved = path.resolve(strict=True)
162 except OSError:
163 return False
164 name = path.name
165 suffix = name.removeprefix(ROOT_PREFIX)
166 return (
167 path.is_absolute()
168 and path.parent == CANONICAL_TMP
169 and resolved == path
170 and name.startswith(ROOT_PREFIX)
171 and len(suffix) == ROOT_SUFFIX_LENGTH
172 and all(character in "0123456789abcdef" for character in suffix)
173 and stat.S_ISDIR(metadata.st_mode)
174 and metadata.st_uid == os.getuid()
175 and metadata.st_gid == os.getgid()
176 and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE
177 and (metadata.st_dev, metadata.st_ino) == identity
178 )
179
180
181def _new_root_path() -> Path:
182 """Choose one absent canonical root path with 128 random bits."""
183 for _attempt in range(20):
184 candidate = CANONICAL_TMP / f"{ROOT_PREFIX}{secrets.token_hex(16)}"
185 if not candidate.exists() and not candidate.is_symlink():
186 return candidate
187 message = "could not select a fresh supervisor runtime root"
188 raise RuntimeMutationError(message)
189
190
191def _create_root() -> tuple[Path, tuple[int, int]]:
192 """Create and bind one private canonical suite root."""
193 root = _new_root_path()
194 root.mkdir(mode=PRIVATE_MODE)
195 return root, _bind_created_root(root)
196
197
198def _bind_created_root(root: Path) -> tuple[int, int]:
199 """Validate and register one already-created private canonical root."""
200 identity: tuple[int, int] | None = None
201 try:
202 identity = _identity(root)
203 if not _root_path_is_safe(root, identity):
204 message = "created supervisor runtime root is unsafe"
205 raise RuntimeMutationError(message) # noqa: TRY301 -- validation is the transaction boundary.
206 except BaseException as primary:
207 cleanup_identity = identity
208 if cleanup_identity is not None:
209 cleanup_error = _created_root_cleanup(root, cleanup_identity)
210 if cleanup_error is not None:
211 raise primary from cleanup_error
212 raise
213 _register_owned_root(root, identity)
214 return identity
215
216
217def _create_replacement_root(root: Path) -> tuple[int, int]:
218 """Create one root-swap replacement with the same transactional binding."""
219 root.mkdir(mode=PRIVATE_MODE)
220 return _bind_created_root(root)
221
222
223def _remove_root(root: Path, identity: tuple[int, int]) -> None:
224 """Remove only one still-bound private runtime root."""
225 if not _root_path_is_safe(root, identity):
226 message = f"refusing unbound supervisor runtime cleanup: {root}"
227 raise RuntimeMutationError(message)
228 if not _no_residue((root,)):
229 message = f"refusing live supervisor runtime cleanup: {root}"
230 raise RuntimeMutationError(message)
231 shutil.rmtree(root)
232 if root.exists() or root.is_symlink():
233 message = f"supervisor runtime root survived cleanup: {root}"
234 raise RuntimeMutationError(message)
235 _forget_owned_root(root, identity)
236
237
238def _move_owned_root(source: Path, destination: Path) -> None:
239 """Move one registry binding across an identity-preserving rename."""
240 registry = getattr(_create_root, "_owned_root_registry", None)
241 if registry is None:
242 return
243 for index, (path, identity) in enumerate(registry):
244 if path == source:
245 registry[index] = destination, identity
246 return
247
248
249def _remove_link(link: Path, identity: tuple[int, int]) -> None:
250 """Remove one exact test-created canonical symlink."""
251 metadata = link.lstat()
252 name = link.name
253 suffix = name.removeprefix(ROOT_PREFIX)
254 safe = (
255 link.is_absolute()
256 and link.parent == CANONICAL_TMP
257 and name.startswith(ROOT_PREFIX)
258 and stat.S_ISLNK(metadata.st_mode)
259 and metadata.st_uid == os.getuid()
260 and metadata.st_gid == os.getgid()
261 and len(suffix) == ROOT_SUFFIX_LENGTH
262 and all(character in "0123456789abcdef" for character in suffix)
263 and (metadata.st_dev, metadata.st_ino) == identity
264 )
265 if not safe:
266 message = "refusing unbound supervisor runtime symlink cleanup"
267 raise RuntimeMutationError(message)
268 link.unlink()
269
270
271def _write_stall_entry(root: Path) -> Path:
272 """Write one payload whose live process retains an observable root marker."""
273 entry = root / "stall-entry.sh"
274 entry.write_text(
275 "#!/bin/bash\n"
276 '[[ "$1" == "--selftest" ]] || exit 64\n'
277 "trap '' HUP INT TERM\n"
278 'exec -a "$0" /bin/sleep 30\n',
279 encoding="ascii",
280 )
281 entry.chmod(PRIVATE_MODE)
282 return entry
283
284
285def _process_group_members(group: int) -> set[int] | None:
286 """Return live Linux process IDs in one exact process group."""
287 members: set[int] = set()
288 try:
289 processes = tuple(Path("/proc").iterdir())
290 except OSError:
291 return None
292 for process in processes:
293 if not process.name.isdigit():
294 continue
295 try:
296 raw = (process / "stat").read_bytes()
297 except FileNotFoundError:
298 continue
299 except OSError:
300 return None
301 closing = raw.rfind(b")")
302 fields = raw[closing + 2 :].split() if closing >= 0 else []
303 if len(fields) <= PROCESS_GROUP_FIELD or not fields[PROCESS_GROUP_FIELD].isdigit():
304 return None
305 if fields[0] == b"Z":
306 continue
307 if int(fields[PROCESS_GROUP_FIELD]) == group:
308 members.add(int(process.name))
309 return members
310
311
312def _wait_group_members(
313 group: int, allowed: set[int], census: Census = _process_group_members
314) -> bool:
315 """Wait until a group contains only the explicitly allowed process IDs."""
316 deadline = time.monotonic() + RESIDUE_TIMEOUT_SECONDS
317 while time.monotonic() < deadline:
318 members = census(group)
319 if members is not None and members <= allowed:
320 return True
321 time.sleep(POLL_SECONDS)
322 return False
323
324
325def _reap_terminal_runner(
326 process: subprocess.Popen[bytes], census: Census = _process_group_members
327) -> tuple[int, bool]:
328 """Remove descendants before reaping one already-terminal group leader."""
329 members = census(process.pid)
330 group_bound = members is not None and members <= {process.pid}
331 if not group_bound:
332 with suppress(ProcessLookupError):
333 os.killpg(process.pid, signal.SIGKILL)
334 group_bound = _wait_group_members(process.pid, {process.pid}, census)
335 if not group_bound:
336 message = "supervisor runtime runner group survived before leader reap"
337 raise RuntimeMutationError(message)
338 status = process.wait(timeout=RESIDUE_TIMEOUT_SECONDS)
339 return status, _wait_group_members(process.pid, set(), census)
340
341
342def _kill_and_reap_runner(
343 process: subprocess.Popen[bytes], census: Census = _process_group_members
344) -> tuple[int | None, bool]:
345 """Immediately close one owned runner without signaling after leader reap."""
346 try:
347 terminal = os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
348 except ChildProcessError:
349 if process.returncode is None:
350 message = "supervisor runtime runner was reaped outside its owner"
351 raise RuntimeMutationError(message) from None
352 return process.returncode, _wait_group_members(process.pid, set(), census)
353 if terminal is None:
354 with suppress(ProcessLookupError):
355 os.killpg(process.pid, signal.SIGKILL)
356 deadline = time.monotonic() + RESIDUE_TIMEOUT_SECONDS
357 while time.monotonic() < deadline:
358 terminal = os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
359 if terminal is not None:
360 break
361 time.sleep(POLL_SECONDS)
362 if terminal is None:
363 message = "supervisor runtime runner survived its bounded SIGKILL deadline"
364 raise RuntimeMutationError(message)
365 return _reap_terminal_runner(process, census)
366
367
368def _wait_runner(
369 process: subprocess.Popen[bytes], census: Census = _process_group_members
370) -> tuple[int | None, bool]:
371 """Retain the direct leader with WNOWAIT until its group is empty."""
372 deadline = time.monotonic() + RUNTIME_TIMEOUT_SECONDS
373 terminal = False
374 while time.monotonic() < deadline:
375 result = os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)
376 if result is not None:
377 terminal = True
378 break
379 time.sleep(POLL_SECONDS)
380 if not terminal:
381 return _kill_and_reap_runner(process, census)
382 return _reap_terminal_runner(process, census)
383
384
385def _close_runner_streams(process: subprocess.Popen[bytes]) -> None:
386 """Close every parent-side pipe owned by one runner."""
387 first_error: OSError | None = None
388 for stream in (process.stdin, process.stdout, process.stderr):
389 if stream is not None:
390 try:
391 stream.close()
392 except OSError as error:
393 if first_error is None:
394 first_error = error
395 if first_error is not None:
396 raise first_error
397
398
399def _dispose_runner(process: subprocess.Popen[bytes]) -> tuple[int | None, bool]:
400 """Terminate, group-clean, reap, and close one runner on a failure path."""
401 result: tuple[int | None, bool] | None = None
402 primary: OSError | RuntimeMutationError | subprocess.TimeoutExpired | None = None
403 try:
404 result = _kill_and_reap_runner(process)
405 except (OSError, RuntimeMutationError, subprocess.TimeoutExpired) as error:
406 primary = error
407 try:
408 _close_runner_streams(process)
409 except OSError as cleanup_error:
410 if primary is not None:
411 raise primary from cleanup_error
412 raise
413 if primary is not None:
414 raise primary
415 if result is None:
416 message = "supervisor runtime runner disposal produced no terminal result"
417 raise RuntimeMutationError(message)
418 return result
419
420
421def _release_owned_descriptor(
422 descriptors: set[int], descriptor: int, closer: runtime_launcher.DescriptorCloser = os.close
423) -> None:
424 """Release numeric authority before the one ambiguous close attempt."""
425 descriptors.remove(descriptor)
426 closer(descriptor)
427
428
429def _start_supervisor(
430 main_path: Path,
431 process_path: Path,
432 cases_path: Path,
433 arguments: tuple[str, ...],
434 launch: SupervisorStart | None = None,
435) -> subprocess.Popen[bytes]:
436 """Start one descriptor-bound supervisor in a retained runner group."""
437 result = runtime_launcher.launch(
438 (main_path, process_path, cases_path), arguments, launch or SupervisorStart()
439 )
440 if result.close_error is not None:
441 cleanup_error: OSError | RuntimeMutationError | subprocess.TimeoutExpired | None = None
442 if result.process is not None:
443 try:
444 _dispose_runner(result.process)
445 except (OSError, RuntimeMutationError, subprocess.TimeoutExpired) as error:
446 cleanup_error = error
447 if result.start_error is not None:
448 raise result.start_error from result.close_error
449 if cleanup_error is not None:
450 raise result.close_error from cleanup_error
451 raise result.close_error
452 if result.start_error is not None:
453 raise result.start_error
454 if result.process is None:
455 message = "supervisor launcher returned no process or error"
456 raise RuntimeMutationError(message)
457 return result.process
458
459
460def _collect_runner(process: subprocess.Popen[bytes]) -> tuple[int | None, bool, bytes]:
461 """Collect one started runner through a finally-owned cleanup funnel."""
462 try:
463 status, clean = _wait_runner(process)
464 _stdout, stderr = process.communicate(timeout=RESIDUE_TIMEOUT_SECONDS)
465 except (OSError, RuntimeMutationError, subprocess.TimeoutExpired, ValueError) as primary:
466 cleanup_error: (
467 OSError | RuntimeMutationError | subprocess.TimeoutExpired | ValueError | None
468 )
469 try:
470 _status, cleanup_clean = _dispose_runner(process)
471 except (OSError, RuntimeMutationError, subprocess.TimeoutExpired, ValueError) as error:
472 cleanup_error = error
473 else:
474 cleanup_error = None
475 if not cleanup_clean:
476 message = "supervisor runtime runner cleanup did not converge"
477 cleanup_error = RuntimeMutationError(message)
478 if cleanup_error is not None:
479 raise primary from cleanup_error
480 raise
481 else:
482 _close_runner_streams(process)
483 return status, clean, stderr
484
485
486def _run_supervisor(
487 main_path: Path,
488 process_path: Path,
489 cases_path: Path,
490 arguments: tuple[str, ...],
491) -> tuple[int | None, bool, bytes]:
492 """Run and reap one supervisor while preserving its diagnostic bytes."""
493 return _collect_runner(_start_supervisor(main_path, process_path, cases_path, arguments))
494
495
496def _receipt_groups(paths: tuple[Path, ...]) -> set[int] | None:
497 """Return numeric group receipts, or unknown on any unsafe observation."""
498 groups: set[int] = set()
499 for root in paths:
500 try:
501 receipts = tuple(root.iterdir())
502 except FileNotFoundError:
503 continue
504 except OSError:
505 return None
506 for receipt in receipts:
507 if receipt.suffix not in (".bound", ".outer"):
508 continue
509 try:
510 value = receipt.read_bytes().strip()
511 except FileNotFoundError:
512 continue
513 except OSError:
514 return None
515 if not value.isdigit() or int(value) <= 0:
516 return None
517 groups.add(int(value))
518 return groups
519
520
521def _descriptor_path_reference(
522 descriptors: tuple[Path, ...], needles: tuple[bytes, ...]
523) -> bool | None:
524 """Return one descriptor table's reference state, or unknown."""
525 for descriptor in descriptors:
526 if not descriptor.name.isdigit():
527 return None
528 try:
529 target = os.fsencode(descriptor.readlink())
530 except FileNotFoundError:
531 continue
532 except OSError:
533 return None
534 if not target:
535 return None
536 if any(needle in target for needle in needles):
537 return True
538 return False
539
540
541def _process_path_reference(process: Path, needles: tuple[bytes, ...]) -> bool | None:
542 """Return one process's reference state, or unknown on malformed observation."""
543 try:
544 command = (process / "cmdline").read_bytes()
545 descriptors = tuple((process / "fd").iterdir())
546 except FileNotFoundError:
547 return False
548 except OSError:
549 return None
550 if command and not command.endswith(b"\0"):
551 return None
552 if any(needle in command for needle in needles):
553 return True
554 return _descriptor_path_reference(descriptors, needles)
555
556
557def _process_has_suite_owner(process: Path) -> bool | None:
558 """Return whether one process has any UID matching this selftest owner."""
559 try:
560 lines = (process / "status").read_text(encoding="ascii").splitlines()
561 except FileNotFoundError:
562 return False
563 except (OSError, UnicodeError):
564 return None
565 uid_lines = [line for line in lines if line.startswith("Uid:")]
566 if len(uid_lines) != 1:
567 return None
568 fields = uid_lines[0].split()[1:]
569 if len(fields) != PROCESS_UID_FIELD_COUNT or not all(field.isdigit() for field in fields):
570 return None
571 return str(os.getuid()) in fields
572
573
574def _paths_have_no_live_references(paths: tuple[Path, ...]) -> bool:
575 """Prove no process command or descriptor retains an owned runtime path."""
576 needles = tuple(os.fsencode(path) for path in paths)
577 deadline = time.monotonic() + RESIDUE_TIMEOUT_SECONDS
578 while time.monotonic() < deadline:
579 referenced = False
580 try:
581 processes = tuple(Path("/proc").iterdir())
582 except OSError:
583 return False
584 for process in processes:
585 if not process.name.isdigit():
586 continue
587 owned = _process_has_suite_owner(process)
588 if owned is None:
589 return False
590 if not owned:
591 continue
592 state = _process_path_reference(process, needles)
593 if state is None:
594 return False
595 if state:
596 referenced = True
597 break
598 if not referenced:
599 return True
600 time.sleep(POLL_SECONDS)
601 return False
602
603
604def _receipt_group_is_absent(group: int) -> bool:
605 """Prove an owned group contains no live process."""
606 members = _process_group_members(group)
607 return members == set()
608
609
610def _no_residue(paths: tuple[Path, ...]) -> bool:
611 """Prove every process group named by an owned receipt is absent."""
612 groups = _receipt_groups(paths)
613 if groups is None:
614 return False
615 deadline = time.monotonic() + RESIDUE_TIMEOUT_SECONDS
616 while time.monotonic() < deadline:
617 if all(_receipt_group_is_absent(group) for group in groups):
618 return True
619 time.sleep(POLL_SECONDS)
620 return False
621
622
623def _watchdog_case(supervisor: str, process_source: str, cases: str) -> tuple[str, bool]:
624 """Require the one-second base to pass and the production-deadline mutant to fail."""
625 assignment = 'SELFTEST_WATCHDOG_TIMEOUT_SECONDS = MAIN_API["SELFTEST_WATCHDOG_TIMEOUT_SECONDS"]'
626 replacement = 'SELFTEST_WATCHDOG_TIMEOUT_SECONDS = MAIN_API["WATCHDOG_TIMEOUT_SECONDS"]'
627 if cases.count(assignment) != 1:
628 message = "watchdog runtime mutation authority is not unique"
629 raise RuntimeMutationError(message)
630 outcomes: list[tuple[str, int | None, bool, bool]] = []
631 roots: list[tuple[Path, tuple[int, int]]] = []
632 try:
633 for label, source in (("base", cases), ("mutant", cases.replace(assignment, replacement))):
634 root, identity = _create_root()
635 roots.append((root, identity))
636 main_path, process_path, cases_path = _write_sources(
637 root, supervisor, process_source, source
638 )
639 entry = _write_stall_entry(root)
640 status, clean, _stderr = _run_supervisor(
641 main_path,
642 process_path,
643 cases_path,
644 ("--selftest-watchdog-expiry", str(entry), str(root), _identity_text(root)),
645 )
646 outcomes.append((label, status, clean, _no_residue((root,))))
647 passed = outcomes == [("base", 0, True, True), ("mutant", 1, True, True)]
648 return "watchdog runtime deadline mutation fires", passed
649 finally:
650 for root, identity in reversed(roots):
651 _remove_root(root, identity)
652
653
654def _observation_case(supervisor: str, process_source: str, cases: str) -> tuple[str, bool]:
655 """Require the injected observation error to remain load-bearing."""
656 call = " _inject_observation_failure()"
657 replacement = " None # runtime mutation: required observation omitted"
658 if cases.count(call) != 1:
659 message = "observation runtime mutation authority is not unique"
660 raise RuntimeMutationError(message)
661 outcomes: list[tuple[str, int | None, bool, bool]] = []
662 roots: list[tuple[Path, tuple[int, int]]] = []
663 try:
664 for label, source in (("base", cases), ("mutant", cases.replace(call, replacement))):
665 root, identity = _create_root()
666 roots.append((root, identity))
667 main_path, process_path, cases_path = _write_sources(
668 root, supervisor, process_source, source
669 )
670 entry = _write_stall_entry(root)
671 status, clean, _stderr = _run_supervisor(
672 main_path,
673 process_path,
674 cases_path,
675 ("--selftest-observation-failure", str(entry), str(root), _identity_text(root)),
676 )
677 outcomes.append((label, status, clean, _no_residue((root,))))
678 passed = outcomes == [("base", 0, True, True), ("mutant", 1, True, True)]
679 return "observation failure runtime mutation fires", passed
680 finally:
681 for root, identity in reversed(roots):
682 _remove_root(root, identity)
683
684
685def _closed_death_group_race_case(
686 supervisor: str, process_source: str, cases: str
687) -> tuple[str, bool]:
688 """Prove a pre-cleanup census cannot leave live group residue."""
689 observation = (
690 " if result is not None:\n"
691 " return result.si_code == os.CLD_KILLED and "
692 "result.si_status == signal.SIGKILL\n"
693 )
694 premature = (
695 " if result is not None:\n"
696 " members = _group_members(child.pid)\n"
697 " return (\n"
698 " result.si_code == os.CLD_KILLED\n"
699 " and result.si_status == signal.SIGKILL\n"
700 " and members is not None\n"
701 " and members <= {child.pid}\n"
702 " )\n"
703 )
704 if cases.count(observation) != 1:
705 message = "closed-death group-census mutation authority is not unique"
706 raise RuntimeMutationError(message)
707 outcomes: list[tuple[str, int | None, int | None, bool, bool]] = []
708 roots: list[tuple[Path, tuple[int, int]]] = []
709 try:
710 for label, source in (("base", cases), ("mutant", cases.replace(observation, premature))):
711 root, identity = _create_root()
712 roots.append((root, identity))
713 main_path, process_path, cases_path = _write_sources(
714 root, supervisor, process_source, source
715 )
716 entry = _write_stall_entry(root)
717 watchdog, watchdog_clean, _stderr = _run_supervisor(
718 main_path,
719 process_path,
720 cases_path,
721 ("--selftest-watchdog-expiry", str(entry), str(root), _identity_text(root)),
722 )
723 closed, closed_clean, _stderr = _run_supervisor(
724 main_path,
725 process_path,
726 cases_path,
727 ("--selftest-closed-death-fd", str(entry), str(root), _identity_text(root)),
728 )
729 outcomes.append(
730 (label, watchdog, closed, watchdog_clean and closed_clean, _no_residue((root,)))
731 )
732 base = ("base", 0, 0, True, True)
733 mutant = outcomes[1]
734 safe = (
735 outcomes[0] == base
736 and mutant[0:2] == ("mutant", 0)
737 and mutant[2] in {0, 1}
738 and mutant[3:] == (True, True)
739 )
740 return "closed-death group census remains terminal-safe", safe
741 finally:
742 for root, identity in reversed(roots):
743 _remove_root(root, identity)
744
745
746def _prepare_root_gate(*args: object, **kwargs: object) -> object:
747 """Delegate root-gate preparation to the focused swap module."""
748 return runtime_root_swap.__dict__["_prepare_root_gate"](*args, **kwargs)
749
750
751def _finalize_root_swap(*args: object, **kwargs: object) -> object:
752 """Delegate root-swap finalization to the focused swap module."""
753 return runtime_root_swap.__dict__["_finalize_root_swap"](*args, **kwargs)
754
755
756def _replace_root_after_gate(
757 sources: SourceBundle,
758 *,
759 phase: str,
760 inject_after_rename: bool = False,
761 hooks: RootSwapHooks | None = None,
762) -> tuple[int | None, bool, bool, bool]:
763 """Replace a suite root at one deterministic retained-authority boundary."""
764 return runtime_root_swap.__dict__["_replace_root_after_gate"](
765 sources, phase=phase, inject_after_rename=inject_after_rename, hooks=hooks
766 )
767
768
769def runtime_cases(inputs: dict[str, str]) -> list[tuple[str, bool]]:
770 """Run every Linux-only descriptor, root, and cleanup mutation proof."""
771 if not Path("/proc/self/stat").is_file():
772 return [("image supervisor runtime mutations are Linux-only", True)]
773 supervisor = inputs["devcontainer_image_selftest_supervisor"]
774 process_source = inputs["devcontainer_image_selftest_process"]
775 cases = inputs["devcontainer_image_selftest_supervisor_cases"]
776 with _owned_root_scope():
777 return [
778 _watchdog_case(supervisor, process_source, cases),
779 _observation_case(supervisor, process_source, cases),
780 _closed_death_group_race_case(supervisor, process_source, cases),
781 ]