From 41f56170bb5a8bc44c0ec77005b5baf3bc68478b Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:41:54 +0200 Subject: [PATCH] learn: engine_trace worker renders request params through the production _render; records query stderr The worker sent tuple params as JSON arrays; taint.sc expects comma-joined strings (ujson InvalidData, exit 1). Implemented with Codex. Co-Authored-By: Claude Opus 5.5 --- benchmarks/learn/engine_trace_worker.py | 23 ++++++- tests/test_engine_trace.py | 88 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/benchmarks/learn/engine_trace_worker.py b/benchmarks/learn/engine_trace_worker.py index a24f676..33a9733 100644 --- a/benchmarks/learn/engine_trace_worker.py +++ b/benchmarks/learn/engine_trace_worker.py @@ -18,7 +18,7 @@ import time from collections import Counter from pathlib import Path -from openultrasast.cpg.backend import JoernBackend +from openultrasast.cpg.backend import JoernBackend, _render from openultrasast.model.trace import parse_trace @@ -141,6 +141,17 @@ class MeasuredBackend(JoernBackend): "oom": b"OutOfMemoryError" in output, } self.stages.append(stage) + if query and process.returncode != 0: + # stderr is merged into stdout above; drop JVM frames before taking + # the tail so they cannot crowd out the exception's message. + lines = [ + line.strip() + for line in output.decode(errors="replace").splitlines() + if line.strip() and not line.strip().startswith(("at ", "... ")) + ] + stage["output_tail"] = "\n".join(lines[-20:]) + if not self.timed_out: + self.fatal = f"joern query exited {process.returncode}" if frontend: # Capture the exact frontend output before build failure cleanup or overlays. output_arg = next((i + 1 for i, arg in enumerate(command[:-1]) if arg in {"-o", "--output"}), None) @@ -149,7 +160,7 @@ class MeasuredBackend(JoernBackend): stage["cpg_bytes"] = cpg_path.stat().st_size if cpg_path is not None and cpg_path.is_file() else 0 if stage["cpg_bytes"] <= MIN_CPG_BYTES: self.fatal = "frontend wrote no CPG" - elif elapsed < 5: + elif elapsed < 5 and not self.fatal: self.fatal = f"implausibly fast JVM step: {command[0]} in {elapsed:.3f}s (<5s)" if b"OutOfMemoryError" in output: self.fatal = "JVM out of memory" @@ -222,7 +233,7 @@ def worker_language(pin, root, out, deadline, question_deadline, persist=write_j if len(languages) != 1: raise ValueError(f"one pin has multiple frontend languages: {sorted(languages)}") language = next(iter(languages)) - requests = pin["questions"] + requests = {rid: {key: _render(value) for key, value in params.items()} for rid, params in pin["questions"].items()} if set(requests) != {u["unit"] for u in active}: raise ValueError("prepared questions do not match supported units") record["questions"] = requests @@ -304,6 +315,12 @@ def worker_language(pin, root, out, deadline, question_deadline, persist=write_j finally: if cpg and cpg.cleanup: cpg.cleanup() + for stage in backend.stages: + if stage["command"] == "joern" and stage.get("exit") and "output_tail" in stage: + detail = f"joern query exited {stage['exit']}: {stage['output_tail'] or 'no output'}" + for row in record["units"]: + if row["supported"]: + row["reason"] = f"{row['reason']}\n{detail}".strip() record["questions_asked"] = list(dict.fromkeys(backend.asked)) record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]] record["done"] = True diff --git a/tests/test_engine_trace.py b/tests/test_engine_trace.py index 862580c..4e4a6c6 100644 --- a/tests/test_engine_trace.py +++ b/tests/test_engine_trace.py @@ -189,6 +189,94 @@ def test_worker_one_batch_and_preserves_witnesses(tmp_path, monkeypatch): assert result["instrument"]["files"][0]["bytes"] > 0 +@pytest.mark.parametrize("json_roundtrip", [False, True]) +def test_worker_writes_string_requests_for_php_and_python(tmp_path, monkeypatch, json_roundtrip): + written = {} + submitted = {} + + class RequestBackend(worker.MeasuredBackend): + def build(self, root, language): + graph = root / f"{language}.bin" + graph.write_bytes(b"x" * 2048) + self.stages.append({"command": "joern-parse", "seconds": 20, "exit": 0, "cpg_bytes": 2048}) + + def batch(query, requests): + submitted.update(requests) + return self._batch_once(graph, query, requests) + + return CpgResult(graph, lambda q, p: None, run_batch_once=batch) + + def _run(self, command, **kwargs): + request_file = Path(next(arg.split("=", 1)[1] for arg in command if arg.startswith("requestsFile="))) + data = request_file.read_bytes() + print(f"read {request_file.name}: {len(data)} bytes") + requests = json.loads(data) + written.update(requests) + self.asked.extend(requests) + self.stages.append({"command": "joern", "seconds": 20, "exit": 0}) + answers = {**dict.fromkeys(requests, []), "__census__": [{"files": "1", "file_names": ["app.php", "app.py"]}]} + return subprocess.CompletedProcess(command, 0, BEGIN + "\n" + json.dumps(answers) + "\n" + END, "") + + monkeypatch.setattr(worker, "MeasuredBackend", RequestBackend) + (tmp_path / "app.php").write_text("