ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_shebangs.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 exhaustive typed first-party shell entry-point authority.
5
6NUL cleanup rejects forged input; paths/symlinks fail; same-UID writes are out of scope.
7"""
8
9from __future__ import annotations
10
11import dataclasses
12import json
13import os
14import shutil
15import stat
16import subprocess
17import sys
18import tempfile
19from pathlib import Path
20
21sys.path.insert(0, str(Path(__file__).resolve().parent))
22
23from lint_targets import files_for
24from privileged_startup_runtime_selftest import (
25 PrivateRun,
26 WrapperVariant,
27 run_private,
28 run_privileged_wrapper_runtime_cases,
29)
30from shell_entrypoint_policy import (
31 PORTABLE_SH_SHEBANG,
32 PORTABLE_SHEBANG,
33 PRIVILEGED_PATHS,
34 PRIVILEGED_REASON,
35 PRIVILEGED_SHEBANG,
36 SHELL_POLICIES,
37 ShellDialect,
38 ShellPolicy,
39 ShellSecurity,
40 ShellUsage,
41 merge_policy_tables,
42)
43from shell_entrypoint_policy_ci import CI_POLICY_ROWS
44from shell_entrypoint_policy_hil import HIL_POLICY_ROWS
45
46REPO_ROOT = Path(__file__).resolve().parents[2]
47EXIT_OK = 0
48EXIT_FAIL = 1
49EXIT_CONFIG = 2
50EARLY_EXIT_STATUS = 43
51
52PRIVILEGED_BODY_OPEN = 'if [[ "$-" == *p* ]]; then'
53FAILED_CLEANUP_EXEC = "_ra8_startup_refuse 'could not enter sanitized process'"
54PRIVILEGED_BODY_PREFIX = (
55 PRIVILEGED_BODY_OPEN,
56 "unset -v BASH_ENV ENV",
57 "declare -a ra8_startup_env_unset=()",
58 "_ra8_startup_refuse() {",
59 " printf 'error: privileged startup %s\\n' \"$1\" >&2",
60 " exit 1",
61 "}",
62 "ra8_startup_env_done_count=0",
63 "while IFS= read -r -d '' ra8_startup_env_row; do",
64 ' ra8_startup_env_name="${ra8_startup_env_row%%=*}"',
65 ' case "$ra8_startup_env_name" in',
66 " RA8_STARTUP_ENV_DONE)",
67 " ra8_startup_env_done_count=$((ra8_startup_env_done_count + 1))",
68 " ;;",
69 # Current Bash uses the %% suffix; patched Bash 3.2 can use ().
70 " BASH_FUNC_*%% | BASH_FUNC_*'()') ra8_startup_env_unset+=(-u \"$ra8_startup_env_name\") ;;",
71 " esac",
72 "done < <(",
73 " /usr/bin/env -u RA8_STARTUP_ENV_DONE -0 &&",
74 " /usr/bin/printf 'RA8_STARTUP_ENV_DONE=1\\0'",
75 ")",
76 (
77 "((ra8_startup_env_done_count == 1)) && "
78 '[[ "$ra8_startup_env_name" == RA8_STARTUP_ENV_DONE ]] || '
79 "_ra8_startup_refuse 'environment enumeration was incomplete'"
80 ),
81 "if ((${#ra8_startup_env_unset[@]})); then",
82 " [[ -z \"${RA8_STARTUP_ENV_SCRUBBED-}\" ]] || _ra8_startup_refuse 'scrub did not converge'",
83 ' ra8_startup_reentry="$0"',
84 " [[ \"$ra8_startup_reentry\" == */* ]] || _ra8_startup_refuse 'requires a script path'",
85 ' if [[ "$ra8_startup_reentry" != /* ]]; then',
86 ' ra8_startup_reentry="$PWD/$ra8_startup_reentry"',
87 " fi",
88 ' ra8_startup_check="$ra8_startup_reentry"',
89 ' while [[ "$ra8_startup_check" != "/" ]]; do',
90 " [[ ! -L \"$ra8_startup_check\" ]] || _ra8_startup_refuse 'refuses a symlinked path'",
91 ' ra8_startup_parent="${ra8_startup_check%/*}"',
92 ' [[ -n "$ra8_startup_parent" ]] || ra8_startup_parent="/"',
93 ' [[ "$ra8_startup_parent" != "$ra8_startup_check" ]] ||',
94 " _ra8_startup_refuse 'cannot validate its script path'",
95 ' ra8_startup_check="$ra8_startup_parent"',
96 " done",
97 " [[ -f \"$ra8_startup_reentry\" ]] || _ra8_startup_refuse 'refuses a non-regular path'",
98 ' if ! exec /usr/bin/env "${ra8_startup_env_unset[@]}" -u BASH_ENV -u ENV \\',
99 " -u RA8_STARTUP_ENV_DONE RA8_STARTUP_ENV_SCRUBBED=1 \\",
100 ' /bin/bash -p -- "$ra8_startup_reentry" "$@"; then',
101 f" {FAILED_CLEANUP_EXEC}",
102 " fi",
103 "fi",
104 "unset -v ra8_startup_check ra8_startup_env_done_count",
105 "unset -v ra8_startup_env_name ra8_startup_env_row",
106 "unset -v ra8_startup_env_unset ra8_startup_parent ra8_startup_reentry",
107 "unset -v RA8_STARTUP_ENV_DONE",
108 "unset -v RA8_STARTUP_ENV_SCRUBBED",
109 "unset -f _ra8_startup_refuse",
110)
111PRIVILEGED_DUAL_BODY_PREFIX = (
112 *PRIVILEGED_BODY_PREFIX[
113 : PRIVILEGED_BODY_PREFIX.index("if ((${#ra8_startup_env_unset[@]})); then") + 1
114 ],
115 ' if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then',
116 " printf 'error: sourced privileged entry refuses inherited Bash functions\\n' >&2",
117 " unset -v ra8_startup_env_done_count",
118 " unset -v ra8_startup_env_name ra8_startup_env_row ra8_startup_env_unset",
119 " unset -v RA8_STARTUP_ENV_DONE RA8_STARTUP_ENV_SCRUBBED",
120 " unset -f _ra8_startup_refuse",
121 " return 2",
122 " fi",
123 *PRIVILEGED_BODY_PREFIX[
124 PRIVILEGED_BODY_PREFIX.index("if ((${#ra8_startup_env_unset[@]})); then") + 1 :
125 ],
126)
127PRIVILEGED_RIG_BODY_PREFIX = tuple(
128 " printf 'error: sourced rig contract refuses inherited Bash functions\\n' >&2"
129 if line
130 == " printf 'error: sourced privileged entry refuses inherited Bash functions\\n' >&2"
131 else line
132 for line in PRIVILEGED_DUAL_BODY_PREFIX
133)
134PRIVILEGED_BODY_CLOSE = ("else", '[[ "$-" == *p* ]]', "fi")
135PRIVILEGED_RUNTIME_VARIANTS = tuple(
136 WrapperVariant(name, prefix, PRIVILEGED_BODY_CLOSE)
137 for name, prefix in (
138 ("plain", PRIVILEGED_BODY_PREFIX),
139 ("dual", PRIVILEGED_DUAL_BODY_PREFIX),
140 ("rig", PRIVILEGED_RIG_BODY_PREFIX),
141 )
142)
143FORBIDDEN_REEXEC_TOKENS = (
144 "builtin unset BASH_ENV",
145 "builtin exec /bin/bash",
146 "command builtin unset BASH_ENV",
147 "command builtin exec /bin/bash",
148)
149PINNED_INTERPRETER_BOUNDARIES = {
150 **dict.fromkeys(
151 PRIVILEGED_PATHS,
152 (
153 PRIVILEGED_SHEBANG,
154 "# SPDX-License-Identifier: MIT",
155 "# Copyright (c) 2026 Brighton Sikarskie",
156 PRIVILEGED_REASON,
157 ),
158 ),
159}
160
161
162def _shell_census() -> tuple[str, ...]:
163 """Return the canonical tracked-and-untracked first-party shell census."""
164 return tuple(files_for(("shell",))["shell"])
165
166
167def _is_executable(path: Path) -> bool:
168 """Return whether any executable mode bit is set."""
169 return bool(path.stat().st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
170
171
172def _header(path: Path) -> tuple[str, ...]:
173 """Return the four-line combined preamble, or an empty tuple when unreadable."""
174 try:
175 return tuple(path.read_text(encoding="utf-8").splitlines()[:4])
176 except (OSError, UnicodeError):
177 return ()
178
179
180def _header_matches(header: tuple[str, ...], expected: tuple[str, ...]) -> bool:
181 """Return whether ``header`` starts with the complete expected preamble."""
182 return header[: len(expected)] == expected
183
184
185def _policy_findings(
186 census: set[str],
187 policies: dict[str, ShellPolicy],
188) -> list[str]:
189 """Return missing and stale typed-authority entries."""
190 missing = sorted(census - policies.keys())
191 findings = [f"unclassified shell entry point: {path}" for path in missing]
192 findings.extend(
193 f"stale shell entry-point authority: {path}" for path in sorted(policies.keys() - census)
194 )
195 for path, policy in sorted(policies.items()):
196 if policy.usage is ShellUsage.SOURCED_ONLY and policy.executable:
197 findings.append(f"{path}: sourced-only policy cannot be executable")
198 if policy.source_requires_privileged_parent and policy.usage is ShellUsage.ENTRY:
199 findings.append(f"{path}: entry-only policy cannot require a privileged source parent")
200 if (
201 policy.security is ShellSecurity.PRIVILEGED
202 and policy.usage is not ShellUsage.ENTRY
203 and not policy.source_requires_privileged_parent
204 ):
205 findings.append(f"{path}: privileged sourced usage requires a privileged parent")
206 if policy.security is ShellSecurity.PRIVILEGED and policy.dialect is not ShellDialect.BASH:
207 findings.append(f"{path}: privileged policy requires the Bash dialect")
208 return findings
209
210
211def _path_findings(rel: str, policy: ShellPolicy) -> list[str]:
212 """Validate one path's exact shebang, reason, and executable mode."""
213 path = REPO_ROOT / rel
214 header = _header(path)
215 findings: list[str] = []
216 if policy.security is ShellSecurity.PRIVILEGED:
217 expected_header = PINNED_INTERPRETER_BOUNDARIES[rel]
218 elif policy.dialect is ShellDialect.POSIX_SH:
219 expected_header = (PORTABLE_SH_SHEBANG,)
220 else:
221 expected_header = (PORTABLE_SHEBANG,)
222 if not _header_matches(header, expected_header):
223 findings.append(
224 f"{rel}: {policy.security.value}/{policy.usage.value} header "
225 f"must start with {expected_header!r}"
226 )
227 try:
228 executable = _is_executable(path)
229 except OSError as exc:
230 findings.append(f"{rel}: cannot inspect executable mode: {exc}")
231 else:
232 if executable != policy.executable:
233 findings.append(
234 f"{rel}: executable={executable} disagrees with typed authority "
235 f"executable={policy.executable}"
236 )
237 return findings
238
239
240def _active_lines(text: str) -> tuple[str, ...]:
241 """Return nonblank, noncomment physical lines stripped for guard checks."""
242 return tuple(
243 line.strip()
244 for line in text.splitlines()
245 if line.strip() and not line.lstrip().startswith("#")
246 )
247
248
249def _single_outer_if(text: str) -> bool:
250 """Return whether shfmt parses the whole program as one outer if."""
251 shfmt = shutil.which("shfmt", path="/usr/local/bin:/usr/bin:/opt/homebrew/bin:/opt/local/bin")
252 if shfmt is None:
253 return False
254 result = subprocess.run( # noqa: S603 - fixed absolute executable from a closed path.
255 [shfmt, "--to-json"],
256 input=text,
257 text=True,
258 capture_output=True,
259 check=False,
260 )
261 if result.returncode != 0:
262 return False
263 try:
264 tree = json.loads(result.stdout)
265 except json.JSONDecodeError:
266 return False
267 statements = tree.get("Stmts", [])
268 return len(statements) == 1 and statements[0].get("Cmd", {}).get("Type") == "IfClause"
269
270
271def privileged_body_findings(
272 rel: str,
273 text: str,
274 policy: ShellPolicy | None = None,
275) -> list[str]:
276 """Require the complete real body to live in the privileged branch."""
277 active = _active_lines(text)
278 findings: list[str] = []
279 if rel == "scripts/hil/lib/rig_contract.sh":
280 prefix = PRIVILEGED_RIG_BODY_PREFIX
281 elif policy is not None and policy.usage is ShellUsage.DUAL_USE:
282 prefix = PRIVILEGED_DUAL_BODY_PREFIX
283 else:
284 prefix = PRIVILEGED_BODY_PREFIX
285 if active[: len(prefix)] != tuple(line.strip() for line in prefix):
286 findings.append(
287 f"{rel}: wrapper and complete descendant startup cleanup are not first active code"
288 )
289 if active[-len(PRIVILEGED_BODY_CLOSE) :] != PRIVILEGED_BODY_CLOSE:
290 findings.append(f"{rel}: privileged-body wrapper does not close the entire real body")
291 if not _single_outer_if(text):
292 findings.append(f"{rel}: active code escapes the outer privileged branch")
293 if any(token in text for token in FORBIDDEN_REEXEC_TOKENS):
294 findings.append(f"{rel}: unsafe in-script sanitization/re-exec remains")
295 return findings
296
297
298def _requires_privileged_body(rel: str, text: str, policy: ShellPolicy) -> bool:
299 """Derive wrappers exhaustively from the typed privilege/usage authority."""
300 del rel, text
301 return (
302 policy.security is ShellSecurity.PRIVILEGED and policy.usage is not ShellUsage.SOURCED_ONLY
303 )
304
305
306def scan() -> tuple[list[str], int, int, int, int]:
307 """Return findings and typed population counts."""
308 census = set(_shell_census())
309 findings = _policy_findings(census, SHELL_POLICIES)
310 for rel in sorted(census & SHELL_POLICIES.keys()):
311 findings.extend(_path_findings(rel, SHELL_POLICIES[rel]))
312 guarded = 0
313 for rel in sorted(census & SHELL_POLICIES.keys()):
314 try:
315 text = (REPO_ROOT / rel).read_text(encoding="utf-8")
316 except (OSError, UnicodeError) as exc:
317 findings.append(f"{rel}: cannot inspect privileged body: {exc}")
318 continue
319 if _requires_privileged_body(rel, text, SHELL_POLICIES[rel]):
320 guarded += 1
321 findings.extend(privileged_body_findings(rel, text, SHELL_POLICIES[rel]))
322 privileged = sum(
323 policy.security is ShellSecurity.PRIVILEGED for policy in SHELL_POLICIES.values()
324 )
325 sourced = sum(policy.usage is ShellUsage.SOURCED_ONLY for policy in SHELL_POLICIES.values())
326 return findings, len(census), privileged, sourced, guarded
327
328
329def _fixture_text(
330 body: str = "printf 'BODY mode=%s\\n' \"$-\"",
331 *,
332 cleanup: bool = True,
333 prefix: tuple[str, ...] | None = None,
334) -> str:
335 """Return one inert exact privileged-body fixture."""
336 if prefix is None:
337 prefix = PRIVILEGED_BODY_PREFIX if cleanup else (PRIVILEGED_BODY_OPEN,)
338 guarded = "\n".join((prefix[0], *(f" {line}" for line in prefix[1:]), f" {body}"))
339 return (
340 f"{PRIVILEGED_SHEBANG}\n"
341 "# SPDX-License-Identifier: MIT\n"
342 "# Copyright (c) 2026 Brighton Sikarskie\n"
343 f"{PRIVILEGED_REASON}\n"
344 f"{guarded}\n"
345 f"{PRIVILEGED_BODY_CLOSE[0]}\n {PRIVILEGED_BODY_CLOSE[1]}\n"
346 f"{PRIVILEGED_BODY_CLOSE[2]}\n"
347 )
348
349
350@dataclasses.dataclass(frozen=True)
351class StartupCase:
352 """One hostile startup fixture driven through the governed wrapper."""
353
354 prefix: tuple[str, ...] | None = None
355 body_override: str | None = None
356 args: tuple[str, ...] = ()
357 extra_env: dict[str, str] | None = None
358 producer: str = "live"
359 entry: str = "direct"
360 raw_function: bool = True
361 count_entries: bool = False
362 timeout: float = 10.0
363
364
365SELFTEST_TALLY = {"runtime": 0, "structural_mutations": 0}
366
367
368def _producer_script(kind: str) -> str:
369 """Return a synthetic NUL-framed producer for one completeness attack."""
370 marker = "RA8_STARTUP_ENV_DONE=1\\0"
371 rows = {
372 "zero": "#!/bin/sh\nexit 0\n",
373 "duplicate": f"#!/bin/sh\nprintf '{marker}{marker}'\n",
374 "not-last": f"#!/bin/sh\nprintf '{marker}AFTER=1\\0'\n",
375 "torn": "#!/bin/sh\nprintf 'BASH_FUNC_probe%%%%=() { :; }\\0'\nexit 42\n",
376 "failed": "#!/bin/sh\nexit 2\n",
377 }
378 return rows[kind]
379
380
381def _startup_fixture_text(root: Path, case: StartupCase, body: str | None, cleanup: bool) -> str:
382 """Render one wrapper fixture, replacing its producer when requested."""
383 fixture = (
384 _fixture_text(cleanup=cleanup, prefix=case.prefix)
385 if body is None
386 else _fixture_text(body, cleanup=cleanup, prefix=case.prefix)
387 )
388 if case.producer != "live":
389 producer = root / "environment-producer"
390 producer.write_text(_producer_script(case.producer), encoding="ascii")
391 producer.chmod(0o700)
392 live = (
393 " /usr/bin/env -u RA8_STARTUP_ENV_DONE -0 &&\n"
394 " /usr/bin/printf 'RA8_STARTUP_ENV_DONE=1\\0'"
395 )
396 if fixture.count(live) != 1:
397 message = "startup producer fixture no longer matches the governed wrapper"
398 raise RuntimeError(message)
399 fixture = fixture.replace(live, f" {producer}", 1)
400 if case.count_entries:
401 entry = 'if [[ "$-" == *p* ]]; then\n'
402 counted = entry + ' /usr/bin/printf x >>"${RA8_STARTUP_ENTRY_LOG:?}"\n'
403 fixture = fixture.replace(entry, counted, 1)
404 return fixture
405
406
407def _startup_bash_env(early_exit: bool, descendant: bool) -> str:
408 """Return hostile startup bytes for one runtime fixture."""
409 if early_exit:
410 return "printf 'EARLY\\n'\nexit 43\n"
411 names = ("command", "builtin", "unset", "exec", "exit", "/bin/bash")
412 definitions = ["shopt -s expand_aliases"]
413 for index, name in enumerate(names):
414 definitions.append(f"function {name} {{ printf 'HOSTILE-{index}\\n'; }}")
415 definitions.append(f"alias {name}='printf ALIAS-{index}\\n'")
416 if descendant:
417 definitions.append("printf 'DESCENDANT-STARTUP\\n'")
418 return "\n".join(definitions) + "\n"
419
420
421def _startup_argv(root: Path, script: Path, entry: str, privileged: bool) -> list[str]:
422 """Materialize and select one script-name shape."""
423 hardlink = root / "hardlink.sh"
424 if entry == "hardlink":
425 os.link(script, hardlink)
426 leaf_link = root / "leaf-link.sh"
427 if entry == "leaf-symlink":
428 leaf_link.symlink_to(script)
429 parent_link = root / "parent-link"
430 if entry == "parent-symlink":
431 parent_link.symlink_to(root, target_is_directory=True)
432 choices = {
433 "absolute": ["/bin/bash", "-p", str(script)],
434 "relative": ["./fixture.sh"],
435 "hardlink": [str(hardlink)],
436 "leaf-symlink": [str(leaf_link)],
437 "parent-symlink": [str(parent_link / script.name)],
438 "bare": ["/bin/bash", "-p", script.name],
439 "dev-fd": ["/bin/bash", "-p", "-c", f"exec /bin/bash -p <(cat {shlex_quote(str(script))})"],
440 "sourced": [
441 "/bin/bash",
442 "-p",
443 "-c",
444 f". {shlex_quote(str(script))}; printf 'SOURCED_RC=%s\\n' \"$?\"",
445 ],
446 }
447 return choices.get(entry, [str(script)] if privileged else ["/bin/bash", str(script)])
448
449
450def _run_fixture(
451 *,
452 privileged: bool,
453 early_exit: bool,
454 descendant: bool = False,
455 cleanup: bool = True,
456 case: StartupCase | None = None,
457) -> subprocess.CompletedProcess[str]:
458 """Run the inert wrapper against hostile startup definitions."""
459 case = case if case is not None else StartupCase()
460 with tempfile.TemporaryDirectory(prefix="ra8-privileged-body-") as raw:
461 # macOS exposes its temporary root through /var -> /private/var. Use
462 # the physical spelling so ordinary fixtures do not accidentally test
463 # the explicit symlink-parent refusal.
464 root = Path(raw).resolve()
465 script = root / "fixture.sh"
466 bash_env = root / "bash-env"
467 body = (
468 '/bin/bash -c "type probe >/dev/null 2>&1 && '
469 "printf 'IMPORTED\\n' || printf 'CHILD\\n'\""
470 if descendant
471 else None
472 )
473 body = case.body_override if case.body_override is not None else body
474 fixture = _startup_fixture_text(root, case, body, cleanup)
475 script.write_text(fixture, encoding="utf-8")
476 script.chmod(0o700)
477 bash_env.write_text(_startup_bash_env(early_exit, descendant), encoding="utf-8")
478 argv = _startup_argv(root, script, case.entry, privileged)
479 environment = {
480 "BASH_ENV": str(bash_env),
481 "ENV": str(bash_env),
482 "PATH": "/usr/bin:/bin",
483 "LC_ALL": "C",
484 }
485 if case.count_entries:
486 environment["RA8_STARTUP_ENTRY_LOG"] = str(root / "entry-log")
487 if case.raw_function:
488 environment["BASH_FUNC_probe%%"] = "() { printf 'RAW-FUNCTION\\n'; }"
489 environment.update(case.extra_env or {})
490 SELFTEST_TALLY["runtime"] += 1
491 return run_private(
492 PrivateRun(
493 (*argv, *case.args),
494 root,
495 environment,
496 timeout=case.timeout,
497 )
498 )
499
500
501def shlex_quote(value: str) -> str:
502 """Quote one fixture-only path without adding a runtime dependency."""
503 return "'" + value.replace("'", "'\\''") + "'"
504
505
506def _authority_selftest_failures() -> list[str]:
507 """Exercise typed census and independent usage/security policies."""
508 failures: list[str] = []
509 policy = ShellPolicy(
510 ShellSecurity.PORTABLE,
511 ShellUsage.ENTRY,
512 ShellDialect.BASH,
513 executable=True,
514 source_requires_privileged_parent=False,
515 )
516 if _policy_findings({"a.sh"}, {"a.sh": policy}):
517 failures.append("matching typed census was rejected")
518 if not _policy_findings({"a.sh", "new.sh"}, {"a.sh": policy}):
519 failures.append("future unclassified shell was accepted")
520 if not _policy_findings({"a.sh"}, {"a.sh": policy, "old.sh": policy}):
521 failures.append("stale authority entry was accepted")
522 sourced = ShellPolicy(
523 ShellSecurity.PRIVILEGED,
524 ShellUsage.SOURCED_ONLY,
525 ShellDialect.BASH,
526 executable=False,
527 source_requires_privileged_parent=True,
528 )
529 if _policy_findings({"lib.sh"}, {"lib.sh": sourced}):
530 failures.append("valid privileged sourced-only policy was rejected")
531 failures.extend(_preamble_selftest_failures())
532 if not _policy_findings(
533 {"lib.sh"},
534 {
535 "lib.sh": ShellPolicy(
536 ShellSecurity.PRIVILEGED,
537 ShellUsage.SOURCED_ONLY,
538 ShellDialect.BASH,
539 executable=True,
540 source_requires_privileged_parent=True,
541 )
542 },
543 ):
544 failures.append("executable sourced-only policy was accepted")
545 dual = ShellPolicy(
546 ShellSecurity.PRIVILEGED,
547 ShellUsage.DUAL_USE,
548 ShellDialect.BASH,
549 executable=False,
550 source_requires_privileged_parent=True,
551 )
552 if _policy_findings({"dual.sh"}, {"dual.sh": dual}):
553 failures.append("valid non-executable dual-use policy was rejected")
554 failures.extend(_privileged_parent_selftest_failures((sourced, dual)))
555 if len(SHELL_POLICIES) != len(set(SHELL_POLICIES)):
556 failures.append("aggregate policy authority contains duplicate paths")
557 for name, domain in (("CI", CI_POLICY_ROWS), ("HIL", HIL_POLICY_ROWS)):
558 if any(row[0] not in SHELL_POLICIES for row in domain):
559 failures.append(f"{name} domain policy rows were not merged")
560 try:
561 merge_policy_tables({domain[0][0]: policy}, domain)
562 except ValueError:
563 pass
564 else:
565 failures.append(f"duplicate {name} domain policy path was accepted")
566 return failures
567
568
569def _privileged_parent_selftest_failures(
570 policies: tuple[ShellPolicy, ...],
571) -> list[str]:
572 """Reject privileged sourced policies without a privileged parent."""
573 failures: list[str] = []
574 for policy in policies:
575 weakened = dataclasses.replace(policy, source_requires_privileged_parent=False)
576 if _policy_findings({"fixture.sh"}, {"fixture.sh": weakened}):
577 continue
578 failures.append(
579 f"privileged {policy.usage.value} policy without a privileged parent was accepted"
580 )
581 return failures
582
583
584def _preamble_selftest_failures() -> list[str]:
585 """Exercise the combined privileged-shell preamble."""
586 failures: list[str] = []
587 protected = PINNED_INTERPRETER_BOUNDARIES["scripts/hil/all.sh"]
588 canonical_protected = (
589 "#!/bin/bash -p",
590 "# SPDX-License-Identifier: MIT",
591 "# Copyright (c) 2026 Brighton Sikarskie",
592 "# SHEBANG-SECURITY: -p blocks BASH_ENV and exported-function startup injection.",
593 )
594 if not _header_matches(canonical_protected, protected):
595 failures.append("canonical combined privileged preamble was rejected")
596 old_order = (
597 PRIVILEGED_SHEBANG,
598 PRIVILEGED_REASON,
599 "# SPDX-License-Identifier: MIT",
600 "# Copyright (c) 2026 Brighton Sikarskie",
601 )
602 if _header_matches(old_order, protected):
603 failures.append("security rationale before attribution was accepted")
604 wrong_reason = (*canonical_protected[:3], "# SHEBANG-SECURITY: vague rationale.")
605 if _header_matches(wrong_reason, protected):
606 failures.append("non-canonical privileged security rationale was accepted")
607 return failures
608
609
610def _guard_structure_mutations(safe: str) -> tuple[str, ...]:
611 """Return independent weakenings of the exact wrapper."""
612 return (
613 safe.replace(PRIVILEGED_BODY_OPEN, 'if [[ "$-" != *p* ]]; then', 1),
614 safe.replace(" unset -v BASH_ENV ENV\n", "", 1),
615 safe.replace(
616 " BASH_FUNC_*%% | BASH_FUNC_*'()') "
617 'ra8_startup_env_unset+=(-u "$ra8_startup_env_name") ;;\n',
618 "",
619 1,
620 ),
621 safe.replace(" /usr/bin/env -u RA8_STARTUP_ENV_DONE -0 &&\n", "", 1),
622 safe.replace(" /usr/bin/printf 'RA8_STARTUP_ENV_DONE=1\\0'\n", "", 1),
623 safe.replace(
624 " ((ra8_startup_env_done_count == 1)) && "
625 '[[ "$ra8_startup_env_name" == RA8_STARTUP_ENV_DONE ]] || '
626 "_ra8_startup_refuse 'environment enumeration was incomplete'\n",
627 " true\n",
628 1,
629 ),
630 safe.replace(
631 ' [[ -z "${RA8_STARTUP_ENV_SCRUBBED-}" ]] || '
632 "_ra8_startup_refuse 'scrub did not converge'\n",
633 " true\n",
634 1,
635 ),
636 safe.replace(
637 ' [[ "$ra8_startup_reentry" == */* ]] || '
638 "_ra8_startup_refuse 'requires a script path'\n",
639 " true\n",
640 1,
641 ),
642 safe.replace(
643 ' [[ ! -L "$ra8_startup_check" ]] || '
644 "_ra8_startup_refuse 'refuses a symlinked path'\n",
645 " true\n",
646 1,
647 ),
648 safe.replace(
649 ' [[ -f "$ra8_startup_reentry" ]] || '
650 "_ra8_startup_refuse 'refuses a non-regular path'\n",
651 " true\n",
652 1,
653 ),
654 safe.replace(
655 ' if ! exec /usr/bin/env "${ra8_startup_env_unset[@]}" -u BASH_ENV -u ENV \\\n',
656 "",
657 1,
658 ),
659 safe.replace(f" {FAILED_CLEANUP_EXEC}\n", "", 1),
660 safe.replace("else\n", "fi\nBODY_AFTER\nelse\n", 1),
661 safe + "BODY_AFTER\n",
662 safe.replace(' [[ "$-" == *p* ]]', " command exit 1", 1),
663 safe.replace(" printf", " command builtin exec /bin/bash -p\n printf", 1),
664 )
665
666
667def _guard_structure_selftest_failures() -> list[str]:
668 """Exercise exact outer-wrapper structure in both directions."""
669 failures: list[str] = []
670 safe = _fixture_text()
671 if privileged_body_findings("fixture.sh", safe):
672 failures.append("exact privileged-body wrapper was rejected")
673 mutations = _guard_structure_mutations(safe)
674 SELFTEST_TALLY["structural_mutations"] += len(mutations)
675 if any(text == safe for text in mutations):
676 failures.append("a structural mutation did not change the fixture")
677 if any(not privileged_body_findings("fixture.sh", text) for text in mutations):
678 failures.append("a weakened privileged-body wrapper was accepted")
679 return failures
680
681
682def _guard_runtime_selftest_failures() -> list[str]:
683 """Exercise weak startup and descendant-channel attacks at runtime."""
684 failures: list[str] = []
685 weak = _run_fixture(privileged=False, early_exit=False)
686 if weak.returncode == 0 or weak.stdout or "BODY" in weak.stderr:
687 failures.append("weak Bash invocation reached output under hostile functions/aliases")
688 direct = _run_fixture(privileged=True, early_exit=True)
689 if direct.returncode != 0 or "BODY mode=" not in direct.stdout or "EARLY" in direct.stdout:
690 failures.append("privileged shebang did not ignore hostile early-exit BASH_ENV")
691 early = _run_fixture(privileged=False, early_exit=True)
692 if (
693 early.returncode != EARLY_EXIT_STATUS
694 or "BODY" in early.stdout
695 or "EARLY" not in early.stdout
696 ):
697 failures.append("weak early-exit BASH_ENV behavior was reported dishonestly")
698 descendant = _run_fixture(privileged=True, early_exit=False, descendant=True)
699 control = _run_fixture(privileged=True, early_exit=False, descendant=True, cleanup=False)
700 if descendant.returncode != 0 or descendant.stdout != "CHILD\n":
701 failures.append("privileged body leaked hostile startup channels to a child Bash")
702 if "DESCENDANT-STARTUP" not in control.stdout or "IMPORTED" not in control.stdout:
703 failures.append("descendant startup/function control did not demonstrate both attacks")
704 return failures
705
706
707def _legacy_phantom_selftest_failures() -> list[str]:
708 """Prove the old line framing loops and NUL framing does not."""
709 legacy_prefix = (
710 PRIVILEGED_BODY_OPEN,
711 "unset -v BASH_ENV ENV",
712 "declare -a ra8_startup_env_unset=()",
713 "while IFS='=' read -r ra8_startup_env_name _; do",
714 'case "$ra8_startup_env_name" in',
715 'BASH_FUNC_*%%) ra8_startup_env_unset+=(-u "$ra8_startup_env_name") ;;',
716 "esac",
717 "done < <(/usr/bin/env)",
718 "if ((${#ra8_startup_env_unset[@]})); then",
719 'exec /usr/bin/env "${ra8_startup_env_unset[@]}" -u BASH_ENV -u ENV \\',
720 '/bin/bash -p -- "$0" "$@"',
721 "fi",
722 "unset -v ra8_startup_env_name ra8_startup_env_unset",
723 )
724 phantom = {"RA8_STARTUP_PHANTOM": "\nBASH_FUNC_phantom%%=() { :; }\n}"}
725 failures: list[str] = []
726 try:
727 _run_fixture(
728 privileged=True,
729 early_exit=False,
730 case=StartupCase(
731 prefix=legacy_prefix,
732 extra_env=phantom,
733 raw_function=False,
734 timeout=0.5,
735 ),
736 )
737 except subprocess.TimeoutExpired:
738 pass
739 else:
740 failures.append("the legacy line-framed negative control did not loop")
741
742 fixed = _run_fixture(
743 privileged=True,
744 early_exit=False,
745 case=StartupCase(extra_env=phantom, raw_function=False),
746 )
747 if fixed.returncode != 0 or fixed.stdout.count("BODY mode=") != 1:
748 failures.append("an embedded-newline phantom row did not reach the body exactly once")
749 return failures
750
751
752def _producer_completion_selftest_failures() -> list[str]:
753 """Require zero, duplicate, misplaced, torn, and failed producers to refuse."""
754 failures: list[str] = []
755 for producer in ("zero", "duplicate", "not-last", "torn", "failed"):
756 result = _run_fixture(
757 privileged=True,
758 early_exit=False,
759 case=StartupCase(producer=producer),
760 )
761 if result.returncode == 0 or "enumeration was incomplete" not in result.stderr:
762 failures.append(f"the {producer} environment producer did not fail closed")
763 return failures
764
765
766def _startup_convergence_selftest_failures() -> list[str]:
767 """Prove NUL framing, bounded re-entry, argv, and producer completeness."""
768 failures = [
769 *_legacy_phantom_selftest_failures(),
770 *_producer_completion_selftest_failures(),
771 ]
772 sentinel = _run_fixture(
773 privileged=True,
774 early_exit=False,
775 case=StartupCase(extra_env={"RA8_STARTUP_ENV_SCRUBBED": "attacker"}),
776 )
777 if sentinel.returncode == 0 or "did not converge" not in sentinel.stderr:
778 failures.append("an attacker-supplied scrub sentinel skipped the real enumeration")
779
780 counted = _run_fixture(
781 privileged=True,
782 early_exit=False,
783 case=StartupCase(
784 body_override='/usr/bin/wc -c <"${RA8_STARTUP_ENTRY_LOG:?}"',
785 count_entries=True,
786 ),
787 )
788 if counted.returncode != 0 or counted.stdout.strip() != "2":
789 failures.append("function cleanup did not converge in exactly two process entries")
790
791 argv = ("plain", "", "two words", "tab\tvalue", "line one\nline two")
792 roundtrip_status = 9
793 roundtrip = _run_fixture(
794 privileged=True,
795 early_exit=False,
796 case=StartupCase(
797 body_override=f"printf 'ARG:%s\\n' \"$@\"\n exit {roundtrip_status}",
798 args=argv,
799 ),
800 )
801 expected = "".join(f"ARG:{value}\n" for value in argv)
802 if roundtrip.returncode != roundtrip_status or roundtrip.stdout != expected:
803 failures.append("the bounded cleanup re-entry corrupted argv or exit status")
804 return failures
805
806
807def _reentry_path_selftest_failures() -> list[str]:
808 """Exercise every supported and refused script-name shape."""
809 failures: list[str] = []
810 for entry in ("direct", "absolute", "relative", "hardlink"):
811 result = _run_fixture(
812 privileged=True,
813 early_exit=False,
814 case=StartupCase(entry=entry),
815 )
816 if result.returncode != 0 or result.stdout.count("BODY mode=") != 1:
817 failures.append(f"the supported {entry} re-entry path was rejected")
818 for entry in ("leaf-symlink", "parent-symlink", "bare", "dev-fd"):
819 result = _run_fixture(
820 privileged=True,
821 early_exit=False,
822 case=StartupCase(entry=entry),
823 )
824 if result.returncode == 0 or "privileged startup" not in result.stderr:
825 failures.append(f"the unsafe {entry} re-entry path was accepted")
826 return failures
827
828
829def _wrapper_variant_selftest_failures() -> list[str]:
830 """Run the plain, dual-use, and rig variants instead of pinning text only."""
831 failures: list[str] = []
832 dual_sourced = _run_fixture(
833 privileged=True,
834 early_exit=False,
835 case=StartupCase(prefix=PRIVILEGED_DUAL_BODY_PREFIX, entry="sourced"),
836 )
837 if "SOURCED_RC=2" not in dual_sourced.stdout or "BODY mode=" in dual_sourced.stdout:
838 failures.append("the sourced dual-use wrapper did not refuse inherited functions")
839 rig_sourced = _run_fixture(
840 privileged=True,
841 early_exit=False,
842 case=StartupCase(prefix=PRIVILEGED_RIG_BODY_PREFIX, entry="sourced"),
843 )
844 if "SOURCED_RC=2" not in rig_sourced.stdout or "sourced rig contract" not in rig_sourced.stderr:
845 failures.append("the sourced rig wrapper did not use its fail-closed branch")
846 for label, prefix in (
847 ("dual-use", PRIVILEGED_DUAL_BODY_PREFIX),
848 ("rig", PRIVILEGED_RIG_BODY_PREFIX),
849 ):
850 result = _run_fixture(
851 privileged=True,
852 early_exit=False,
853 case=StartupCase(prefix=prefix),
854 )
855 if result.returncode != 0 or result.stdout.count("BODY mode=") != 1:
856 failures.append(f"the directly executed {label} wrapper did not run exactly once")
857 return failures
858
859
860def _guard_derivation_selftest_failures() -> list[str]:
861 """Prove typed policy, not historical repair tokens, derives wrappers."""
862 failures: list[str] = []
863 weak_policy = ShellPolicy(
864 ShellSecurity.PRIVILEGED,
865 ShellUsage.ENTRY,
866 ShellDialect.BASH,
867 executable=True,
868 source_requires_privileged_parent=False,
869 )
870 weak_text = (
871 f"{PRIVILEGED_SHEBANG}\n"
872 "# SPDX-License-Identifier: MIT\n"
873 "# Copyright (c) 2026 Brighton Sikarskie\n"
874 f"{PRIVILEGED_REASON}\n"
875 'if [[ "$-" != *p* ]]; then\n'
876 ' exec /bin/bash -p "$0" "$@"\n'
877 "fi\nprintf 'BODY\\n'\n"
878 )
879 if not _requires_privileged_body("future.sh", weak_text, weak_policy):
880 failures.append("future privileged weak re-exec did not derive a body-wrapper requirement")
881 elif not privileged_body_findings("future.sh", weak_text):
882 failures.append("future privileged weak re-exec passed without the governed wrapper")
883 no_repair_tokens = (
884 f"{PRIVILEGED_SHEBANG}\n"
885 "# SPDX-License-Identifier: MIT\n"
886 "# Copyright (c) 2026 Brighton Sikarskie\n"
887 f"{PRIVILEGED_REASON}\n"
888 "printf 'FUTURE BODY\\n'\n"
889 )
890 if not _requires_privileged_body("future.sh", no_repair_tokens, weak_policy):
891 failures.append("privileged entry without historical repair tokens escaped the wrapper")
892 portable_policy = ShellPolicy(
893 ShellSecurity.PORTABLE,
894 ShellUsage.ENTRY,
895 ShellDialect.BASH,
896 executable=True,
897 source_requires_privileged_parent=False,
898 )
899 if _requires_privileged_body("portable.sh", no_repair_tokens, portable_policy):
900 failures.append("portable entry incorrectly inherited the privileged-body wrapper")
901 source_policy = ShellPolicy(
902 ShellSecurity.PRIVILEGED,
903 ShellUsage.SOURCED_ONLY,
904 ShellDialect.BASH,
905 executable=False,
906 source_requires_privileged_parent=True,
907 )
908 if _requires_privileged_body("source.sh", no_repair_tokens, source_policy):
909 failures.append("sourced-only helper incorrectly became a launchable guarded entry")
910 return failures
911
912
913def _guard_selftest_failures() -> list[str]:
914 """Return all structural, runtime, and policy-derivation failures."""
915 failures = (
916 _guard_structure_selftest_failures()
917 + _guard_runtime_selftest_failures()
918 + _startup_convergence_selftest_failures()
919 + _reentry_path_selftest_failures()
920 + _wrapper_variant_selftest_failures()
921 + _guard_derivation_selftest_failures()
922 )
923 try:
924 with tempfile.TemporaryDirectory(prefix="ra8-wrapper-state-") as temporary:
925 SELFTEST_TALLY["runtime"] += run_privileged_wrapper_runtime_cases(
926 Path(temporary), PRIVILEGED_RUNTIME_VARIANTS
927 )
928 except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
929 failures.append(f"hostile wrapper state matrix failed: {exc}")
930 return failures
931
932
933def _selftest_failures() -> list[str]:
934 """Return every authority and full-body wrapper selftest failure."""
935 minimum_runtime_cases = 25 + (5 * len(PRIVILEGED_RUNTIME_VARIANTS))
936 minimum_structural_mutations = 15
937 minimum_privileged_paths = 81
938 minimum_guarded_paths = 71
939 SELFTEST_TALLY.update(runtime=0, structural_mutations=0)
940 failures = _authority_selftest_failures() + _guard_selftest_failures()
941 if SELFTEST_TALLY["runtime"] < minimum_runtime_cases:
942 failures.append(f"the runtime attack matrix collapsed below {minimum_runtime_cases} cases")
943 if SELFTEST_TALLY["structural_mutations"] < minimum_structural_mutations:
944 failures.append("the structural mutation corpus collapsed below 15 cases")
945 try:
946 _, _, privileged, _, guarded = scan()
947 except (OSError, subprocess.SubprocessError, UnicodeError) as exc:
948 failures.append(f"the live wrapper census could not be measured: {exc}")
949 else:
950 if privileged < minimum_privileged_paths or guarded < minimum_guarded_paths:
951 failures.append(
952 f"the live wrapper census collapsed to {privileged} privileged/{guarded} guarded"
953 )
954 return failures
955
956
957def selftest() -> int:
958 """Run both-direction typed-authority and startup-attack fixtures."""
959 failures = _selftest_failures()
960 for failure in failures:
961 print(f"check_shebangs.py --selftest: FAIL: {failure}", file=sys.stderr)
962 if failures:
963 return EXIT_FAIL
964 print(
965 "check_shebangs.py --selftest: PASS "
966 f"({SELFTEST_TALLY['runtime']} runtime cases, "
967 f"{SELFTEST_TALLY['structural_mutations']} structural mutations)"
968 )
969 return EXIT_OK
970
971
972def main(argv: list[str]) -> int:
973 """Run the selftest or the exhaustive live authority scan."""
974 if argv[1:] == ["--selftest"]:
975 return selftest()
976 if argv[1:]:
977 print("usage: check_shebangs.py [--selftest]", file=sys.stderr)
978 return EXIT_CONFIG
979 try:
980 findings, total, privileged, sourced, guarded = scan()
981 except (OSError, subprocess.SubprocessError, UnicodeError) as exc:
982 print(f"check_shebangs.py: FATAL: {exc}", file=sys.stderr)
983 return EXIT_CONFIG
984 if findings:
985 print("check_shebangs.py: typed shell-entrypoint finding(s):", file=sys.stderr)
986 for finding in findings:
987 print(f" {finding}", file=sys.stderr)
988 return EXIT_FAIL
989 portable = total - privileged
990 print(
991 "check_shebangs.py: exhaustive authority clean "
992 f"({total} shell files: {privileged} privileged, {portable} portable, "
993 f"{sourced} sourced-only, {guarded} structurally guarded)"
994 )
995 return EXIT_OK
996
997
998if __name__ == "__main__":
999 raise SystemExit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298