3"""Transactional suite-root swap proofs for the image supervisor."""
5from __future__
import annotations
11from collections.abc
import Callable
12from contextlib
import suppress
13from pathlib
import Path
15import hil_convergence_safety_runtime_mutations
as runtime_mutations
17PipeFactory = Callable[[int], tuple[int, int]]
18SourceWriter = Callable[[Path, str, str, str], tuple[Path, Path, Path]]
19RootSwapHooks = tuple[Callable[..., object], Callable[[], Path]]
22def _runtime_api(name: str) -> Callable[..., object]:
23 """Resolve one mutation helper dynamically to preserve test patching."""
24 return runtime_mutations.__dict__[name]
27def _root_gate_sources(
28 sources: runtime_mutations.SourceBundle, phase: str, handshake: str
29) -> runtime_mutations.SourceBundle:
30 """Inject one descriptor handshake at an exact root-authority boundary."""
31 supervisor, process_source, cases = sources
32 if phase ==
"pre-open":
33 anchor =
" before = root.lstat()\n"
34 if supervisor.count(anchor) != 1:
35 message =
"pre-open suite-root mutation authority is not unique"
36 raise runtime_mutations.RuntimeMutationError(message)
37 supervisor = supervisor.replace(anchor, anchor + handshake, 1)
38 elif phase ==
"post-open":
39 anchor =
" anchored_root = _anchored_root_path(descriptor)\n"
40 if cases.count(anchor) != 1:
41 message =
"post-open suite-root mutation authority is not unique"
42 raise runtime_mutations.RuntimeMutationError(message)
43 cases = cases.replace(anchor, anchor + handshake, 1)
45 message = f
"unknown root replacement phase: {phase}"
46 raise runtime_mutations.RuntimeMutationError(message)
47 return supervisor, process_source, cases
50def _retained_receipts_match(saved: Path, baseline: set[str], phase: str) -> bool:
51 """Require exact retained-inode effects and their bound receipt contents."""
52 added = {path.name
for path
in saved.iterdir()} - baseline
53 if phase ==
"pre-open":
56 "supervisor-missing-entry.bound",
57 "supervisor-missing-entry.outer",
58 "supervisor-missing-entry.status",
63 bound = (saved /
"supervisor-missing-entry.bound").read_bytes().strip()
64 outer = (saved /
"supervisor-missing-entry.outer").read_bytes().strip()
65 status = (saved /
"supervisor-missing-entry.status").read_bytes()
68 return bound.isdigit()
and outer == bound
and status == b
"127\n"
71def _complete_root_swap(
72 process: subprocess.Popen[bytes],
73 descriptors: tuple[int, int],
74 paths: tuple[Path, Path],
75 fixture: tuple[set[str], str, bool],
76) -> tuple[int |
None, bool, bool, bool, tuple[int, int] |
None]:
77 """Perform one synchronized swap and report effects on both root inodes."""
78 ready_read, go_write = descriptors
79 original, saved = paths
80 baseline, phase, inject_after_rename = fixture
81 readable, _, _ = select.select((ready_read,), (), (), runtime_mutations.RESIDUE_TIMEOUT_SECONDS)
82 if not readable
or os.read(ready_read, 1) != b
"R":
83 with suppress(ProcessLookupError):
84 os.killpg(process.pid, signal.SIGKILL)
85 status, clean, _stderr = _runtime_api(
"_collect_runner")(process)
86 return status, clean,
False,
False,
None
87 original.rename(saved)
88 _runtime_api(
"_move_owned_root")(original, saved)
89 if inject_after_rename:
90 message =
"injected root-swap failure after rename"
91 raise runtime_mutations.RuntimeMutationError(message)
92 replacement_identity = _runtime_api(
"_create_replacement_root")(original)
93 os.write(go_write, b
"G")
94 status, clean, _stderr = _runtime_api(
"_collect_runner")(process)
97 clean
and _runtime_api(
"_no_residue")((original, saved)),
98 not tuple(original.iterdir()),
99 _retained_receipts_match(saved, baseline, phase),
100 replacement_identity,
104def _path_is_present(path: Path) -> bool:
105 """Report path presence without treating a broken symlink as absent."""
106 return path.exists()
or path.is_symlink()
109def _cleanup_swap_roots(
110 paths: tuple[Path, Path],
111 identities: tuple[tuple[int, int], tuple[int, int] |
None],
112) -> runtime_mutations.RuntimeMutationError |
None:
113 """Attempt every bound root removal while preserving the first refusal."""
114 original, saved = paths
115 original_identity, replacement_identity = identities
116 first_error: runtime_mutations.RuntimeMutationError |
None =
None
117 candidates: list[tuple[Path, tuple[int, int]]] = []
118 if replacement_identity
is not None and _path_is_present(original):
119 candidates.append((original, replacement_identity))
120 if _path_is_present(saved):
121 candidates.append((saved, original_identity))
122 elif _path_is_present(original):
123 candidates.append((original, original_identity))
124 for path, identity
in candidates:
125 error = _root_removal_error(path, identity)
126 if first_error
is None:
131def _root_removal_error(
132 path: Path, identity: tuple[int, int]
133) -> runtime_mutations.RuntimeMutationError |
None:
134 """Attempt one bound root removal and return its refusal."""
136 _runtime_api(
"_remove_root")(path, identity)
137 except runtime_mutations.RuntimeMutationError
as error:
142def _finalize_root_swap(
143 process: subprocess.Popen[bytes] |
None,
144 descriptors: set[int],
145 paths: tuple[Path, Path],
146 identities: tuple[tuple[int, int], tuple[int, int] |
None],
147 proof: tuple[bool, bool, set[str]],
148) -> tuple[bool, bool]:
149 """Dispose the runner, prove descriptor closure, then remove bound roots."""
150 cleanup_clean, injected, baseline = proof
152 OSError | runtime_mutations.RuntimeMutationError | subprocess.TimeoutExpired |
None
154 process_resolved = process
is None
155 if process
is not None:
157 _status, cleanup_clean = _runtime_api(
"_dispose_runner")(process)
158 process_resolved = cleanup_clean
161 runtime_mutations.RuntimeMutationError,
162 subprocess.TimeoutExpired,
166 _runtime_api(
"_close_owned_descriptors")(descriptors)
167 except OSError
as error:
168 if first_error
is None:
170 if not process_resolved
and first_error
is None:
171 first_error = runtime_mutations.RuntimeMutationError(
172 "root-swap runner authority remained unresolved"
174 original, saved = paths
177 and not _path_is_present(original)
178 and _path_is_present(saved)
179 and {path.name
for path
in saved.iterdir()} == baseline
180 and _runtime_api(
"_no_residue")(paths)
183 root_error = _cleanup_swap_roots(paths, identities)
184 if first_error
is None:
185 first_error = root_error
186 if first_error
is not None:
188 return cleanup_clean, injected_state
191def _prepare_root_gate(
192 sources: runtime_mutations.SourceBundle,
195 hooks: tuple[PipeFactory, SourceWriter] |
None =
None,
196) -> tuple[Path, Path, Path, dict[str, str], tuple[int, int, int, int], set[int]]:
197 """Create one handshake whose descriptors remain in an exact owned set."""
198 owned: set[int] = set()
199 pipe_factory, source_writer = hooks
or (os.pipe2, _runtime_api(
"_write_sources"))
201 ready_read, ready_write = pipe_factory(os.O_CLOEXEC)
202 owned.update((ready_read, ready_write))
203 go_read, go_write = pipe_factory(os.O_CLOEXEC)
204 owned.update((go_read, go_write))
205 descriptors = (ready_read, ready_write, go_read, go_write)
206 environment = dict(os.environ)
207 environment[
"RA8_RUNTIME_READY_FD"] = str(ready_write)
208 environment[
"RA8_RUNTIME_GO_FD"] = str(go_read)
210 ' os.write(int(os.environ["RA8_RUNTIME_READY_FD"]), b"R")\n'
211 ' os.read(int(os.environ["RA8_RUNTIME_GO_FD"]), 1)\n'
213 supervisor, process_source, cases = _root_gate_sources(sources, phase, handshake)
214 main_path, process_path, cases_path = source_writer(root, supervisor, process_source, cases)
215 except (OSError, runtime_mutations.RuntimeMutationError)
as primary:
217 _runtime_api(
"_close_owned_descriptors")(owned)
218 except OSError
as cleanup_error:
219 raise primary
from cleanup_error
222 return main_path, process_path, cases_path, environment, descriptors, owned
225def _public_root_swap_result(
227 result: tuple[int |
None, bool, bool, bool, tuple[int, int] |
None] |
None,
229 injected_state: bool,
230) -> tuple[int |
None, bool, bool, bool]:
231 """Return one public result only after the cleanup proof completed."""
233 return None, cleanup_clean, injected_state, injected_state
235 message =
"root-swap runtime produced no result"
236 raise runtime_mutations.RuntimeMutationError(message)
240def _create_swap_paths(
241 saved_selector: Callable[[], Path],
242) -> tuple[Path, Path, tuple[int, int]]:
243 """Create the first root only when the absent saved path is also acquired."""
244 original, identity = _runtime_api(
"_create_root")()
246 saved = saved_selector()
247 except runtime_mutations.RuntimeMutationError
as primary:
249 _runtime_api(
"_remove_root")(original, identity)
250 except runtime_mutations.RuntimeMutationError
as cleanup_error:
251 raise primary
from cleanup_error
253 if saved == original:
254 _runtime_api(
"_remove_root")(original, identity)
255 message =
"saved supervisor runtime root aliases its live original"
256 raise runtime_mutations.RuntimeMutationError(message)
257 return original, saved, identity
260def _start_root_swap_runner(
261 paths: tuple[Path, Path, Path],
262 environment: dict[str, str],
263 descriptors: tuple[int, int, int, int],
266) -> subprocess.Popen[bytes]:
267 """Launch a synchronized swap runner and release its inherited gate ends."""
268 main_path, process_path, cases_path = paths
269 _ready_read, ready_write, go_read, _go_write = descriptors
270 process = _runtime_api(
"_start_supervisor")(
274 (
"--selftest-missing-entry", str(root), _runtime_api(
"_identity_text")(root)),
275 runtime_mutations.SupervisorStart(
276 extra_descriptors=(ready_write, go_read), environment=environment
279 _runtime_api(
"_release_owned_descriptor")(owned, ready_write)
280 _runtime_api(
"_release_owned_descriptor")(owned, go_read)
284def _replace_root_after_gate(
285 sources: runtime_mutations.SourceBundle,
288 inject_after_rename: bool =
False,
289 hooks: RootSwapHooks |
None =
None,
290) -> tuple[int |
None, bool, bool, bool]:
291 """Replace a suite root at one deterministic retained-authority boundary."""
292 gate_preparer, saved_selector = hooks
or (
293 _runtime_api(
"_prepare_root_gate"),
294 _runtime_api(
"_new_root_path"),
296 original, saved, original_identity = _create_swap_paths(saved_selector)
297 replacement_identity: tuple[int, int] |
None =
None
298 process: subprocess.Popen[bytes] |
None =
None
300 owned_descriptors: set[int] = set()
301 baseline: set[str] = set()
302 result: tuple[int |
None, bool, bool, bool, tuple[int, int] |
None] |
None =
None
304 injected_state =
False
313 ) = gate_preparer(sources, phase, original)
314 ready_read, _ready_write, _go_read, go_write = descriptors
315 baseline = {path.name
for path
in original.iterdir()}
316 process = _start_root_swap_runner(
317 (main_path, process_path, cases_path),
323 result = _complete_root_swap(
325 (ready_read, go_write),
327 (baseline, phase, inject_after_rename),
329 _status, cleanup_clean, _replacement_empty, _retained_bound, replacement_identity = result
331 _runtime_api(
"_release_owned_descriptor")(owned_descriptors, go_write)
332 except runtime_mutations.RuntimeMutationError
as error:
333 if not inject_after_rename
or str(error) !=
"injected root-swap failure after rename":
337 cleanup_clean, injected_state = _finalize_root_swap(
341 (original_identity, replacement_identity),
342 (cleanup_clean, injected, baseline),
344 return _public_root_swap_result(inject_after_rename, result, cleanup_clean, injected_state)