Ran the humanizer rules over 79 prose files in the active specs (brief, design, requirements, research and narrative docs). Removed em dashes, significance inflation, forced rule-of-three, bold-label list items and filler. Skipped tasks.md checklists and the retired specs (harnessx-*, openrouter-sast-harness, unseen-repo-evaluation). Every number, path, command, hash and identifier was preserved: checked each file against HEAD for drift in numbers, inline code and code fences, zero drift across all 79 files. Co-Authored-By: Claude Opus 4.8 <[email protected]>
8.1 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: CLOSED by call-graph trace (2026-10-12). No shipped product path and no in-repo
benchmark/test reached the real single-query JoernBackend.query("taint"); the bug was latent but unreached
by the product. Traced:
- Shipped push/scan/replay (
cli.py:1334→scan_repository→_scan_repository_impl) is DOUBLE shielded: Phase 2 fetches taint via the batch route (run_batch, always wired bybuild()), and Phase 3'sscan_region(scan.py:559) runs against a_prefetchedCpgResult whoserunignores the query and returns the already-fetched batch rows (scan.py:1472). Soscan_region → taint_verdict → _usable_flows → cpg.run("taint", …)is a no-op replay that never reachesJoernBackend.query(). - The non-batching fallback (
scan.py:519 cpg.run(kind, params)) only runs for a backend withoutrun_batch;JoernBackendalways provides it (backend.py:625/658), so it is dead for Joern. - In-repo real-backend benchmarks both use the batch path:
benchmarks/ranking/position.pyviascan_repository;benchmarks/learn/engine_trace_worker.pyviacpg.run_batch_once("taint", …). tests/test_model_taint.py/tests/test_model_pipeline.pydriveverdict/scan_regionagainst FAKE canned-rowCpgResults, not a realJoernBackend, so they never exercisedquery()with these params.- The only callers that reached the real single-query path were out-of-product ad-hoc harnesses (pair /
measurement scripts, the banks MVP). Regression onset:
562b670(2026-09-25), whenrequest_paramsbegan emittingfixedOrigin/originAnchors(taint.sc never declared them onexec; they are batch-JSON only by design). Residual unknown: any EXTERNAL/historical pair harness that drove single-query taint against a real backend after that date would have silently zeroed; none is present in-repo to confirm.
The fix restores the single-query path for pair/direct use without altering push behaviour.
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.