Files
Open_Ultra_SAST/.kiro/specs/engine-change-scoped-analysis/bugs/taint-sc-single-query-signature-skew.md
T
Marius CiepluchandClaude Opus 4.8 a024656da0 fix(cpg): single-query path drops params the script does not declare
taint.request_params emits guards/fixedOrigin/originAnchors/quotedSanitizers/
fieldParameterSources, which taint.sc reads from per-request JSON in its BATCH
branch but does NOT declare on `@main def exec` and does NOT consume in its
single-request branch (it defaults them). The single-query path passed them as
`--param guards=...` (disposable) or named args (session), so joern errored
`Unknown arguments` and every cpg.run("taint", ...) failed -- a silent zero of
the shape AGENTS.md warns about. scan_region->taint_verdict->_usable_flows and
the pair-path verdict()/verdicts() were dead; the push/scan batch route was
unaffected (it carries the extras as JSON the script parses itself).

Fix: parse the script's `@main def exec(...)` signature once (_declared_exec_params
+ memoised _declared_params) and, in query(), filter params to the declared set
before building either route. Faithful, not lossy -- the single-request branch
could never use those 5 fields. No-op for config/dominance (their request_params
emit only declared params; verified). Only taint sheds the 5 extras.

Verified end-to-end on banks (patched backend in the replay image): single-query
path now runs; baseline 0 findings, annotated search 2 path/model_entailed
findings (prompt->write_text). Regression test asserts no undeclared param
reaches the command line and the query parses, with a fake runner (no Joern).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:16:36 +02:00

