ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_download_installers.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"""Require download-to-file and SHA-256 verification for bootstrap installers."""
5
6from __future__ import annotations
7
8import argparse
9import re
10import subprocess
11import sys
12import tempfile
13from pathlib import Path
14
15from download_installers_macos import check_macos_cleanup, macos_cleanup_mutations
16
17ROOT = Path(
18 subprocess.run(
19 ["git", "rev-parse", "--show-toplevel"], # noqa: S607 -- Git from PATH is intended
20 check=True,
21 capture_output=True,
22 text=True,
23 ).stdout.strip()
24)
25
26DOCKERFILE = ".devcontainer/Dockerfile"
27PROVISION = "scripts/dev/provision_dev_box_toolchain.sh"
28MAC_SETUP = "scripts/emu/setup_macos.sh"
29SHA_ARGS = (
30 "SHELLCHECK_SHA256_X86_64",
31 "SHELLCHECK_SHA256_AARCH64",
32 "SHFMT_SHA256_AMD64",
33 "SHFMT_SHA256_ARM64",
34 "ACTIONLINT_SHA256_AMD64",
35 "ACTIONLINT_SHA256_ARM64",
36 "JUST_SHA256_X86_64",
37 "JUST_SHA256_AARCH64",
38 "HADOLINT_SHA256_X86_64",
39 "HADOLINT_SHA256_ARM64",
40)
41ARM_SHA_ARGS = (
42 "ARM_GCC_SHA256_X86_64",
43 "ARM_GCC_SHA256_AARCH64",
44 "ARM_GCC_SHA256_DARWIN_X86_64",
45 "ARM_GCC_SHA256_DARWIN_ARM64",
46)
47ARM_RELEASE_RE = re.compile(r"^[0-9]+\.[0-9]+\.rel[1-9][0-9]*$")
48MAC_DOCKER_ARG_READER = r"""{
49 local name="$1" value
50 if ! value="$(awk -v name="${name}" '
51 function inspect_instruction( body, line, lower, target) {
52 line = logical
53 sub(/^[[:blank:]]*/, "", line)
54 lower = tolower(line)
55 if (substr(lower, 1, 3) != "arg" ||
56 substr(line, 4, 1) !~ /[[:blank:]]/) {
57 return
58 }
59 body = substr(line, 5)
60 sub(/^[[:blank:]]+/, "", body)
61 target = "(^|[[:blank:]])" name "([=[:blank:]]|$)"
62 if (body !~ target) {
63 return
64 }
65 count++
66 if (continued || index(body, name "=") != 1) {
67 invalid = 1
68 return
69 }
70 value = substr(body, length(name) + 2)
71 }
72 {
73 physical = $0
74 if (physical ~ /^[[:blank:]]*(#.*)?$/) {
75 next
76 }
77 has_continuation = physical ~ /\\[[:blank:]]*$/
78 if (has_continuation) {
79 sub(/\\[[:blank:]]*$/, "", physical)
80 logical = logical physical
81 continued = 1
82 next
83 }
84 logical = logical physical
85 inspect_instruction()
86 logical = ""
87 continued = 0
88 }
89 END {
90 if (logical != "") {
91 invalid = 1
92 inspect_instruction()
93 }
94 if (count != 1 || invalid) exit 2
95 printf "%s", value
96 }
97 ' "${dockerfile}")"; then
98 echo "ERROR: expected one canonical ARG ${name}=... in ${dockerfile}." >&2
99 return 1
100 fi
101 printf '%s' "${value}"
102}"""
103TLS_FLAGS = "--proto '=https' --proto-redir '=https' --tlsv1.2"
104DOCKER_DOWNLOAD_MARKER = {
105 "shellcheck": f"curl {TLS_FLAGS} -fsSL -o /tmp/shellcheck.tar.xz",
106 "shfmt": f"curl {TLS_FLAGS} -fsSL -o /tmp/shfmt",
107 "actionlint": f"curl {TLS_FLAGS} -fsSL -o /tmp/actionlint.tar.gz",
108 "just": f"curl {TLS_FLAGS} -fsSL -o /tmp/just.tar.gz",
109 "hadolint": f"curl {TLS_FLAGS} -fsSL -o /tmp/hadolint",
110}
111TOOLS = ("shellcheck", "shfmt", "actionlint", "just", "hadolint")
112ARCHIVE_TOOLS = ("shellcheck", "actionlint", "just")
113DOCKER_VERIFY_MARKER = {
114 "shellcheck": '"${scsha}" /tmp/shellcheck.tar.xz | sha256sum -c -',
115 "shfmt": '"${shsha}" /tmp/shfmt | sha256sum -c -',
116 "actionlint": '"${alsha}" /tmp/actionlint.tar.gz | sha256sum -c -',
117 "just": '"${justsha}" /tmp/just.tar.gz | sha256sum -c -',
118 "hadolint": '"${hsha}" /tmp/hadolint | sha256sum -c -',
119}
120DOCKER_ARCHIVE_MEMBER = {
121 "shellcheck": '"shellcheck-v${SHELLCHECK_VERSION}/shellcheck"',
122 "actionlint": "-C /tmp/actionlint actionlint",
123 "just": "-C /tmp/just just",
124}
125PROVISION_ARCHIVE_MEMBER = {
126 "shellcheck": '"shellcheck-v${version}/shellcheck"',
127 "actionlint": '-C "${tmp}/extract" actionlint',
128 "just": '-C "${tmp}/extract" just',
129}
130DOCKER_END = {
131 "shellcheck": "# cmake-format",
132 "shfmt": "# cmake-format",
133 "actionlint": "# just",
134 "just": "# hadolint",
135 "hadolint": "# Create a non-root user",
136}
137PROVISION_END = {
138 "shellcheck": "install_shfmt()",
139 "shfmt": "install_actionlint()",
140 "actionlint": "install_hadolint()",
141 "hadolint": "install_just()",
142 "just": "install_doxygen()",
143}
144
145
146def _active(text: str) -> str:
147 """Discard comment-only lines and join shell continuations."""
148 code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
149 return re.sub(r"\\\n\s*", " ", code)
150
151
152def _normalise_shell(text: str) -> str:
153 """Collapse insignificant shell whitespace for exact statement checks."""
154 return " ".join(_active(text).split())
155
156
157def _exact_hex_assignment(text: str, name: str, length: int) -> str | None:
158 """Return one indented literal hex assignment and reject every duplicate."""
159 pattern = rf'(?m)^[ \t]*{re.escape(name)}="([0-9a-f]{{{length}}})"[ \t]*$'
160 matches = re.findall(pattern, text)
161 assignments = re.findall(rf"(?m)^[ \t]*{re.escape(name)}[+?]?=", text)
162 return matches[0] if len(matches) == 1 and len(assignments) == 1 else None
163
164
165def _unsafe_pipeline(text: str) -> bool:
166 """Return whether downloaded bytes flow directly to a parser or shell."""
167 active = _active(text)
168 return bool(
169 re.search(r"\bcurl\b[^\n;]*\|\s*(?:bash|sh|tar)\b", active)
170 or re.search(r"\$\‍(\s*curl\b", active)
171 )
172
173
174def _section(text: str, start: str, end: str) -> str:
175 """Return a required text section, or an empty string when anchors drift."""
176 before, marker, rest = text.partition(start)
177 del before
178 if not marker:
179 return ""
180 body, marker, _after = rest.partition(end)
181 return body if marker else ""
182
183
184def _docker_instructions(text: str) -> tuple[tuple[str, bool], ...]:
185 """Return Docker logical instructions and whether they used continuation."""
186 instructions: list[tuple[str, bool]] = []
187 logical = ""
188 continued = False
189 for physical in text.splitlines():
190 if not physical.strip() or physical.lstrip().startswith("#"):
191 continue
192 match = re.search(r"\\[ \t]*$", physical)
193 if match is not None:
194 logical += physical[: match.start()]
195 continued = True
196 continue
197 logical += physical
198 instructions.append((logical, continued))
199 logical = ""
200 continued = False
201 if logical:
202 instructions.append((logical, True))
203 return tuple(instructions)
204
205
206def _docker_args(text: str, name: str) -> tuple[str | None, ...]:
207 """Return every Docker ARG occurrence, marking non-canonical forms invalid."""
208 values: list[str | None] = []
209 target = re.compile(rf"(?:^|[ \t]){re.escape(name)}(?=$|[= \t])")
210 instruction = re.compile(r"^[ \t]*(?i:ARG)[ \t]+(.*)$")
211 for logical, continued in _docker_instructions(text):
212 match = instruction.fullmatch(logical)
213 if match is None:
214 continue
215 body = match.group(1).lstrip()
216 if target.search(body) is None:
217 continue
218 prefix = f"{name}="
219 if continued or not body.startswith(prefix):
220 values.append(None)
221 continue
222 values.append(body[len(prefix) :])
223 return tuple(values)
224
225
226def _docker_arg(text: str, name: str) -> str | None:
227 """Return a canonical Docker ARG only when it occurs exactly once."""
228 values = _docker_args(text, name)
229 return values[0] if len(values) == 1 and values[0] is not None else None
230
231
232def _check_arm_docker_pins(text: str) -> list[str]:
233 """Require exact-one, well-formed Arm release and digest ARGs."""
234 findings: list[str] = []
235 release_values = _docker_args(text, "ARM_GCC_RELEASE")
236 if len(release_values) != 1 or release_values[0] is None:
237 findings.append(f"{DOCKERFILE}: expected one canonical ARG ARM_GCC_RELEASE")
238 elif ARM_RELEASE_RE.fullmatch(release_values[0]) is None:
239 findings.append(f"{DOCKERFILE}: ARM_GCC_RELEASE must match N.N.relN")
240 for name in ARM_SHA_ARGS:
241 values = _docker_args(text, name)
242 if len(values) != 1 or values[0] is None:
243 findings.append(f"{DOCKERFILE}: expected one canonical ARG {name}")
244 elif re.fullmatch(r"[0-9a-f]{64}", values[0]) is None:
245 findings.append(f"{DOCKERFILE}: ARG {name} is not a 64-hex sha256")
246 return findings
247
248
249def check_dockerfile(text: str) -> list[str]:
250 """Check the canonical release pins and Docker install blocks."""
251 findings: list[str] = []
252 findings.extend(
253 f"{DOCKERFILE}: missing 64-hex ARG {name}"
254 for name in SHA_ARGS
255 if not re.search(rf"^ARG {name}=[0-9a-f]{{64}}$", text, re.MULTILINE)
256 )
257 findings.extend(_check_arm_docker_pins(text))
258 if _unsafe_pipeline(text):
259 findings.append(f"{DOCKERFILE}: curl output is piped directly to a shell/archive parser")
260 if "just.systems/install.sh" in text:
261 findings.append(f"{DOCKERFILE}: mutable Just installer script is forbidden")
262 for tool in TOOLS:
263 start = "ARG SHELLCHECK_VERSION=" if tool == "shfmt" else f"ARG {tool.upper()}_VERSION="
264 block = _section(text, start, DOCKER_END[tool])
265 active = _active(block)
266 if (
267 not block
268 or DOCKER_VERIFY_MARKER[tool] not in active
269 or DOCKER_DOWNLOAD_MARKER[tool] not in active
270 ):
271 findings.append(f"{DOCKERFILE}: {tool} must download to /tmp and verify sha256")
272 if tool in ARCHIVE_TOOLS and DOCKER_ARCHIVE_MEMBER[tool] not in active:
273 findings.append(f"{DOCKERFILE}: {tool} archive extraction is not file-based/exact")
274 if "hadolint-Linux-" in text:
275 findings.append(f"{DOCKERFILE}: hadolint asset name has stale uppercase Linux spelling")
276 return findings
277
278
279def check_provision(text: str) -> list[str]:
280 """Check native dev-box release installers use the verified helper."""
281 findings: list[str] = []
282 if _unsafe_pipeline(text):
283 findings.append(f"{PROVISION}: curl output is piped directly to a shell/archive parser")
284 helper = _section(text, "download_verified()", "install_shellcheck()")
285 if (
286 TLS_FLAGS not in _active(helper)
287 or "-fsSL" not in helper
288 or '-o "${output}"' not in helper
289 or "sha256sum -c -" not in helper
290 ):
291 findings.append(f"{PROVISION}: download_verified must fetch to disk and check sha256")
292 for tool in TOOLS:
293 block = _section(text, f"install_{tool}()", PROVISION_END[tool])
294 active = _active(block)
295 if "download_verified" not in block:
296 findings.append(f"{PROVISION}: install_{tool} bypasses download_verified")
297 if tool in ARCHIVE_TOOLS and PROVISION_ARCHIVE_MEMBER[tool] not in active:
298 findings.append(f"{PROVISION}: install_{tool} lacks file-based exact extraction")
299 findings.extend(
300 f"{PROVISION}: canonical Docker ARG {name} is not consumed"
301 for name in SHA_ARGS
302 if name not in text
303 )
304 if "hadolint-Linux-" in text or re.search(r"as_root\s+curl", text):
305 findings.append(f"{PROVISION}: stale asset spelling or privileged direct download")
306 return findings
307
308
309def _check_macos_arm_mapping(text: str) -> list[str]:
310 """Require immutable final host-to-Darwin asset selection."""
311 findings: list[str] = []
312 actual = _section(text, "arm_toolchain_asset()", "install_homebrew()")
313 expected = r"""{
314 case "$1" in
315 arm64)
316 printf '%s\t%s\n' "darwin-arm64" "ARM_GCC_SHA256_DARWIN_ARM64"
317 ;;
318 x86_64)
319 printf '%s\t%s\n' "darwin-x86_64" "ARM_GCC_SHA256_DARWIN_X86_64"
320 ;;
321 *)
322 echo "ERROR: unsupported macOS architecture $1 for Arm GNU Toolchain." >&2
323 return 1
324 ;;
325 esac
326 }"""
327 if _normalise_shell(actual) != _normalise_shell(expected):
328 findings.append(f"{MAC_SETUP}: Arm asset selector function is not exact")
329 expected_counts = {
330 "arm64)": 1,
331 "x86_64)": 1,
332 "darwin-arm64": 1,
333 "darwin-x86_64": 1,
334 "ARM_GCC_SHA256_X86_64": 1,
335 "ARM_GCC_SHA256_AARCH64": 1,
336 "ARM_GCC_SHA256_DARWIN_X86_64": 2,
337 "ARM_GCC_SHA256_DARWIN_ARM64": 2,
338 }
339 if any(text.count(token) != count for token, count in expected_counts.items()):
340 findings.append(f"{MAC_SETUP}: Arm case labels, assets, and hash tokens must be unique")
341 assignment_counts = {
342 name: len(re.findall(rf"(?m)^\s*{name}=", text))
343 for name in ("arm_asset_arch", "arm_sha_arg", "arm_sha256")
344 }
345 if assignment_counts != {"arm_asset_arch": 0, "arm_sha_arg": 0, "arm_sha256": 1}:
346 findings.append(f"{MAC_SETUP}: Arm asset/hash variables have an override assignment")
347 order_tokens = (
348 "read -r arm_asset_arch arm_sha_arg",
349 'arm_sha256="$(dockerfile_arg "${arm_sha_arg}")"',
350 "readonly arm_asset_arch arm_sha_arg arm_sha256",
351 'arm_url="',
352 )
353 offsets = tuple(text.find(token) for token in order_tokens)
354 if any(offset < 0 for offset in offsets) or offsets != tuple(sorted(offsets)):
355 findings.append(
356 f"{MAC_SETUP}: Arm asset selection must be final and readonly before URL use"
357 )
358 return findings
359
360
361def _check_macos_arm_delete(text: str) -> list[str]:
362 """Require a strict release and canonical readonly Arm prefix."""
363 findings: list[str] = []
364 required = (
365 'arm_release="$(dockerfile_arg ARM_GCC_RELEASE)" || exit 1\nreadonly arm_release',
366 r"^[0-9]+\.[0-9]+\.rel[1-9][0-9]*$",
367 'readonly arm_version="${arm_release%.rel*}"',
368 'home_root="$(cd "$HOME" && pwd -P)" || exit 1\nreadonly home_root',
369 'readonly arm_root="${home_root}/opt"',
370 'arm_prefix="$(canonical_cleanup_target '
371 '"${arm_root}/arm-gnu-toolchain-${arm_version}" "$arm_root")" || exit 1\n'
372 "readonly arm_prefix",
373 'if [[ -z "$arm_prefix" ]]',
374 )
375 active = _normalise_shell(text)
376 if any(_normalise_shell(token) not in active for token in required):
377 findings.append(f"{MAC_SETUP}: Arm release/prefix derivation is not strict and readonly")
378 return [*findings, *check_macos_cleanup(text)]
379
380
381def _check_macos_arm(text: str, docker: str) -> list[str]:
382 """Check Darwin Arm selection, pin ownership, and deletion safety."""
383 findings = [*_check_macos_arm_mapping(text), *_check_macos_arm_delete(text)]
384 reader = _section(text, "dockerfile_arg()", "require_arm_hash_pins()")
385 if _normalise_shell(reader) != _normalise_shell(MAC_DOCKER_ARG_READER):
386 findings.append(f"{MAC_SETUP}: Docker ARG reader active body is not exact")
387 pin_validator = _section(text, "require_arm_hash_pins()", 'arm_release="')
388 if (
389 any(name not in pin_validator for name in ARM_SHA_ARGS)
390 or len(re.findall(r"(?m)^\s*require_arm_hash_pins\s*$", text)) != 1
391 ):
392 findings.append(f"{MAC_SETUP}: bootstrap must validate all four Arm hash pins")
393 expected_url = "arm-gnu-toolchain-${arm_release}-${arm_asset_arch}-arm-none-eabi.tar.xz"
394 if expected_url not in text:
395 findings.append(f"{MAC_SETUP}: Arm URL must include the Darwin asset selector")
396 if TLS_FLAGS not in _active(_section(text, 'arm_url="', 'echo "[emu-setup] using')):
397 findings.append(f"{MAC_SETUP}: Arm archive download must enforce HTTPS/TLS")
398 for linux_name, darwin_name in (
399 ("ARM_GCC_SHA256_AARCH64", "ARM_GCC_SHA256_DARWIN_ARM64"),
400 ("ARM_GCC_SHA256_X86_64", "ARM_GCC_SHA256_DARWIN_X86_64"),
401 ):
402 linux_sha = _docker_arg(docker, linux_name)
403 darwin_sha = _docker_arg(docker, darwin_name)
404 if darwin_sha is not None and darwin_sha == linux_sha:
405 findings.append(f"{DOCKERFILE}: {darwin_name} must not reuse the Linux archive hash")
406 return findings
407
408
409def check_macos(text: str, docker: str) -> list[str]:
410 """Check macOS bootstrap downloads and Darwin Arm archive selection."""
411 findings: list[str] = []
412 commit = _exact_hex_assignment(text, "homebrew_installer_commit", 40)
413 digest = _exact_hex_assignment(text, "homebrew_installer_sha256", 64)
414 if commit is None or digest is None:
415 findings.append(f"{MAC_SETUP}: Homebrew installer commit/sha256 pin is missing")
416 required = (
417 "Homebrew/install/${homebrew_installer_commit}/install.sh",
418 TLS_FLAGS,
419 '-o "${installer}"',
420 "shasum -a 256 -c -",
421 '/bin/bash -p "${installer}"',
422 )
423 if any(token not in text for token in required):
424 findings.append(f"{MAC_SETUP}: Homebrew installer is not download-verify-execute")
425 if "/HEAD/install.sh" in text or _unsafe_pipeline(text):
426 findings.append(f"{MAC_SETUP}: mutable or in-memory Homebrew execution is forbidden")
427 return [*findings, *_check_macos_arm(text, docker)]
428
429
430def live_findings() -> list[str]:
431 """Return all findings across the three installer ownership surfaces."""
432 return [
433 *check_dockerfile((ROOT / DOCKERFILE).read_text(encoding="utf-8")),
434 *check_provision((ROOT / PROVISION).read_text(encoding="utf-8")),
435 *check_macos(
436 (ROOT / MAC_SETUP).read_text(encoding="utf-8"),
437 (ROOT / DOCKERFILE).read_text(encoding="utf-8"),
438 ),
439 ]
440
441
442def _arm_pin_mutations(docker: str, name: str) -> tuple[tuple[str, str], ...]:
443 """Return alternate Docker ARG spellings that can override an Arm pin."""
444 alternate = "99.9.rel1" if name == "ARM_GCC_RELEASE" else "0" * 64
445 return (
446 (docker + f"\nARG {name}={alternate}\n", "duplicate"),
447 (docker + f"\nARG {name}\\\n={alternate}\n", "continued"),
448 (docker + f"\nARG UNUSED=ok {name}={alternate}\n", "multiple-name"),
449 (docker + f"\nARG {name} {alternate}\n", "legacy"),
450 (docker + f"\nARG {name}\\\n# ignored\n={alternate}\n", "comment continuation"),
451 (
452 docker + f"\nARG {name}\\\n # indented ignored\n={alternate}\n",
453 "indented-comment continuation",
454 ),
455 (docker + f"\nARG {name}\\\n\n={alternate}\n", "empty-line continuation"),
456 )
457
458
459def _arm_pin_noninstructions(docker: str, name: str) -> tuple[tuple[str, str], ...]:
460 """Return continued non-ARG instructions that contain inert ARG-shaped text."""
461 value = _docker_arg(docker, name)
462 if value is None:
463 return ()
464 alternate = "99.9.rel1" if name == "ARM_GCC_RELEASE" else "0" * 64
465 canonical = f"ARG {name}={value}\n"
466 decoys = (
467 (f"RUN printf decoy \\\n# ignored\nARG {name}={alternate}\n", "comment"),
468 (
469 f"RUN printf decoy \\\n # indented ignored\nARG {name}={alternate}\n",
470 "indented comment",
471 ),
472 (f"RUN printf decoy \\\n\nARG {name}={alternate}\n", "empty line"),
473 )
474 return tuple(
475 (docker.replace(canonical, decoy + canonical, 1), label) for decoy, label in decoys
476 )
477
478
479def _docker_arm_selftest(docker: str) -> list[str]:
480 """Return failures from malformed/missing/duplicate Arm pin cases."""
481 failures: list[str] = []
482 release_line = "ARG ARM_GCC_RELEASE=13.3.rel1"
483 mutations = (
484 (
485 docker.replace(release_line, "ARG ARM_GCC_RELEASE=../../tmp", 1),
486 "hostile Arm release",
487 ),
488 (docker + "\n ARG ARM_GCC_RELEASE=13.3.rel1\n", "indented duplicate release"),
489 (docker.replace(f"{release_line}\n", "", 1), "missing Arm release"),
490 )
491 for mutated, label in mutations:
492 if not check_dockerfile(mutated):
493 failures.append(f"Dockerfile {label} must fire")
494 for name in ("ARM_GCC_RELEASE", *ARM_SHA_ARGS):
495 value = _docker_arg(docker, name)
496 if value is None:
497 failures.append(f"canonical {name} must exist for mutation")
498 continue
499 for mutated, label in _arm_pin_mutations(docker, name):
500 if not check_dockerfile(mutated):
501 failures.append(f"Dockerfile {label} {name} must fire")
502 for mutated, label in _arm_pin_noninstructions(docker, name):
503 if check_dockerfile(mutated):
504 failures.append(f"Dockerfile continued RUN {label} {name} must stay quiet")
505 first_hash = ARM_SHA_ARGS[0]
506 first_value = _docker_arg(docker, first_hash)
507 if first_value and not check_dockerfile(
508 docker.replace(f"ARG {first_hash}={first_value}\n", "", 1)
509 ):
510 failures.append(f"Dockerfile missing {first_hash} must fire")
511 return failures
512
513
514def _macos_arm_selftest(macos: str, docker: str) -> list[str]:
515 """Return failures from Darwin-vs-Linux archive mutation cases."""
516 failures: list[str] = []
517 mutations = (
518 (macos.replace("darwin-arm64", "aarch64", 1), "arm64 Linux archive"),
519 (
520 macos.replace("ARM_GCC_SHA256_DARWIN_ARM64", "ARM_GCC_SHA256_AARCH64", 1),
521 "arm64 Linux hash",
522 ),
523 (macos.replace("darwin-x86_64", "x86_64", 1), "x86_64 Linux archive"),
524 (
525 macos.replace("ARM_GCC_SHA256_DARWIN_X86_64", "ARM_GCC_SHA256_X86_64", 1),
526 "x86_64 Linux hash",
527 ),
528 )
529 for mutated, label in mutations:
530 if not check_macos(mutated, docker):
531 failures.append(f"macOS {label} selection must fire")
532 pairs = (
533 ("ARM_GCC_SHA256_AARCH64", "ARM_GCC_SHA256_DARWIN_ARM64", "arm64"),
534 ("ARM_GCC_SHA256_X86_64", "ARM_GCC_SHA256_DARWIN_X86_64", "x86_64"),
535 )
536 for linux_name, darwin_name, label in pairs:
537 linux_sha = _docker_arg(docker, linux_name)
538 darwin_sha = _docker_arg(docker, darwin_name)
539 if not linux_sha or not darwin_sha:
540 failures.append(f"canonical {label} hashes must exist for mutation")
541 continue
542 if not check_macos(macos, docker.replace(darwin_sha, linux_sha, 1)):
543 failures.append(f"Darwin {label} duplicate Linux hash pin must fire")
544 branch = 'printf \'%s\\t%s\\n\' "darwin-arm64" "ARM_GCC_SHA256_DARWIN_ARM64"'
545 branch_override = f'{branch}\n arm_asset_arch="aarch64"'
546 if not check_macos(macos.replace(branch, branch_override, 1), docker):
547 failures.append("appended arm64 branch override must fire")
548 readonly_line = " readonly arm_asset_arch arm_sha_arg arm_sha256"
549 final_override = f'{readonly_line}\n arm_sha256="{_docker_arg(docker, ARM_SHA_ARGS[0])}"'
550 if not check_macos(macos.replace(readonly_line, final_override, 1), docker):
551 failures.append("appended final hash override must fire")
552 dead_case = """ case "$1" in
553 dead)
554 printf '%s\\t%s\\n' "darwin-arm64" "ARM_GCC_SHA256_DARWIN_ARM64"
555 ;;
556"""
557 if not check_macos(macos.replace(' case "$1" in\n', dead_case, 1), docker):
558 failures.append("prepended dead asset case must fire")
559 return failures
560
561
562def _macos_parser_safety_mutations(macos: str) -> tuple[tuple[str, str], ...]:
563 """Return hostile parser, hash validation, release, and prefix mutations."""
564 return (
565 (macos.replace("count != 1", "count < 1", 1), "duplicate-tolerant ARG parser"),
566 (
567 macos.replace(
568 "ARM_GCC_SHA256_X86_64 ARM_GCC_SHA256_AARCH64", "ARM_GCC_SHA256_AARCH64", 1
569 ),
570 "incomplete hash-pin validation",
571 ),
572 (
573 macos.replace(r"^[0-9]+\.[0-9]+\.rel[1-9][0-9]*$", r"^.*$", 1),
574 "hostile release grammar",
575 ),
576 (
577 macos.replace(
578 'readonly arm_root="${home_root}/opt"',
579 'readonly arm_root="${home_root}"',
580 1,
581 ),
582 "prefix outside HOME/opt",
583 ),
584 )
585
586
587def _macos_safety_selftest(macos: str, docker: str) -> list[str]:
588 """Return failures from hostile parser, prefix, and deletion mutations."""
589 mutations = (
590 *_macos_parser_safety_mutations(macos),
591 *macos_cleanup_mutations(macos),
592 )
593 failures: list[str] = []
594 for mutated, label in mutations:
595 if not check_macos(mutated, docker):
596 failures.append(f"macOS {label} must fire")
597 return failures
598
599
600def _run_docker_reader(function: str, docker: str, name: str) -> tuple[bool, str]:
601 """Execute only the extracted Docker ARG reader against an inert fixture."""
602 with tempfile.TemporaryDirectory(prefix="ra8-arm-reader-") as raw:
603 dockerfile = Path(raw) / "Dockerfile"
604 dockerfile.write_text(docker, encoding="utf-8")
605 script = f'set -u\ndockerfile="$1"\ndockerfile_arg(){function}\ndockerfile_arg "$2"\n'
606 result = subprocess.run( # noqa: S603 -- extracted audited reader, fixed arguments.
607 [ # noqa: S607 -- fixed Bash from PATH runs an inert parser fixture.
608 "bash",
609 "-c",
610 script,
611 "reader-selftest",
612 str(dockerfile),
613 name,
614 ],
615 check=False,
616 capture_output=True,
617 text=True,
618 )
619 return result.returncode == 0, result.stdout
620
621
622def _docker_reader_execution_selftest(macos: str, docker: str) -> list[str]:
623 """Exercise canonical and hostile Docker forms through the macOS reader."""
624 function = _section(macos, "dockerfile_arg()", "require_arm_hash_pins()")
625 failures: list[str] = []
626 for name in ("ARM_GCC_RELEASE", *ARM_SHA_ARGS):
627 expected = _docker_arg(docker, name)
628 if expected is None:
629 failures.append(f"canonical {name} must exist for reader execution")
630 continue
631 accepted, value = _run_docker_reader(function, docker, name)
632 if not accepted or value != expected:
633 failures.append(f"macOS reader canonical {name} must stay quiet")
634 for mutated, label in _arm_pin_mutations(docker, name):
635 accepted, _value = _run_docker_reader(function, mutated, name)
636 if accepted:
637 failures.append(f"macOS reader {label} {name} must fire")
638 for mutated, label in _arm_pin_noninstructions(docker, name):
639 accepted, value = _run_docker_reader(function, mutated, name)
640 if not accepted or value != expected:
641 failures.append(f"macOS reader continued RUN {label} {name} must stay quiet")
642 return failures
643
644
645def _run_cleanup_validator(function: str, target: str, root: str, cwd: Path) -> tuple[bool, str]:
646 """Run only the canonicalizer; never invoke the recursive removal helper."""
647 script = f'set -u\ncanonical_cleanup_target(){function}\ncanonical_cleanup_target "$1" "$2"\n'
648 result = subprocess.run( # noqa: S603 -- extracted audited validator, fixed arguments.
649 [ # noqa: S607 -- fixed Bash from PATH runs an inert path fixture.
650 "bash",
651 "-c",
652 script,
653 "cleanup-selftest",
654 target,
655 root,
656 ],
657 check=False,
658 cwd=cwd,
659 capture_output=True,
660 text=True,
661 )
662 return result.returncode == 0, result.stdout.strip()
663
664
665def _cleanup_execution_selftest(macos: str) -> list[str]:
666 """Exercise physical root/target boundaries without deleting anything."""
667 function = _section(macos, "canonical_cleanup_target()", "safe_remove_tree()")
668 failures: list[str] = []
669 with tempfile.TemporaryDirectory(prefix="ra8-cleanup-guard-") as raw:
670 base = Path(raw)
671 safe_root = base / "safe-root"
672 safe_root.mkdir()
673 safe_target = safe_root / "safe-target"
674 safe_target.mkdir()
675 missing_target = safe_root / "missing-target"
676 missing_root = base / "missing-root"
677 link_root = base / "link-root"
678 link_root.symlink_to(safe_root, target_is_directory=True)
679 link_target = safe_root / "link-target"
680 link_target.symlink_to(base, target_is_directory=True)
681 file_target = safe_root / "file-target"
682 file_target.write_text("fixture", encoding="ascii")
683 cases = (
684 (safe_target, safe_root, True, "existing directory"),
685 (missing_target, safe_root, True, "missing direct child"),
686 (missing_root / "child", missing_root, True, "missing root under physical parent"),
687 (Path("/child"), Path("/"), False, "root allowed boundary"),
688 (Path("relative"), safe_root, False, "relative target"),
689 (safe_root, safe_root, False, "target equals allowed root"),
690 (link_root / "child", link_root, False, "symlinked allowed root"),
691 (link_target, safe_root, False, "symlinked target"),
692 (file_target, safe_root, False, "non-directory target"),
693 )
694 for target, root, expected, label in cases:
695 accepted, canonical = _run_cleanup_validator(function, str(target), str(root), base)
696 if accepted != expected or (accepted and canonical != str(target)):
697 failures.append(f"executable cleanup validator case failed: {label}")
698 return failures
699
700
701def _macos_privileged_exec_selftest(macos: str, docker: str) -> list[str]:
702 """Require privileged Bash for the downloaded installer."""
703 mutations = (
704 (
705 macos.replace('/bin/bash -p "${installer}"', '/bin/bash "${installer}"', 1),
706 "non-privileged Homebrew installer shell",
707 ),
708 )
709 return [label for mutated, label in mutations if not check_macos(mutated, docker)]
710
711
712def _basic_selftest_failures(docker: str, provision: str, macos: str) -> list[str]:
713 """Exercise canonical installer inputs and direct unsafe mutations."""
714 duplicate_pin = (
715 macos + '\nhomebrew_installer_commit="0000000000000000000000000000000000000000"\n'
716 )
717 cases = (
718 (bool(check_dockerfile(docker)), "canonical Dockerfile must stay quiet"),
719 (bool(check_provision(provision)), "canonical native provisioner must stay quiet"),
720 (bool(check_macos(macos, docker)), "canonical macOS installer must stay quiet"),
721 (
722 not check_dockerfile(docker.replace(DOCKER_VERIFY_MARKER["just"], "sha256sum", 1)),
723 "Dockerfile missing verification must fire",
724 ),
725 (
726 not check_dockerfile(docker.replace("--proto-redir '=https'", "", 1)),
727 "Dockerfile redirect downgrade must fire",
728 ),
729 (
730 not check_dockerfile(docker.replace(DOCKER_ARCHIVE_MEMBER["actionlint"], ".", 1)),
731 "Dockerfile unbounded archive extraction must fire",
732 ),
733 (
734 not check_dockerfile(docker + "\nRUN curl https://example.invalid/x | bash\n"),
735 "Dockerfile curl-to-shell must fire",
736 ),
737 (
738 not check_provision(provision.replace("download_verified", "download_unchecked", 1)),
739 "native provisioner helper bypass must fire",
740 ),
741 (
742 not check_provision(provision.replace(PROVISION_ARCHIVE_MEMBER["just"], ".", 1)),
743 "native provisioner unbounded archive extraction must fire",
744 ),
745 (
746 not check_macos(macos.replace("${homebrew_installer_commit}", "HEAD", 1), docker),
747 "mutable Homebrew installer ref must fire",
748 ),
749 (not check_macos(duplicate_pin, docker), "duplicate Homebrew installer commit must fire"),
750 )
751 return [label for failed, label in cases if failed]
752
753
754def selftest() -> int:
755 """Prove clean live inputs stay quiet and each unsafe direction fires."""
756 docker = (ROOT / DOCKERFILE).read_text(encoding="utf-8")
757 provision = (ROOT / PROVISION).read_text(encoding="utf-8")
758 macos = (ROOT / MAC_SETUP).read_text(encoding="utf-8")
759 failures = _basic_selftest_failures(docker, provision, macos)
760 failures.extend(
761 f"{label} must fire" for label in _macos_privileged_exec_selftest(macos, docker)
762 )
763 failures.extend(_docker_arm_selftest(docker))
764 failures.extend(_macos_arm_selftest(macos, docker))
765 failures.extend(_macos_safety_selftest(macos, docker))
766 failures.extend(_docker_reader_execution_selftest(macos, docker))
767 failures.extend(_cleanup_execution_selftest(macos))
768 if failures:
769 print("check_download_installers.py --selftest: FAIL", file=sys.stderr)
770 for failure in failures:
771 print(f" {failure}", file=sys.stderr)
772 return 1
773 print("check_download_installers.py --selftest: PASS (222 both-direction cases)")
774 return 0
775
776
777def main() -> int:
778 """Run selftests or the live installer policy audit."""
779 parser = argparse.ArgumentParser()
780 parser.add_argument("--selftest", action="store_true")
781 args = parser.parse_args()
782 if args.selftest:
783 return selftest()
784 findings = live_findings()
785 if findings:
786 print("\n".join(findings), file=sys.stderr)
787 return 1
788 print("check_download_installers.py: pinned download/verify/install flows are intact")
789 return 0
790
791
792if __name__ == "__main__":
793 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298