3"""Executable-order policy for the devcontainer image-lock selftest receipts."""
5from __future__
import annotations
9PROCESS_SOURCE_SPECS = (
11 "suite-root nested allocation selection removed",
12 "devcontainer_image_selftest",
14 ' if [[ -n "$SELFTEST_SUITE_ROOT" ]]; then\n selftest_suite_root_is_safe || return 1',
17 "shared worker process-group binding removed",
18 "devcontainer_image_lock_selftest",
19 "record_worker_group",
20 ' [[ "$pgid" == "$PPID" && "$pgid" != "$pid" ]] ||',
23 "isolated worker process-group binding removed",
24 "devcontainer_image_lock_selftest",
25 "record_worker_group",
26 ' [[ "$pgid" == "$pid" ]] || die "selftest isolated worker is not its group leader"',
29 "suite anchor canonical path binding removed",
30 "devcontainer_image_selftest",
31 "selftest_suite_anchor_is_safe",
32 ' "$anchor" == "$canonical/ra8-devcontainer-image-selftest.$suffix" &&',
35 "suite anchor identity binding removed",
36 "devcontainer_image_selftest",
37 "selftest_suite_anchor_is_safe",
38 ' "$(file_identity "$anchor")" == "$SELFTEST_SUITE_ANCHOR_IDENTITY" &&',
41 "suite anchor owner binding removed",
42 "devcontainer_image_selftest",
43 "selftest_suite_anchor_is_safe",
44 ' "$(file_owner_id "$anchor")" == "$SELFTEST_SUITE_ANCHOR_OWNER_UID" &&',
47 "suite anchor mode binding removed",
48 "devcontainer_image_selftest",
49 "selftest_suite_anchor_is_safe",
50 ' "$(file_mode "$anchor")" == "700" ]]',
53 "suite anchor child depth binding removed",
54 "devcontainer_image_selftest",
55 "selftest_suite_path_is_safe",
56 ' [[ "$suite" == "$SELFTEST_SUITE_ANCHOR/ra8-devcontainer-image-selftest.$suffix" &&',
59 "suite anchor producer path removed",
60 "devcontainer_image_selftest",
61 "establish_selftest_suite_root",
62 ' SELFTEST_SUITE_ANCHOR="$SELFTEST_TMP_DIR"',
65 "suite anchor producer identity removed",
66 "devcontainer_image_selftest",
67 "establish_selftest_suite_root",
68 ' SELFTEST_SUITE_ANCHOR_IDENTITY="$SELFTEST_TMP_IDENTITY"',
71 "suite anchor producer owner removed",
72 "devcontainer_image_selftest",
73 "establish_selftest_suite_root",
74 ' SELFTEST_SUITE_ANCHOR_OWNER_UID="$SELFTEST_TMP_OWNER_UID"',
77 "suite anchor receiver path removed",
78 "devcontainer_image_selftest",
79 "configure_selftest_suite_authority",
80 ' SELFTEST_SUITE_ANCHOR="$3"',
83 "suite anchor receiver identity removed",
84 "devcontainer_image_selftest",
85 "configure_selftest_suite_authority",
86 ' SELFTEST_SUITE_ANCHOR_IDENTITY="$4"',
89 "suite anchor receiver owner removed",
90 "devcontainer_image_selftest",
91 "configure_selftest_suite_authority",
92 ' SELFTEST_SUITE_ANCHOR_OWNER_UID="$5"',
95 "suite anchor dispatcher propagation removed",
96 "devcontainer_image_selftest_cases",
97 "selftest_one_allocation_signal",
98 ' "$tmp" "$SELFTEST_TMP_IDENTITY" "$SELFTEST_SUITE_ANCHOR" \\\n'
99 ' "$SELFTEST_SUITE_ANCHOR_IDENTITY" "$SELFTEST_SUITE_ANCHOR_OWNER_UID"; then',
102LEGACY_PROCESS_FINDINGS = {
103 "image lock jobs-table lookup bypassed": (
104 "devcontainer image lifecycle selftest: required process-authority token is not unique: "
105 "done < <(jobs -r -l)",
107 "image lock verified PID signal reverted to stale jobspec": (
108 "devcontainer image lifecycle selftest: required process-authority token is not unique: "
109 'builtin kill -"$signal" "$child"',
111 "allocation KILL direct-child guard removed": (
112 "devcontainer image cases selftest: required process-authority token is not unique: "
113 'signal_owned_live_child KILL "$child" ||',
115 "suite-root parent grammar reverted to ten characters": (
116 "devcontainer image lifecycle selftest: required process-authority token is not unique: "
117 '"$suffix" =~ ^[0-9a-f]{32}$ && ! -L "$canonical"',
119 "portable Bash 3 shell identity replaced by BASHPID": (
120 "devcontainer image lifecycle selftest: required process-authority token is not unique: "
121 'local destination="$1" value="$$:${BASH_SUBSHELL:-0}"',
122 "devcontainer image selftest: macOS Bash 3.2 BASHPID dependency returned",
124 "fresh allocation signal process bypassed": (
125 "devcontainer image cases selftest: required process-authority token is not unique: "
126 '--selftest-allocation-checkpoint-child "$phase" "$receipt"',
128 "selftest atomic directory allocation replaced by mktemp create": (
129 "devcontainer image lifecycle selftest: required process-authority token is not unique: "
130 'if (umask 077 && mkdir -m 0700 -- "$candidate"); then',
131 "devcontainer image lifecycle selftest: allocation-before-binding returned",
133 "selftest suite-root binding removed": (
134 "devcontainer image cases selftest: required process-authority token is not unique: "
135 'establish_selftest_suite_root || die "selftest: could not bind its suite root"',
137 "selftest suite-root completion proof removed": (
138 "devcontainer image cases selftest: required process-authority token is not unique: "
139 'clear_selftest_suite_root || die "selftest: suite-root cleanup did not complete"',
141 "worker group direct-child authority removed": (
142 "dev box image lock: bounded selftest harness is not load-bearing",
144 "worker leader TERM resistance removed": (
145 "devcontainer image-lock selftest: required process-authority token is not unique: "
146 "pre-ready-hang | signal-controller | post-ready-build-hang) trap '' HUP INT TERM ;;",
148 "process-enumeration failure scenario removed": (
149 "devcontainer image cases selftest: required process-authority token is not unique: "
150 'selftest_ps_failure_cleanup "$tmp"',
152 "rebound process-group refusal removed": (
153 "devcontainer image-lock selftest: required process-authority token is not unique: "
154 "rebound numeric process group gained signal authority",
156 "process-enumeration descendant proof removed": (
157 "devcontainer image-lock selftest: required process-authority token is not unique: "
158 "repeated ps failure left a signal-ignoring descendant",
163def _process_source_finding(key: str, owner: str, token: str) -> str:
164 """Return one exact Bash source-policy diagnostic."""
165 return f
"devcontainer image source policy: {key}:{owner} token is not unique: {token}"
168def semantic_process_findings(label: str) -> tuple[str, ...] |
None:
169 """Return one exact legacy Bash process-authority finding set."""
170 if (findings := LEGACY_PROCESS_FINDINGS.get(label))
is not None:
172 for candidate, key, owner, token
in PROCESS_SOURCE_SPECS:
173 if label == candidate:
174 return (_process_source_finding(key, owner, token),)
178def process_source_errors(inputs: dict[str, str]) -> list[str]:
179 """Bind nested-root selection and shared/isolated group identity by owner."""
181 for _label, key, owner, token
in PROCESS_SOURCE_SPECS:
182 body = _body(inputs[key], owner, 0)
183 if body
is None or body.count(token) != 1:
184 errors.append(_process_source_finding(key, owner, token))
188def _body(source: str, name: str, indent: int) -> str |
None:
189 """Return one Bash function body whose closing brace shares its indentation."""
190 prefix =
" " * indent
191 opening = f
"{prefix}{name}() {{\n"
192 if source.count(opening) != 1:
194 start = source.find(opening)
197 body_start = start + len(opening)
198 match = re.search(rf
"(?m)^{re.escape(prefix)}}}\s*$", source[body_start:])
201 return source[body_start : body_start + match.start()]
204def _ordered(body: str, anchors: tuple[str, ...]) -> bool:
205 """Require active anchors exactly once and in strictly increasing order."""
206 active =
"\n".join(line
for line
in body.splitlines()
if not line.lstrip().startswith(
"#"))
208 for anchor
in anchors:
209 if active.count(anchor) != 1:
211 position = active.find(anchor, position + 1)
217def _early_success(body: str, indent: int) -> bool:
218 """Detect a top-level success return injected before completion accounting."""
219 prefix =
" " * (indent + 2)
221 line
in (f
"{prefix}return", f
"{prefix}return 0", f
"{prefix}exit 0")
222 for line
in body.splitlines()
226def _function_errors(source: str, names: tuple[str, ...], indent: int) -> list[str]:
227 """Require functions to exist and refuse top-level early-success exits."""
230 body = _body(source, name, indent)
232 errors.append(f
"image lock receipt: missing {name}")
233 elif _early_success(body, indent):
234 errors.append(f
"image lock receipt: {name} can return success before its proof")
238def _order_error(source: str, name: str, indent: int, anchors: tuple[str, ...]) -> str |
None:
239 """Return one finding when a function's executable proof order drifts."""
240 body = _body(source, name, indent)
241 if body
is None or not _ordered(body, anchors):
242 return f
"image lock receipt: {name} proof order drifted"
246def _main_order_errors(image_source: str, cases_source: str) -> list[str]:
247 """Bind the lock, command, and main completion paths in executable order."""
249 'exec 9<"$IMAGE_LOCK_FILE"',
250 "validate_opened_image_lock 9\n if ! flock -n 9; then",
251 "if ! flock -n 9; then",
252 "fi\n validate_opened_image_lock 9",
253 'build_image "$want"\n )',
256 'SELFTEST_RECEIPT_DIR="$tmp/image-lock-selftest-receipts"',
257 'mkdir -m 0700 "$SELFTEST_RECEIPT_DIR"',
258 'dispatch_image_lock_selftest suite "$tmp"',
259 '[[ "$SELFTEST_DISPATCH_COMPLETE" == "1" ]]',
260 "SELFTEST_FILE_RECEIPTS_VERIFIED=0",
261 "verify_scenario_receipt_files early-exit pre-ready-hang forced-build-contention",
262 '[[ "$SELFTEST_FILE_RECEIPTS_VERIFIED" == "1" ]]',
263 'selftest_runtime_labels "$base"',
264 "SELFTEST_COMMAND_COMPLETE=1",
267 for source, name, indent, anchors
in (
268 (image_source,
"build_locked", 2, build_anchors),
269 (cases_source,
"cmd_selftest", 0, command_anchors),
271 error = _order_error(source, name, indent, anchors)
275 "SELFTEST_COMMAND_COMPLETE=0",
277 '[[ "$SELFTEST_COMMAND_COMPLETE" == "1" ]]',
278 "SELFTEST_MAIN_COMPLETE=1",
280 main_body = _body(image_source,
"main", 2)
281 if main_body
is None or not _ordered(main_body, main_order):
282 errors.append(
"image lock receipt: main selftest completion check drifted")
284 "SELFTEST_MAIN_COMPLETE=0",
286 '[[ "$SELFTEST_MAIN_COMPLETE" == "1" ]] || die "selftest main returned before completion"',
288 if not _ordered(image_source, top_order):
289 errors.append(
"image lock receipt: top-level main completion check drifted")
293def _worker_cleanup_checks() -> tuple[tuple[str, tuple[str, ...]], ...]:
294 """Return lock release and direct worker cleanup proof order."""
297 "release_parent_lock",
299 "flock -u 8 || release_failed=1",
300 "if exec 8>&-; then",
301 "SELFTEST_PARENT_LOCK_OPEN=0",
302 'return "$release_failed"',
306 "cleanup_image_lock_case",
308 'SELFTEST_CLEANUP_RECEIPT=""',
309 'kill -TERM -- "-$SELFTEST_WORKER_PGID"',
310 "release_parent_lock",
313 'bounded_process_absent "$SELFTEST_WORKER_PID"',
314 'bounded_group_gone "$SELFTEST_WORKER_PGID"',
315 "assert_no_surviving_descendants",
316 "parent_lock_fd_is_closed",
318 'SELFTEST_CLEANUP_RECEIPT="$(expected_cleanup_receipt)"',
319 'return "$cleanup_failed"',
325def _controller_cleanup_checks() -> tuple[tuple[str, tuple[str, ...]], ...]:
326 """Return forced and signal-controller cleanup proof order."""
329 "force_signal_controller_cleanup",
331 'SELFTEST_FORCE_CLEANUP_RECEIPT=""',
332 'signal_owned_controller_group TERM "$controller"',
333 'bounded_process_terminal "$controller" "$SELFTEST_CONTROLLER_CLEANUP_STEPS"',
334 "verify_signal_controller_cleanup",
335 'SELFTEST_FORCE_CLEANUP_RECEIPT="$(expected_force_cleanup_receipt "$case_dir")"',
339 "verify_signal_controller_cleanup",
342 'bounded_process_absent "$controller"',
343 "read_worker_group_from_case",
344 'bounded_process_absent "$SELFTEST_WORKER_PID"',
345 'bounded_group_gone "$SELFTEST_WORKER_PGID"',
346 "assert_no_surviving_descendants",
347 "parent_lock_fd_is_closed",
349 "write_controller_cleanup_receipt_file",
350 "require_controller_cleanup_receipt_file",
354 "image_lock_case_signal",
356 "cleanup_image_lock_case",
357 "require_cleanup_receipt",
358 "write_worker_cleanup_proof_file",
365def _cleanup_order_errors(source: str) -> list[str]:
366 """Bind cleanup receipts after every termination and lock-release fact."""
368 checks = (*_worker_cleanup_checks(), *_controller_cleanup_checks())
369 for name, anchors
in checks:
370 error = _order_error(source, name, 0, anchors)
376def _simple_worker_scenario_checks() -> tuple[tuple[str, tuple[str, ...], str], ...]:
377 """Return complete semantic tails for exit and pre-ready scenarios."""
378 pre_ready_negative = (
379 'if wait_for_status_file "$SELFTEST_CASE_DIR/ready.status" '
380 '"$SELFTEST_WORKER_PID"; then\n'
381 ' die "selftest: pre-ready hang unexpectedly became ready"\n'
386 "selftest_early_exit",
388 "start_image_lock_worker early-exit",
390 '[[ "$SELFTEST_REAP_STATUS" == "23" ]]',
391 "cleanup_image_lock_case",
392 "require_cleanup_receipt",
393 "clear_image_lock_case_traps",
398 "selftest_pre_ready_hang",
400 "start_image_lock_worker pre-ready-hang",
402 "cleanup_image_lock_case",
403 "require_cleanup_receipt",
404 "clear_image_lock_case_traps",
411def _contention_scenario_checks() -> tuple[tuple[str, tuple[str, ...], str], ...]:
412 """Return complete semantic tails for lock-contention scenarios."""
413 post_ready_negative = (
414 'if wait_for_status_file "$SELFTEST_CASE_DIR/done.status" '
415 '"$SELFTEST_WORKER_PID"; then\n'
416 ' die "selftest: build-hang worker unexpectedly completed"\n'
420 "for ((attempt = 0; attempt < 20; ++attempt)); do\n"
421 ' [[ ! -e "$SELFTEST_CASE_DIR/build-entered.status" ]] ||\n'
422 ' die "selftest: forced rebuild bypassed the held lock"\n'
423 ' process_is_terminal "$SELFTEST_WORKER_PID" &&\n'
424 ' die "selftest: forced rebuild exited while the lock was held"\n'
430 "selftest_forced_build_contention",
433 "if fresh_lock_probe; then",
434 "start_image_lock_worker normal",
435 'wait_for_status_file "$SELFTEST_CASE_DIR/ready.status"',
437 "release_parent_lock",
438 'wait_for_status_file "$SELFTEST_CASE_DIR/done.status"',
440 '[[ "$SELFTEST_REAP_STATUS" == "0" ]]',
441 "fresh_lock_probe || die "
442 '"selftest: fresh lock probe failed after worker completion"',
443 "cleanup_image_lock_case",
444 "require_cleanup_receipt",
445 "clear_image_lock_case_traps",
447 "forced-build-contention",
450 "selftest_post_ready_hang",
452 "start_image_lock_worker post-ready-build-hang",
453 'wait_for_status_file "$SELFTEST_CASE_DIR/ready.status"',
454 "release_parent_lock",
455 'wait_for_status_file "$SELFTEST_CASE_DIR/build-entered.status"',
457 "cleanup_image_lock_case",
458 "require_cleanup_receipt",
459 "clear_image_lock_case_traps",
466def _signal_scenario_checks() -> tuple[tuple[str, tuple[str, ...], str], ...]:
467 """Return complete semantic tails for the two signal scenarios."""
469 'if wait_for_status_file "$case_dir/controller-ready.status" "$controller"; then\n'
470 ' die "selftest: delayed signal controller unexpectedly became ready"\n'
475 "selftest_signal_ready_timeout",
477 'start_signal_controller controller "$managed" "$case_dir" delay-controller-ready',
479 "force_signal_controller_cleanup",
480 "require_force_cleanup_receipt",
482 "signal-ready-timeout",
485 "selftest_signal_cleanup",
487 "for signal in HUP INT TERM; do",
488 'case "$signal" in HUP) expected=129 ;; INT) expected=130 ;; '
489 "TERM) expected=143 ;; esac",
490 'wait_for_status_file "$case_dir/controller-ready.status" "$controller"',
491 'signal_owned_controller_group "$signal" "$controller"',
492 'bounded_process_terminal "$controller" "$SELFTEST_CONTROLLER_CLEANUP_STEPS"',
493 "verify_signal_controller_cleanup",
494 '[[ "$SELFTEST_REAP_STATUS" == "$expected" ]]',
495 "require_worker_cleanup_proof_file",
496 "require_controller_cleanup_receipt_file",
504def _scenario_order_errors(lock_source: str, signal_source: str) -> list[str]:
505 """Require each complete scenario proof before its final unique receipt."""
508 *_simple_worker_scenario_checks(),
509 *_contention_scenario_checks(),
510 *_signal_scenario_checks(),
512 for function, semantics, receipt
in checks:
513 source = signal_source
if function.startswith(
"selftest_signal_")
else lock_source
514 body = _body(source, function, 0)
515 final = f
"write_scenario_receipt {receipt}"
516 if body
is None or not _ordered(body, (*semantics, final)):
517 errors.append(f
"image lock receipt: {function} completion order drifted")
521 for line
in body.splitlines()
522 if line.strip()
and not line.lstrip().startswith(
"#")
524 if not executable
or executable[-1] != final:
525 errors.append(f
"image lock receipt: {function} receipt is not its final action")
529def _dispatch_order_errors(lock_source: str, receipt_source: str, cases_source: str) -> list[str]:
530 """Bind exact scenario identities, count, file proof, and dispatcher latch."""
532 'run_image_lock_scenario early-exit selftest_early_exit "$tmp"',
533 'run_image_lock_scenario pre-ready-hang selftest_pre_ready_hang "$tmp"',
534 'run_image_lock_scenario forced-build-contention selftest_forced_build_contention "$tmp"',
535 'run_image_lock_scenario post-ready-hang selftest_post_ready_hang "$tmp"',
536 'run_image_lock_scenario signal-ready-timeout selftest_signal_ready_timeout "$tmp"',
537 'run_image_lock_scenario signal-cleanup selftest_signal_cleanup "$tmp"',
539 dispatcher = _body(cases_source,
"run_managed_image_lock_suite", 0)
540 runner = _body(lock_source,
"run_image_lock_scenario", 0)
541 verifier = _body(receipt_source,
"verify_scenario_receipt_files", 0)
543 if dispatcher
is None or not _ordered(
544 dispatcher, (*calls,
"verify_image_lock_suite_receipts",
"SELFTEST_DISPATCH_COMPLETE=1")
546 errors.append(
"image lock receipt: dispatcher scenario set or latch drifted")
548 '"$scenario" "$tmp"',
549 "require_scenario_receipt",
550 "SELFTEST_SUITE_RECEIPTS+=",
551 "SELFTEST_SUITE_COUNT",
553 if runner
is None or not _ordered(runner, runner_order):
554 errors.append(
"image lock receipt: scenario runner accounting drifted")
556 "SELFTEST_FILE_RECEIPTS_VERIFIED=0",
558 "require_scenario_receipt",
560 '[[ "$count" == "6" ]]',
561 "SELFTEST_FILE_RECEIPTS_VERIFIED=1",
563 if verifier
is None or not _ordered(verifier, verifier_order):
564 errors.append(
"image lock receipt: independent receipt verifier drifted")
571 selftest_source: str,
575 """Return completion-accounting findings for the image-lock implementation."""
576 main_names = (
"build_locked",
"main")
577 receipt_helper_names = (
578 "expected_image_lock_suite_receipts",
579 "scenario_receipt_value",
580 "validate_scenario_receipt_directory",
581 "write_scenario_receipt",
582 "require_scenario_receipt",
583 "verify_scenario_receipt_files",
584 "expected_cleanup_receipt",
585 "expected_force_cleanup_receipt",
586 "parent_lock_fd_is_closed",
587 "require_cleanup_receipt",
588 "require_force_cleanup_receipt",
589 "write_worker_cleanup_proof_file",
590 "require_worker_cleanup_proof_file",
591 "write_controller_cleanup_receipt_file",
592 "require_controller_cleanup_receipt_file",
594 lock_helper_names = (
597 "release_parent_lock",
599 "assert_no_surviving_descendants",
600 "cleanup_image_lock_case",
601 "force_signal_controller_cleanup",
602 "verify_signal_controller_cleanup",
603 "verify_image_lock_suite_receipts",
604 "run_image_lock_scenario",
605 "dispatch_image_lock_selftest",
606 "selftest_early_exit",
607 "selftest_pre_ready_hang",
608 "selftest_forced_build_contention",
609 "selftest_post_ready_hang",
611 signal_helper_names = (
612 "selftest_signal_ready_timeout",
613 "selftest_signal_cleanup",
616 _function_errors(image_source, main_names, 2)
617 + _function_errors(cases_source, (
"cmd_selftest",), 0)
618 + _function_errors(receipt_source, receipt_helper_names, 0)
619 + _function_errors(selftest_source, lock_helper_names, 0)
620 + _function_errors(cases_source, (
"run_managed_image_lock_suite",), 0)
621 + _function_errors(signal_source, signal_helper_names, 0)
622 + _main_order_errors(image_source, cases_source)
623 + _cleanup_order_errors(selftest_source)
624 + _scenario_order_errors(selftest_source, signal_source)
625 + _dispatch_order_errors(selftest_source, receipt_source, cases_source)