fix(cve-gate): stderr must not be a tty, or grype returns nothing #99

Merged
ryangr0 merged 1 commit from fix/gate-stderr-not-a-tty into main 2026-08-07 13:44:08 +00:00
Owner

Run 257: grype produced output this gate cannot parse: unexpected end of JSON input — grype exiting 0, writing a 0-byte report, logging nothing.

The mechanism

The gate exec's grype with its stdout on a pipe (it parses the JSON) and passes its own stderr straight through. Under act, that stderr is a terminal — so grype selects its interactive progress UI, and in that mode the report never reaches the pipe.

Measured on the runner in run 261, same binary and arguments throughout:

stdout stderr bytes returned
file tty 29,495 bash redirect — works
pipe tty 0 the gate — broken
pipe file 30,006 this fix — works

Redirecting the gate's stderr to a file makes grype choose its plain writer. The file is replayed to stderr immediately after, so ::error:: and ::warning:: annotations still reach the log — the runner parses those from the stream, not from a particular fd.

Verified the exit code still propagates through the redirect for all three contract values (0/1/2) and that stderr survives.

Why this only appeared now

The gate had been broken in five earlier ways that all killed it before grype ran — seccomp denying clone, a 2 GB tmpfs, root-owned volumes, root without CAP_CHOWN. Run 257 was grype's first real execution, so it was also the first time this could surface.

Worth saying plainly: I guessed twice here — first --vex, then path resolution — and both were wrong. Each was disproved by a probe rather than by burning a release. The mechanism above came from testing the three stream combinations directly on the runner.

⚠️ Not fixed here — VEX is suppressing nothing

The same run gave the first verdict the gate has ever produced:

cve-gate: critical=0/0 OK  high=1/0 OVER  vex-suppressed=0

grype's own summary agrees — by status: 1 fixed, 2 not-fixed, 0 ignoreddespite five reviewed statements being composed and passed with --vex. The finding it should suppress is github.com/docker/docker, which is exactly what CVE-2026-34040/41567/42306 are written for.

So ADR-0005's VEX mechanism has never actually suppressed anything in a real scan — it couldn't, because no scan ever completed.

Impact: images in warn mode (ci-runner: 8 critical / 136 high) are unaffected and will sign. cve-gate is enforce 0/0 and will still fail. That's a separate defect and deserves its own change rather than being smuggled in here.

Run 257: `grype produced output this gate cannot parse: unexpected end of JSON input` — grype exiting **0**, writing a **0-byte** report, logging **nothing**. ## The mechanism The gate exec's grype with its stdout on a **pipe** (it parses the JSON) and passes its own stderr straight through. **Under act, that stderr is a terminal** — so grype selects its interactive progress UI, and in that mode the report never reaches the pipe. Measured on the runner in run 261, same binary and arguments throughout: | stdout | stderr | bytes returned | | |---|---|---:|---| | file | tty | 29,495 | bash redirect — works | | **pipe** | **tty** | **0** | the gate — broken | | pipe | file | 30,006 | **this fix** — works | Redirecting the gate's stderr to a file makes grype choose its plain writer. The file is replayed to stderr immediately after, so `::error::` and `::warning::` annotations still reach the log — the runner parses those from the stream, not from a particular fd. Verified the exit code still propagates through the redirect for all three contract values (`0`/`1`/`2`) and that stderr survives. ## Why this only appeared now The gate had been broken in five earlier ways that all killed it **before grype ran** — seccomp denying `clone`, a 2 GB tmpfs, root-owned volumes, root without `CAP_CHOWN`. Run 257 was grype's first real execution, so it was also the first time this could surface. Worth saying plainly: I guessed twice here — first `--vex`, then path resolution — and **both were wrong**. Each was disproved by a probe rather than by burning a release. The mechanism above came from testing the three stream combinations directly on the runner. ## ⚠️ Not fixed here — VEX is suppressing nothing The same run gave the first verdict the gate has ever produced: ``` cve-gate: critical=0/0 OK high=1/0 OVER vex-suppressed=0 ``` grype's own summary agrees — `by status: 1 fixed, 2 not-fixed, 0 ignored` — **despite five reviewed statements being composed and passed with `--vex`**. The finding it should suppress is `github.com/docker/docker`, which is exactly what CVE-2026-34040/41567/42306 are written for. So ADR-0005's VEX mechanism has never actually suppressed anything in a real scan — it couldn't, because no scan ever completed. **Impact:** images in `warn` mode (ci-runner: 8 critical / 136 high) are unaffected and will sign. **cve-gate is `enforce 0/0` and will still fail.** That's a separate defect and deserves its own change rather than being smuggled in here.
Run 257 failed with "grype produced output this gate cannot parse: unexpected end of JSON input" —
grype exiting 0, writing a 0-byte report, logging nothing at all.

The gate exec's grype with its stdout on a PIPE, because it parses the JSON, and passes its own
stderr straight through. Under act that stderr IS a terminal, so grype selects its interactive
progress UI, and in that mode the report never reaches the pipe.

Measured on the runner (run 261), same binary and arguments throughout:

    stdout=file, stderr=tty   -> 29,495 bytes   bash redirect: works
    stdout=pipe, stderr=tty   ->      0 bytes   the gate: broken
    stdout=pipe, stderr=file  -> 30,006 bytes   this fix: works

Redirecting the gate's stderr to a file is enough to make grype choose its plain writer. The file
is replayed to stderr immediately afterwards so ::error:: and :⚠️: annotations still reach
the log — the runner parses those from the stream, not from a particular fd. Verified that the exit
code still propagates through the redirect for all three contract values and that stderr survives.

This is why the failure only appeared now. The gate has been broken in five earlier ways that all
killed it BEFORE grype ran, so grype's first real execution was also the first time this could
surface. Two rounds of my own guessing (--vex, then path resolution) were both wrong and both
disproved by probe rather than by another release; the mechanism above came from testing the three
stream combinations directly.

NOT FIXED HERE, and visible for the first time in the same run: VEX is suppressing nothing.
The gate reached a verdict of `high=1/0 OVER  vex-suppressed=0` on cve-gate, and grype's own
summary agrees — "by status: 1 fixed, 2 not-fixed, 0 ignored" — despite five reviewed statements
being composed and passed with --vex. The finding it should suppress (github.com/docker/docker) is
exactly what CVE-2026-34040/41567/42306 are written for. Images in `warn` mode are unaffected and
will sign; cve-gate is `enforce 0/0` and will still fail. That is a separate defect and it gets its
own change rather than being smuggled in here.
Sign in to join this conversation.
No reviewers
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
webgrip/infrastructure!99
No description provided.