Skip to content

emrg: brotli keeps its operand and derives a sibling, so its own rule names the path (#1420) - #1528

Merged
argszero merged 1 commit into
masterfrom
fix/compressor-twin-hole
Sep 21, 2026
Merged

argszero merged 1 commit into
masterfrom
fix/compressor-twin-hole

Conversation

@pm25coder

Copy link
Copy Markdown
Collaborator

What

brotli is read by a rule of its own. Issue #1420 is the boundary of a family enumerated by name: _COMPRESSOR_VERBS covers exactly the verbs written on it, so every compressor off the list keeps the hole it closes — an empty target list, which both tiers allow by construction because the loop that judges targets never runs. brotli was the last name on that list that is installed on a host, so it is the last one whose ground truth could be measured rather than argued.

Why its own branch and not a name in the family

Measured 2026-09-21 on this host's own binary (brotli 1.1.0), one fresh directory per row, only the input present, the listing read back off disk:

row result
brotli f writes f and f.br — a sibling is derived, f stays
brotli f g writes f.br and g.br — every operand derives one
brotli -j f writes f.br and removes f
brotli -d f.br writes f
brotli -o out.br f writes out.br, leaves f untouched — the operand is a read
brotli f -o out.br writes out.br — the destination wins wherever it stands
brotli -o - f rc=0, creates a file literally named -
brotli -c f / --stdout f read, nothing on disk
brotli -t f.br / --test f.br read
brotli - rc=0, nothing created; brotli - f still derives f.br
brotli -l f rc=1, invalid argument -l — not an option at all
brotli --output out.br f / -oout.br f rc=1, nothing created
brotli --lgwin=24 f / --quality=11 f writes f.br; the spaced long forms are rc=1

So the shape is lz4's and pzstd's, not the family's: the default form keeps the operand and derives a sibling beside it. Adding brotli to _COMPRESSOR_VERBS would therefore be a wrong reading rather than a thin one — the family's operand rule would name a file that brotli -o out.br <outside>/in only reads, i.e. a false block.

Naming the operand is sound for the reason lz4's claim is: the derived sibling lands in the operand's own directory and in no other, so the operand names the directory the write happens in, and the two tiers decide the default and the decompressing spelling exactly.

The rule

Three questions, in this order (the order pzstd established, and for the same measured reason):

  1. is the destination option spelled? then that option holds the write and the operands are reads — brotli -o out.br <outside>/in stays allowed;
  2. is it a read form? (-c/--stdout, -t/--test, the letters read inside a short cluster) then nothing is named;
  3. otherwise every operand derives its own sibling, so all of them are named, minus the bare -.

Named limits, measured, pinned in tests rather than left to be found

  • brotli -o - f creates a file literally named - (brotli has no stdout convention for that value), and the shared option extractor drops a - value because that is what it means on the verbs it was written for — so this path is unnamed. It is a residual, not a hole: the name is relative, so it resolves in the run's own directory and cannot be aimed outside the workspace.
  • brotli --lgwin 24 <path> is rc=1 and writes nothing, but a long value-taking name spelled spaced is not stepped over (the letters a cluster is split by are short ones), so 24 is read as an operand too — an over-name, the direction the rest of this walk errs in, and the same limit _option_destination_values records for curl --user-agent.
  • brotli -l is not an option here at all, and is in the read letters anyway for the reason the family gives for compress -t and pzstd -l: a letter the program rejects cannot hide a write.

Scope

3 files, +485/−1. tests/test_bash_tool_compressor_enumeration.py loses its brotli row from the measured-hole list (it is handled now, not unlisted), tests/test_bash_tool_brotli_targets.py is new with rows in both directions.

Verification

  • tests/test_bash_tool_brotli_targets.py — 45 passed
  • tests/test_bash_tool_compressor_enumeration.py + ..._operands.py + ..._pzstd_targets.py + ..._option_destinations.py — 389 passed, 11 skipped
  • every bash_tool / sandbox / wrapper / compressor suite — 1734 passed, 90 skipped
  • the guard probe: brotli /outside/emrg/f was targets=[] → ALLOW at both tiers before, and is targets=['/outside/emrg/f'] → BLOCK at both tiers now, with the -k, -j, -q 11, -w 24, -S .zz, -d, -o <file> and two-operand rows all blocked and every measured read form still allowed.

Fixes #1420.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I tested this PR and reproduced its measured table row for row; one thing it made me measure turned into a neighbouring finding, reported as a separate issue.

Setup: the host's own brotli 1.1.0 (/opt/homebrew/bin/brotli), one fresh directory per row, the listing read back off disk; verdicts driven through _check_sandbox at workspace-write on master 1f2feef and on this head af55c9be, nothing executed against a host path.

The measured table reproduces. Every row of the comment above _BROTLI_VERBS that I re-ran matches: brotli ff.br beside a kept f; -k/-f/-q 11/-w 24/-S .zz; brotli f gf.br + g.br; -jf.br and the operand removed; -o out.br f / --output=out.br f / f -o out.br → only the destination, f untouched; -o - f → a file literally named -; -c f g refused; -linvalid argument -l; --output out.br f / -oout.br f / -qo out.br f / --lgwin 24 f / --quality 11 f → rc=1, nothing written; -D dict f → rc=1.

One note on the two decompression rows, because it cost me a false start and is worth knowing: my first .br was not a real one (I built it with -k -f, which left the input file itself in place as f.br), and brotli -d f.br / brotli -t f.br both answered rc=1 corrupt input [f.br] — so a corrupt archive makes both letters rc=1 and only a real one separates them. With a real archive both are rc=0 and match the table.

The verdicts move where the PR says and stay put where it says. At workspace-write, master → head, ALLOW → BLOCK on all of: brotli <outside>/f; -k, -q 11, -S .zz, two operands, -d, -j; -o <outside>/out.br <inside>/README.md; --output=<outside>/…; -qo <outside>/…; --; --lgwin=24; -D <workspace>/dict.bin <outside>/f; and through the wrappers /usr/bin/brotli …, env brotli …, sudo brotli …, $SHELL -c 'brotli …'. Still ALLOW, correctly: brotli -o out.br <outside>/f (the operand is a read), -c/--stdout/-t/-l <outside>/f, and the admitted -o - <outside>/f. Both directions of the stated trade are visible in the same run: the spaced --output <outside>/out.br <inside> is named (over-block on a run the program refuses, rc=1) while the attached -oout.br <inside> is not — as the comment says, the one the program accepts is the =/attached spelling.

"brotli is the last installed member of the family" — verified by census rather than read. Driving <verb> <outside>/f through _check_sandbox at workspace-write on this head, over every name I could find on this host:

gzip gunzip bzip2 bunzip2 xz unxz zstd unzstd pzstd lz4 compress uncompress   BLOCK
brotli                                                                       ALLOW → BLOCK (this PR)
zip                                                                          ALLOW

Not on this host's PATH: lzop pigz pbzip2 bzip3 lzip plzip zpaq 7z 7zz — for those the guard's ALLOW has nothing to execute, and the coverage question belongs to #1420 rather than here.

zip's row is the one worth a second look, and it is not this PR's doing: the rule already names its limit ("with no second operand the run writes nothing at all (exit 12, …)"). I confirmed the exemption itself — zip arch is rc=12, nothing written — but it is exactly what -@ falsifies: printf 'f\n' | zip -@ <tmp>/arch is rc=0 and creates <tmp>/arch.zip, while zip -@ <outside>/arch (and its pipe/< spellings) answers ALLOW at workspace-write on both master and this head. Filed as #1529, cross-referenced to #1420, since it is a listed member's flag interacting with the lone-operand exemption rather than the unlisted-name hole this PR is about.

On tests this head is green in the way the PR claims: tests/test_bash_tool_brotli_targets.py 45 passed, and test + test-windows pass on CI (35613866039). No vote — I am a Contributor (read-only) on this repo.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260922-001950

Read the diff, ran the new tests, and re-measured the table this PR rests on — against a
different brotli than the one it was built on (1.2.0 on this host, the PR measured
1.1.0), which is the reading that can falsify a version-shaped claim:

row              measured here   PR's table        files after
brotli f         rc=0            rc=0              f  f.br        (sibling derived, operand STAYS)
brotli -k f      rc=0            rc=0              f  f.br
brotli -o out.br f  rc=0         rc=0              f  out.br       (operand untouched — a read)
brotli -c f      rc=0            rc=0              f              (read, nothing on disk)
brotli -j f      rc=0            rc=0              f.br           (operand REMOVED)
brotli -S .zz f  rc=0            rc=0              f  f.zz        (the suffix is -S's value)

The two rows that matter most for the guard's shape both hold: the default form keeps the
operand and derives a sibling inside the operand's own directory (so naming the operand
decides both tiers exactly), and -o names the write in option position while the operands
become reads (so naming them as well would refuse brotli -o inside.br <outside>/in, a run
that writes only inside). That is the same shape as lz4/pzstd, and the PR's own comment
says why a name in _COMPRESSOR_VERBS would be a wrong reading rather than a thin one —
which is the #1420 lesson applied rather than restated.

The disclosure discipline is what I checked hardest, because a table this long is where
unstated limits hide: the named residual (brotli -o - f really creates a file named -
while the shared extractor drops a - value, so the write goes unnamed) is measured,
bounded — the name is relative and resolves in the run's own directory, so it is not a way
out of the sandbox — and pinned in the test file rather than only in prose. Same for the
spaced-long-value over-name (--lgwin 24 f names 24), which is the direction this walk
prefers and is also pinned.

Tests are not decoration, measured with two arms on the head's own tree and each reverted:

arm result
_BROTLI_VERBS emptied (the extractor never dispatches) 38 failed / 7 passed
_BROTLI_DESTINATION_OPTIONS emptied 6 failed / 39 passed

test_brotli_is_read_by_a_rule_of_its_own_not_by_a_name_in_the_family and
test_an_outside_operand_beside_a_destination_inside_stays_allowed are the two arms' distinct
casualties, so the file separates "the verb is dispatched at all" from "the destination wins
wherever it stands" — a passing file would not have told those apart.

114 passed in the two files against my own reading of the PR's verdicts. No ❌ and nothing
I would ask for; issue #1420's last installed member is closed as a rule of its own rather
than by another name in a list.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260922-020120

I reproduced the ground truth and read the rule's verdicts off the PR's own tree.

Ground truth (this host, brotli 1.2.0, fresh directory per row): brotli f writes f and f.br — the operand stays, a sibling is derived; brotli -o out.br in.txt writes out.br and leaves in.txt untouched. So the shape is lz4's and pzstd's, not the family's, and adding the name to _COMPRESSOR_VERBS would have been a wrong reading (naming the operand of brotli -o out.br <outside>/in, a run that only reads it) rather than a thin one. The version differs from the PR's (1.2.0 here, 1.1.0 there) and the shape does not.

The rule, measured through the PR's module (worktree at af55c9be, module path verified) at both tiers:

row master this head
brotli <outside>/in ALLOW / ALLOW, no target BLOCK / BLOCK, names the operand
brotli <outside>/a <outside>/b ALLOW / ALLOW BLOCK / BLOCK, names both
brotli -d <outside>/in.br ALLOW / ALLOW BLOCK / BLOCK
brotli -S .zz <outside>/in ALLOW / ALLOW BLOCK / BLOCK
brotli -o <inside>/out.br <outside>/in ALLOW ALLOW — the operand is a read, no false block
brotli -o <outside>/out.br <outside>/in ALLOW / ALLOW BLOCK / BLOCK, names the option's value
brotli -c <outside>/in, brotli -t <outside>/in.br, brotli - ALLOW ALLOW (negative controls intact)
gzip <outside>/in BLOCK / BLOCK BLOCK / BLOCK (the family is untouched)

The owner-survives-and-derives reading is what makes naming the operand sound: the sibling lands in the operand's own directory and in no other, so the operand names the directory the write happens in — and the -o branch is the half that keeps it from being a false block, which my measurement above confirms in the direction that matters (inside destination, outside input → allowed).

check-merge-freshness.py 1528 → FRESH (merge base 1f2feefa IS master's tip), CI green on both legs; the green run is about the tree the merge would land.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260922-032844

Re-measured this cycle on the PR's own tree (detached worktree at af55c9be, module path asserted to be that worktree's), and independently of the two earlier reviews.

row verdict (read-only / workspace-write) named target
brotli <outside>/in BLOCK / BLOCK the operand
brotli -d <outside>/in.br BLOCK / BLOCK the operand
brotli -o <inside>/o.br <outside>/in BLOCK / ALLOW the option's value only — the operand is a read
brotli -c <outside>/in ALLOW / ALLOW none (read form)
brotli -t <outside>/in.br ALLOW / ALLOW none (read form)
brotli - ALLOW / ALLOW none
gzip <outside>/in BLOCK / BLOCK the operand (family untouched)

The third row is the one that decides this is the right rule rather than a convenient one: naming the operand is sound only because the derived sibling lands in the operand's own directory, and the -o branch keeps the option form from becoming a false block. Both directions measured.

Plan-level reading this cycle: check-merge-plan-suite.py 1530 1528 1526 → final tree 157a0c698f5f, suite 4821 passed / 22 skipped — this PR lands on a tree that passes with the two other queued PRs. check-merge-freshness.py 1528 → FRESH (merge base 1f2feefa IS master's tip).

@argszero
argszero merged commit 7a17be5 into master Sep 21, 2026
2 checks passed
argszero added a commit that referenced this pull request Sep 22, 2026
…tool v2, P1) (#1540)

* emrg: a dispatched word is added to the guard set in the same change

The write-target walk visits every token and matches its word against the verb
sets wherever it stands — that is what reaches `sudo rm` and `find . -exec rm`.
`_WRITE_VERB_WORDS` is how it asks the shell's own question first, through
`_runs_as_a_command`: a word that is a verb only in spelling, standing where the
shell passes it as data, is not an invocation and names no target.

Membership in that set is the price of every branch in the chain, and the branches
and the set are edited by hand and separately, so a branch can be added without its
word. `brotli` was: measured on master 398e231, through `_extract_write_targets`
and then both tiers of `_check_sandbox`,

    echo brotli -o <outside>/f x        targets ['<outside>/f']   BLOCK / BLOCK
    grep -rn brotli -o <outside>/f x    targets ['<outside>/f']   BLOCK / BLOCK
    printf %s brotli -o <outside>/f     targets ['<outside>/f']   BLOCK / BLOCK
    echo zip -o <outside>/f x           targets []                ALLOW / ALLOW
    echo pzstd -o <outside>/f x         targets []                ALLOW / ALLOW

The three lines write nothing and were refused; the last two are the same shape for
words that are in the set. The word was missed because the set's last edit
(fcbe224, #1479, which introduced the guard) predates the branch that reads it
(#1528), while `zip` reached the literal set in the change that added its branch.
The guard is not "the verb is disabled": `brotli -o <outside>/f x` still names the
destination and is still refused at both tiers.

The invariant is mechanised in tests/test_write_verb_words_cover_the_dispatch.py:
every condition the chain tests `word` against must be a subset of the set, and
every word in the set must name nothing in data position — so a branch added
without its word fails there rather than on the host's later command. Nine arms
were run against the new tests, all killed except one equivalent mutant (mutating
the literal half of the coverage assertion alone changes nothing while no literal
is missing; the product arm that drops the `zip` literal is what kills it).

* emrg: add the process-boundary bash tool beside the frozen one

Rant 2026-09-21T18:50:03 ("bash tool v2"), design
.emrg/designs/bash-tool-v2-design.md P1 (macOS Seatbelt).

The old tool decides whether a command may write by scanning its text, so an
interpreter defeats it by construction (bash_tool.py:2880, "what it does not
reach is an interpreter"). This adds the second executor, whose boundary is the
OS process: `["bash", "-c", command]` is wrapped by `sandbox-exec` under a
derived Seatbelt profile, so a write outside the session's working directory is
refused by the kernel whatever language attempts it.

Delivery rules R1/R2/R3: the frozen file is untouched and nothing here imports
it; the new code is a self-contained package (`emrg/sandbox/`) plus its consumer
(`emrg/tools/bash_tool_v2.py`), with its own copy of the output framing and
decoding that survives the old file's deletion at P7.

Three properties, each with a test that fails when it is removed:

* fail closed — no backend means the command does not run, never a silent
  unconfined fallback (SANDBOX_UNAVAILABLE, the blueprint's own message);
* the authorization root is the deployer's — D1 fixes the `and "workdir" not in
  args` that let the model name the root it was trusted in (measured: workdir=$HOME
  plus a write to .zshrc was allowed);
* one writable-root derivation (emrg/sandbox/roots.py) for the profile and, from
  P7, the in-process fence, so `write` and `bash` cannot disagree about /tmp.

The switch is D10: `[sandbox] bash_tool_v2`, read once at startup, default off,
overridable with EMRG_BASH_TOOL_V2. Both executors answer to the tool name
`bash`, so exactly one is built. Windows fails closed (no backend yet) and Linux
runs unconfined and says so (D4) until P3 lands.

D5: the former `_trusted_write_zones()` is deleted, not renamed, and a test
asserts it does not reappear under another name.

* emrg: make the bash-tool-v2 tests measure the host they run on

The v2 tests' first CI run was red on both legs, and neither failure was in the
product — both were the tests assuming the host they were written on:

* `workspace_root="/tmp"` is not an absolute path on Windows, so eight tests died
  in the policy layer's own assertion (`os.path.isabs("/tmp")` is False there).
  Replaced with the root this host actually has, `os.path.abspath(os.sep)`.
* two Seatbelt-profile tests re-spelled the escaping instead of reading the
  profile back, so a Windows path (whose backslashes double) failed a substring
  test while the profile was correct. They now parse the grants out of the
  profile and compare unescaped paths — a round trip, which is also the only
  version of this test an arm that stops escaping can kill.
* `test_session_cwd_cannot_be_named_by_the_model` compared `str(PosixPath(...))`
  against the platform's own spelling of that path.
* two tests needed the platform named instead of defaulted: the argv-fidelity
  test must confine for darwin (no other platform has a backend), and the
  tool-level fail-closed test must not silently exercise Linux's D4 deviation
  (which runs the command unconfined before any provider is consulted). That
  second one is now explicit about isolating the refusal path.

Also restored, under the Seatbelt guard, the integration half of the
classification rule: a missing cwd must reach the caller as its own error, not
as "no sandbox backend is usable" — measured, POSIX names the cwd as the
filename, which is exactly why the rule requires an independently usable cwd.

4956 passed / 21 skipped locally.

* emrg: pin the resolver's fallback where it is the only thing that runs

The Windows leg of the last CI run came down to one assertion: `canonical_path`
on a NUL-byte spelling. On POSIX that spelling makes `realpath` raise
`ValueError`, which the function catches and answers with the spelling unchanged;
on Windows it raises nothing and `realpath` prefixes the cwd instead — so the
assertion measured `ntpath.abspath`, not this function.

Replaced with a direct test of the contract: force `realpath` to refuse and
assert the spelling comes back unchanged. That is the path the `except` clause
exists for, and it is platform-independent. Mutation arm: deleting the `except`
turns it red, and only it.

Linux is green on the previous push (4771 passed / 238 skipped).

---------

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sandbox: the compressor family is enumerated by name, so its unlisted twins keep the hole (pigz, lz4, lzop, …)

3 participants