From 9f881b433e52248d595e89e51f3107a016151df0 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:32:00 +0200 Subject: [PATCH] fix(cpg): the closure-defect exclusion stops at Joern 4.0.625, where upstream fixed it joern#6281 is closed as fixed in v4.0.625 (verified here on the reproducer). The version is read off the jar beside the launcher; below 4.0.625 the miscompiled file is still excluded, at or above it nothing is, and an install whose version cannot be read keeps the workaround. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- src/openultrasast/cpg/backend.py | 31 +++++++++++++++++++++++++++++++ tests/test_cpg_backend.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index 5521a93..92f67b3 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -128,6 +128,34 @@ _INTERPRETERS: dict[str, str] = {"php": "php"} # other 99.8%, which is the difference between a scan and no scan at all. They are reported as `unparsed`, # so the coverage section names them rather than implying they were clean. _CLOSURE = re.compile(r"\bfunction\s*\(") + +# The release that fixes the miscompile (joernio/joern#6281, same root cause as #6269, fixed by #6270). +# At or above it the exclusion below is a loss with nothing to buy, so it is not applied. +CLOSURE_DEFECT_FIXED_IN = (4, 0, 625) +_JOERN_JAR = re.compile(r"^io\.joern\.joern-cli-(\d+)\.(\d+)\.(\d+)\.jar$") + + +def joern_version() -> tuple[int, int, int] | None: + """The installed Joern's version, read off the jar beside the launcher; ``None`` when it cannot be told. + + The launcher is a shell script with no `--version`, and every release ships `lib/io.joern.joern-cli-X.Y.Z.jar` + next to it. Unknown is unknown: a caller gating a workaround on this keeps the workaround. + """ + launcher = shutil.which("joern") + if launcher is None: + return None + lib = Path(launcher).resolve().parent / "lib" + try: + names = [entry.name for entry in lib.iterdir()] + except OSError: + return None + for name in names: + match = _JOERN_JAR.match(name) + if match: + return (int(match.group(1)), int(match.group(2)), int(match.group(3))) + return None + + _GLOBAL_IN_BODY = re.compile(r"\bglobal\s+\$") FRONTEND_BUILD_ATTEMPTS = 4 @@ -344,6 +372,9 @@ class JoernBackend: """ if language.lower() != "php": return () + version = joern_version() + if version is not None and version >= CLOSURE_DEFECT_FIXED_IN: + return () # fixed upstream; nothing to exclude and every file to keep found: list[str] = [] for path in sorted(root.rglob("*.php")): if not path.is_file(): diff --git a/tests/test_cpg_backend.py b/tests/test_cpg_backend.py index 497df83..c7e5d13 100644 --- a/tests/test_cpg_backend.py +++ b/tests/test_cpg_backend.py @@ -1042,3 +1042,34 @@ def test_a_build_that_fails_on_both_launchers_leaves_no_scratch_behind(tmp_path: monkeypatch.setattr("shutil.which", lambda name: f"/opt/bin/{name}") assert JoernBackend(runner=runner).build(tmp_path, language="javascript") is None assert list(scratch_root.iterdir()) == [], "the failed build's scratch directory is gone" + + +def test_the_closure_exclusion_stops_at_the_release_that_fixed_it(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """joern#6281 is fixed in v4.0.625. Below it the miscompiled file is excluded (the whole graph is the + alternative); at or above it nothing is excluded, because a loss with nothing to buy is only a loss. + An install whose version cannot be read keeps the exclusion: unknown is not "fixed".""" + from openultrasast.cpg.backend import JoernBackend, joern_version + + (tmp_path / "bad.php").write_text(' Path: + home = tmp_path / f"joern-{version}" + (home / "lib").mkdir(parents=True) + (home / "lib" / f"io.joern.joern-cli-{version}.jar").write_bytes(b"") + (home / "joern").write_text("#!/bin/sh\n") + return home / "joern" + + old, new = install("4.0.623"), install("4.0.625") + backend = JoernBackend(runner=lambda c, **k: None) + + monkeypatch.setattr("shutil.which", lambda name: str(old) if name == "joern" else None) + assert joern_version() == (4, 0, 623) + assert [Path(n).name for n in backend._files_with_frontend_defect(tmp_path, "php")] == ["bad.php"] + + monkeypatch.setattr("shutil.which", lambda name: str(new) if name == "joern" else None) + assert joern_version() == (4, 0, 625) + assert backend._files_with_frontend_defect(tmp_path, "php") == () + + monkeypatch.setattr("shutil.which", lambda name: "/opt/bin/joern") + assert joern_version() is None + assert [Path(n).name for n in backend._files_with_frontend_defect(tmp_path, "php")] == ["bad.php"], "unknown keeps the workaround"