ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_hil_privilege_boundary.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Enforce the root-owned HIL privilege boundary from structural facts."""
5
6from __future__ import annotations
7
8import argparse
9import ast
10import hashlib
11import json
12import re
13import sys
14from pathlib import Path
15from typing import cast
16
17import yaml
18
19REPO_ROOT = Path(__file__).resolve().parents[2]
20HELPER_REL = "infra/ansible/roles/dev_box/files/ra8-hil-privileged.py"
21POLICY_TEMPLATE_REL = "infra/ansible/roles/dev_box/templates/ra8-hil-privileged-policy.json.j2"
22ROLE_ENTRY_REL = "infra/ansible/roles/dev_box/tasks/hil_runner.yml"
23ROLE_REL = "infra/ansible/roles/dev_box/tasks/hil_runner_transaction.yml"
24MANIFEST_REL = "scripts/hil/lib/ra8-hil-privileged.sha256"
25CALLER_PATHS = (
26 "scripts/hil/ppps.sh",
27 "scripts/hil/flash_retry.sh",
28 "scripts/hil/exit_low_power.sh",
29 "scripts/hil/eth_tcp.sh",
30)
31WORKFLOW_PATHS = (
32 HELPER_REL,
33 POLICY_TEMPLATE_REL,
34 ROLE_ENTRY_REL,
35 "infra/fleet.yml",
36 "scripts/dev/fleet_hil.py",
37 "infra/ansible/roles/hil_bench/tasks/main.yml",
38)
39SPAWN_ARGC = 2
40EXACT_POLICY_TEMPLATE = """{
41 "board_iface": {{ dev_box_hil_runner_bench_iface | to_json }},
42 "declaration_sha256": {{ dev_box_hil_runner_bench_policy_sha256 | to_json }},
43 "mac": {{ dev_box_hil_runner_bench_mac | to_json }},
44 "phc_index": {{ dev_box_hil_runner_bench_phc_index | int }},
45 "sysfs_device": {{ dev_box_hil_runner_bench_sysfs_device | to_json }},
46 "version": 1
47}
48"""
49LOAD_POLICY_PREFIX = (
50 "def _load_policy(path: Path = POLICY_PATH) -> dict[str, object]:\n"
51 ' """Open the fixed root-owned policy without following a final symlink."""\n'
52 " try:\n"
53)
54LOAD_POLICY_NOFOLLOW = (
55 LOAD_POLICY_PREFIX
56 + " descriptor = os.open(path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)"
57)
58LOAD_POLICY_DEAD_NOFOLLOW = (
59 LOAD_POLICY_PREFIX
60 + " descriptor = os.open(path, os.O_RDONLY | os.O_CLOEXEC) # dead os.O_NOFOLLOW"
61)
62
63
64def _copy_tasks(source: str) -> tuple[list[dict[str, object]], list[str]]:
65 """Parse an Ansible task list or return one attributed error."""
66 try:
67 tasks = yaml.safe_load(source)
68 except yaml.YAMLError:
69 return [], ["hil_runner.yml: YAML is malformed"]
70 if not isinstance(tasks, list) or any(not isinstance(task, dict) for task in tasks):
71 return [], ["hil_runner.yml: role task file is not a task list"]
72 return cast(list[dict[str, object]], tasks), []
73
74
75def _module_tasks(tasks: list[dict[str, object]], module: str) -> list[dict[str, object]]:
76 """Return exact mappings for one Ansible module."""
77 return [
78 cast(dict[str, object], task[module])
79 for task in tasks
80 if isinstance(task.get(module), dict)
81 ]
82
83
84def _exact_task(
85 tasks: list[dict[str, object]],
86 module: str,
87 destination: str,
88 expected: dict[str, object],
89) -> bool:
90 """Return whether exactly one destination task has every required field."""
91 matches = [task for task in _module_tasks(tasks, module) if task.get("dest") == destination]
92 return len(matches) == 1 and all(
93 matches[0].get(key) == value for key, value in expected.items()
94 )
95
96
97def _role_errors(source: str, template: str) -> list[str]:
98 """Return structural Ansible install/sudo/policy errors."""
99 tasks, errors = _copy_tasks(source)
100 if errors:
101 return errors
102 helper = {
103 "src": "ra8-hil-privileged.py",
104 "dest": "/usr/local/libexec/ra8-hil-privileged",
105 "owner": "root",
106 "group": "root",
107 "mode": "0755",
108 }
109 if not _exact_task(tasks, "ansible.builtin.copy", str(helper["dest"]), helper):
110 errors.append("hil_runner.yml: root helper copy boundary is not exact")
111 policy = {
112 "src": "ra8-hil-privileged-policy.json.j2",
113 "dest": "/etc/ra8-hil-privileged-policy.json",
114 "owner": "root",
115 "group": "root",
116 "mode": "0644",
117 "validate": "/usr/bin/python3 -m json.tool %s",
118 }
119 if not _exact_task(tasks, "ansible.builtin.template", str(policy["dest"]), policy):
120 errors.append("hil_runner.yml: root policy template boundary is not exact")
121 errors.extend(_sudoers_errors(tasks))
122 if template != EXACT_POLICY_TEMPLATE:
123 errors.append("HIL helper policy template is not the exact fleet-derived document")
124 return errors
125
126
127def _sudoers_errors(tasks: list[dict[str, object]]) -> list[str]:
128 """Require one exact sudo executable and no wildcarded privileged tool."""
129 copies = _module_tasks(tasks, "ansible.builtin.copy")
130 matches = [copy for copy in copies if copy.get("dest") == "/etc/sudoers.d/ra8-hil"]
131 if len(matches) != 1:
132 return ["hil_runner.yml: exact HIL sudoers file is missing or duplicated"]
133 copy = matches[0]
134 expected = {
135 "owner": "root",
136 "group": "root",
137 "mode": "0440",
138 "validate": "/usr/sbin/visudo -cf %s",
139 }
140 errors = []
141 if any(copy.get(key) != value for key, value in expected.items()):
142 errors.append("hil_runner.yml: sudoers ownership/mode/validation is not exact")
143 content = copy.get("content")
144 line = (
145 "{{ dev_box_hil_runner_bench_user }} ALL=(root) NOPASSWD: "
146 "/usr/local/libexec/ra8-hil-privileged"
147 )
148 lines = (
149 [item.strip() for item in content.splitlines() if item.strip()]
150 if isinstance(content, str)
151 else []
152 )
153 if lines != [line]:
154 errors.append("hil_runner.yml: sudoers must grant only the fixed helper executable")
155 return errors
156
157
158def _call_name(call: ast.Call) -> str:
159 """Return one static call target, including a one-level receiver."""
160 if isinstance(call.func, ast.Name):
161 return call.func.id
162 if isinstance(call.func, ast.Attribute) and isinstance(call.func.value, ast.Name):
163 return f"{call.func.value.id}.{call.func.attr}"
164 return ""
165
166
167def _calls(node: ast.AST) -> set[str]:
168 """Return every statically named call below an AST node."""
169 return {_call_name(item) for item in ast.walk(node) if isinstance(item, ast.Call)}
170
171
172def _definitions(tree: ast.Module) -> tuple[dict[str, ast.FunctionDef], list[str]]:
173 """Return unique synchronous top-level definitions and definition errors."""
174 sync: dict[str, ast.FunctionDef] = {}
175 errors = []
176 names: list[str] = []
177 for node in tree.body:
178 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
179 names.append(node.name)
180 if isinstance(node, ast.AsyncFunctionDef):
181 errors.append(f"privileged helper: {node.name} must be synchronous")
182 else:
183 sync[node.name] = node
184 duplicates = sorted({name for name in names if names.count(name) > 1})
185 if duplicates:
186 errors.append(f"privileged helper: duplicate definitions: {', '.join(duplicates)}")
187 return sync, errors
188
189
190def _argv_zero(node: ast.AST) -> bool:
191 """Return whether a node is exactly argv[0]."""
192 return (
193 isinstance(node, ast.Subscript)
194 and isinstance(node.value, ast.Name)
195 and node.value.id == "argv"
196 and isinstance(node.slice, ast.Constant)
197 and node.slice.value == 0
198 )
199
200
201def _allow_guard(node: ast.AST) -> bool:
202 """Return whether a test is exactly argv[0] not in allowed."""
203 if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
204 return any(_allow_guard(value) for value in node.values)
205 return (
206 isinstance(node, ast.Compare)
207 and _argv_zero(node.left)
208 and len(node.ops) == 1
209 and isinstance(node.ops[0], ast.NotIn)
210 and len(node.comparators) == 1
211 and isinstance(node.comparators[0], ast.Name)
212 and node.comparators[0].id == "allowed"
213 )
214
215
216def _direct_fail(node: ast.If) -> bool:
217 """Return whether the guard directly and unconditionally rejects."""
218 if len(node.body) != 1 or node.orelse:
219 return False
220 statement = node.body[0]
221 return (
222 isinstance(statement, ast.Expr)
223 and isinstance(statement.value, ast.Call)
224 and _call_name(statement.value) == "_fail"
225 )
226
227
228def _run_boundary_errors(function: ast.FunctionDef) -> list[str]:
229 """Prove the executable allowlist dominates the one spawn call."""
230 assignments = [
231 node
232 for node in function.body
233 if isinstance(node, ast.Assign)
234 and any(isinstance(target, ast.Name) and target.id == "allowed" for target in node.targets)
235 ]
236 exact_sets = [
237 {item.value for item in node.value.elts}
238 for node in assignments
239 if isinstance(node.value, ast.Set)
240 ]
241 guards = [
242 node for node in function.body if isinstance(node, ast.If) and _allow_guard(node.test)
243 ]
244 spawns = [
245 node
246 for node in ast.walk(function)
247 if isinstance(node, ast.Call) and _call_name(node) == "os.posix_spawn"
248 ]
249 errors = []
250 if exact_sets != [{"/usr/sbin/ip", "/usr/sbin/uhubctl"}]:
251 errors.append("privileged helper: executable allowlist assignment is not exact")
252 if len(guards) != 1 or not _direct_fail(guards[0]):
253 errors.append("privileged helper: executable rejection guard is not active")
254 if len(spawns) != 1 or len(spawns[0].args) < SPAWN_ARGC or not _argv_zero(spawns[0].args[0]):
255 errors.append("privileged helper: spawn executable is not guarded argv[0]")
256 elif not isinstance(spawns[0].args[1], ast.Name) or spawns[0].args[1].id != "argv":
257 errors.append("privileged helper: spawn does not receive the validated argv")
258 if guards and spawns and cast(int, guards[0].end_lineno) >= spawns[0].lineno:
259 errors.append("privileged helper: executable guard does not dominate spawn")
260 return errors
261
262
263def _call_requirements() -> dict[str, set[str]]:
264 """Return the live call graph required at each privileged boundary."""
265 return {
266 "_mutate": {
267 "fcntl.flock",
268 "_recover_restore",
269 "_perform_cycle",
270 "_network_prepare",
271 "_network_cleanup",
272 },
273 "_perform_cycle": {
274 "ops.save",
275 "ops.apply",
276 "ops.pause",
277 "ops.clear",
278 "_restore_action",
279 },
280 "_recover_restore": {
281 "_load_restore",
282 "ops.apply",
283 "ops.clear",
284 "_restore_action",
285 },
286 "_network_prepare": {
287 "_validate_live_iface",
288 "_save_state",
289 "_run",
290 "_address_present",
291 "_link_is_up",
292 "_route_present",
293 },
294 "_network_cleanup": {
295 "_load_state",
296 "_authenticate_cleanup_iface",
297 "_cleanup_route",
298 "_cleanup_link",
299 "_cleanup_address",
300 },
301 "_network_neigh_flush": {
302 "_load_state",
303 "_authenticate_cleanup_iface",
304 "_run",
305 },
306 "_cleanup_route": {"_run", "_route_present", "_checkpoint_absent"},
307 "_cleanup_link": {"_run", "_link_is_up", "_checkpoint_absent"},
308 "_cleanup_address": {"_run", "_address_present", "_checkpoint_absent"},
309 "_validate_live_iface": {"_authenticate_live_iface"},
310 "_authenticate_cleanup_iface": {"_authenticate_live_iface"},
311 "_authenticate_live_iface": {
312 "_live_physical_identity",
313 "_iface_facts",
314 "_validate_iface_facts",
315 },
316 "_usb_authorize": {"_resolve_usb_device", "os.open", "os.fstat"},
317 }
318
319
320def _required_calls(functions: dict[str, ast.FunctionDef]) -> list[str]:
321 """Prove privileged transactions remain reachable and checkpointed."""
322 errors = []
323 for name, expected in _call_requirements().items():
324 missing = expected - _calls(functions[name])
325 if missing:
326 errors.append(f"privileged helper: {name} omits live calls {sorted(missing)}")
327 errors.extend(_live_power_dispatch_errors(functions["_mutate"]))
328 return errors
329
330
331def _live_power_dispatch_errors(function: ast.FunctionDef) -> list[str]:
332 """Prove the fixed argv builder is nested in the live execution call."""
333 live_power = any(
334 isinstance(call, ast.Call)
335 and _call_name(call) == "_run"
336 and any(
337 isinstance(argument, ast.Call) and _call_name(argument) == "_usb_power_command"
338 for argument in call.args
339 )
340 for call in ast.walk(function)
341 )
342 if not live_power:
343 return ["privileged helper: persistent USB power dispatch is a no-op"]
344 return []
345
346
347def _live_cycle_errors(tree: ast.Module) -> list[str]:
348 """Prove the production cycle backend reaches fixed live boundaries."""
349 methods = [
350 node
351 for parent in tree.body
352 if isinstance(parent, ast.ClassDef) and parent.name == "_LiveCycleOps"
353 for node in parent.body
354 if isinstance(node, ast.FunctionDef) and node.name == "apply"
355 ]
356 required = {"_usb_power_command", "_run", "_usb_authorize"}
357 if len(methods) != 1 or not required.issubset(_calls(methods[0])):
358 return ["privileged helper: live cycle backend does not reach fixed boundaries"]
359 return []
360
361
362def _has_nofollow_open(function: ast.FunctionDef) -> bool:
363 """Return whether one real os.open call receives O_NOFOLLOW."""
364 nofollow_names = {
365 target.id
366 for assignment in function.body
367 if isinstance(assignment, ast.Assign)
368 for target in assignment.targets
369 if isinstance(target, ast.Name)
370 and any(
371 isinstance(item, ast.Attribute)
372 and isinstance(item.value, ast.Name)
373 and item.value.id == "os"
374 and item.attr == "O_NOFOLLOW"
375 for item in ast.walk(assignment.value)
376 )
377 }
378 for call in ast.walk(function):
379 if not isinstance(call, ast.Call) or _call_name(call) != "os.open":
380 continue
381 attributes = {
382 item.attr
383 for arg in call.args
384 for item in ast.walk(arg)
385 if isinstance(item, ast.Attribute)
386 and isinstance(item.value, ast.Name)
387 and item.value.id == "os"
388 }
389 named_flags = {arg.id for arg in call.args if isinstance(arg, ast.Name)}
390 if "O_NOFOLLOW" in attributes or nofollow_names & named_flags:
391 return True
392 return False
393
394
395def _fixed_power_argv(function: ast.FunctionDef, kind: str) -> tuple[str, ...] | None:
396 """Return the direct argv for one live kind-guarded builder branch."""
397 matches: list[ast.If] = []
398 for statement in function.body:
399 if not isinstance(statement, ast.If):
400 continue
401 constants = {
402 node.value
403 for node in ast.walk(statement.test)
404 if isinstance(node, ast.Constant) and isinstance(node.value, str)
405 }
406 if kind in constants:
407 matches.append(statement)
408 if len(matches) != 1:
409 return None
410 returns = [statement for statement in matches[0].body if isinstance(statement, ast.Return)]
411 if len(returns) != 1 or not isinstance(returns[0].value, ast.List):
412 return None
413 values = []
414 for element in returns[0].value.elts:
415 if isinstance(element, ast.Constant) and isinstance(element.value, str):
416 values.append(element.value)
417 elif isinstance(element, ast.Name) and element.id in {"port", "action"}:
418 values.append(f"${element.id}")
419 else:
420 return None
421 return tuple(values)
422
423
424def _topology_errors(functions: dict[str, ast.FunctionDef]) -> list[str]:
425 """Prove topology/policy operands occur in live enforcing functions."""
426 errors = []
427 expected_power = {
428 "usb-port-power": (
429 "/usr/sbin/uhubctl",
430 "-S",
431 "-l",
432 "2-1.3",
433 "-p",
434 "$port",
435 "-a",
436 "$action",
437 ),
438 "usb-root-power": (
439 "/usr/sbin/uhubctl",
440 "-S",
441 "-l",
442 "2-1",
443 "-a",
444 "$action",
445 ),
446 }
447 actual_power = {
448 kind: _fixed_power_argv(functions["_usb_power_command"], kind) for kind in expected_power
449 }
450 if actual_power != expected_power:
451 errors.append("privileged helper: live USB argv builder lost fixed topology")
452 iface_strings = {
453 node.value
454 for node in ast.walk(functions["_iface_facts"])
455 if isinstance(node, ast.Constant) and isinstance(node.value, str)
456 }
457 if not {"-4", "-6", "table", "all", "default"}.issubset(iface_strings):
458 errors.append("privileged helper: all-table IPv4/IPv6 uplink census is incomplete")
459 nofollow = (
460 "_load_policy",
461 "_load_state",
462 "_load_restore",
463 "_save_state",
464 "_save_restore",
465 "_usb_authorize",
466 )
467 missing = [name for name in nofollow if not _has_nofollow_open(functions[name])]
468 if missing:
469 errors.append(f"privileged helper: live no-follow open missing in {missing}")
470 if not {"_canonical_policy", "secrets.compare_digest"}.issubset(
471 _calls(functions["_strict_policy"])
472 ):
473 errors.append("privileged helper: installed policy is not bound to its declaration digest")
474 return errors
475
476
477def _helper_errors(source: str) -> list[str]:
478 """Return definition, execution, call-graph, and topology errors."""
479 try:
480 tree = ast.parse(source)
481 except SyntaxError:
482 return ["privileged helper: Python syntax is malformed"]
483 functions, errors = _definitions(tree)
484 required = {
485 "_run",
486 "_mutate",
487 "_perform_cycle",
488 "_recover_restore",
489 "_usb_power_command",
490 "_usb_authorize",
491 "_resolve_usb_device",
492 "_strict_policy",
493 "_load_policy",
494 "_validate_iface_facts",
495 "_iface_facts",
496 "_authenticate_live_iface",
497 "_validate_live_iface",
498 "_authenticate_cleanup_iface",
499 "_strict_state",
500 "_save_state",
501 "_load_state",
502 "_network_prepare",
503 "_network_cleanup",
504 "_network_neigh_flush",
505 "_cleanup_route",
506 "_cleanup_link",
507 "_cleanup_address",
508 "_save_restore",
509 "_load_restore",
510 }
511 missing = sorted(required - functions.keys())
512 if missing:
513 message = f"privileged helper: synchronous functions missing: {', '.join(missing)}"
514 return [*errors, message]
515 forbidden = {"eval", "exec", "os.system", "subprocess.run", "subprocess.Popen"}
516 bad = sorted(forbidden & _calls(tree))
517 if bad:
518 errors.append(f"privileged helper: forbidden calls: {', '.join(bad)}")
519 return (
520 errors
521 + _run_boundary_errors(functions["_run"])
522 + _required_calls(functions)
523 + _topology_errors(functions)
524 + _live_cycle_errors(tree)
525 )
526
527
528def _active_shell(source: str) -> str:
529 """Return executable-looking shell lines with comments removed."""
530 return "\n".join(
531 line.strip()
532 for line in source.splitlines()
533 if line.strip() and not line.lstrip().startswith("#")
534 )
535
536
537def _caller_errors(callers: dict[str, str]) -> list[str]:
538 """Require live identity invocations, transactions, and no sudo bypass."""
539 active = {path: _active_shell(source) for path, source in callers.items()}
540 required = {
541 "scripts/hil/ppps.sh": (
542 r'^ra8_hil_privileged_verify_remote "\$PI_HOST" \|\| exit \$\?$',
543 r"usb-port-cycle",
544 r"usb-authorize-cycle",
545 ),
546 "scripts/hil/flash_retry.sh": (
547 r'^ra8_hil_privileged_verify_remote "\$PI_HOST" \|\| exit \$\?$',
548 r"usb-root-cycle",
549 ),
550 "scripts/hil/exit_low_power.sh": (
551 r"^ra8_hil_privileged_verify_local \|\| exit \$\?$",
552 r"usb-root-cycle",
553 ),
554 "scripts/hil/eth_tcp.sh": (
555 r"actual_identity=.*--identity",
556 r"net-prepare \"\$BOARD_IP\"",
557 r"--policy-interface",
558 ),
559 }
560 errors = []
561 for path, patterns in required.items():
562 errors.extend(
563 f"{path}: active exact helper/identity invocation missing"
564 for pattern in patterns
565 if re.search(pattern, active[path], re.MULTILINE) is None
566 )
567 bypass = re.compile(
568 r"sudo\s+-n(?:\s+--)?\s+(?:/usr/sbin/)?(?:ip|uhubctl|tcpdump|timeout)\b"
569 r"|sudo\s+-n(?:\s+--)?\s+(?:/usr/bin/)?tee\b"
570 )
571 for path, source in active.items():
572 if bypass.search(source):
573 errors.append(f"{path}: direct privileged executable bypasses the fixed helper")
574 if "usb-root-power off" in active["scripts/hil/flash_retry.sh"]:
575 errors.append("flash retry must use one transactional root-cycle operation")
576 if "usb-root-power off" in active["scripts/hil/exit_low_power.sh"]:
577 errors.append("low-power recovery must use one transactional root-cycle operation")
578 return errors
579
580
581def _fleet_policy(fleet_source: str) -> tuple[dict[str, object] | None, list[str]]:
582 """Read the sole fleet-owned board-interface declaration."""
583 try:
584 fleet = yaml.safe_load(fleet_source)
585 interface = fleet["hosts"]["star"]["board_interface"]
586 except (yaml.YAMLError, KeyError, TypeError):
587 return None, ["infra/fleet.yml: star board_interface declaration is missing"]
588 keys = {"name", "mac", "sysfs_device", "phc_index"}
589 if not isinstance(interface, dict) or set(interface) != keys:
590 return None, ["infra/fleet.yml: star board_interface schema is not exact"]
591 policy = {
592 "board_iface": interface["name"],
593 "mac": interface["mac"],
594 "phc_index": interface["phc_index"],
595 "sysfs_device": interface["sysfs_device"],
596 "version": 1,
597 }
598 return policy, []
599
600
601def _identity_errors(helper: bytes, manifest: str, fleet: str) -> list[str]:
602 """Bind caller identity independently to helper bytes and fleet policy."""
603 policy, errors = _fleet_policy(fleet)
604 if errors or policy is None:
605 return errors
606 payload = (json.dumps(policy, sort_keys=True, separators=(",", ":")) + "\n").encode()
607 expected = f"{hashlib.sha256(helper).hexdigest()}:{hashlib.sha256(payload).hexdigest()}"
608 if re.fullmatch(r"[0-9a-f]{64}:[0-9a-f]{64}\n?", manifest) is None:
609 return ["privileged helper identity manifest is malformed"]
610 return [] if manifest.strip() == expected else ["privileged helper or policy identity is stale"]
611
612
613def _strings(value: object) -> list[str]:
614 """Flatten scalar strings in parsed YAML."""
615 if isinstance(value, str):
616 return [value]
617 if isinstance(value, list):
618 return [item for child in value for item in _strings(child)]
619 if isinstance(value, dict):
620 return [item for child in value.values() for item in _strings(child)]
621 return []
622
623
624def _workflow_errors(source: str) -> list[str]:
625 """Require trigger coverage while forbidding provisioning mutation."""
626 try:
627 document = yaml.safe_load(source)
628 except yaml.YAMLError:
629 return ["hil.yml: workflow YAML is malformed"]
630 triggers = document.get("on", document.get(True)) if isinstance(document, dict) else None
631 if not isinstance(document, dict) or not isinstance(triggers, dict):
632 return ["hil.yml: workflow triggers are malformed"]
633 errors = []
634 for event in ("push", "pull_request"):
635 config = triggers.get(event)
636 paths = config.get("paths") if isinstance(config, dict) else None
637 if not isinstance(paths, list) or any(path not in paths for path in WORKFLOW_PATHS):
638 errors.append(f"hil.yml: trusted policy paths missing from {event}")
639 forbidden = ("ansible-playbook", "just infra::apply", "infra::apply")
640 values = _strings(document.get("jobs", {}))
641 errors.extend(
642 f"hil.yml: workflow must not auto-apply trusted provisioning: {token}"
643 for token in forbidden
644 if any(token in value for value in values)
645 )
646 return errors
647
648
649def _scan(inputs: dict[str, object]) -> list[str]:
650 """Return every privilege-boundary structural error."""
651 return (
652 _role_errors(cast(str, inputs["role"]), cast(str, inputs["template"]))
653 + _helper_errors(cast(str, inputs["helper"]))
654 + _identity_errors(
655 cast(bytes, inputs["helper_bytes"]),
656 cast(str, inputs["manifest"]),
657 cast(str, inputs["fleet"]),
658 )
659 + _caller_errors(cast(dict[str, str], inputs["callers"]))
660 + _workflow_errors(cast(str, inputs["workflow"]))
661 )
662
663
664def _repo_inputs(root: Path) -> dict[str, object]:
665 """Load the exact governed repository files."""
666 helper = root / HELPER_REL
667 return {
668 "role": (root / ROLE_REL).read_text(encoding="utf-8"),
669 "template": (root / POLICY_TEMPLATE_REL).read_text(encoding="utf-8"),
670 "helper": helper.read_text(encoding="utf-8"),
671 "helper_bytes": helper.read_bytes(),
672 "manifest": (root / MANIFEST_REL).read_text(encoding="ascii"),
673 "fleet": (root / "infra/fleet.yml").read_text(encoding="utf-8"),
674 "callers": {path: (root / path).read_text(encoding="utf-8") for path in CALLER_PATHS},
675 "workflow": (root / ".github/workflows/hil.yml").read_text(encoding="utf-8"),
676 }
677
678
679def _refresh_helper_identity(inputs: dict[str, object]) -> None:
680 """Refresh only the helper half after an in-memory helper mutation."""
681 manifest = cast(str, inputs["manifest"]).strip().split(":")
682 inputs["helper_bytes"] = cast(str, inputs["helper"]).encode()
683 helper_sha = hashlib.sha256(cast(bytes, inputs["helper_bytes"])).hexdigest()
684 inputs["manifest"] = f"{helper_sha}:{manifest[1]}\n"
685
686
687def _mutated(inputs: dict[str, object], key: str, old: str, new: str) -> dict[str, object]:
688 """Return one exact single-replacement mutation."""
689 result = dict(inputs)
690 source = cast(str, inputs[key])
691 if source.count(old) != 1:
692 message = f"selftest fixture for {key} is not unique: {old!r}"
693 raise RuntimeError(message)
694 result[key] = source.replace(old, new)
695 if key == "helper":
696 _refresh_helper_identity(result)
697 return result
698
699
700def _helper_mutations() -> list[tuple[str, str, str, str]]:
701 """Return executable helper mutations found by adversarial review."""
702 return [
703 (
704 "async execution boundary",
705 "helper",
706 "def _run(argv: list[str]",
707 "async def _run(argv: list[str]",
708 ),
709 (
710 "dead allowlist guard",
711 "helper",
712 "if (\n not argv",
713 "if False and (\n not argv",
714 ),
715 (
716 "dead nested rejection",
717 "helper",
718 ' _fail("child command is outside the fixed executable boundary")',
719 (
720 " if False:\n"
721 ' _fail("child command is outside the fixed executable boundary")'
722 ),
723 ),
724 (
725 "no-op mutation dispatch",
726 "helper",
727 "_run(_usb_power_command(command, args))",
728 "_usb_power_command(command, args)",
729 ),
730 ]
731
732
733def _helper_policy_mutations() -> list[tuple[str, str, str, str]]:
734 """Return network-authentication and topology mutations."""
735 return [
736 (
737 "cleanup physical-auth bypass",
738 "helper",
739 (
740 " _authenticate_cleanup_iface(policy, state)\n"
741 " _cleanup_route(path, state, policy)"
742 ),
743 " _cleanup_route(path, state, policy)",
744 ),
745 (
746 "neighbour physical-auth bypass",
747 "helper",
748 (
749 " _authenticate_cleanup_iface(policy, state)\n"
750 ' _run(["/usr/sbin/ip", "neigh", "flush", "dev", str(state["iface"])])'
751 ),
752 ' _run(["/usr/sbin/ip", "neigh", "flush", "dev", str(state["iface"])])',
753 ),
754 (
755 "dead topology constant",
756 "helper",
757 ' return ["/usr/sbin/uhubctl", "-S", "-l", "2-1.3", "-p", port, "-a", action]',
758 (
759 ' dead_topology = "2-1.3"\n'
760 ' return ["/usr/sbin/uhubctl", "-S", "-l", "9-9", '
761 '"-p", port, "-a", action]'
762 ),
763 ),
764 (
765 "dead no-follow token",
766 "helper",
767 LOAD_POLICY_NOFOLLOW,
768 LOAD_POLICY_DEAD_NOFOLLOW,
769 ),
770 ]
771
772
773def _configuration_mutations() -> list[tuple[str, str, str, str]]:
774 """Return trusted provisioning and workflow mutations."""
775 return [
776 ("policy template drift", "template", ' "version": 1', ' "version": 2'),
777 (
778 "workflow auto-apply",
779 "workflow",
780 "just quality::local::gate hil-all",
781 "just quality::local::gate hil-all\n ansible-playbook live.yml",
782 ),
783 ]
784
785
786def _selftest_cases(inputs: dict[str, object]) -> list[tuple[str, bool]]:
787 """Apply independent reviewer mutations that every checker must catch."""
788 cases = [("complete boundary stays quiet", not _scan(inputs))]
789 mutations = _helper_mutations() + _helper_policy_mutations() + _configuration_mutations()
790 for label, key, old, new in mutations:
791 cases.append((f"{label} fires", bool(_scan(_mutated(inputs, key, old, new)))))
792 changed = dict(inputs)
793 callers = dict(cast(dict[str, str], inputs["callers"]))
794 line = 'ra8_hil_privileged_verify_remote "$PI_HOST" || exit $?'
795 callers["scripts/hil/flash_retry.sh"] = callers["scripts/hil/flash_retry.sh"].replace(
796 line, f"# {line}"
797 )
798 changed["callers"] = callers
799 cases.append(("commented identity invocation fires", bool(_scan(changed))))
800 cases.extend(_stale_identity_cases(inputs))
801 return cases
802
803
804def _stale_identity_cases(inputs: dict[str, object]) -> list[tuple[str, bool]]:
805 """Prove helper and fleet-policy drift fail independently."""
806 first, second = cast(str, inputs["manifest"]).strip().split(":")
807 stale_helper = dict(inputs)
808 stale_helper["manifest"] = f"{'0' * 64}:{second}\n"
809 stale_policy = dict(inputs)
810 stale_policy["manifest"] = f"{first}:{'0' * 64}\n"
811 return [
812 ("stale helper identity fires independently", bool(_scan(stale_helper))),
813 ("stale policy identity fires independently", bool(_scan(stale_policy))),
814 ]
815
816
817def run_selftest() -> int:
818 """Run quiet and must-fire mutations against the live governed shape."""
819 cases = _selftest_cases(_repo_inputs(REPO_ROOT))
820 for label, passed in cases:
821 print(f" [{'PASS' if passed else 'FAIL'}] {label}")
822 ok = all(passed for _, passed in cases)
823 print(f"check_hil_privilege_boundary.py --selftest: {'PASS' if ok else 'FAIL'}")
824 return 0 if ok else 1
825
826
827def _parser() -> argparse.ArgumentParser:
828 """Build the strict CLI."""
829 parser = argparse.ArgumentParser(description=__doc__)
830 parser.add_argument("--selftest", action="store_true")
831 return parser
832
833
834def main(argv: list[str] | None = None) -> int:
835 """Run the checker or its mutation selftest."""
836 args = _parser().parse_args(argv)
837 if args.selftest:
838 return run_selftest()
839 errors = _scan(_repo_inputs(REPO_ROOT))
840 for error in errors:
841 print(error, file=sys.stderr)
842 if errors:
843 print(f"check_hil_privilege_boundary.py: {len(errors)} error(s)", file=sys.stderr)
844 return 1
845 print("check_hil_privilege_boundary.py: PASS")
846 return 0
847
848
849if __name__ == "__main__":
850 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298