posix_script_gate uses a WHOLE-FILE predicate for a call-scoped hazard: a file that merely quotes a script path beside an unrelated Command::new becomes an offender #244

Closed
opened 2026-09-09 22:07:21 +02:00 by buildagent · 1 comment
Member

Found by the #239 lane while adding a test that executes a checked-in script. The gate was right about the code actually written, so this cost nothing real — but the population rule is wrong in a way that will bite a future lane, and its only escape hatch is the thing the same message tells you not to use.

The fact

posix_script_gate::no_test_spawns_a_checked_in_script_as_an_image decides membership with a whole-file predicate — roughly code.contains("Command::new") — for a hazard that is call-scoped: spawning a checked-in .sh as a process image.

crates/indexer/tests/release_gate.rs has spawned git and bash for unrelated reasons since long before #239. The moment that change put the string .forgejo/scripts/require_fixtures_graded.sh into the file, it became an offender — with no script being spawned anywhere in it.

So the gate's population is "files containing a Command::new anywhere AND a script path anywhere", which is not the same set as "files that spawn a checked-in script", and the two only coincide today by luck.

Why it is worth fixing rather than living with

Two properties compound:

  1. False membership grows over time. Any future test file that spawns git for a fixture and mentions a .forgejo/scripts/... path in an assertion message, a doc comment, or a parsed constant joins the population. Neither of those facts is the hazard.
  2. The escape hatch is one the gate itself forbids. The only way out is #![cfg(unix)] on the whole file — and the gate's own message tells you not to reach for that, because it silently removes the file from Windows coverage. So a lane that trips it falsely is pushed toward the exact remedy the gate exists to discourage.

That combination is what makes it worth a number: a gate whose false-positive path leads to a worse tree than not having the gate.

What must NOT be done

  • Do not relax the hazard. Spawning a checked-in .sh as a process image is a real portability defect and the gate has earned its place. This is about who is in the population, not about what the rule says.
  • Do not special-case release_gate.rs. An exemption list for a file that is not an offender records a wrong fact and rots.
  • Do not narrow to "files whose name matches *_gate.rs" or similar. The hazard is not correlated with a filename.

Shape of a fix

Scope the predicate to the call, not the file: an offender is a Command::new(...) whose argument resolves to a checked-in script path, not a file that contains both tokens somewhere. This tree has precedent for exactly that narrowing — #180's fix established that "the defect is the SCOPE, not the spelling", and the dispatch_arm_bodies splitter in argument_registry_e2e is the worked example of scoping a scan to the unit that can actually contain the fault.

A cheap intermediate, if full call-scoping is too much: require the script path and the Command::new to be within one statement or one function body of each other, the same way #239's own fixture-count predicate ended up requiring the number to sit within three tokens of the word it counts.

What a fix must prove

  • A file that spawns a checked-in .sh as an image is still flagged — the hazard's own arm must survive, and this is the one that matters.
  • Anti-vacuity: a file that spawns git/bash for unrelated reasons and separately mentions a script path is not flagged. That is the exact shape release_gate.rs has today, so it is a real fixture, not a synthetic one.
  • The gate's own detector still fails when its predicate is weakened — a scan over the compliant tree cannot discover that its predicate went vacuous, which is #204's portable result and applies here directly.
  • No file gains #![cfg(unix)] as part of the fix.

#204 (a scan over real inputs is structurally incapable of noticing its own predicate has gone vacuous), #180 (the defect is the scope, not the spelling — its token had 41 legitimate users), #239 (where this surfaced).

Filed 2026-09-09 against master 34b0fd5.

