ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
download_installers_macos.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Structural macOS recursive-cleanup policy and adversarial fixtures."""
4
5from __future__ import annotations
6
7import re
8import shlex
9
10from check_shebangs import PRIVILEGED_BODY_PREFIX
11
12MAC_SETUP = "scripts/emu/setup_macos.sh"
13MAC_CANONICAL_CLEANUP = r""" {
14 if [[ "$#" -ne 2 ]]; then
15 echo "ERROR: cleanup validation requires one target and one allowed root." >&2
16 return 1
17 fi
18 local target="$1" allowed_root="$2"
19 local root_name root_parent root_physical target_name target_parent canonical
20 if [[ "$target" != /* ]] || [[ "$allowed_root" != /* ]] || [[ "$allowed_root" == "/" ]]; then
21 echo "ERROR: refusing relative target or unsafe cleanup root." >&2
22 return 1
23 fi
24 root_name="$(basename -- "$allowed_root")"
25 target_name="$(basename -- "$target")"
26 if [[ -z "$root_name" ]] || [[ "$root_name" == "." ]] || [[ "$root_name" == ".." ]] ||
27 [[ -z "$target_name" ]] || [[ "$target_name" == "." ]] || [[ "$target_name" == ".." ]]; then
28 echo "ERROR: refusing unsafe cleanup path component." >&2
29 return 1
30 fi
31 root_parent="$(cd "$(dirname -- "$allowed_root")" && pwd -P)" || return 1
32 root_physical="${root_parent}/${root_name}"
33 if [[ -e "$allowed_root" ]] || [[ -L "$allowed_root" ]]; then
34 if [[ ! -d "$allowed_root" ]] || [[ -L "$allowed_root" ]] ||
35 [[ "$(cd "$allowed_root" && pwd -P)" != "$root_physical" ]]; then
36 echo "ERROR: cleanup root is not a physical directory: $allowed_root" >&2
37 return 1
38 fi
39 fi
40 if [[ -d "$(dirname -- "$target")" ]]; then
41 target_parent="$(cd "$(dirname -- "$target")" && pwd -P)" || return 1
42 elif [[ "$(dirname -- "$target")" == "$root_physical" ]] && [[ ! -e "$allowed_root" ]]; then
43 target_parent="$root_physical"
44 else
45 echo "ERROR: cleanup target parent cannot be canonicalized: $target" >&2
46 return 1
47 fi
48 canonical="${target_parent}/${target_name}"
49 if [[ "$target_parent" != "$root_physical" ]] || [[ -L "$canonical" ]] ||
50 { [[ -e "$canonical" ]] && [[ ! -d "$canonical" ]]; }; then
51 echo "ERROR: cleanup target escapes its allowed root: $target" >&2
52 return 1
53 fi
54 printf '%s\n' "$canonical"
55}"""
56MAC_SAFE_REMOVE_TREE = r""" {
57 if [[ "$#" -ne 2 ]]; then
58 echo "ERROR: safe removal requires one target and one allowed root." >&2
59 return 1
60 fi
61 local target="$1" allowed_root="$2" canonical
62 canonical="$(canonical_cleanup_target "$target" "$allowed_root")" || return 1
63 if [[ "$canonical" != "$target" ]]; then
64 echo "ERROR: cleanup target changed after validation: $target" >&2
65 return 1
66 fi
67 command rm -rf -- "$canonical"
68}"""
69
70SENSITIVE_HELPER_DEFINITIONS = (
71 ("dockerfile_arg", "dockerfile_arg() {"),
72 ("require_arm_hash_pins", "require_arm_hash_pins() {"),
73 ("canonical_cleanup_target", "canonical_cleanup_target() {"),
74 ("safe_remove_tree", "safe_remove_tree() {"),
75 ("install_homebrew", "install_homebrew() ("),
76)
77SENSITIVE_HELPER_NAMES = tuple(name for name, _header in SENSITIVE_HELPER_DEFINITIONS)
78_REEXEC_START = next(
79 index for index, line in enumerate(PRIVILEGED_BODY_PREFIX) if "exec /usr/bin/env" in line
80)
81_REEXEC_LINES = PRIVILEGED_BODY_PREFIX[_REEXEC_START : _REEXEC_START + 3]
82_REEXEC_LITERAL = " ".join(line.removesuffix("\\").strip() for line in _REEXEC_LINES)
83_REEXEC_SOURCE = _REEXEC_LITERAL.removeprefix("if ! ").removesuffix("; then")
84_REEXEC_WORDS = tuple(shlex.split(_REEXEC_SOURCE))
85_REEXEC_RECORD = "if ! " + " ".join(_REEXEC_WORDS)
86ALLOWED_INDIRECT_EXECUTION = (
87 (
88 _REEXEC_RECORD,
89 (),
90 _REEXEC_WORDS[0],
91 _REEXEC_WORDS[1:],
92 ),
93)
94
95
96CLEANUP_NAMES = (
97 "home_root",
98 "arm_root",
99 "arm_prefix",
100 "work_candidate",
101 "work_root",
102 "work",
103 "arm_work_candidate",
104 "arm_work_root",
105 "arm_work",
106 "emu_root",
107 "emu_build",
108)
109CLEANUP_STATIC_READONLY = ('readonly arm_root="${home_root}/opt"',)
110CLEANUP_ASSIGNMENT_PAIRS = (
111 (
112 'home_root="$(cd "$HOME" && pwd -P)" || exit 1',
113 "readonly home_root",
114 ),
115 (
116 'arm_prefix="$(canonical_cleanup_target '
117 '"${arm_root}/arm-gnu-toolchain-${arm_version}" "$arm_root")" || exit 1',
118 "readonly arm_prefix",
119 ),
120 (
121 'work_candidate="$(mktemp -d)" || return 1',
122 "readonly work_candidate",
123 ),
124 (
125 'work_root="$(cd "$(dirname -- "$work_candidate")" && pwd -P)" || return 1',
126 "readonly work_root",
127 ),
128 (
129 'work="$(canonical_cleanup_target "$work_candidate" "$work_root")" || return 1',
130 "readonly work",
131 ),
132 (
133 'arm_work_candidate="$(mktemp -d)" || exit 1',
134 "readonly arm_work_candidate",
135 ),
136 (
137 'arm_work_root="$(cd "$(dirname -- "$arm_work_candidate")" && pwd -P)" || exit 1',
138 "readonly arm_work_root",
139 ),
140 (
141 'arm_work="$(canonical_cleanup_target "$arm_work_candidate" "$arm_work_root")" || exit 1',
142 "readonly arm_work",
143 ),
144 (
145 'emu_root="$(cd "$root/tools/ra8_emulator" && pwd -P)" || exit 1',
146 "readonly emu_root",
147 ),
148 (
149 'emu_build="$(canonical_cleanup_target "${emu_root}/build" "$emu_root")" || exit 1',
150 "readonly emu_build",
151 ),
152)
153CLEANUP_DEFINITION_LINES = CLEANUP_STATIC_READONLY + tuple(
154 line for pair in CLEANUP_ASSIGNMENT_PAIRS for line in pair
155)
156CLEANUP_DEFINITION_ORDER = (
157 (
158 *CLEANUP_ASSIGNMENT_PAIRS[0],
159 'if [[ -z "$home_root" ]]; then',
160 CLEANUP_STATIC_READONLY[0],
161 *CLEANUP_ASSIGNMENT_PAIRS[1],
162 'if [[ -z "$arm_prefix" ]]; then',
163 'safe_remove_tree "$arm_prefix" "$arm_root"',
164 ),
165 (
166 *CLEANUP_ASSIGNMENT_PAIRS[2],
167 'if [[ -z "$work_candidate" ]]; then',
168 *CLEANUP_ASSIGNMENT_PAIRS[3],
169 'if [[ -z "$work_root" ]]; then',
170 *CLEANUP_ASSIGNMENT_PAIRS[4],
171 '[[ -n "$work" ]] || return 1',
172 'trap \'safe_remove_tree "${work}" "${work_root}"\' EXIT',
173 ),
174 (
175 *CLEANUP_ASSIGNMENT_PAIRS[5],
176 'if [[ -z "$arm_work_candidate" ]]; then',
177 *CLEANUP_ASSIGNMENT_PAIRS[6],
178 'if [[ -z "$arm_work_root" ]]; then',
179 *CLEANUP_ASSIGNMENT_PAIRS[7],
180 '[[ -n "$arm_work" ]] || exit 1',
181 'trap \'safe_remove_tree "$arm_work" "$arm_work_root"\' EXIT',
182 ),
183 (
184 *CLEANUP_ASSIGNMENT_PAIRS[8],
185 'if [[ -z "$emu_root" ]]; then',
186 *CLEANUP_ASSIGNMENT_PAIRS[9],
187 '[[ -n "$emu_build" ]] || exit 1',
188 'safe_remove_tree "$emu_build" "$emu_root"',
189 ),
190)
191
192
193def _active(text: str) -> str:
194 """Discard comment-only lines and join shell continuations."""
195 code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
196 return re.sub(r"\\\n\s*", " ", code)
197
198
199def _normalise_shell(text: str) -> str:
200 """Collapse insignificant shell whitespace for exact statement checks."""
201 return " ".join(_active(text).split())
202
203
204def _section(text: str, start: str, end: str) -> str:
205 """Return a required text section, or an empty string when anchors drift."""
206 _before, marker, rest = text.partition(start)
207 if not marker:
208 return ""
209 body, marker, _after = rest.partition(end)
210 return body if marker else ""
211
212
213def _without_heredocs(text: str) -> str:
214 """Remove literal heredoc bodies before structural command decoding."""
215 kept: list[str] = []
216 delimiter: str | None = None
217 for line in text.splitlines():
218 if delimiter is not None:
219 if line.strip() == delimiter:
220 delimiter = None
221 continue
222 kept.append(line)
223 match = re.search(r"<<-?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1", line)
224 if match:
225 delimiter = match.group(2)
226 return "\n".join(kept)
227
228
229def _protect_parameter_lengths(text: str) -> str:
230 """Keep Bash ``${#name}`` expansions from becoming shlex comments."""
231 return re.sub(r"\$\{#([A-Za-z_][A-Za-z0-9_]*)(\‍[@\‍])?\}", r"${RA8_LENGTH_\1\2}", text)
232
233
234def _shell_words(text: str) -> tuple[str, ...]:
235 """Decode shell quoting/escaping into conservative lexical words."""
236 code = _protect_parameter_lengths(_active(_without_heredocs(text)))
237 lexer = shlex.shlex(code, posix=True, punctuation_chars=";&|()<>")
238 lexer.commenters = "#"
239 lexer.whitespace_split = True
240 try:
241 return tuple(word for word in lexer if word.strip(";&|()<>"))
242 except ValueError:
243 return ()
244
245
246def _shell_segments(text: str) -> tuple[tuple[str, tuple[str, ...]], ...] | None:
247 """Split decoded shell commands while respecting multiline quoting."""
248 code = _protect_parameter_lengths(_active(_without_heredocs(text)))
249 lexer = shlex.shlex(code, posix=True, punctuation_chars=";&|\n")
250 lexer.commenters = "#"
251 lexer.whitespace = " \t\r"
252 lexer.whitespace_split = True
253 try:
254 words = tuple(lexer)
255 except ValueError:
256 return None
257 segments: list[tuple[str, tuple[str, ...]]] = []
258 start = 0
259 for index, word in enumerate((*words, ";")):
260 if set(word) <= set(";&|\n"):
261 if index > start:
262 command = words[start:index]
263 segments.append((" ".join(command), command))
264 start = index + 1
265 return tuple(segments)
266
267
268def _consume_env_options(pending: list[str]) -> tuple[str, ...] | None:
269 """Consume ordinary env options, returning unsafe split-string arguments."""
270 while pending:
271 option = pending[0]
272 if option.startswith(("-S", "--split-string")):
273 return tuple(pending)
274 if option in {"-u", "--unset", "-C", "--chdir"}:
275 del pending[:2]
276 continue
277 if option == "--":
278 pending.pop(0)
279 break
280 if option.startswith("-") or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", option):
281 pending.pop(0)
282 continue
283 break
284 return None
285
286
287def _normalise_command(
288 words: tuple[str, ...],
289) -> tuple[tuple[str, ...], str, tuple[str, ...]] | None:
290 """Return wrapper prefixes, decoded command, and arguments."""
291 pending = list(words)
292 while pending and pending[0] in {"!", "{", "}", "if", "elif", "then", "while", "until", "do"}:
293 pending.pop(0)
294 while pending and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", pending[0]):
295 pending.pop(0)
296 prefixes: list[str] = []
297 while pending:
298 command = pending.pop(0).rsplit("/", 1)[-1]
299 if command == "command":
300 prefixes.append(command)
301 if pending and pending[0] in {"-v", "-V"}:
302 return tuple(prefixes), "command-v", tuple(pending[1:])
303 while pending and pending[0] in {"--", "-p"}:
304 pending.pop(0)
305 continue
306 if command == "builtin":
307 prefixes.append(command)
308 if pending and pending[0] == "--":
309 pending.pop(0)
310 continue
311 if command == "env":
312 prefixes.append(command)
313 dynamic = _consume_env_options(pending)
314 if dynamic is not None:
315 return tuple(prefixes), "env-dynamic", dynamic
316 continue
317 return tuple(prefixes), command, tuple(pending)
318 return None
319
320
321def _command_records(
322 text: str,
323) -> tuple[tuple[str, tuple[str, ...], str, tuple[str, ...]], ...] | None:
324 """Return decoded command records after normalizing execution wrappers."""
325 segments = _shell_segments(text)
326 if segments is None:
327 return None
328 records = []
329 for line, words in segments:
330 command = _normalise_command(words)
331 if command is not None:
332 prefixes, name, arguments = command
333 records.append((line, prefixes, name, arguments))
334 return tuple(records)
335
336
337def _recursive_option(word: str) -> bool:
338 """Return whether one decoded word enables recursive removal."""
339 return bool(re.fullmatch(r"--recursive(?:=.*)?|-[A-Za-z]*[rR][A-Za-z]*", word))
340
341
342def _dynamic_command(command: str) -> bool:
343 """Return whether a decoded command is selected through expansion."""
344 return command.startswith("$") or "$(" in command or "`" in command
345
346
347def _shell_interpreter(command: str) -> bool:
348 """Return whether a normalized basename can execute shell source with -c."""
349 return command.endswith("sh") or command in {"fish", "nu"}
350
351
352def _sensitive_function_definitions(text: str) -> tuple[tuple[str, str], ...]:
353 """Return active definitions of helpers that anchor the safety policy."""
354 names = "|".join(re.escape(name) for name in SENSITIVE_HELPER_NAMES)
355 pattern = re.compile(
356 rf"(?m)^[ \t]*(?:"
357 rf"function[ \t]+({names})(?:[ \t]*\‍(\‍))?|"
358 rf"({names})[ \t]*\‍(\‍)"
359 r")[ \t\r\n]*(?:\{|\‍()"
360 )
361 active = _active(_without_heredocs(text))
362 return tuple(
363 (match.group(1) or match.group(2), match.group(0).strip())
364 for match in pattern.finditer(active)
365 )
366
367
368def _function_flag(argument: str) -> bool:
369 """Return whether one builtin option selects function attributes."""
370 return argument in {"--function", "--functions"} or bool(
371 re.fullmatch(r"-[A-Za-z]*[fF][A-Za-z]*", argument)
372 )
373
374
375def _check_sensitive_helpers(text: str) -> list[str]:
376 """Require one canonical definition and forbid function-attribute mutation."""
377 findings: list[str] = []
378 if _sensitive_function_definitions(text) != SENSITIVE_HELPER_DEFINITIONS:
379 findings.append(f"{MAC_SETUP}: sensitive helper definition inventory is not exact")
380 records = _command_records(text)
381 function_builtins = {"unset", "readonly", "declare", "typeset", "local", "export"}
382 if records is not None and any(
383 command in function_builtins
384 and any(_function_flag(argument) for argument in arguments)
385 and any(
386 argument in SENSITIVE_HELPER_NAMES or argument.startswith("$") for argument in arguments
387 )
388 for _line, _prefixes, command, arguments in records
389 ):
390 findings.append(f"{MAC_SETUP}: sensitive helper function mutation is forbidden")
391 return findings
392
393
394def _check_cleanup_tokens(text: str) -> list[str]:
395 """Reject direct, prefixed, or dynamically assembled execution surfaces."""
396 findings: list[str] = []
397 words = _shell_words(text)
398 direct_rm = tuple(word for word in words if word.rsplit("/", 1)[-1] == "rm")
399 if direct_rm != ("rm",):
400 findings.append(f"{MAC_SETUP}: raw recursive removal inventory is not exact")
401 records = _command_records(text)
402 if records is None:
403 return [*findings, f"{MAC_SETUP}: shell command structure cannot be decoded"]
404 evals = tuple((line, prefixes) for line, prefixes, cmd, _args in records if cmd == "eval")
405 if evals != (("eval $($brew shellenv)", ()),):
406 findings.append(f"{MAC_SETUP}: eval inventory is not exact")
407 dynamic = tuple(
408 (line, prefixes, cmd) for line, prefixes, cmd, _args in records if _dynamic_command(cmd)
409 )
410 if dynamic != (("$brew install ${brew_missing[@]}", (), "$brew"),):
411 findings.append(f"{MAC_SETUP}: dynamic command inventory is not exact")
412 if any(
413 _shell_interpreter(cmd) and any(re.fullmatch(r"-[A-Za-z]*c[A-Za-z]*", arg) for arg in args)
414 for _line, _prefixes, cmd, args in records
415 ):
416 findings.append(f"{MAC_SETUP}: dynamic shell wrapper is forbidden")
417 indirect = tuple(
418 record
419 for record in records
420 if record[2] in {"env-dynamic", "exec", "source", ".", "xargs", "parallel"}
421 )
422 if indirect != ALLOWED_INDIRECT_EXECUTION:
423 findings.append(f"{MAC_SETUP}: indirect execution inventory is not exact")
424 if any(words[index : index + 2] == ("set", "--") for index in range(len(words) - 1)):
425 findings.append(f"{MAC_SETUP}: positional-command indirection is forbidden")
426 assignments = (
427 word.split("=", 1)[1] for word in words if re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", word)
428 )
429 if any(value.rsplit("/", 1)[-1] == "rm" or _recursive_option(value) for value in assignments):
430 findings.append(f"{MAC_SETUP}: command/flag assignment indirection is forbidden")
431 if re.search(r"\$\{[^}\n]*(?::?[-+=?])(?:[^}\n]*/)?rm(?:[^A-Za-z]|})", text):
432 findings.append(f"{MAC_SETUP}: parameter-default removal command is forbidden")
433 return findings
434
435
436def _tokens_in_order(text: str, tokens: tuple[str, ...]) -> bool:
437 """Return whether unique tokens occur once in strict source order."""
438 offsets = tuple(text.find(token) for token in tokens)
439 return all(
440 offset >= 0 and text.count(token) == 1
441 for offset, token in zip(offsets, tokens, strict=True)
442 ) and offsets == tuple(sorted(offsets))
443
444
445def _lines_in_order(lines: tuple[str, ...], tokens: tuple[str, ...]) -> bool:
446 """Return whether exact logical lines occur once in strict order."""
447 positions: list[int] = []
448 for token in tokens:
449 matches = tuple(index for index, line in enumerate(lines) if line == token)
450 if len(matches) != 1:
451 return False
452 positions.append(matches[0])
453 return positions == sorted(positions)
454
455
456def _check_cleanup_calls(text: str) -> list[str]:
457 """Require the exact safe helper and deferred-trap call inventory."""
458 active_lines = tuple(line.strip() for line in _active(text).splitlines())
459 call_lines = tuple(line for line in active_lines if "safe_remove_tree" in line)
460 expected_calls = (
461 "safe_remove_tree() {",
462 'trap \'safe_remove_tree "${work}" "${work_root}"\' EXIT',
463 'trap \'safe_remove_tree "$arm_work" "$arm_work_root"\' EXIT',
464 'safe_remove_tree "$arm_prefix" "$arm_root"',
465 'safe_remove_tree "$emu_build" "$emu_root"',
466 )
467 trap_lines = tuple(line for line in active_lines if line.startswith("trap "))
468 if call_lines == expected_calls and trap_lines == expected_calls[1:3]:
469 return []
470 return [f"{MAC_SETUP}: safe removal call/trap inventory is not exact"]
471
472
473def _mentions_cleanup_name(word: str) -> bool:
474 """Return whether a decoded mutator operand names a cleanup variable."""
475 return any(
476 re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", word)
477 for name in CLEANUP_NAMES
478 )
479
480
481def _direct_cleanup_mutation(line: str) -> bool:
482 """Return whether a logical line directly assigns a cleanup variable."""
483 names = "|".join(re.escape(name) for name in CLEANUP_NAMES)
484 direct = rf"^(?:{names})(?:\‍[[^]]*\‍])?\s*(?:\+?=)"
485 arithmetic = rf"^\‍(\‍(.*(?:{names})\s*(?:\+\+|--|[+*/%&|^-]?=)"
486 return bool(re.search(direct, line) or re.search(arithmetic, line))
487
488
489def _builtin_cleanup_mutation(command: str, arguments: tuple[str, ...]) -> bool:
490 """Return whether one assignment builtin can mutate a cleanup variable."""
491 allowed_readonly = {
492 tuple(shlex.split(definition))[1:]
493 for definition in CLEANUP_DEFINITION_LINES
494 if definition.startswith("readonly ")
495 }
496 if command == "readonly" and arguments in allowed_readonly:
497 return False
498 if command in {"declare", "typeset", "local"} and "-n" in arguments:
499 return True
500 if command == "printf":
501 if "-v" not in arguments:
502 return False
503 target = arguments[arguments.index("-v") + 1 : arguments.index("-v") + 2]
504 return bool(target and (_mentions_cleanup_name(target[0]) or target[0].startswith("$")))
505 mutators = {
506 "declare",
507 "typeset",
508 "local",
509 "readonly",
510 "export",
511 "unset",
512 "read",
513 "mapfile",
514 "readarray",
515 "let",
516 }
517 return command in mutators and any(_mentions_cleanup_name(arg) for arg in arguments)
518
519
520def _check_cleanup_mutations(text: str) -> list[str]:
521 """Reject every non-authoritative way to mutate a cleanup variable."""
522 logical_lines = tuple(line.strip() for line in _active(text).splitlines())
523 if any(
524 line not in CLEANUP_DEFINITION_LINES and _direct_cleanup_mutation(line)
525 for line in logical_lines
526 ):
527 return [f"{MAC_SETUP}: cleanup variable has a direct or compound override"]
528 records = _command_records(text)
529 if records is None:
530 return [f"{MAC_SETUP}: cleanup variable command structure cannot be decoded"]
531 if any(_builtin_cleanup_mutation(command, args) for _line, _p, command, args in records):
532 return [f"{MAC_SETUP}: cleanup variable uses a forbidden assignment builtin"]
533 return []
534
535
536def _check_cleanup_variables(text: str) -> list[str]:
537 """Require status-preserving assignments, adjacent readonly, and use order."""
538 findings: list[str] = []
539 logical_lines = tuple(line.strip() for line in _active(text).splitlines())
540 if any(logical_lines.count(definition) != 1 for definition in CLEANUP_DEFINITION_LINES):
541 findings.append(f"{MAC_SETUP}: cleanup assignment/readonly inventory is not exact")
542 if any(
543 not any(
544 tuple(logical_lines[index : index + 2]) == pair for index in range(len(logical_lines))
545 )
546 for pair in CLEANUP_ASSIGNMENT_PAIRS
547 ):
548 findings.append(f"{MAC_SETUP}: cleanup assignment and readonly are not adjacent")
549 if any(not _lines_in_order(logical_lines, tokens) for tokens in CLEANUP_DEFINITION_ORDER):
550 findings.append(f"{MAC_SETUP}: cleanup definition/guard/use order is not exact")
551 return [*findings, *_check_cleanup_mutations(text)]
552
553
554def check_macos_cleanup(text: str) -> list[str]:
555 """Require canonical readonly targets and one safe deletion primitive."""
556 findings = _check_cleanup_tokens(text)
557 canonical = _section(text, "canonical_cleanup_target()", "safe_remove_tree()")
558 safe_remove = _section(text, "safe_remove_tree()", 'arm_release="')
559 if _normalise_shell(canonical) != _normalise_shell(MAC_CANONICAL_CLEANUP):
560 findings.append(f"{MAC_SETUP}: cleanup canonicalizer active body is not exact")
561 if _normalise_shell(safe_remove) != _normalise_shell(MAC_SAFE_REMOVE_TREE):
562 findings.append(f"{MAC_SETUP}: safe removal active body is not exact")
563 return [
564 *findings,
565 *_check_sensitive_helpers(text),
566 *_check_cleanup_calls(text),
567 *_check_cleanup_variables(text),
568 ]
569
570
571def _macos_direct_delete_mutations(macos: str) -> tuple[tuple[str, str], ...]:
572 """Return direct, additive, multi-target, and raw-trap removals."""
573 arm_call = ' safe_remove_tree "$arm_prefix" "$arm_root"'
574 return (
575 (macos.replace(f"{arm_call}\n", "", 1), "removed Arm safe removal"),
576 (
577 macos.replace(arm_call, ' safe_remove_tree "$arm_prefix" "$home_root"', 1),
578 "wrong allowed root",
579 ),
580 (
581 macos.replace(arm_call, ' command rm -rf -- "$arm_prefix"', 1),
582 "direct Arm removal",
583 ),
584 (macos + '\nrm -rf -- "$HOME"\n', "appended HOME removal"),
585 (macos + '\nrm -rf -- "$arm_prefix/.."\n', "appended Arm parent removal"),
586 (macos + '\nrm -r -f -- "$HOME"\n', "split recursive flags"),
587 (macos + '\nrm --recursive -- "$HOME"\n', "long recursive flag"),
588 (macos + '\nrm -rf -- "$arm_prefix" "$HOME"\n', "multiple removal targets"),
589 (
590 macos.replace(arm_call, f'{arm_call}\n rm -rf -- "$HOME"', 1),
591 "unsafe removal after safe helper",
592 ),
593 (macos + '\nsafe_remove_tree "$emu_build" "$emu_root"\n', "additive safe helper call"),
594 (
595 macos.replace(
596 'trap \'safe_remove_tree "${work}" "${work_root}"\' EXIT',
597 "trap 'rm -rf -- \"${work}\"' EXIT",
598 1,
599 ),
600 "deferred raw trap removal",
601 ),
602 )
603
604
605def _macos_indirect_delete_mutations(macos: str) -> tuple[tuple[str, str], ...]:
606 """Return command, flag, positional, wrapper, and immutability indirections."""
607 work_pair = " " + "\n ".join(CLEANUP_ASSIGNMENT_PAIRS[4])
608 emu_pair = " " + "\n ".join(CLEANUP_ASSIGNMENT_PAIRS[9])
609 return (
610 (macos + '\nrm_flags=-rf\nrm "$rm_flags" -- "$HOME"\n', "recursive flag variable"),
611 (macos + '\ndelete_cmd=rm\n"$delete_cmd" -rf -- "$HOME"\n', "removal command variable"),
612 (macos + '\nset -- rm -rf -- "$HOME"\n"$@"\n', "positional command indirection"),
613 (macos + '\nr""m -rf -- "$HOME"\n', "quoted removal command"),
614 (macos + '\n\\rm -rf -- "$HOME"\n', "escaped removal command"),
615 (
616 macos + '\nwipe_tree() { command rm -rf -- "$HOME"; }\nwipe_tree\n',
617 "recursive removal wrapper",
618 ),
619 (macos + '\n"${delete_cmd:-rm}" -rf -- "$HOME"\n', "parameter-default command"),
620 (macos + "\neval 'rm -rf -- \"$HOME\"'\n", "eval removal"),
621 (macos + "\nsh -c 'rm -rf -- \"$HOME\"'\n", "dynamic shell wrapper"),
622 (macos + '\n"$DELETE_CMD" -rf -- "$HOME"\n', "external dynamic command"),
623 (
624 macos + '\n"$DELETE_CMD" "$RM_FLAGS" -- "$HOME"\n',
625 "external dynamic command and flags",
626 ),
627 (
628 macos.replace(
629 work_pair,
630 work_pair.replace("\n readonly work", '\n work="$HOME"\n readonly work'),
631 1,
632 ),
633 "intervening work override before readonly",
634 ),
635 (
636 macos.replace(
637 work_pair,
638 work_pair.replace("\n readonly work", "\n echo wait\n readonly work"),
639 1,
640 ),
641 "intervening command before readonly",
642 ),
643 (
644 macos.replace(emu_pair, CLEANUP_ASSIGNMENT_PAIRS[9][0], 1),
645 "missing emulator readonly",
646 ),
647 )
648
649
650def _macos_variable_mutations(macos: str) -> tuple[tuple[str, str], ...]:
651 """Return alternate Bash assignment forms targeting cleanup variables."""
652 arm_static = CLEANUP_STATIC_READONLY[0]
653 return (
654 (macos + "\ndeclare arm_root=/tmp\n", "declare cleanup override"),
655 (macos + "\ntypeset arm_prefix=/tmp\n", "typeset cleanup override"),
656 (macos + "\nlocal emu_build=/tmp\n", "local cleanup shadow"),
657 (macos + "\nprintf -v work %s /tmp\n", "printf-v cleanup override"),
658 (macos + "\nread arm_work_root </dev/null\n", "read cleanup override"),
659 (macos + "\nmapfile work_root </dev/null\n", "mapfile cleanup override"),
660 (macos + "\nunset arm_prefix\n", "unset cleanup target"),
661 (macos + "\nemu_build+=(/tmp)\n", "compound cleanup assignment"),
662 (macos + "\narm_root[0]=/tmp\n", "array cleanup assignment"),
663 (
664 macos + "\ndeclare -n cleanup_ref=arm_prefix\ncleanup_ref=/tmp\n",
665 "indirect cleanup assignment",
666 ),
667 (macos + "\n((work_root=0))\n", "arithmetic cleanup assignment"),
668 (
669 macos.replace(arm_static, arm_static + '\narm_root="/tmp"\n', 1),
670 "intervening cleanup-root override",
671 ),
672 )
673
674
675def _macos_prefixed_execution_mutations(macos: str) -> tuple[tuple[str, str], ...]:
676 """Return prefixed and indirect execution forms that must fail closed."""
677 return (
678 (macos + "\ncommand eval 'true'\n", "command-prefixed eval"),
679 (macos + "\nbuiltin eval 'true'\n", "builtin-prefixed eval"),
680 (macos + "\nenv eval 'true'\n", "env-prefixed eval"),
681 (macos + "\ncommand sh -c 'true'\n", "command-prefixed shell-c"),
682 (macos + "\nenv bash -c 'true'\n", "env-prefixed shell-c"),
683 (macos + "\nbuiltin sh -c 'true'\n", "builtin-prefixed shell-c"),
684 (macos + '\ncommand "$DELETE_CMD" "$HOME"\n', "command-prefixed dynamic command"),
685 (macos + '\nbuiltin "$DELETE_CMD" "$HOME"\n', "builtin-prefixed dynamic command"),
686 (macos + '\nenv "$DELETE_CMD" "$HOME"\n', "env-prefixed dynamic command"),
687 (macos + '\nexec "$DELETE_CMD" "$HOME"\n', "exec dynamic command"),
688 (macos + '\nsource "$DELETE_SCRIPT"\n', "source dynamic command"),
689 (macos + '\nprintf x | xargs "$DELETE_CMD"\n', "xargs dynamic command"),
690 (macos + "\ncommand -- eval 'true'\n", "optioned command-prefixed eval"),
691 (macos + "\nbuiltin -- eval 'true'\n", "optioned builtin-prefixed eval"),
692 (macos + "\nenv -i command eval 'true'\n", "nested env-command eval"),
693 (macos + "\nenv -C /tmp sh -c 'true'\n", "optioned env shell-c"),
694 (macos + "\nenv -S 'eval true'\n", "env split-string execution"),
695 (macos + "\nenv --split-string='eval true'\n", "long env split-string execution"),
696 (macos + "\nenv -S'eval true'\n", "attached env split-string execution"),
697 (macos + "\n/bin/zsh -c 'true'\n", "absolute zsh shell-c"),
698 (macos + "\nzsh -c 'true'\n", "zsh shell-c"),
699 (macos + "\ndash -c 'true'\n", "dash shell-c"),
700 (macos + "\n/usr/bin/ksh -c 'true'\n", "absolute ksh shell-c"),
701 (macos + "\nenv /bin/ash -c 'true'\n", "env-prefixed ash shell-c"),
702 (macos + "\ncommand /usr/local/bin/fish -c 'true'\n", "command-prefixed fish shell-c"),
703 (macos + "\nenv nu -c 'true'\n", "env-prefixed nu shell-c"),
704 )
705
706
707def _macos_helper_definition_mutations(macos: str) -> tuple[tuple[str, str], ...]:
708 """Return duplicate and alternate definitions of policy-sensitive helpers."""
709 return (
710 (macos + "\ndockerfile_arg() { :; }\n", "later Docker parser definition"),
711 ("safe_remove_tree() { :; }\n" + macos, "earlier removal-helper definition"),
712 (
713 macos + "\nfunction canonical_cleanup_target { :; }\n",
714 "later function-keyword canonicalizer",
715 ),
716 (
717 macos + "\nfunction require_arm_hash_pins() { :; }\n",
718 "later function-keyword pin validator",
719 ),
720 (macos + "\ninstall_homebrew() ( : )\n", "later Homebrew helper definition"),
721 (
722 macos + "\nfunction safe_remove_tree\n{\n :\n}\n",
723 "multiline function-keyword removal helper",
724 ),
725 (
726 "dockerfile_arg ()\n{\n :\n}\n" + macos,
727 "earlier multiline Docker parser",
728 ),
729 )
730
731
732def _macos_helper_mutation_commands(macos: str) -> tuple[tuple[str, str], ...]:
733 """Return function removal and attribute mutation commands."""
734 return (
735 (macos + "\nunset -f safe_remove_tree\n", "unset removal helper"),
736 (macos + "\ncommand unset -f dockerfile_arg\n", "prefixed unset Docker parser"),
737 (
738 macos + "\nbuiltin readonly -f canonical_cleanup_target\n",
739 "readonly canonicalizer mutation",
740 ),
741 (macos + "\ndeclare -fx require_arm_hash_pins\n", "declare pin-validator mutation"),
742 (macos + "\nexport -f install_homebrew\n", "export Homebrew helper"),
743 (macos + "\ntypeset -f dockerfile_arg\n", "typeset Docker parser"),
744 (macos + '\nunset -f "$HELPER"\n', "dynamic function removal"),
745 (macos + "\nlocal -f safe_remove_tree\n", "local removal helper attribute"),
746 )
747
748
749def _macos_delete_safety_mutations(macos: str) -> tuple[tuple[str, str], ...]:
750 """Return every hostile recursive-deletion or execution mutation."""
751 return (
752 *_macos_direct_delete_mutations(macos),
753 *_macos_indirect_delete_mutations(macos),
754 *_macos_variable_mutations(macos),
755 *_macos_prefixed_execution_mutations(macos),
756 *_macos_helper_definition_mutations(macos),
757 *_macos_helper_mutation_commands(macos),
758 )
759
760
761def _macos_guard_safety_mutations(macos: str) -> tuple[tuple[str, str], ...]:
762 """Return hostile physical-boundary canonicalizer mutations."""
763 return (
764 (
765 macos.replace(
766 '[[ "$target_parent" != "$root_physical" ]]',
767 '[[ "$target_parent" == "$root_physical" ]]',
768 1,
769 ),
770 "inverted cleanup-root boundary",
771 ),
772 (
773 macos.replace('[[ -L "$canonical" ]]', '[[ ! -L "$canonical" ]]', 1),
774 "symlink cleanup target allowed",
775 ),
776 )
777
778
779def macos_cleanup_mutations(macos: str) -> tuple[tuple[str, str], ...]:
780 """Return all hostile macOS recursive-cleanup mutations."""
781 return (*_macos_delete_safety_mutations(macos), *_macos_guard_safety_mutations(macos))