Skip to content

Ask two data-flow tools the question a matcher cannot ask - #41

Merged
HackingGate merged 1 commit into
mainfrom
docs/the-semantic-tier
Aug 15, 2026
Merged

Ask two data-flow tools the question a matcher cannot ask#41
HackingGate merged 1 commit into
mainfrom
docs/the-semantic-tier

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

Answers the Semgrep and CodeQL deliverables of #13. ADR 0003 ended where a pattern over one syntax tree stops: this repository can enforce "no command is built outside the helper" and would rather enforce "every command this module runs came from the helper". ADR 0004 is what two data-flow tools said when they were asked the second one.

Semgrep: one real step past the structural tier, one step short of the property

Taint mode joins a construction and a use that are separate statements linked by a variable, which no single AST pattern does. Thirteen lines, one finding on the fixture, and it is the right one.

Then, measured on this repository's own src/probe.rs with the defect planted behind a wrapper one function away:

$ semgrep --config rule.yml src/probe.rs
Findings: 0
$ semgrep --config rule.yml --pro src/probe.rs
Run `semgrep login` before running `semgrep scan --pro`.

Cross-function taint is the Pro engine. Also learned: the helper's own body reads as a violation without a carve-out, because detached sets current_dir before it calls env_remove and the sanitizer is therefore after the sink in source order.

CodeQL: the property, answered

It resolves calls rather than matching their spelling -- Command::new in the database is <std::process::Command>::new, from type inference -- which is what lets the query state the positive form:

not Flow::flowTo(sink)   // no value out of the helper reaches this execution

A negation over a flow relation, which is exactly the required-shape direction ADR 0003 records a matcher cannot express at all. On the same planted defect Semgrep missed, it reports the one finding and nothing else.

Three measurements, none of them free

  • The models are yours. The first run gave three findings, all false, all detached(..).args([..]).output() -- value flow does not cross Command::arg, because nothing in the standard model says those return the same command. The query needs a hand-written isAdditionalFlowStep before it says anything true. An unmodelled API produces confident wrong answers in both directions, and the silent direction is the one nobody investigates.
  • The toolchain. 600 MB CLI, 174 MB database, two minutes to build, 13 s to compile the query, 4 s to evaluate.
  • A clean answer still does not mean the file was read. With one unparseable function above the planted defect, codeql database create prints Successfully created database and the query returns nothing -- the same output as a compliant module. Recoverable from parse_error diagnostics, and the recovery is worse than it sounds: the clean tree already carries 1,689 diagnostics of that severity, mostly macro expansion failed from assert! and format!. The signal is four more rows against that, at severity Warning, in a channel the query result never mentions.

Decisions

Neither is adopted. The property is already enforced here by a structural test at a cost of milliseconds, and the extra case CodeQL proves -- a wrapper bypassing the helper -- is one src/probe.rs does not contain. A second checker over one answer is two answers free to disagree. The query is kept in the record, where it is reproducible, rather than in a directory with no runner.

Semgrep OSS is not the semantic tier; it is a better structural tier, and a repository that needs the cross-function property should be told that rather than discover it from a green run.

This is the third tier to produce the same requirement for the provider contract: a clean run is evidence only when something separately establishes that the analyzer could read what it was pointed at.

Documentation only -- no code changes.

ADR 0003 ends where a pattern over one syntax tree stops: this repository can
enforce "no command is built outside the helper" and would rather enforce
"every command this module runs came FROM the helper". ADR 0004 is what Semgrep
and CodeQL said when they were asked the second one.

Semgrep's taint mode takes one real step past the structural tier -- it joins a
construction and a use that are separate statements linked by a variable, which
no single pattern does -- and stops one step short of the property. Measured on
this repository's own `src/probe.rs`, with the defect planted behind a wrapper
one function away: `Findings: 0`. Cross-function taint is the Pro engine and
wants a login. Two other things it taught: the helper's own body reads as a
violation without a carve-out, because the sanitizer runs after the sink in
source order.

CodeQL answers it. It resolves calls rather than matching their spelling, which
is what lets the query state the POSITIVE form -- `not Flow::flowTo(sink)`, no
value out of the helper reaches this execution -- which is exactly the
required-shape direction ADR 0003 records a matcher cannot express at all. On
the same planted defect it reports the one finding and nothing else.

Three measurements went into not adopting it. The first run produced three
findings, all false, all `detached(..).args([..]).output()`: value flow does not
cross `Command::arg` because nothing models it, so the query needs a hand-written
step before it says anything true -- and an unmodelled API produces confident
wrong answers in both directions. The toolchain is 600 MB, the database 174 MB
and two minutes. And the third is the one that matters here.

A clean answer still does not mean the file was read, at any height on the cost
hierarchy. With one unparseable function above the planted defect, `codeql
database create` prints "Successfully created" and the query returns nothing --
the same output as a compliant module. The state is recoverable from
`parse_error` diagnostics, and the recovery is worse than it sounds: the clean
tree already carries 1,689 diagnostics of that severity from macro expansion, so
the signal is four more rows against that background, at severity Warning, in a
channel the query's own result never mentions.

Neither is adopted. The property is already enforced here by a structural test
at a cost of milliseconds, and the case CodeQL proves in addition is one this
module does not contain. The query is kept in the record, where it is
reproducible, rather than in a directory with no runner.

Third tier to produce the same requirement for the provider contract: a clean
run is evidence only when something separately establishes that the analyzer
could read what it was pointed at.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@HackingGate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 382f1b15-8bd0-4f24-a997-2a60a787b726

📥 Commits

Reviewing files that changed from the base of the PR and between b65773f and 4b43d78.

📒 Files selected for processing (1)
  • docs/adr/0004-the-semantic-tier-and-what-it-costs-to-be-sure.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.44%. Comparing base (b65773f) to head (4b43d78).

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #41   +/-   ##
=======================================
  Coverage   89.44%   89.44%           
=======================================
  Files          31       31           
  Lines        9732     9732           
=======================================
  Hits         8705     8705           
  Misses       1027     1027           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@HackingGate
HackingGate merged commit db77832 into main Aug 15, 2026
12 checks passed
@HackingGate
HackingGate deleted the docs/the-semantic-tier branch August 15, 2026 05:02
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.

2 participants