Found by the #239 lane while adding a test that executes a checked-in script. The gate was **right about the code actually written**, so this cost nothing real — but the population rule is wrong in a way that will bite a future lane, and its only escape hatch is the thing the same message tells you not to use. ## The fact `posix_script_gate::no_test_spawns_a_checked_in_script_as_an_image` decides membership with a **whole-file** predicate — roughly `code.contains("Command::new")` — for a hazard that is **call-scoped**: spawning a checked-in `.sh` as a process image. `crates/indexer/tests/release_gate.rs` has spawned `git` and `bash` for unrelated reasons since long before #239. The moment that change put the string `.forgejo/scripts/require_fixtures_graded.sh` into the file, it became an offender — **with no script being spawned anywhere in it**. So the gate's population is "files containing a `Command::new` anywhere AND a script path anywhere", which is not the same set as "files that spawn a checked-in script", and the two only coincide today by luck. ## Why it is worth fixing rather than living with Two properties compound: 1. **False membership grows over time.** Any future test file that spawns `git` for a fixture *and* mentions a `.forgejo/scripts/...` path in an assertion message, a doc comment, or a parsed constant joins the population. Neither of those facts is the hazard. 2. **The escape hatch is one the gate itself forbids.** The only way out is `#![cfg(unix)]` on the whole file — and the gate's own message tells you not to reach for that, because it silently removes the file from Windows coverage. So a lane that trips it falsely is pushed toward the exact remedy the gate exists to discourage. That combination is what makes it worth a number: a gate whose false-positive path leads to a worse tree than not having the gate. ## What must NOT be done - **Do not relax the hazard.** Spawning a checked-in `.sh` as a process image is a real portability defect and the gate has earned its place. This is about *who is in the population*, not about what the rule says. - **Do not special-case `release_gate.rs`.** An exemption list for a file that is not an offender records a wrong fact and rots. - **Do not narrow to "files whose name matches `*_gate.rs`"** or similar. The hazard is not correlated with a filename. ## Shape of a fix Scope the predicate to the **call**, not the file: an offender is a `Command::new(...)` whose argument resolves to a checked-in script path, not a file that contains both tokens somewhere. This tree has precedent for exactly that narrowing — `#180`'s fix established that *"the defect is the SCOPE, not the spelling"*, and the `dispatch_arm_bodies` splitter in `argument_registry_e2e` is the worked example of scoping a scan to the unit that can actually contain the fault. A cheap intermediate, if full call-scoping is too much: require the script path and the `Command::new` to be within one statement or one function body of each other, the same way #239's own fixture-count predicate ended up requiring the number to sit within three tokens of the word it counts. ## What a fix must prove - A file that spawns a checked-in `.sh` as an image is **still** flagged — the hazard's own arm must survive, and this is the one that matters. - **Anti-vacuity:** a file that spawns `git`/`bash` for unrelated reasons *and* separately mentions a script path is **not** flagged. That is the exact shape `release_gate.rs` has today, so it is a real fixture, not a synthetic one. - The gate's own detector still fails when its predicate is weakened — a scan over the compliant tree cannot discover that its predicate went vacuous, which is #204's portable result and applies here directly. - No file gains `#![cfg(unix)]` as part of the fix. ## Related #204 (a scan over real inputs is structurally incapable of noticing its own predicate has gone vacuous), #180 (the defect is the scope, not the spelling — its token had 41 legitimate users), #239 (where this surfaced). Filed 2026-09-09 against `master` `34b0fd5`.
Author
Member

Fixed by 8fff4e0 ("posix_script_gate grades the CALL, not the file") and 0a42731 ("a QUOTED call is not a call"), merged at 12dfbcb. Closing late — the fix shipped and the issue was never closed.

Verified in the tree, not just in the commit subjects: crates/indexer/tests/posix_script_gate.rs:817 builds the quoted-call fixture and asserts the gate does not grade it —

let quoted = "fn f() {\n// `Command::new(&s)` on a `.sh` fails at spawn on Windows\n…
assert!(graded_spawns(&code_only(quoted)).is_empty(), …);

so the call-scoped predicate is graded by a fixture that a whole-file predicate would have failed.

Fixed by `8fff4e0` ("posix_script_gate grades the CALL, not the file") and `0a42731` ("a QUOTED call is not a call"), merged at `12dfbcb`. Closing late — the fix shipped and the issue was never closed. Verified in the tree, not just in the commit subjects: `crates/indexer/tests/posix_script_gate.rs:817` builds the quoted-call fixture and asserts the gate does not grade it — ```rust let quoted = "fn f() {\n// `Command::new(&s)` on a `.sh` fails at spawn on Windows\n… assert!(graded_spawns(&code_only(quoted)).is_empty(), …); ``` so the call-scoped predicate is graded by a fixture that a whole-file predicate would have failed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#244
No description provided.