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:falseand 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`.*
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The
#964fix makes_deny_pathevaluate both the raw and the resolved path form. That fix is correct and I verified it holds at8aff7d8. 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.shat8aff7d8— the exact pre-fix defect, reintroduced into the new code:test-wake-preimage.shFor 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:
MOSAIC_HOMEitself behind a symlink, so the decoy is still matched by the resolved-anchor rule (rhome) when the raw form is dropped.Both assertions therefore still pass with the raw form removed. The configuration that only the raw form saves is not represented:
MOSAIC_HOME,credentials.json),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.jsonis a symlink to a store with a different filename.Fix
Add one needle to
test-wake-preimage.shwith the fixture above:MOSAIC_HOMEa real directory, an opted-in symlink at$MOSAIC_HOME/credentials.jsonpointing to a renamed target outside any store, benign content, assertingcaptured:falseand that the planted bytes appear nowhere under the state dir. It must fail when_deny_pathis 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_allowedmatches 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
#964is CLEAR at8aff7d8: 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.