Files
Open_Ultra_SAST/.kiro/specs/engine-change-scoped-analysis/bugs/taint-sc-single-query-signature-skew.md
T
Marius CiepluchandClaude Opus 4.8 8f7fc4ae83 measure(g2): banks fixed-search MVP -- "no wrong targets, only wrong searching" demonstrated
Same CPG, only the search changes. Baseline (shipped path spec) resolves 0
prompt->write_text flows on banks; injecting the two annotations an LLM steer
would supply (parameter_sources=True + write_text/read_text path sinks) makes the
engine resolve prompt -> path-join -> prompt_file.write_text(prompt.raw). First
measured instance of "#2 guided by #1" closing on an unseen target; fix needed
exactly the param-source + file-write-sink annotations.

Files the instrument bug the first MVP hit: single-query cpg.run("taint",
request_params(...)) is dead -- request_params emits 5 keys (guards/fixedOrigin/
originAnchors/quotedSanitizers/fieldParameterSources) absent from taint.sc's exec
signature; both single-query sub-routes reject them (silent None). Batch path
(run_batch->requestsFile JSON) unaffected; push/scan route uses batch.

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

4.8 KiB
Raw Blame History

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: OPEN

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.