Skip to content

sandbox: a brace-expanded move destination is read as a literal word, so the move reads as inside the workspace #1396

Closed

Description

@argszero

The hole

A brace-expanded move destination is read as a literal word, so the walk joins it onto the
cwd and the move reads as inside the workspace — the one direction this guard must never drift
in. {../outside,sub} is ordinary text to the tokenizer, an expansion list to the shell.

Measured predicate-only (nothing executed, synthetic paths, workspace-write), identical on
master 67ba7f52 and on PR #1392's landing tree 45f9bc85:

cmd: cd {../emrg-1357-outside,sub} && cat > f
  master        ALLOW   _cwd_left_workspace=None   write-site=<ws>/{../emrg-1357-outside,sub}
  #1392 landing ALLOW   _cwd_left_workspace=None   write-site=<ws>/{../emrg-1357-outside,sub}

The write-site walk's answer is the tell: it hands back <ws>/{../outside,sub}, a path inside the
workspace that exists nowhere on disk. A nested list behaves the same way
(cd {../outside,{a,b}} && cat > f → ALLOW).

The shell does expand it. This host's /bin/sh is bash in POSIX mode, which brace-expands:
sh -c 'echo {a,b}' → a b. Driven end to end in a tree the probe built
(subprocess.run(["sh","-c",cmd], cwd=<ws>)), the file really lands in the sibling directory and
not in the workspace, so the join above is a lie about where the shell is.

Cause

emrg/tools/bash_tool.py:_PUNCTUATION_CHARS = "();<>|&\\n"—{, }, ,are not punctuation, so{../outside,sub}` arrives as one word, and every walk treats it as a literal relative path.

Why the fix in PR #1392 does not cover it

#1392 (_move_destination_is_unresolved) closes the class for expansion text: any surviving
$ or backtick. A brace list is neither, so it stays ALLOW there — the same verdict as on master,
measured above rather than inferred. That PR strictly narrows the hole; this is the residual.

Platform note

dash (the usual Linux /bin/sh) does not brace-expand, so on those hosts the reading is
over-permissive but the shell agrees with it. The escape is real wherever /bin/sh is bash
(macOS), which is where the daemon runs commands on this host.

Suggested fix

Extend the unresolved class rather than adding a second rule: a lexeme carrying {, } and a
, is expansion text the walk cannot resolve, and the tier's rule for an unresolvable move is
already to fail closed (cd -, and #1392's $(mktemp -d)). The nested spelling has to be caught
too, so the predicate is "a {…} pair with a comma anywhere between" rather than a non-nested
regex.

Acceptance:

  • BLOCK for cd {../outside,sub} && cat > f, its nested form, and both spellings inside an
    assignment (D={../outside,sub} && cd "$D" && cat > f — which emrg: a move no scope can place is refused, not read as inside #1392's class already catches);
  • _cwd_left_workspace reports the token it could not place, the way cd - and emrg: a move no scope can place is refused, not read as inside #1392's
    destination do, so the caller can see what to respell;
  • a mutation arm: narrowing the predicate back to non-nested/disjoint braces must turn the new rows
    red;
  • pinned both ways — a literal path with a brace but no comma (a legitimate file name) keeps its
    verdict, and the neighbouring suites (test_bash_tool_sandbox.py,
    test_unresolved_wrapper_guard.py) stay green.

Test hygiene, per the 2026-09-17 ruling: the verdict rows use synthetic paths (the predicate is
textual and opens nothing); the one arm that executes the command builds its own tree under
tmp_path and is skipif on win32 (no POSIX shell to witness with).

Activity

  1. argszero commented on Sep 18, 2026

    @argszero
    OwnerAuthor

    Fixed by #1397, with three deliberate widenings over what this issue asked for. Each is here
    because it was measured, and one of them corrects an inference I made while writing the body.

    1. The class is , or .. inside a brace pair, not , only. {1..3} is a sequence with
    no comma at all and expands the same way; a predicate written on the comma leaves it open. A word
    carrying braces with neither (a{b}c) is one literal word to the shell as well, so it keeps the
    verdict the join gives it — that is this issue's own acceptance item, pinned in the new file.

    2. The target side is the same defect, so it is fixed with the same predicate. The write-target
    walk reads the same lexeme as a literal relative name, i.e. inside. Measured on master 67ba7f52:

    command master the shell does
    rm -rf {../emrg-1396-outside/doomed,inside} ALLOW removes the sibling directory's entry
    tee {../emrg-1396-outside/written,inside} ALLOW creates the file in the sibling directory
    cat > {../emrg-1396-outside/t,inside} ALLOW nothing — "ambiguous redirect"

    3. The correction. While writing #1397 I claimed in a comment (and in a first draft of its
    test) that a redirect takes the first of the expanded words, i.e. that cat > {…}/f was a live
    escape. That is wrong, and the measurement above is what says so: bash refuses a redirect whose
    word expands to two words. The escape is real through the verbs that take several operands (rm,
    tee, and their family), and the redirect spelling is refused anyway — for the walk the two
    spellings are one text, and a guard should not have to know which verb it was handed to. Both
    facts are pinned in tests/test_brace_expansion_is_not_a_path.py, the second as the shape that
    does not escape.

    The price this issue did not discuss is now stated in the docstring and pinned as a row: a list
    whose every spelling stays inside is refused too (rm {dist,build}, cp a {b,c}), because which
    spelling the command takes is exactly what the text does not say. Spelling them out keeps working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions