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]>
6.5 KiB
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--paramlist (disposable route) and the in-session named-arg call (session route). This is faithful, not lossy: the single-request branch oftaint.sc(line 1542) callsrowsFor(...)with top-level params only and letsfixedOrigin/originAnchors/quotedSanitizers/guards/fieldParameterSourcesdefault — it never could consume them. The batch path keeps carrying them as JSON the script parses itself.- No-op for
config/dominance: their ownrequest_paramsemit only params their.scdeclares (verified), so nothing is dropped. Onlytaintsheds 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 errorsUnknown arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources(and, as a knock-on,Missing argument: --cpgFile). Observed verbatim viabanks_debug.pyon 2026-10-12. - Session route (lines 1321–1326): defines
def ousast_taint(...)by renaming@main def exec(and then callsousast_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 exactexecparameter 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_regiondispatches taint families totaint_verdict(pipeline.py:121) → so per-region single-query taint viascan_regionis dead whenever the disposable or session route is taken.
Unaffected:
- The push/scan batch path:
scan.pyusesrun_batch("taint", …)(scan.py:1259, 1264, 1387). This is why the engine-on pilot produced results. configanddominancesingle-queries (config_value.py:46,dominance.py:54) — their.scsignatures 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_textsinks); the v1 single-query attempt returned 0/0 oncpg 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:
- 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. - Add the five params to
taint.sc'sexecsignature as ignored passthroughs (mirrors batch semantics but duplicates the JSON parsing already in the script; narrower). - Route
_usable_flowsthroughrun_batchwith 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.