From 6089c67bf693af2a5927d18535c1574913e52993 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:58:42 +0200 Subject: [PATCH] fix(cpg): report the line that names an engine failure, not its frames Every PHP failure logged `cpg batch taint failed: dure1.java:10)`, which is the middle of a stack frame and says nothing. A trace puts its exception on the FIRST line and frames after it, and the backend logged the last 400 characters, so the cause was discarded and the noise kept. Four investigations were spent on that. Failures now report the leading non-frame lines, which is where the exception and its message are, with the frames dropped. A control asserts an OutOfMemoryError survives while a frame does not, and that a timeout and an empty exit stay distinguishable. 1,411 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- src/openultrasast/cpg/backend.py | 21 +++++++++++++++++++-- tests/test_cpg_backend.py | 13 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index b1cd311..89244b4 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -1129,7 +1129,7 @@ class JoernBackend: command += ["--param", f"{name}={value}"] completed = self._run(command, timeout=self.query_timeout, cwd=cpg_path.parent) if completed is None or completed.returncode != 0: - detail = (completed.stderr or "")[-400:] if completed is not None else "timeout" + detail = _engine_failure(completed) logger.warning("cpg batch %s failed: %s", query, detail) return None body = completed.stdout or "" @@ -1162,7 +1162,7 @@ class JoernBackend: # it can never land in the repository being analysed. completed = self._run(command, timeout=self.query_timeout, cwd=cpg_path.parent) if completed is None or completed.returncode != 0: - detail = (completed.stderr or "")[-400:] if completed is not None else "timeout" + detail = _engine_failure(completed) logger.warning("cpg query %s failed: %s", query, detail) return None body = completed.stdout or "" @@ -1541,6 +1541,23 @@ def _configured_timeout() -> int: return QUERY_TIMEOUT_SECONDS +def _engine_failure(completed: subprocess.CompletedProcess[str] | None) -> str: + """What the engine said, keeping the line that names the cause. + + A stack trace's exception is on its FIRST line and its frames follow, so logging only the tail + reports frames and discards the cause. Four PHP investigations were spent on `cpg batch taint + failed: dure1.java:10)`, which is the middle of a frame and says nothing at all. + """ + if completed is None: + return "timeout" + text = (completed.stderr or completed.stdout or "").strip() + if not text: + return f"exit {completed.returncode} with no output" + lines = [line for line in text.splitlines() if line.strip() and not line.strip().startswith("at ")] + head = " | ".join(lines[:4])[:600] + return head or text[:300] + + def _render(value: object) -> str: """CPGQL parameters are strings; a sequence becomes a comma-separated list the script splits.""" if isinstance(value, str): diff --git a/tests/test_cpg_backend.py b/tests/test_cpg_backend.py index 6a45803..386ca32 100644 --- a/tests/test_cpg_backend.py +++ b/tests/test_cpg_backend.py @@ -1217,3 +1217,16 @@ def test_a_batch_no_part_can_answer_is_still_unavailable(tmp_path, monkeypatch): graph.write_bytes(b"graph") # None means "could not ask", which must never become an empty answer. assert backend.query_batch(graph, "taint", {str(i): {"file": f"f{i}.php"} for i in range(8)}) is None + + +def test_an_engine_failure_reports_the_line_that_names_the_cause(tmp_path, monkeypatch): + """A stack trace's exception is on its first line; logging the tail reports frames only.""" + from openultrasast.cpg.backend import JoernBackend, _engine_failure + + trace = "java.lang.OutOfMemoryError: Java heap space\n\tat foo.Bar.baz(Bar.java:10)\n\tat x.Procedure1.java:10)" + named = _engine_failure(subprocess.CompletedProcess(["joern"], 1, "", trace)) + assert "OutOfMemoryError" in named and "Java heap space" in named + assert "Bar.java:10" not in named, "frames are noise; the cause is not" + assert _engine_failure(None) == "timeout" + assert "no output" in _engine_failure(subprocess.CompletedProcess(["joern"], 3, "", "")) + assert isinstance(JoernBackend(), JoernBackend)