fix(cpg): a batch goes through a file, not the command line

Scanning a 637-file WordPress plugin reported 500 regions examined, zero
findings, and a taint query time of **0.05 seconds** -- less than a JVM
takes to start. Dominance ran for 29.56s and config for 22.44s in the same
scan, so only taint had failed.

Linux caps a SINGLE argument at MAX_ARG_STRLEN (128KB) whatever ARG_MAX
says. Measured on that plugin:

  taint      2500 requests   1.49 MB   <- over the cap
  dominance    13 requests   0.04 MB
  config       13 requests   0.00 MB

As `--param requests=<json>` execve returned E2BIG, `_run` caught the
OSError, `query_batch` returned {} -- and {} reads as "no rows found". Only
taint crossed the cap, which is exactly why the failure looked like a
result rather than an error.

This is the quiet failure the rung ladder cannot catch. Every other honesty
mechanism in this codebase labels a verdict; here no verdict was produced
to label, and a scan of a vulnerable plugin reported it clean.

The payload is now staged to a file beside the CPG and the path is passed
instead; all three queries read `requestsFile` when it is set and keep
`requests` for the single-request forms and the tests. A test asserts no
argument approaches the cap, since the failure mode is invisible at the
scale the unit tests run at -- batching worked on every small tree it was
ever tried on.

Verified on vampi first: 4 entailed, unchanged, all three known
vulnerabilities still found.

Gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-09 16:27:58 +02:00
co-authored by Claude Opus 5
parent 9c50e17c11
commit 92677136d7
5 changed files with 87 additions and 10 deletions
+35
View File
@@ -303,3 +303,38 @@ def test_an_unknown_language_has_nothing_to_retry_with(tmp_path: Path, monkeypat
monkeypatch.setenv("OPENULTRASAST_JOERN_PROBE", "on")
assert JoernBackend(runner=runner).build(tmp_path, language="cobol") is None
assert JoernBackend(runner=runner).build(tmp_path) is None
def test_a_batch_goes_through_a_file_not_the_command_line(tmp_path: Path) -> None:
"""Linux caps a SINGLE argument at MAX_ARG_STRLEN (128KB) whatever ARG_MAX says.
2500 taint requests over a 637-file WordPress plugin is a 1.49MB payload. As `--param requests=<json>`
execve returned E2BIG, the runner caught the OSError, query_batch returned {} -- and {} reads as "no rows
found". The scan reported 500 regions examined, nothing found, and 0.05 seconds of taint query: a quiet
failure the rung ladder cannot catch, because no verdict was ever produced to label.
"""
from openultrasast.cpg.backend import BEGIN, END, JoernBackend
commands: list[list[str]] = []
class _Done:
returncode = 0
stderr = ""
stdout = f'{BEGIN}\n{{"r0": []}}\n{END}\n'
def runner(command, **kwargs): # type: ignore[no-untyped-def]
commands.append(list(command))
return _Done()
cpg = tmp_path / "cpg.bin"
cpg.write_text("cpg")
big = {f"r{i}": {"sources": ("request.args",) * 40, "sinks": ("execute",) * 40} for i in range(2000)}
JoernBackend(runner=runner).query_batch(cpg, "taint", big)
argv = commands[0]
assert not any(len(arg) > 131072 for arg in argv), "no argument may approach MAX_ARG_STRLEN"
staged = [arg for arg in argv if arg.startswith("requestsFile=")]
assert staged, "the batch is staged to a file"
written = Path(staged[0].split("=", 1)[1])
assert written.is_file() and len(written.read_text()) > 200_000, "and the file holds the whole payload"