From e4e2be8f22384cebff239c8f300ab973c23897f4 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 21 Sep 2026 05:02:04 +0800 Subject: [PATCH] emrg: a variable reference in operand position is not a wrapper, so its arguments stay data --- emrg/tools/bash_tool.py | 15 ++++++++- tests/test_unresolved_wrapper_guard.py | 43 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/emrg/tools/bash_tool.py b/emrg/tools/bash_tool.py index 71ed095b..982d4792 100644 --- a/emrg/tools/bash_tool.py +++ b/emrg/tools/bash_tool.py @@ -6278,6 +6278,19 @@ def _unresolved_wrapper_payloads(tokens: list[str]) -> list[str]: follows it is read as a command that may run: the same over-approximation a named wrapper already gets, and one that can only add blocking, never remove it (issue #1244). + + **Where** that word stands decides whether the words behind it are a payload + at all, and `_runs_as_a_command` is the file's one answer to that question. A + variable reference in *operand* position names no program the shell will run + — `wc -c "$F"` hands `$F` to `wc` — and reading the rest of the line as its + payload turned data into a command: measured on master `c1a70c94`, + `wc -c "$F" && echo "patch rc=$?"` answered **BLOCK** at `read-only` naming + `rc=$?` as a write target, because the payload token `patch rc=$?` was + re-tokenized into the words `patch` and `rc=$?` and `patch` is a write verb + (issue #1467). Nothing in that command writes anything, and since a refusal + aborts the whole compound command the reads sharing the call are lost with + it. The wrapper class itself is unchanged: every spelling in the corpus stands + where a command can begin. """ out: list[str] = [] for i, tok in enumerate(tokens): @@ -6291,7 +6304,7 @@ def _unresolved_wrapper_payloads(tokens: list[str]) -> list[str]: if ( _UNRESOLVED_VAR_RE.fullmatch(tok) or _UNRESOLVED_VAR_RE.fullmatch(_basename(tok)) - ): + ) and _runs_as_a_command(tokens, i): out.extend(tokens[i + 1:]) return out diff --git a/tests/test_unresolved_wrapper_guard.py b/tests/test_unresolved_wrapper_guard.py index 6e075e65..20be0a03 100644 --- a/tests/test_unresolved_wrapper_guard.py +++ b/tests/test_unresolved_wrapper_guard.py @@ -181,6 +181,49 @@ def test_workspace_write_keeps_its_boundaries(cmd: str) -> None: assert allowed, reason +# Issue #1467: the same variable reference standing in **operand** position. The +# words behind it are that command's arguments, not a wrapper's payload — and +# reading them as one turned data into a command. Measured on master `c1a70c94`, +# every row below answered BLOCK while writing nothing, naming `rc=$?`: the +# argument token `patch rc=$?` was re-tokenized into the words `patch` and +# `rc=$?`, and `patch` is a write verb. A refusal aborts the whole compound +# command, so the reads sharing the call are lost with it. +OPERAND_POSITION_VARIABLES = [ + 'wc -c "$F" && echo "patch rc=$?"', + 'F=/tmp/x && wc -c "$F" && echo "patch rc=$?"', + 'ls "$HOME" && echo "patch rc=$?"', + 'echo $SHELL && echo "patch rc=$?"', + 'printf %s $0 && echo "patch rc=$?"', + 'cat "$F" | echo "patch rc=$?"', + 'stat "$F"; echo "patch rc=$?"', +] + +# The narrowing must not touch the class it was written for: a wrapper that +# shares a command with an operand-position variable is still read. +WRAPPER_WITH_AN_OPERAND_POSITION_VARIABLE = [ + 'wc -c "$F" && $SHELL -c \'echo x > OUT.txt\'', + 'ls "$HOME" && ${SHELL//x/y} -c \'git checkout .\'', +] + + +@pytest.mark.parametrize("cmd", OPERAND_POSITION_VARIABLES) +def test_a_variable_in_operand_position_is_not_a_wrapper(cmd: str) -> None: + """Both tiers: the false block was in the read-only arm, the rule is one rule. + + The assertion is the verdict *and* its reason — a command that writes nothing + must not be refused for naming a target it never writes. + """ + for tier in (READ_ONLY, WW): + allowed, reason, _enforcement = _check_sandbox(cmd, tier, WORKDIR) + assert allowed, (cmd, tier, reason) + + +@pytest.mark.parametrize("cmd", WRAPPER_WITH_AN_OPERAND_POSITION_VARIABLE) +def test_a_wrapper_beside_an_operand_position_variable_is_still_read(cmd: str) -> None: + allowed, reason, _enforcement = _check_sandbox(cmd, READ_ONLY, WORKDIR) + assert not allowed, (cmd, reason) + + def test_the_named_wrapper_is_the_control() -> None: """The hole was the spelling, so the named twin must reach the same verdict.""" named, _r1, _e1 = _check_sandbox("sh -c 'git checkout .'", READ_ONLY, WORKDIR)