109 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BUG: `taint.sc` single-query path broken by exec-signature skew
Filed: 2026-10-12 · Repo HEAD at filing: `7ab5c2a` · Severity: **high (silent zero)** · Status: **FIXED (2026-10-12)**
## Resolution (2026-10-12)
Fixed via **option 1** (filter params to the declared signature) in `src/openultrasast/cpg/backend.py`:
- New `_declared_exec_params(source)` parses the `@main def exec(...)` parameter names from a shipped script
(brace-aware, tolerant of an unfindable signature → empty set → "do not filter").
- New memoised `JoernBackend._declared_params(query)` reads+caches a script's declared names.
- `JoernBackend.query()` now drops any param the script does not declare before building the `--param` list
(disposable route) and the in-session named-arg call (session route). This is faithful, not lossy: the
single-request branch of `taint.sc` (line 1542) calls `rowsFor(...)` with top-level params only and lets
`fixedOrigin`/`originAnchors`/`quotedSanitizers`/`guards`/`fieldParameterSources` default — it never could
consume them. The batch path keeps carrying them as JSON the script parses itself.
- No-op for `config`/`dominance`: their own `request_params` emit only params their `.sc` declares (verified),
so nothing is dropped. Only `taint` sheds the 5 extras.
**Verified end-to-end** (patched backend mounted into the replay image, single-query path on banks): the
queries now RUN (no `cpg query taint failed`); baseline = 0 findings, fixed search (annotation) = 2 scored
`path`/`model_entailed` findings (`from_prompt_path` → `prompt_file.write_text(prompt.raw)` L45; `_scan` →
`self._index_path.write_text(...)` L155). Regression test:
`tests/test_cpg_backend.py::test_a_single_query_passes_only_params_the_script_declares` (fake runner, no Joern
— asserts the 5 undeclared params never reach the command line and the query parses).
## Summary
Any call on the **single-query** taint path — `cpg.run("taint", request_params(...))` — fails on **both**
of its sub-routes, returns `None`, and is read upstream as "no flows / clean". `request_params()` emits five
keys that `taint.sc`'s `@main def exec(...)` does not declare, and the single-query dispatch passes every
param straight to the script. The **batch** path (`run_batch` → `requestsFile` JSON) is unaffected, which is
why the engine-on scans ran while the single-query MVP did not.
This is exactly the failure shape AGENTS.md warns about: a tool that could not read/execute its input reports
a plausible zero that is indistinguishable from a clean result.
## Mechanism
`request_params()` (`src/openultrasast/model/taint.py`) always emits, among others:
- `fieldParameterSources` (line 54)
- `fixedOrigin` (line 65)
- `originAnchors` (line 66)
- `quotedSanitizers` (line 67)
- `guards` (line 68)
`taint.sc`'s signature (`src/openultrasast/cpg/queries/taint.sc:41` `@main def exec(...)`) declares **none**
of those five. Its 19 params are: `cpgFile, sources, sinks, sanitizers, function, file, callDepth,
boundedSinks, parameterSources, fieldSources, evidenceOnly, hookCallbacks, dispatchApply, dispatchValue,
contextFilter, contextPaths, requests, requestsFile, trace`.
`backend.query()` (`src/openultrasast/cpg/backend.py:1266`) dispatches a single query two ways, **both** of
which choke on the undeclared keys:
- **Disposable / CLI route** (lines 1276–1278): appends `--param {key}={value}` for every param. joern errors
`Unknown arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`
(and, as a knock-on, `Missing argument: --cpgFile`). Observed verbatim via `banks_debug.py` on 2026-10-12.
- **Session route** (lines 1321–1326): defines `def ousast_taint(...)` by renaming `@main def exec(` and then
calls `ousast_taint(key = value, ...)` with named args. The same five keys are not parameters of that
method → the Scala named-arg call fails to compile. (By construction; the rename preserves the exact
`exec` parameter list.)
Why the batch path survives: `run_batch` writes the per-request params **inside** the `requestsFile` JSON
object, and `taint.sc` parses that JSON itself (`requestsJson`, taint.sc:67). The extra fields ride as JSON
keys, never as `exec` arguments, so they are tolerated.
## Blast radius
Broken (single-query taint):
- `taint._usable_flows` (`taint.py:124`) → `verdicts()` (`taint.py:131`) → `verdict()` (`taint.py:99`).
- `pipeline.scan_region` dispatches taint families to `taint_verdict` (`pipeline.py:121`) → so **per-region
single-query taint via `scan_region` is dead** whenever the disposable or session route is taken.
Unaffected:
- The push/scan **batch** path: `scan.py` uses `run_batch("taint", …)` (scan.py:1259, 1264, 1387). This is
why the engine-on pilot produced results.
- `config` and `dominance` single-queries (`config_value.py:46`, `dominance.py:54`) — their `.sc` signatures
are presumed to match what their specs emit (confirm separately; not exercised here).
Open question to close: enumerate every production path that reaches `scan_region`/`verdict()` taint rather
than the batch route, to state whether any shipped push/replay flow silently loses taint flows today, or
whether the single-query path is only hit by pair measurements and ad-hoc harnesses.
## Evidence
- `benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/` — the batch path works (11 flow rows,
`write_text` sinks); the v1 single-query attempt returned 0/0 on `cpg query taint failed`.
- Raw joern error captured by `banks_debug.py` (scratchpad): `Missing argument: --cpgFile; Unknown
arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`.
## Suggested fix (not yet applied)
Pick one, lowest-risk first:
1. **Make `backend.query()` filter params to the script's declared signature** before building `--param` /
named-arg lists (parse the `@main def exec(` arg names once, intersect). Fixes every current and future
signature skew for all single-query scripts, symmetrically with how the batch path already tolerates
extras. Preferred.
2. Add the five params to `taint.sc`'s `exec` signature as ignored passthroughs (mirrors batch semantics but
duplicates the JSON parsing already in the script; narrower).
3. Route `_usable_flows` through `run_batch` with a one-element request (makes single-query taint share the
tolerant path; changes the call shape upstream).
Add a regression test under the joern-gated suite: `cpg.run("taint", request_params(path_spec, …))` must
return a parsed payload (not `None`) on a fixture CPG, asserting the instrument actually executed.