ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_source_fixtures.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Structural mutations for image-lock suppression rationale authorities."""
4
5from __future__ import annotations
6
7Mutation = tuple[str, str, str, str]
8
9CLOSED_COMMAND_BLOCK = """ return (
10 sys.executable,
11 "-B",
12 "-I",
13 "-S",
14 SUPERVISOR_PROGRAM,
15 "--controller",
16 entry_authority,
17 str(status),
18 str(death_descriptor),
19 str(root_descriptor),
20 root_identity,
21 str(SELFTEST_WATCHDOG_TIMEOUT_SECONDS),
22 )"""
23CLOSED_POPEN_BLOCK = (
24 " child = subprocess.Popen( # noqa: S603 -- pinned "
25 "interpreter and supervisor FD\n"
26 " command,\n"
27 " pass_fds=tuple(inherited),\n"
28 " start_new_session=True,\n"
29 " )"
30)
31REFUSED_POPEN_BLOCK = (
32 " child = subprocess.Popen( # noqa: S603 -- pinned "
33 "interpreter and supervisor FD\n"
34 " (\n"
35 " sys.executable,\n"
36 ' "-B",\n'
37 ' "-I",\n'
38 ' "-S",\n'
39 " SUPERVISOR_PROGRAM,\n"
40 ' "--controller",\n'
41 ' "99",\n'
42 " str(status),\n"
43 " str(CLOSED_DESCRIPTOR),\n"
44 " str(root_descriptor),\n"
45 " identity,\n"
46 " str(SELFTEST_WATCHDOG_TIMEOUT_SECONDS),\n"
47 " ),\n"
48 " pass_fds=(source_descriptor, root_descriptor),\n"
49 " start_new_session=True,\n"
50 " )"
51)
52MAIN_POPEN_BLOCK = (
53 " child = subprocess.Popen( # noqa: S603 -- current pinned "
54 "interpreter and helper\n"
55 " argv,\n"
56 " pass_fds=tuple(inherited),\n"
57 " start_new_session=True,\n"
58 " )\n"
59 " self.bind_spawned_child(child)"
60)
61SUPERVISOR_LAUNCH_BLOCK = (
62 ' RA8_SELFTEST_BOUND_ENTRY="$1" \\\n'
63 ' /usr/bin/python3 -B -I -S "$program" --process-fd 9 --cases-fd 8 "$@" || status=$?'
64)
65PAYLOAD_ENTRY_BLOCK = ''' descriptor = int(entry_authority)
66 metadata = os.fstat(descriptor)
67 if not _entry_metadata_is_safe(metadata):
68 message = "controller entry descriptor is unsafe"
69 _refuse_entry(message)
70 entry = f"/proc/self/fd/{descriptor}"'''
71ENTRY_INTEGRITY_BLOCK = """ self.entry_integrity = (
72 _entry_metadata_is_safe(metadata)
73 and path_metadata is not None
74 and _entry_metadata_is_safe(path_metadata)
75 and (path_metadata.st_dev, path_metadata.st_ino) == self.entry_identity
76 and current_identity == self.entry_identity
77 and current_digest == self.entry_digest
78 )"""
79MAIN_ROOT_PATH_BLOCK = """ canonical = CANONICAL_TMP.resolve(strict=True)
80 resolved = root.resolve(strict=True)
81 except OSError:
82 return False
83 suffix = root.name.removeprefix(SUITE_ROOT_PREFIX)
84 return (
85 root.is_absolute()
86 and root.parent == canonical
87 and resolved == root
88 and len(suffix) == SUITE_ROOT_SUFFIX_LENGTH
89 and all(character in "0123456789abcdef" for character in suffix)
90 and _suite_root_metadata_is_safe(metadata)
91 )"""
92MAIN_ROOT_METADATA_BLOCK = """ return (
93 stat.S_ISDIR(metadata.st_mode)
94 and metadata.st_uid == os.getuid()
95 and metadata.st_gid == os.getgid()
96 and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE
97 )"""
98CASES_ROOT_BLOCK = """ canonical = CANONICAL_TMP.resolve(strict=True)
99 metadata = root.lstat()
100 suffix = root.name.removeprefix("ra8-devcontainer-image-selftest.")
101 identity = f"{metadata.st_dev}:{metadata.st_ino}"
102 resolved = root.resolve(strict=True)
103 except OSError:
104 return False
105 return (
106 root.is_absolute()
107 and root.parent == canonical
108 and resolved == root
109 and len(suffix) == SUITE_ROOT_SUFFIX_LENGTH
110 and all(character in "0123456789abcdef" for character in suffix)
111 and stat.S_ISDIR(metadata.st_mode)
112 and metadata.st_uid == os.getuid()
113 and metadata.st_gid == os.getgid()
114 and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE
115 and identity == expected_identity
116 )"""
117ROOT_CANONICAL_VALUE = (
118 ' "/tmp" # noqa: S108 -- fixed physical parent; random mode-0700 inode-bound direct child'
119)
120MAIN_ROOT_IDENTITY_BLOCK = """ if not _suite_root_metadata_is_safe(after) or identity != (
121 before.st_dev,
122 before.st_ino,
123 ):
124 message = "suite root identity changed while opening"
125 _refuse_entry(message)"""
126HANDLER_VALIDATION_BLOCK = (
127 ' [[ "$handler" =~ ^[a-z_][a-z0-9_]*$ ]] && declare -F "$handler" >/dev/null ||\n return 1'
128)
129CASES_EMBEDDED_EXEC_BLOCK = """os.execv("/bin/bash", ["/bin/bash", "-p", "--", sys.argv[1],
130 "--selftest-allocation-signal-child", *sys.argv[2:]])"""
131CASES_PYTHON_LAUNCH_BLOCK = (
132 ' /usr/bin/python3 -B -I -S -c "$supervisor" '
133 '"$SCRIPT_DIR/devcontainer_image.sh" \\\n'
134 ' "$ready" "$tmp" "$SELFTEST_TMP_IDENTITY" "$path" "$identity" \\\n'
135 ' "$SELFTEST_SUITE_ANCHOR" "$SELFTEST_SUITE_ANCHOR_IDENTITY" \\\n'
136 ' "$SELFTEST_SUITE_ANCHOR_OWNER_UID" &'
137)
138SIGNAL_EMBEDDED_EXEC_BLOCK = (
139 ' if os.path.isfile(ack): os.execv("/bin/bash", ["/bin/bash", "-p", "--", entry, *args])'
140)
141SIGNAL_PYTHON_LAUNCH_BLOCK = ''' exec /usr/bin/setsid /usr/bin/python3 -B -I -S -c "$launcher" \\
142 "$SELFTEST_IMAGE_ENTRY" "$ready" "$ack" "$launcher_mode" \\
143 --selftest-image-lock-signal-controller "$managed" "$case_dir" "$readiness_mode"'''
144REPLACED_LOCK_TRIPWIRE_BLOCK = (
145 ' marker="$tmp/unexpected-swap-build"\n'
146 ' mv "$IMAGE_LOCK_FILE" "$managed/original-image.lock"\n'
147 ' : >"$IMAGE_LOCK_FILE"\n'
148 ' chgrp "$SELFTEST_GROUP_GID" "$IMAGE_LOCK_FILE"\n'
149 ' chmod 0660 "$IMAGE_LOCK_FILE"\n'
150 " if (\n"
151 " # shellcheck disable=SC2329 # must-not-fire tripwire records a build after "
152 "replaced-lock refusal.\n"
153 ' build_image() { : >"$marker"; }\n'
154 ' build_locked dead forced "" 1 >/dev/null 2>&1\n'
155 " ); then"
156)
157MISSING_LOCK_TRIPWIRE_BLOCK = (
158 ' rm -f "$IMAGE_LOCK_FILE"\n'
159 " if (\n"
160 " # shellcheck disable=SC2329 # must-not-fire tripwire records a build after "
161 "missing-lock refusal.\n"
162 ' build_image() { : >"$marker"; }\n'
163 ' build_locked dead forced "" 1 >/dev/null 2>&1\n'
164 " ); then"
165)
166CASES_SOURCE_READ_BLOCK = '''def _read_cases_source(descriptor: int) -> bytes:
167 """Read one bounded authenticated cases module without using its pathname."""
168 metadata = os.fstat(descriptor)
169 safe = (
170 stat.S_ISREG(metadata.st_mode)
171 and metadata.st_nlink == 1
172 and metadata.st_uid == os.getuid()
173 and metadata.st_gid == os.getgid()
174 and stat.S_IMODE(metadata.st_mode) == CASES_MODE
175 and 0 < metadata.st_size <= CASES_MAX_BYTES
176 )
177 if not safe:
178 message = "supervisor cases metadata is unsafe"
179 _refuse_entry(message)
180 parts = []
181 offset = 0
182 for _step in range(CASES_READ_STEPS):
183 chunk = os.pread(descriptor, 4096, offset)
184 if not chunk:
185 break
186 parts.append(chunk)
187 offset += len(chunk)
188 source = b"".join(parts)
189 if len(source) != metadata.st_size or len(source) > CASES_MAX_BYTES:
190 message = "supervisor cases read is incomplete"
191 _refuse_entry(message)
192 return source'''
193CASES_LOADER_BLOCK = (
194 "def _load_cases_dispatch(descriptor: int) -> Callable[[list[str]], int | None]:\n"
195 ' """Load the authenticated source-only cases dispatcher from its bound FD."""\n'
196 """ try:
197 source = _read_cases_source(descriptor)
198 digest = hashlib.sha256(source).hexdigest()
199 if digest != CASES_RAW_SHA256:
200 message = "supervisor cases digest drifted"
201 _refuse_entry(message)
202 namespace = {
203 "__name__": "_ra8_supervisor_cases",
204 "__file__": f"/proc/self/fd/{descriptor}",
205 "_RA8_SUPERVISOR_CASES_VERSION": 1,
206 }
207 exec( # noqa: S102 -- exact digest-bound source-only FD
208 compile(source, namespace["__file__"], "exec"), namespace
209 )
210 grant = namespace.pop("_RA8_SUPERVISOR_CASES_VERSION", None)
211 if grant != 1 or "_RA8_SUPERVISOR_CASES_VERSION" in namespace:
212 message = "supervisor cases load grant was not consumed"
213 _refuse_entry(message)
214 if hashlib.sha256(_read_cases_source(descriptor)).hexdigest() != digest:
215 message = "supervisor cases changed while loading"
216 _refuse_entry(message)
217 dispatch = namespace.get("dispatch_supervisor_cases")
218 if not callable(dispatch):
219 message = "supervisor cases dispatcher is absent"
220 _refuse_entry(message)
221 if dispatch.__globals__ is not namespace:
222 message = "supervisor cases dispatcher escaped its private namespace"
223 _refuse_entry(message)
224 return dispatch
225 finally:
226 os.close(descriptor)"""
227)
228
229
230def process_authority_mutations() -> tuple[Mutation, ...]:
231 """Return every scoped suppression-rationale structural mutation."""
232 return (
233 *_closed_controller_command_mutations(),
234 *_refused_controller_popen_mutations(),
235 *_closed_controller_popen_mutations(),
236 *_main_supervisor_launch_mutations(),
237 *_payload_entry_mutations(),
238 *_main_suite_root_path_mutations(),
239 *_main_suite_root_identity_mutations(),
240 *_cases_suite_root_safety_mutations(),
241 *_spawn_handler_mutations(),
242 *_embedded_launcher_mutations(),
243 *_build_tripwire_mutations(),
244 *_cases_source_read_mutations(),
245 *_cases_loader_binding_mutations(),
246 *_cases_loader_postcondition_mutations(),
247 )
248
249
250def _replace_in_block(block: str, old: str, new: str) -> str:
251 """Return one scoped source mutation with a unique inner authority."""
252 if block.count(old) != 1:
253 message = f"runtime fixture inner authority is not unique: {old}"
254 raise ValueError(message)
255 return block.replace(old, new)
256
257
258def _closed_controller_command_mutations() -> tuple[Mutation, ...]:
259 """Bind every executable component of the closed-controller command."""
260 key = "devcontainer_image_selftest_supervisor_cases"
261 changes = (
262 (
263 "closed-controller interpreter changed",
264 " sys.executable,",
265 ' "/bin/false",',
266 ),
267 ("closed-controller no-bytecode flag removed", ' "-B",', ' "--version",'),
268 ("closed-controller isolation flag removed", ' "-I",', ' "--version",'),
269 ("closed-controller site-import refusal removed", ' "-S",', ' "--version",'),
270 (
271 "closed-controller supervisor authority removed",
272 " SUPERVISOR_PROGRAM,",
273 ' "/tmp/supervisor.py",',
274 ),
275 (
276 "closed-controller mode removed",
277 ' "--controller",',
278 ' "--version",',
279 ),
280 )
281 return tuple(
282 (label, key, CLOSED_COMMAND_BLOCK, _replace_in_block(CLOSED_COMMAND_BLOCK, old, new))
283 for label, old, new in changes
284 )
285
286
287def _refused_controller_popen_mutations() -> tuple[Mutation, ...]:
288 """Bind the refused-controller Popen argv, descriptors, and session."""
289 key = "devcontainer_image_selftest_supervisor_cases"
290 refused_changes = (
291 (
292 "refused-controller interpreter changed",
293 " sys.executable,",
294 ' "/bin/false",',
295 ),
296 (
297 "refused-controller no-bytecode flag removed",
298 ' "-B",',
299 ' "--version",',
300 ),
301 (
302 "refused-controller isolation flag removed",
303 ' "-I",',
304 ' "--version",',
305 ),
306 (
307 "refused-controller site-import refusal removed",
308 ' "-S",',
309 ' "--version",',
310 ),
311 (
312 "refused-controller supervisor authority removed",
313 " SUPERVISOR_PROGRAM,",
314 ' "/tmp/supervisor.py",',
315 ),
316 (
317 "refused-controller mode removed",
318 ' "--controller",',
319 ' "--version",',
320 ),
321 (
322 "refused-controller descriptor propagation removed",
323 " pass_fds=(source_descriptor, root_descriptor),",
324 " pass_fds=(),",
325 ),
326 (
327 "refused-controller session isolation removed",
328 " start_new_session=True,",
329 " start_new_session=False,",
330 ),
331 )
332 return tuple(
333 (label, key, REFUSED_POPEN_BLOCK, _replace_in_block(REFUSED_POPEN_BLOCK, old, new))
334 for label, old, new in refused_changes
335 )
336
337
338def _closed_controller_popen_mutations() -> tuple[Mutation, ...]:
339 """Bind the closed-controller Popen descriptors and isolated session."""
340 key = "devcontainer_image_selftest_supervisor_cases"
341 return (
342 (
343 "closed-controller Popen descriptor propagation removed",
344 key,
345 CLOSED_POPEN_BLOCK,
346 _replace_in_block(
347 CLOSED_POPEN_BLOCK,
348 " pass_fds=tuple(inherited),",
349 " pass_fds=(),",
350 ),
351 ),
352 (
353 "closed-controller Popen session isolation removed",
354 key,
355 CLOSED_POPEN_BLOCK,
356 _replace_in_block(
357 CLOSED_POPEN_BLOCK,
358 " start_new_session=True,",
359 " start_new_session=False,",
360 ),
361 ),
362 )
363
364
365def _main_supervisor_launch_mutations() -> tuple[Mutation, ...]:
366 """Bind the main supervisor's Popen and protected Bash launcher argv."""
367 process = "devcontainer_image_selftest_process"
368 lifecycle = "devcontainer_image_selftest"
369 popen = (
370 (
371 "parent-death Popen descriptor propagation removed",
372 process,
373 MAIN_POPEN_BLOCK,
374 _replace_in_block(
375 MAIN_POPEN_BLOCK,
376 " pass_fds=tuple(inherited),",
377 " pass_fds=(),",
378 ),
379 ),
380 (
381 "parent-death Popen session isolation removed",
382 process,
383 MAIN_POPEN_BLOCK,
384 _replace_in_block(
385 MAIN_POPEN_BLOCK,
386 " start_new_session=True,",
387 " start_new_session=False,",
388 ),
389 ),
390 )
391 changes = (
392 ("supervisor launcher interpreter changed", "/usr/bin/python3", "/bin/false"),
393 ("supervisor launcher no-bytecode flag removed", " -B ", " --version "),
394 ("supervisor launcher isolation flag removed", " -I ", " --version "),
395 ("supervisor launcher site-import refusal removed", " -S ", " --version "),
396 ("supervisor launcher bound program removed", '"$program"', '"$SELFTEST_SUPERVISOR"'),
397 (
398 "supervisor launcher bound entry environment removed",
399 'RA8_SELFTEST_BOUND_ENTRY="$1"',
400 'RA8_SELFTEST_BOUND_ENTRY=""',
401 ),
402 ("supervisor launcher process descriptor option removed", " --process-fd ", " --version "),
403 ("supervisor launcher process descriptor changed", " --process-fd 9 ", " --process-fd 7 "),
404 ("supervisor launcher cases descriptor option removed", " --cases-fd ", " --version "),
405 ("supervisor launcher cases descriptor changed", " --cases-fd 8 ", " --cases-fd 7 "),
406 )
407 launch = tuple(
408 (
409 label,
410 lifecycle,
411 SUPERVISOR_LAUNCH_BLOCK,
412 _replace_in_block(SUPERVISOR_LAUNCH_BLOCK, old, new),
413 )
414 for label, old, new in changes
415 )
416 return (*popen, *launch)
417
418
419def _payload_entry_mutations() -> tuple[Mutation, ...]:
420 """Bind the S606 payload to its opened FD and post-exec digest."""
421 key = "devcontainer_image_selftest_supervisor"
422 process = "devcontainer_image_selftest_process"
423 spawn_changes = (
424 (
425 "payload entry descriptor metadata binding removed",
426 " if not _entry_metadata_is_safe(metadata):",
427 " if False:",
428 ),
429 (
430 "payload procfd path binding removed",
431 ' entry = f"/proc/self/fd/{descriptor}"',
432 " entry = entry_authority",
433 ),
434 )
435 spawn = tuple(
436 (label, key, PAYLOAD_ENTRY_BLOCK, _replace_in_block(PAYLOAD_ENTRY_BLOCK, old, new))
437 for label, old, new in spawn_changes
438 )
439 integrity_changes = (
440 (
441 "payload post-exec pathname identity removed",
442 " and (path_metadata.st_dev, path_metadata.st_ino) "
443 "== self.entry_identity",
444 " and True",
445 ),
446 (
447 "payload post-exec descriptor identity removed",
448 " and current_identity == self.entry_identity",
449 " and True",
450 ),
451 (
452 "payload post-exec digest proof removed",
453 " and current_digest == self.entry_digest",
454 " and True",
455 ),
456 )
457 integrity = tuple(
458 (
459 label,
460 process,
461 ENTRY_INTEGRITY_BLOCK,
462 _replace_in_block(ENTRY_INTEGRITY_BLOCK, old, new),
463 )
464 for label, old, new in integrity_changes
465 )
466 return (*spawn, *integrity)
467
468
469def _main_suite_root_path_mutations() -> tuple[Mutation, ...]:
470 """Bind the main supervisor to one canonical direct suite-root path."""
471 main = "devcontainer_image_selftest_supervisor"
472 main_path_changes = (
473 ("main suite-root canonical tmp changed", ROOT_CANONICAL_VALUE, ' "/var/tmp"'),
474 (
475 "main suite-root canonical parent removed",
476 " and root.parent == canonical",
477 " and True",
478 ),
479 (
480 "main suite-root direct resolution removed",
481 " and resolved == root",
482 " and True",
483 ),
484 (
485 "main suite-root suffix length removed",
486 " and len(suffix) == SUITE_ROOT_SUFFIX_LENGTH",
487 " and True",
488 ),
489 (
490 "main suite-root hex suffix removed",
491 ' and all(character in "0123456789abcdef" for character in suffix)',
492 " and True",
493 ),
494 )
495 return tuple(
496 (
497 label,
498 main,
499 ROOT_CANONICAL_VALUE if "canonical tmp" in label else MAIN_ROOT_PATH_BLOCK,
500 (
501 _replace_in_block(ROOT_CANONICAL_VALUE, old, new)
502 if "canonical tmp" in label
503 else _replace_in_block(MAIN_ROOT_PATH_BLOCK, old, new)
504 ),
505 )
506 for label, old, new in main_path_changes
507 )
508
509
510def _main_suite_root_identity_mutations() -> tuple[Mutation, ...]:
511 """Bind main suite-root directory metadata and retained inode identity."""
512 main = "devcontainer_image_selftest_supervisor"
513 metadata_changes = (
514 (
515 "main suite-root directory type removed",
516 " stat.S_ISDIR(metadata.st_mode)",
517 " True",
518 ),
519 (
520 "main suite-root owner removed",
521 " and metadata.st_uid == os.getuid()",
522 " and True",
523 ),
524 (
525 "main suite-root group removed",
526 " and metadata.st_gid == os.getgid()",
527 " and True",
528 ),
529 (
530 "main suite-root private mode removed",
531 " and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE",
532 " and True",
533 ),
534 )
535 main_metadata = tuple(
536 (
537 label,
538 main,
539 MAIN_ROOT_METADATA_BLOCK,
540 _replace_in_block(MAIN_ROOT_METADATA_BLOCK, old, new),
541 )
542 for label, old, new in metadata_changes
543 )
544 identity = (
545 (
546 "main suite-root inode identity removed",
547 main,
548 MAIN_ROOT_IDENTITY_BLOCK,
549 _replace_in_block(
550 MAIN_ROOT_IDENTITY_BLOCK,
551 " if not _suite_root_metadata_is_safe(after) or identity != (\n"
552 " before.st_dev,\n"
553 " before.st_ino,\n"
554 " ):",
555 " if not _suite_root_metadata_is_safe(after):",
556 ),
557 ),
558 )
559 return (*main_metadata, *identity)
560
561
562def _cases_suite_root_changes() -> tuple[tuple[str, str, str], ...]:
563 """Return each independent supervisor-cases root weakening."""
564 return (
565 ("cases suite-root canonical tmp changed", ROOT_CANONICAL_VALUE, ' "/var/tmp"'),
566 (
567 "cases suite-root canonical parent removed",
568 " and root.parent == canonical",
569 " and True",
570 ),
571 (
572 "cases suite-root direct resolution removed",
573 " and resolved == root",
574 " and True",
575 ),
576 (
577 "cases suite-root suffix length removed",
578 " and len(suffix) == SUITE_ROOT_SUFFIX_LENGTH",
579 " and True",
580 ),
581 (
582 "cases suite-root hex suffix removed",
583 ' and all(character in "0123456789abcdef" for character in suffix)',
584 " and True",
585 ),
586 (
587 "cases suite-root directory type removed",
588 " and stat.S_ISDIR(metadata.st_mode)",
589 " and True",
590 ),
591 (
592 "cases suite-root owner removed",
593 " and metadata.st_uid == os.getuid()",
594 " and True",
595 ),
596 (
597 "cases suite-root group removed",
598 " and metadata.st_gid == os.getgid()",
599 " and True",
600 ),
601 (
602 "cases suite-root private mode removed",
603 " and stat.S_IMODE(metadata.st_mode) == PRIVATE_MODE",
604 " and True",
605 ),
606 (
607 "cases suite-root inode identity removed",
608 " and identity == expected_identity",
609 " and True",
610 ),
611 )
612
613
614def _cases_suite_root_safety_mutations() -> tuple[Mutation, ...]:
615 """Bind supervisor cases to the same canonical private suite root."""
616 cases = "devcontainer_image_selftest_supervisor_cases"
617 return tuple(
618 (
619 label,
620 cases,
621 ROOT_CANONICAL_VALUE if "canonical tmp" in label else CASES_ROOT_BLOCK,
622 (
623 _replace_in_block(ROOT_CANONICAL_VALUE, old, new)
624 if "canonical tmp" in label
625 else _replace_in_block(CASES_ROOT_BLOCK, old, new)
626 ),
627 )
628 for label, old, new in _cases_suite_root_changes()
629 )
630
631
632def _spawn_handler_mutations() -> tuple[Mutation, ...]:
633 """Bind the approved handler grammar and all four SC2064 trap expansions."""
634 key = "devcontainer_image_selftest"
635 validation = (
636 (
637 "spawn handler name grammar removed",
638 key,
639 HANDLER_VALIDATION_BLOCK,
640 _replace_in_block(
641 HANDLER_VALIDATION_BLOCK,
642 '[[ "$handler" =~ ^[a-z_][a-z0-9_]*$ ]]',
643 "true",
644 ),
645 ),
646 (
647 "spawn handler function binding removed",
648 key,
649 HANDLER_VALIDATION_BLOCK,
650 _replace_in_block(HANDLER_VALIDATION_BLOCK, 'declare -F "$handler" >/dev/null', "true"),
651 ),
652 )
653 trap_lines = (
654 ("spawn EXIT handler binding removed", ' trap "$handler \\$?" EXIT'),
655 ("spawn HUP handler binding removed", ' trap "$handler 129" HUP'),
656 ("spawn INT handler binding removed", ' trap "$handler 130" INT'),
657 ("spawn TERM handler binding removed", ' trap "$handler 143" TERM'),
658 )
659 traps = tuple((label, key, line, " true") for label, line in trap_lines)
660 return (*validation, *traps)
661
662
663def _embedded_exec_mutations(key: str, prefix: str, block: str) -> tuple[Mutation, ...]:
664 """Bind one embedded Python execv to protected Bash and exact argv."""
665 changes = (
666 (f"{prefix} execv removed", "os.execv(", "os.spawnv(os.P_WAIT, "),
667 (f"{prefix} Bash executable changed", 'os.execv("/bin/bash"', 'os.execv("/bin/false"'),
668 (f"{prefix} Bash argv0 changed", '["/bin/bash",', '["bash",'),
669 (f"{prefix} protected Bash flag removed", '"-p",', '"-c",'),
670 (f"{prefix} option terminator removed", '"--",', '"-c",'),
671 )
672 return tuple(
673 (label, key, block, _replace_in_block(block, old, new)) for label, old, new in changes
674 )
675
676
677def _python_launcher_mutations(
678 key: str, prefix: str, block: str, *, setsid: bool
679) -> tuple[Mutation, ...]:
680 """Bind one embedded-code launcher to isolated system Python."""
681 changes = [
682 (f"{prefix} Python interpreter changed", "/usr/bin/python3", "/bin/false"),
683 (f"{prefix} no-bytecode flag removed", " -B ", " --version "),
684 (f"{prefix} isolation flag removed", " -I ", " --version "),
685 (f"{prefix} site-import refusal removed", " -S ", " --version "),
686 (f"{prefix} command-string mode removed", " -c ", " --version "),
687 ]
688 if setsid:
689 changes.append((f"{prefix} session isolation removed", "/usr/bin/setsid ", ""))
690 return tuple(
691 (label, key, block, _replace_in_block(block, old, new)) for label, old, new in changes
692 )
693
694
695def _embedded_launcher_mutations() -> tuple[Mutation, ...]:
696 """Return scoped mutations for both Python-to-protected-Bash launchers."""
697 cases = "devcontainer_image_selftest_cases"
698 signal = "devcontainer_image_signal_selftest"
699 return (
700 *_embedded_exec_mutations(cases, "allocation launcher", CASES_EMBEDDED_EXEC_BLOCK),
701 *_python_launcher_mutations(
702 cases, "allocation launcher", CASES_PYTHON_LAUNCH_BLOCK, setsid=False
703 ),
704 *_embedded_exec_mutations(signal, "signal launcher", SIGNAL_EMBEDDED_EXEC_BLOCK),
705 *_python_launcher_mutations(
706 signal, "signal launcher", SIGNAL_PYTHON_LAUNCH_BLOCK, setsid=True
707 ),
708 )
709
710
711def _build_tripwire_mutations() -> tuple[Mutation, ...]:
712 """Prove both intentionally uncalled build callbacks remain must-not-fire tripwires."""
713 key = "devcontainer_image_selftest_cases"
714 callback = ' build_image() { : >"$marker"; }'
715 replacement = " true"
716 return (
717 (
718 "replaced-lock build tripwire removed",
719 key,
720 REPLACED_LOCK_TRIPWIRE_BLOCK,
721 _replace_in_block(REPLACED_LOCK_TRIPWIRE_BLOCK, callback, replacement),
722 ),
723 (
724 "missing-lock build tripwire removed",
725 key,
726 MISSING_LOCK_TRIPWIRE_BLOCK,
727 _replace_in_block(MISSING_LOCK_TRIPWIRE_BLOCK, callback, replacement),
728 ),
729 )
730
731
732def _cases_source_read_mutations() -> tuple[Mutation, ...]:
733 """Bind the cases source to regular owned bounded descriptor bytes."""
734 key = "devcontainer_image_selftest_supervisor"
735 changes = (
736 (
737 "cases source metadata regular binding removed",
738 " stat.S_ISREG(metadata.st_mode)",
739 " True",
740 ),
741 (
742 "cases source metadata link binding removed",
743 " and metadata.st_nlink == 1",
744 " and True",
745 ),
746 (
747 "cases source metadata owner binding removed",
748 " and metadata.st_uid == os.getuid()",
749 " and True",
750 ),
751 (
752 "cases source metadata group binding removed",
753 " and metadata.st_gid == os.getgid()",
754 " and True",
755 ),
756 (
757 "cases source metadata mode binding removed",
758 " and stat.S_IMODE(metadata.st_mode) == CASES_MODE",
759 " and True",
760 ),
761 (
762 "cases source metadata byte bound removed",
763 " and 0 < metadata.st_size <= CASES_MAX_BYTES",
764 " and True",
765 ),
766 (
767 "cases source read step bound removed",
768 " for _step in range(CASES_READ_STEPS):",
769 " for _step in iter(int, 1):",
770 ),
771 (
772 "cases source descriptor pread binding removed",
773 " chunk = os.pread(descriptor, 4096, offset)",
774 " chunk = os.read(descriptor, 4096)",
775 ),
776 (
777 "cases source complete-size postcondition removed",
778 " if len(source) != metadata.st_size or len(source) > CASES_MAX_BYTES:",
779 " if False:",
780 ),
781 )
782 return tuple(
783 (
784 label,
785 key,
786 CASES_SOURCE_READ_BLOCK,
787 _replace_in_block(CASES_SOURCE_READ_BLOCK, old, new),
788 )
789 for label, old, new in changes
790 )
791
792
793def _cases_loader_binding_mutations() -> tuple[Mutation, ...]:
794 """Bind cases compilation to authenticated source and namespace bytes."""
795 key = "devcontainer_image_selftest_supervisor"
796 changes = (
797 (
798 "cases loader bound source read removed",
799 " source = _read_cases_source(descriptor)",
800 " source = b''",
801 ),
802 (
803 "cases loader pre-exec digest binding removed",
804 " if digest != CASES_RAW_SHA256:",
805 " if False:",
806 ),
807 (
808 "cases loader namespace name binding removed",
809 ' "__name__": "_ra8_supervisor_cases",',
810 ' "__name__": "__main__",',
811 ),
812 (
813 "cases loader namespace filename binding removed",
814 ' "__file__": f"/proc/self/fd/{descriptor}",',
815 ' "__file__": "supervisor_cases.py",',
816 ),
817 (
818 "cases loader grant version binding removed",
819 ' "_RA8_SUPERVISOR_CASES_VERSION": 1,',
820 ' "_RA8_SUPERVISOR_CASES_VERSION": 0,',
821 ),
822 (
823 "cases loader authenticated exec removed",
824 " exec( # noqa: S102 -- exact digest-bound source-only FD",
825 " eval( # exact mutation",
826 ),
827 (
828 "cases loader compile source binding removed",
829 " compile(source,",
830 " compile(b'',",
831 ),
832 (
833 "cases loader compile filename binding removed",
834 'compile(source, namespace["__file__"],',
835 'compile(source, "supervisor_cases.py",',
836 ),
837 (
838 "cases loader compile exec-mode binding removed",
839 'namespace["__file__"], "exec")',
840 'namespace["__file__"], "eval")',
841 ),
842 )
843 return tuple(
844 (label, key, CASES_LOADER_BLOCK, _replace_in_block(CASES_LOADER_BLOCK, old, new))
845 for label, old, new in changes
846 )
847
848
849def _cases_loader_postcondition_mutations() -> tuple[Mutation, ...]:
850 """Bind grant consumption, stable source, dispatch, and descriptor close."""
851 key = "devcontainer_image_selftest_supervisor"
852 changes = (
853 (
854 "cases loader grant exact consumption removed",
855 ' grant = namespace.pop("_RA8_SUPERVISOR_CASES_VERSION", None)',
856 " grant = 1",
857 ),
858 (
859 "cases loader grant absence postcondition removed",
860 ' if grant != 1 or "_RA8_SUPERVISOR_CASES_VERSION" in namespace:',
861 " if grant != 1:",
862 ),
863 (
864 "cases loader post-exec same-FD digest removed",
865 " if hashlib.sha256(_read_cases_source(descriptor)).hexdigest() != digest:",
866 " if False:",
867 ),
868 (
869 "cases loader callable dispatch binding removed",
870 " if not callable(dispatch):",
871 " if False:",
872 ),
873 (
874 "cases loader dispatch namespace identity removed",
875 " if dispatch.__globals__ is not namespace:",
876 " if False:",
877 ),
878 (
879 "cases loader descriptor final-close removed",
880 " finally:\n os.close(descriptor)",
881 " finally:\n pass",
882 ),
883 )
884 return tuple(
885 (label, key, CASES_LOADER_BLOCK, _replace_in_block(CASES_LOADER_BLOCK, old, new))
886 for label, old, new in changes
887 )