3"""Structural macOS recursive-cleanup policy and adversarial fixtures."""
5from __future__
import annotations
10from check_shebangs
import PRIVILEGED_BODY_PREFIX
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
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
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
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
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"
45 echo "ERROR: cleanup target parent cannot be canonicalized: $target" >&2
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
54 printf '%s\n' "$canonical"
56MAC_SAFE_REMOVE_TREE =
r""" {
57 if [[ "$#" -ne 2 ]]; then
58 echo "ERROR: safe removal requires one target and one allowed root." >&2
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
67 command rm -rf -- "$canonical"
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() ("),
77SENSITIVE_HELPER_NAMES = tuple(name
for name, _header
in SENSITIVE_HELPER_DEFINITIONS)
79 index
for index, line
in enumerate(PRIVILEGED_BODY_PREFIX)
if "exec /usr/bin/env" in line
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 = (
103 "arm_work_candidate",
109CLEANUP_STATIC_READONLY = (
'readonly arm_root="${home_root}/opt"',)
110CLEANUP_ASSIGNMENT_PAIRS = (
112 'home_root="$(cd "$HOME" && pwd -P)" || exit 1',
113 "readonly home_root",
116 'arm_prefix="$(canonical_cleanup_target '
117 '"${arm_root}/arm-gnu-toolchain-${arm_version}" "$arm_root")" || exit 1',
118 "readonly arm_prefix",
121 'work_candidate="$(mktemp -d)" || return 1',
122 "readonly work_candidate",
125 'work_root="$(cd "$(dirname -- "$work_candidate")" && pwd -P)" || return 1',
126 "readonly work_root",
129 'work="$(canonical_cleanup_target "$work_candidate" "$work_root")" || return 1',
133 'arm_work_candidate="$(mktemp -d)" || exit 1',
134 "readonly arm_work_candidate",
137 'arm_work_root="$(cd "$(dirname -- "$arm_work_candidate")" && pwd -P)" || exit 1',
138 "readonly arm_work_root",
141 'arm_work="$(canonical_cleanup_target "$arm_work_candidate" "$arm_work_root")" || exit 1',
145 'emu_root="$(cd "$root/tools/ra8_emulator" && pwd -P)" || exit 1',
149 'emu_build="$(canonical_cleanup_target "${emu_root}/build" "$emu_root")" || exit 1',
150 "readonly emu_build",
153CLEANUP_DEFINITION_LINES = CLEANUP_STATIC_READONLY + tuple(
154 line
for pair
in CLEANUP_ASSIGNMENT_PAIRS
for line
in pair
156CLEANUP_DEFINITION_ORDER = (
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"',
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',
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',
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"',
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)
199def _normalise_shell(text: str) -> str:
200 """Collapse insignificant shell whitespace for exact statement checks."""
201 return " ".join(_active(text).split())
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)
209 body, marker, _after = rest.partition(end)
210 return body
if marker
else ""
213def _without_heredocs(text: str) -> str:
214 """Remove literal heredoc bodies before structural command decoding."""
216 delimiter: str |
None =
None
217 for line
in text.splitlines():
218 if delimiter
is not None:
219 if line.strip() == delimiter:
223 match = re.search(
r"<<-?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1", line)
225 delimiter = match.group(2)
226 return "\n".join(kept)
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)
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
241 return tuple(word
for word
in lexer
if word.strip(
";&|()<>"))
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
257 segments: list[tuple[str, tuple[str, ...]]] = []
259 for index, word
in enumerate((*words,
";")):
260 if set(word) <= set(
";&|\n"):
262 command = words[start:index]
263 segments.append((
" ".join(command), command))
265 return tuple(segments)
268def _consume_env_options(pending: list[str]) -> tuple[str, ...] |
None:
269 """Consume ordinary env options, returning unsafe split-string arguments."""
272 if option.startswith((
"-S",
"--split-string")):
273 return tuple(pending)
274 if option
in {
"-u",
"--unset",
"-C",
"--chdir"}:
280 if option.startswith(
"-")
or re.fullmatch(
r"[A-Za-z_][A-Za-z0-9_]*=.*", option):
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"}:
294 while pending
and re.fullmatch(
r"[A-Za-z_][A-Za-z0-9_]*=.*", pending[0]):
296 prefixes: list[str] = []
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"}:
306 if command ==
"builtin":
307 prefixes.append(command)
308 if pending
and pending[0] ==
"--":
312 prefixes.append(command)
313 dynamic = _consume_env_options(pending)
314 if dynamic
is not None:
315 return tuple(prefixes),
"env-dynamic", dynamic
317 return tuple(prefixes), command, tuple(pending)
323) -> tuple[tuple[str, tuple[str, ...], str, tuple[str, ...]], ...] |
None:
324 """Return decoded command records after normalizing execution wrappers."""
325 segments = _shell_segments(text)
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)
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))
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
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"}
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(
357 rf
"function[ \t]+({names})(?:[ \t]*\(\))?|"
358 rf
"({names})[ \t]*\(\)"
359 r")[ \t\r\n]*(?:\{|\()"
361 active = _active(_without_heredocs(text))
363 (match.group(1)
or match.group(2), match.group(0).strip())
364 for match
in pattern.finditer(active)
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)
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)
386 argument
in SENSITIVE_HELPER_NAMES
or argument.startswith(
"$")
for argument
in arguments
388 for _line, _prefixes, command, arguments
in records
390 findings.append(f
"{MAC_SETUP}: sensitive helper function mutation is forbidden")
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)
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")
408 (line, prefixes, cmd)
for line, prefixes, cmd, _args
in records
if _dynamic_command(cmd)
410 if dynamic != ((
"$brew install ${brew_missing[@]}", (),
"$brew"),):
411 findings.append(f
"{MAC_SETUP}: dynamic command inventory is not exact")
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
416 findings.append(f
"{MAC_SETUP}: dynamic shell wrapper is forbidden")
419 for record
in records
420 if record[2]
in {
"env-dynamic",
"exec",
"source",
".",
"xargs",
"parallel"}
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")
427 word.split(
"=", 1)[1]
for word
in words
if re.match(
r"^[A-Za-z_][A-Za-z0-9_]*=", word)
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")
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)
440 offset >= 0
and text.count(token) == 1
441 for offset, token
in zip(offsets, tokens, strict=
True)
442 )
and offsets == tuple(sorted(offsets))
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] = []
449 matches = tuple(index
for index, line
in enumerate(lines)
if line == token)
450 if len(matches) != 1:
452 positions.append(matches[0])
453 return positions == sorted(positions)
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)
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"',
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]:
470 return [f
"{MAC_SETUP}: safe removal call/trap inventory is not exact"]
473def _mentions_cleanup_name(word: str) -> bool:
474 """Return whether a decoded mutator operand names a cleanup variable."""
476 re.search(rf
"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", word)
477 for name
in CLEANUP_NAMES
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))
489def _builtin_cleanup_mutation(command: str, arguments: tuple[str, ...]) -> bool:
490 """Return whether one assignment builtin can mutate a cleanup variable."""
492 tuple(shlex.split(definition))[1:]
493 for definition
in CLEANUP_DEFINITION_LINES
494 if definition.startswith(
"readonly ")
496 if command ==
"readonly" and arguments
in allowed_readonly:
498 if command
in {
"declare",
"typeset",
"local"}
and "-n" in arguments:
500 if command ==
"printf":
501 if "-v" not in arguments:
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(
"$")))
517 return command
in mutators
and any(_mentions_cleanup_name(arg)
for arg
in arguments)
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())
524 line
not in CLEANUP_DEFINITION_LINES
and _direct_cleanup_mutation(line)
525 for line
in logical_lines
527 return [f
"{MAC_SETUP}: cleanup variable has a direct or compound override"]
528 records = _command_records(text)
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"]
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")
544 tuple(logical_lines[index : index + 2]) == pair
for index
in range(len(logical_lines))
546 for pair
in CLEANUP_ASSIGNMENT_PAIRS
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)]
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")
565 *_check_sensitive_helpers(text),
566 *_check_cleanup_calls(text),
567 *_check_cleanup_variables(text),
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"'
575 (macos.replace(f
"{arm_call}\n",
"", 1),
"removed Arm safe removal"),
577 macos.replace(arm_call,
' safe_remove_tree "$arm_prefix" "$home_root"', 1),
578 "wrong allowed root",
581 macos.replace(arm_call,
' command rm -rf -- "$arm_prefix"', 1),
582 "direct Arm removal",
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"),
590 macos.replace(arm_call, f
'{arm_call}\n rm -rf -- "$HOME"', 1),
591 "unsafe removal after safe helper",
593 (macos +
'\nsafe_remove_tree "$emu_build" "$emu_root"\n',
"additive safe helper call"),
596 'trap \'safe_remove_tree "${work}" "${work_root}"\' EXIT',
597 "trap 'rm -rf -- \"${work}\"' EXIT",
600 "deferred raw trap removal",
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])
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"),
616 macos +
'\nwipe_tree() { command rm -rf -- "$HOME"; }\nwipe_tree\n',
617 "recursive removal wrapper",
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"),
624 macos +
'\n"$DELETE_CMD" "$RM_FLAGS" -- "$HOME"\n',
625 "external dynamic command and flags",
630 work_pair.replace(
"\n readonly work",
'\n work="$HOME"\n readonly work'),
633 "intervening work override before readonly",
638 work_pair.replace(
"\n readonly work",
"\n echo wait\n readonly work"),
641 "intervening command before readonly",
644 macos.replace(emu_pair, CLEANUP_ASSIGNMENT_PAIRS[9][0], 1),
645 "missing emulator readonly",
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]
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"),
664 macos +
"\ndeclare -n cleanup_ref=arm_prefix\ncleanup_ref=/tmp\n",
665 "indirect cleanup assignment",
667 (macos +
"\n((work_root=0))\n",
"arithmetic cleanup assignment"),
669 macos.replace(arm_static, arm_static +
'\narm_root="/tmp"\n', 1),
670 "intervening cleanup-root override",
675def _macos_prefixed_execution_mutations(macos: str) -> tuple[tuple[str, str], ...]:
676 """Return prefixed and indirect execution forms that must fail closed."""
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"),
707def _macos_helper_definition_mutations(macos: str) -> tuple[tuple[str, str], ...]:
708 """Return duplicate and alternate definitions of policy-sensitive helpers."""
710 (macos +
"\ndockerfile_arg() { :; }\n",
"later Docker parser definition"),
711 (
"safe_remove_tree() { :; }\n" + macos,
"earlier removal-helper definition"),
713 macos +
"\nfunction canonical_cleanup_target { :; }\n",
714 "later function-keyword canonicalizer",
717 macos +
"\nfunction require_arm_hash_pins() { :; }\n",
718 "later function-keyword pin validator",
720 (macos +
"\ninstall_homebrew() ( : )\n",
"later Homebrew helper definition"),
722 macos +
"\nfunction safe_remove_tree\n{\n :\n}\n",
723 "multiline function-keyword removal helper",
726 "dockerfile_arg ()\n{\n :\n}\n" + macos,
727 "earlier multiline Docker parser",
732def _macos_helper_mutation_commands(macos: str) -> tuple[tuple[str, str], ...]:
733 """Return function removal and attribute mutation commands."""
735 (macos +
"\nunset -f safe_remove_tree\n",
"unset removal helper"),
736 (macos +
"\ncommand unset -f dockerfile_arg\n",
"prefixed unset Docker parser"),
738 macos +
"\nbuiltin readonly -f canonical_cleanup_target\n",
739 "readonly canonicalizer mutation",
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"),
749def _macos_delete_safety_mutations(macos: str) -> tuple[tuple[str, str], ...]:
750 """Return every hostile recursive-deletion or execution mutation."""
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),
761def _macos_guard_safety_mutations(macos: str) -> tuple[tuple[str, str], ...]:
762 """Return hostile physical-boundary canonicalizer mutations."""
766 '[[ "$target_parent" != "$root_physical" ]]',
767 '[[ "$target_parent" == "$root_physical" ]]',
770 "inverted cleanup-root boundary",
773 macos.replace(
'[[ -L "$canonical" ]]',
'[[ ! -L "$canonical" ]]', 1),
774 "symlink cleanup target allowed",
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))