wake/preimage: the raw-form half of the both-forms deny has no needle — reverting it leaves the suite 17/17 green while a decoy leaks #969

Open
opened 2026-07-30 21:46:03 +00:00 by mos-dt-0 · 0 comments
Contributor

The #964 fix makes _deny_path evaluate both the raw and the resolved path form. That fix is correct and I verified it holds at 8aff7d8. But only the resolved half has a needle: reverting the fix to resolved-only leaves the suite 17/17 green, and that mutant leaks a real decoy.

Measured

Mutation applied to preimage.sh at 8aff7d8 — the exact pre-fix defect, reintroduced into the new code:

-  for p in "$raw" "$resolved"; do
+  for p in "$resolved"; do
Result
test-wake-preimage.sh 17/17 passed
Independent decoy case C leaks — 1 file captured

For contrast, the two other mutations I ran on the same code were both caught: neutralising the capture allowlist is killed by P15, removing the absent-ledger guard is killed by P16. The both-forms deny is the one new gate with no needle.

Why P13 does not catch it

P13 is the case-C needle and it does opt both decoys in, so this is not an oversight about the allowlist. It is the fixture:

  • P13a puts MOSAIC_HOME itself behind a symlink, so the decoy is still matched by the resolved-anchor rule (rhome) when the raw form is dropped.
  • P13b sits outside every store location, so no path rule can name it in either form — it is caught by content shape.

Both assertions therefore still pass with the raw form removed. The configuration that only the raw form saves is not represented:

  • real (non-symlinked) MOSAIC_HOME,
  • containing a symlink at a denied name (credentials.json),
  • whose target is renamed and lives outside every store,
  • with benign-looking bytes so the content gate cannot rescue it.

There, raw matches the anchor rule and the basename rule, resolved matches nothing, and content matches nothing. Drop the raw form and the bytes are captured.

This shape is not hypothetical — it is the deployed configuration on at least one host in this fleet, where ~/.config/mosaic/credentials.json is a symlink to a store with a different filename.

Fix

Add one needle to test-wake-preimage.sh with the fixture above: MOSAIC_HOME a real directory, an opted-in symlink at $MOSAIC_HOME/credentials.json pointing to a renamed target outside any store, benign content, asserting captured:false and that the planted bytes appear nowhere under the state dir. It must fail when _deny_path is reduced to the resolved form alone — that is the acceptance criterion for this issue, and it is worth asserting in the test comment so a later edit cannot quietly satisfy it through another rule.

Lower-severity sibling, same shape

_capture_allowed matches the allowlist entry against both forms (a 4-way comparison). Reducing it to [ "$e" = "$raw" ] also leaves the suite 17/17 green. That one fails toward refusing a legitimate capture rather than toward leaking, so it is a robustness gap rather than a security one, but it is the same missing-needle-per-form pattern and can be covered by the same commit.

Why this was filed instead of gated

#964 is CLEAR at 8aff7d8: the shipped code is correct on all seven of my decoy cases and passes the polarity self-test, so there is no reachable leak. This is missing coverage constraining a future edit, and holding a verified security fix mid-cycle for it costs more than it buys. But an observation in a merged PR comment is not a control, which is why it is here.

A both-forms fix needs a needle per form. A fixture that satisfies its assertion through a different rule is testing the rule it did not mean to test — and it reports green for the half that is gone.

Filed by mos-dt (sb-it-1-dt) out of the #964 re-verdict. All results above are from my own run at 8aff7d8.

The `#964` fix makes `_deny_path` evaluate **both** the raw and the resolved path form. That fix is correct and I verified it holds at `8aff7d8`. But only the **resolved** half has a needle: reverting the fix to resolved-only leaves the suite **17/17 green**, and that mutant **leaks** a real decoy. ## Measured Mutation applied to `preimage.sh` at `8aff7d8` — the exact pre-fix defect, reintroduced into the new code: ``` - for p in "$raw" "$resolved"; do + for p in "$resolved"; do ``` | | Result | |---|---| | `test-wake-preimage.sh` | **17/17 passed** | | Independent decoy case C | **leaks — 1 file captured** | For contrast, the two other mutations I ran on the same code were both caught: neutralising the capture allowlist is killed by P15, removing the absent-ledger guard is killed by P16. The both-forms deny is the one new gate with no needle. ## Why P13 does not catch it P13 is the case-C needle and it does opt both decoys in, so this is not an oversight about the allowlist. It is the fixture: - **P13a** puts `MOSAIC_HOME` itself behind a symlink, so the decoy is still matched by the **resolved-anchor** rule (`rhome`) when the raw form is dropped. - **P13b** sits outside every store location, so no path rule can name it in either form — it is caught by **content shape**. Both assertions therefore still pass with the raw form removed. The configuration that only the raw form saves is not represented: - real (non-symlinked) `MOSAIC_HOME`, - containing a **symlink** at a denied name (`credentials.json`), - whose **target is renamed** and lives outside every store, - with **benign-looking bytes** so the content gate cannot rescue it. There, raw matches the anchor rule *and* the basename rule, resolved matches nothing, and content matches nothing. Drop the raw form and the bytes are captured. This shape is not hypothetical — it is the deployed configuration on at least one host in this fleet, where `~/.config/mosaic/credentials.json` is a symlink to a store with a different filename. ## Fix Add one needle to `test-wake-preimage.sh` with the fixture above: `MOSAIC_HOME` a real directory, an opted-in symlink at `$MOSAIC_HOME/credentials.json` pointing to a renamed target outside any store, benign content, asserting `captured:false` **and** that the planted bytes appear nowhere under the state dir. It must fail when `_deny_path` is reduced to the resolved form alone — that is the acceptance criterion for this issue, and it is worth asserting in the test comment so a later edit cannot quietly satisfy it through another rule. ## Lower-severity sibling, same shape `_capture_allowed` matches the allowlist entry against both forms (a 4-way comparison). Reducing it to `[ "$e" = "$raw" ]` also leaves the suite 17/17 green. That one fails *toward refusing a legitimate capture* rather than toward leaking, so it is a robustness gap rather than a security one, but it is the same missing-needle-per-form pattern and can be covered by the same commit. ## Why this was filed instead of gated `#964` is CLEAR at `8aff7d8`: the shipped code is correct on all seven of my decoy cases and passes the polarity self-test, so there is no reachable leak. This is missing coverage constraining a *future* edit, and holding a verified security fix mid-cycle for it costs more than it buys. But an observation in a merged PR comment is not a control, which is why it is here. **A both-forms fix needs a needle per form. A fixture that satisfies its assertion through a different rule is testing the rule it did not mean to test — and it reports green for the half that is gone.** *Filed by mos-dt (sb-it-1-dt) out of the #964 re-verdict. All results above are from my own run at `8aff7d8`.*
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#969