learn: engine_trace fetches real source at the pinned commit, 2.5 GB JVM heap, one CPG per language

Root causes of the 50 failed pins in trace run 1: advisory pairs were fed as
excerpts (invalid Java/JS fragments), the JVM defaulted to ~768 MB heap in a
3 GB container, and mixed-language pins were refused. Implemented with Codex.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-03 22:36:23 +02:00
co-authored by Claude Opus 5.5
parent 36a3355b81
commit fe2a424018
3 changed files with 509 additions and 53 deletions
+194 -42
View File
@@ -1,9 +1,7 @@
"""Resumable, serial, labelled-function Joern traces. No network or live-source mounts. """Resumable, serial labelled-function traces; host fetches pinned sources, Docker stays offline.
Pair pins are excerpt blob identities, NOT commits: materialize their catalog side Pair blob identities resolve through catalogs to real source commits. The analyzer
and registration documents. Repository pins are exported from existing local clones. is git archive HEAD src plus a hashed snapshot of this uncommitted tool.
The analyzer is git archive HEAD src plus a hashed snapshot of this uncommitted tool's
implementation files; this allows review/worktree use without making a commit.
""" """
from __future__ import annotations from __future__ import annotations
@@ -13,6 +11,7 @@ import fcntl
import hashlib import hashlib
import json import json
import os import os
import re
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
@@ -21,7 +20,7 @@ import tomllib
from collections import Counter, defaultdict from collections import Counter, defaultdict
from pathlib import Path from pathlib import Path
from engine_trace_worker import safe_path, unit_record, write_json from engine_trace_worker import EXCLUDED_DIRS, safe_path, unit_record, write_json
from openultrasast.learn.examples import read_jsonl from openultrasast.learn.examples import read_jsonl
from openultrasast.learn.labels import load_sources, repo_name from openultrasast.learn.labels import load_sources, repo_name
@@ -42,6 +41,9 @@ SNAPSHOT_FILES = (
"src/openultrasast/model/taint.py", "src/openultrasast/model/taint.py",
"src/openultrasast/cpg/queries/taint.sc", "src/openultrasast/cpg/queries/taint.sc",
) )
MATERIALIZATION_VERSION = 2
EXPORT_CAP = 20 * 1024 * 1024
STATUSES = ("path", "asked-nothing", "failed", "timeout", "unsupported") STATUSES = ("path", "asked-nothing", "failed", "timeout", "unsupported")
@@ -81,7 +83,7 @@ def join_units(units, examples, include_typescript=False):
"function": function, "function": function,
"language": language, "language": language,
"source": example["source"], "source": example["source"],
"excerpt": example["source"] == "pairs", "excerpt": example.get("excerpt", example["source"] == "pairs" or example["source"].startswith("advisory-fixes")),
"pin_role": example.get("pin_role", ""), "pin_role": example.get("pin_role", ""),
"supported": spec is not None, "supported": spec is not None,
"reason": "" if spec else f"no admitted taint model: {language}/{unit['family']}", "reason": "" if spec else f"no admitted taint model: {language}/{unit['family']}",
@@ -90,23 +92,48 @@ def join_units(units, examples, include_typescript=False):
return result return result
def make_plan(units, out, only_family=None, limit=0, *, include_typescript=False): def make_plan(
units,
out,
only_family=None,
limit=0,
*,
include_typescript=False,
only_source=(),
rerun_status=("failed",),
allow_excerpt_fallback=False,
):
grouped = defaultdict(list) grouped = defaultdict(list)
for unit in units: for unit in units:
if not only_family or unit["family"] == only_family: if (not only_family or unit["family"] == only_family) and (not only_source or unit["source"] in only_source):
# Include admission options even when a unit's support happens to stay # Include admission options even when a unit's support happens to stay
# unchanged. Old results cannot mark a different selection as complete. # unchanged. Old results cannot mark a different selection as complete.
identity = {"unit": unit, "include_typescript": include_typescript} identity = {
"unit": unit,
"include_typescript": include_typescript,
"materialization_version": MATERIALIZATION_VERSION,
"allow_excerpt_fallback": allow_excerpt_fallback,
}
grouped[unit["repo"], unit["pin"]].append({**unit, "input_digest": digest(identity)}) grouped[unit["repo"], unit["pin"]].append({**unit, "input_digest": digest(identity)})
plan = [] plan = []
for (repo, pin), rows in sorted(grouped.items()): for (repo, pin), rows in sorted(grouped.items()):
item = {"repo": repo, "pin": pin, "units": sorted(rows, key=lambda r: r["unit"])} item = {
"repo": repo,
"pin": pin,
"materialization_version": MATERIALIZATION_VERSION,
"units": sorted(rows, key=lambda r: r["unit"]),
}
item["selection"] = digest(item["units"]) item["selection"] = digest(item["units"])
done = out / pin_name(item) done = out / pin_name(item)
if done.exists(): if done.exists():
old = json.loads(done.read_text()) old = json.loads(done.read_text())
recorded = {r["unit"]: r.get("input_digest") for r in old.get("units", [])} recorded = {r["unit"]: r.get("input_digest") for r in old.get("units", [])}
if old.get("done") and all(recorded.get(r["unit"]) == r["input_digest"] for r in rows): if (
old.get("done")
and old.get("materialization_version") == MATERIALIZATION_VERSION
and not any(r.get("status") in rerun_status for r in old.get("units", []) if r["unit"] in {u["unit"] for u in rows})
and all(recorded.get(r["unit"]) == r["input_digest"] for r in rows)
):
continue continue
sources = {r["source"] for r in rows} sources = {r["source"] for r in rows}
# Inventory medians: harvest sides 31s, advisory sides 47s; v1 pins 182s, v2 pins 498s. # Inventory medians: harvest sides 31s, advisory sides 47s; v1 pins 182s, v2 pins 498s.
@@ -133,10 +160,15 @@ def make_plan(units, out, only_family=None, limit=0, *, include_typescript=False
class Inputs: class Inputs:
"""Resolve only the explicitly listed, spent corpora. Never fetch a missing clone.""" """Resolve spent corpora; fetch pair source commits into the host cache."""
def __init__(self, root, cache): def __init__(self, root, cache, allow_excerpt_fallback=False):
self.cache = cache self.cache = cache
self.allow_excerpt_fallback = allow_excerpt_fallback
self.catalog_rows = defaultdict(list)
for catalog in (root / "benchmarks/pairs").rglob("catalog.toml"):
for item in tomllib.loads(catalog.read_text()).get("pair", []):
self.catalog_rows[item["name"]].append((catalog.parent.name, item))
self.pairs = [] self.pairs = []
self.repos = defaultdict(list) self.repos = defaultdict(list)
sources = load_sources() sources = load_sources()
@@ -159,6 +191,8 @@ class Inputs:
self.hashes = {} self.hashes = {}
def pair_for(self, row): def pair_for(self, row):
if row["pin_role"] not in {"fixed", "vulnerable", "vuln"}:
raise ValueError(f"ambiguous pair side: {row['pin_role']}")
side = "fixed" if row["pin_role"] == "fixed" else "vuln" side = "fixed" if row["pin_role"] == "fixed" else "vuln"
candidates = [ candidates = [
c for c in self.pairs if c.relpath == row["file"] and repo_name(c.repo or f"{c.slice}:{c.name}") == repo_name(row["repo"]) c for c in self.pairs if c.relpath == row["file"] and repo_name(c.repo or f"{c.slice}:{c.name}") == repo_name(row["repo"])
@@ -188,42 +222,146 @@ class Inputs:
return case, side return case, side
raise ValueError(f"cannot uniquely resolve excerpt {row['repo']}@{row['pin']}:{row['file']}") raise ValueError(f"cannot uniquely resolve excerpt {row['repo']}@{row['pin']}:{row['file']}")
def catalog_for(self, case):
matches = [
(source, item)
for source, item in self.catalog_rows[case.name]
if item.get("relpath") == case.relpath and repo_name(item.get("repo", "")) == repo_name(case.repo)
]
if len(matches) != 1:
raise ValueError(f"ambiguous catalog source for {case.name}")
return matches[0]
def source_identity(self, case, side):
_, item = self.catalog_for(case)
parent, commit = item.get("parent", ""), item.get("commit", "")
if side not in {"vuln", "fixed"} or not all(re.fullmatch(r"[0-9a-fA-F]{40}", sha) for sha in (parent, commit)) or parent == commit:
raise ValueError(f"ambiguous catalog commits for {case.name}: need distinct parent and commit SHAs")
repo = item.get("repo", "")
if not repo:
raise ValueError(f"missing catalog repository for {case.name}")
return repo, parent if side == "vuln" else commit
def fetch_source(self, repo, commit, roots):
clone = self.cache / "trace-sources" / digest([repo, commit, roots])
clone.mkdir(parents=True, exist_ok=True)
url = repo if repo.startswith("https://") else f"https://github.com/{repo.removesuffix('.git')}.git"
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
env.pop("GIT_NO_LAZY_FETCH", None) # sparse checkout may fetch the selected blobs
def git(*args):
return subprocess.run(["git", "-C", str(clone), *args], env=env, capture_output=True, text=True, check=True, timeout=300)
git("init")
git("config", "remote.origin.url", url)
git("config", "remote.origin.promisor", "true")
git("config", "remote.origin.partialclonefilter", "blob:none")
git("fetch", "--depth", "1", "--filter=blob:none", url, commit)
git("sparse-checkout", "init", "--cone")
git("sparse-checkout", "set", "--", *roots)
git("checkout", "--detach", "--force", commit)
actual = git("rev-parse", "HEAD").stdout.strip()
if actual != commit:
raise ValueError(f"fetched commit mismatch: {actual} != {commit}")
return clone
def export_local(self, clone, pin, target):
with tempfile.TemporaryDirectory(prefix="trace-export-", dir=target.parent) as scratch:
export_pin(clone, pin["pin"], Path(scratch))
proof = export_sources(Path(scratch), target, pin["units"])
return {**proof, "mode": "source", "commit": pin["pin"]}
def materialize(self, pin, target): def materialize(self, pin, target):
rows = pin["units"] rows = pin["units"]
for row in rows: for row in rows:
safe_path(target, row["file"]) safe_path(target, row["file"])
if row.get("mapping_error"): if row.get("mapping_error"):
raise ValueError(row["mapping_error"]) raise ValueError(row["mapping_error"])
if all(r.get("excerpt") for r in rows): if any(r.get("excerpt") or r["source"].startswith("advisory-fixes") for r in rows):
seen = set() resolved = [self.pair_for(row) for row in rows]
for row in rows: identities = {self.source_identity(case, side) for case, side in resolved}
case, side = self.pair_for(row) if len(identities) != 1:
if (case.name, side) not in seen: raise ValueError("ambiguous source commits at one pair pin")
files = [(case.relpath, case.fixed_file if side == "fixed" else case.vuln_file)] repo, commit = identities.pop()
files.extend((rel, fixed if side == "fixed" else vuln) for rel, vuln, fixed in case.context_files) roots = sorted({source_root(row["file"], row["language"]) for row in rows})
for rel, source in files: try:
dest = safe_path(target, rel) clone = self.fetch_source(repo, commit, roots)
if dest.exists() and dest.read_bytes() != source.read_bytes(): proof = export_sources(clone, target, rows, roots)
raise ValueError(f"conflicting excerpt/context at one pin: {rel}") except (OSError, ValueError, subprocess.SubprocessError) as exc:
if not self.allow_excerpt_fallback:
raise
shutil.rmtree(target, ignore_errors=True)
for case, side in resolved:
_materialize_side(target, case, side=side) _materialize_side(target, case, side=side)
seen.add((case.name, side)) return {"mode": "excerpt-fallback", "reason": str(exc), "repo": repo, "commit": commit}
return verify_functions(target, rows)
return {**proof, "mode": "source", "repo": repo, "commit": commit, "roots": roots}
if all(r["source"] == "dev-php" for r in rows): if all(r["source"] == "dev-php" for r in rows):
# Export the exact requested commit from the local recipe checkout. # Export the exact requested commit from the local recipe checkout.
for recipe in (ROOT / "benchmarks/repos").glob("*.toml"): for recipe in (ROOT / "benchmarks/repos").glob("*.toml"):
data = tomllib.loads(recipe.read_text()) data = tomllib.loads(recipe.read_text())
if repo_name(str(data.get("url", ""))) == repo_name(pin["repo"]): if repo_name(str(data.get("url", ""))) == repo_name(pin["repo"]):
checkout = self.cache / "repos" / data["name"] / pin["pin"][:12] checkout = self.cache / "repos" / data["name"] / pin["pin"][:12]
export_pin(checkout, pin["pin"], target) return self.export_local(checkout, pin, target)
return
for clone, _ in self.repos[repo_name(pin["repo"])]: for clone, _ in self.repos[repo_name(pin["repo"])]:
check = subprocess.run(["git", "-C", str(clone), "cat-file", "-e", pin["pin"] + "^{commit}"], capture_output=True) check = subprocess.run(["git", "-C", str(clone), "cat-file", "-e", pin["pin"] + "^{commit}"], capture_output=True)
if check.returncode == 0: if check.returncode == 0:
export_pin(clone, pin["pin"], target) return self.export_local(clone, pin, target)
return
raise ValueError(f"no local clone contains {pin['repo']}@{pin['pin']}") raise ValueError(f"no local clone contains {pin['repo']}@{pin['pin']}")
def source_root(file, language):
parts = Path(file).parts
if language == "java":
for i in range(len(parts) - 2):
if parts[i : i + 3] == ("src", "main", "java"):
return "/".join(parts[: i + 3])
return parts[0] if len(parts) > 1 else "."
def export_sources(source, target, rows, roots=(".",), cap=EXPORT_CAP):
"""Preserve labelled paths first; cap context and exclude non-source trees."""
labelled = {r["file"] for r in rows}
languages = {r["language"] for r in rows}
files = set()
for directory, dirs, names in os.walk(source):
dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRS and d != ".git" and not (Path(directory) / d).is_symlink())
for name in names:
file = Path(directory) / name
rel = file.relative_to(source).as_posix()
if (
not file.is_symlink()
and detect_language(file) in languages
and any(root == "." or rel.startswith(root + "/") for root in roots)
):
files.add(rel)
missing = labelled - files
if missing:
raise ValueError(f"labelled files missing or excluded from export: {sorted(missing)}")
proof = {"bytes": 0, "files": 0, "cap_bytes": cap, "cap_hit": False, "excludes": sorted(EXCLUDED_DIRS)}
for rel in sorted(files, key=lambda rel: (rel not in labelled, rel)):
file = safe_path(source, rel)
size = file.stat().st_size
if proof["bytes"] + size > cap:
proof["cap_hit"] = True
if rel in labelled:
raise ValueError("labelled source exceeds 20 MB export cap")
continue
dest = safe_path(target, rel)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(file.read_bytes())
proof["bytes"] += size
proof["files"] += 1
return proof
def verify_functions(root, rows):
for row in rows:
data = safe_path(root, row["file"]).read_text(errors="replace")
if not re.search(r"(?<![\w$])" + re.escape(row["function"]) + r"(?![\w$])", data):
raise ValueError(f"labelled function not found in fetched source: {row['file']}::{row['function']}")
def freeze_source(target): def freeze_source(target):
target.mkdir(parents=True) target.mkdir(parents=True)
archive = target.parent / "source.tar" archive = target.parent / "source.tar"
@@ -280,10 +418,6 @@ def docker_command(source, checkout, out, name, image, deadline, question_deadli
def prepare_questions(pin, root, question_deadline): def prepare_questions(pin, root, question_deadline):
"""Prepare metadata on the host, before entering the dependency-minimal image.""" """Prepare metadata on the host, before entering the dependency-minimal image."""
active = [u for u in pin["units"] if u["supported"]] active = [u for u in pin["units"] if u["supported"]]
languages = {"javascript" if u["language"] == "typescript" else u["language"] for u in active}
if len(languages) != 1:
raise ValueError(f"one pin has multiple frontend languages: {sorted(languages)}")
language = next(iter(languages))
from openultrasast.learn.features import entry_names from openultrasast.learn.features import entry_names
from openultrasast.mapping import php_hook_callbacks from openultrasast.mapping import php_hook_callbacks
from openultrasast.plane.harvest import declares from openultrasast.plane.harvest import declares
@@ -293,11 +427,11 @@ def prepare_questions(pin, root, question_deadline):
entries, _ = entry_names(root, {u["file"] for u in active}) entries, _ = entry_names(root, {u["file"] for u in active})
hooks = ";".join(f"{h}:{f}" for h, functions in php_hook_callbacks(root, targets).items() for f in functions) hooks = ";".join(f"{h}:{f}" for h, functions in php_hook_callbacks(root, targets).items() for f in functions)
requests = {} requests = {}
specs = taint_specs(language=language)
for u in active: for u in active:
specs = taint_specs(language="javascript" if u["language"] == "typescript" else u["language"])
if not declares(safe_path(root, u["file"]).read_text(errors="replace").splitlines(), u["file"], u["function"]): if not declares(safe_path(root, u["file"]).read_text(errors="replace").splitlines(), u["file"], u["function"]):
raise ValueError(f"labelled function not declared: {u['file']}::{u['function']}") raise ValueError(f"labelled function not declared: {u['file']}::{u['function']}")
excerpt = u.get("excerpt", False) excerpt = pin.get("materialization", {}).get("mode") == "excerpt-fallback"
params = request_params( params = request_params(
specs[u["family"]], specs[u["family"]],
file=u["file"], file=u["file"],
@@ -318,7 +452,7 @@ def run_pin(pin, source, provenance, inputs, out, args):
checkout, output = scratch / "case", scratch / "output" checkout, output = scratch / "case", scratch / "output"
output.mkdir() output.mkdir()
try: try:
inputs.materialize(pin, checkout) pin = {**pin, "materialization": inputs.materialize(pin, checkout)}
write_json(output / "pin.json", {**pin, "questions": prepare_questions(pin, checkout, args.question_deadline)}) write_json(output / "pin.json", {**pin, "questions": prepare_questions(pin, checkout, args.question_deadline)})
name = "ousast-trace-" + pin_name(pin)[:16] name = "ousast-trace-" + pin_name(pin)[:16]
command = docker_command(source, checkout, output, name, args.image, args.deadline, args.question_deadline) command = docker_command(source, checkout, output, name, args.image, args.deadline, args.question_deadline)
@@ -438,12 +572,20 @@ def main():
parser.add_argument("--question-deadline", type=int, default=120) parser.add_argument("--question-deadline", type=int, default=120)
parser.add_argument("--limit", type=int, default=0) parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--only-family") parser.add_argument("--only-family")
parser.add_argument("--only-source", default="", help="comma-separated source catalog IDs")
parser.add_argument("--rerun-status", default="failed", help="re-run completed pins with any selected unit in these statuses")
parser.add_argument(
"--allow-excerpt-fallback", action="store_true", help="allow fragments only if fetching/exporting resolved source fails"
)
parser.add_argument("--include-typescript", action="store_true") parser.add_argument("--include-typescript", action="store_true")
parser.add_argument("--dry-run", action="store_true") parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--summary", action="store_true") parser.add_argument("--summary", action="store_true")
args = parser.parse_args() args = parser.parse_args()
if min(args.deadline, args.question_deadline) <= 0 or args.limit < 0: if min(args.deadline, args.question_deadline) <= 0 or args.limit < 0:
parser.error("deadlines must be positive; limit must be nonnegative") parser.error("deadlines must be positive; limit must be nonnegative")
rerun_status = set(filter(None, args.rerun_status.split(",")))
if rerun_status - set(STATUSES):
parser.error("unknown --rerun-status value")
if args.summary: if args.summary:
print(json.dumps(summary(args.out), indent=2)) print(json.dumps(summary(args.out), indent=2))
return 0 return 0
@@ -456,17 +598,27 @@ def main():
units = join_units(read_jsonl(args.units), examples, args.include_typescript) units = join_units(read_jsonl(args.units), examples, args.include_typescript)
except ValueError as exc: except ValueError as exc:
parser.error(str(exc)) parser.error(str(exc))
inputs = Inputs(ROOT, args.cache) inputs = Inputs(ROOT, args.cache, args.allow_excerpt_fallback)
# Resolve pair source names for leak-audit coverage before calculating the resume key. # Resolve pair source names for leak-audit coverage before calculating the resume key.
for unit in units: for unit in units:
if unit["source"] == "pairs": if unit.get("excerpt") or unit["source"].startswith("advisory-fixes"):
try: try:
case, _ = inputs.pair_for(unit) case, _ = inputs.pair_for(unit)
unit["source"] = case.slice catalog_source, _ = inputs.catalog_for(case)
unit["source"] = catalog_source if catalog_source.startswith("advisory-fixes") else case.slice
unit["excerpt"] = True unit["excerpt"] = True
except ValueError as exc: except ValueError as exc:
unit["mapping_error"] = str(exc) unit["mapping_error"] = str(exc)
plan = make_plan(units, args.out, args.only_family, args.limit, include_typescript=args.include_typescript) plan = make_plan(
units,
args.out,
args.only_family,
args.limit,
include_typescript=args.include_typescript,
only_source=set(filter(None, args.only_source.split(","))),
rerun_status=rerun_status,
allow_excerpt_fallback=args.allow_excerpt_fallback,
)
# Planning is read-only, including unsupported pins. All persistence stays below this return. # Planning is read-only, including unsupported pins. All persistence stays below this return.
if args.dry_run: if args.dry_run:
print( print(
+68 -6
View File
@@ -38,9 +38,13 @@ def safe_path(root, name):
FRONTENDS = {"php2cpg", "pysrc2cpg", "jssrc2cpg", "javasrc2cpg", "c2cpg", "joern-parse"} FRONTENDS = {"php2cpg", "pysrc2cpg", "jssrc2cpg", "javasrc2cpg", "c2cpg", "joern-parse"}
MIN_CPG_BYTES = 1024 MIN_CPG_BYTES = 1024
HEAP_MB = 2560
EXCLUDED_DIRS = frozenset({"tests", "test", "docs", "node_modules", "vendor", "dist", "build", "templates", "migrations"})
def instrument_failure(proof): def instrument_failure(proof):
if any(stage.get("oom") for stage in proof.get("jvm", [])):
return "JVM out of memory"
if not proof.get("files") or not proof.get("bytes"): if not proof.get("files") or not proof.get("bytes"):
return "instrument read zero files or zero bytes" return "instrument read zero files or zero bytes"
if proof.get("cpg_bytes", 0) <= MIN_CPG_BYTES: if proof.get("cpg_bytes", 0) <= MIN_CPG_BYTES:
@@ -60,7 +64,7 @@ class MeasuredBackend(JoernBackend):
"""Use the shipped backend with a process watchdog, no concurrent query threads.""" """Use the shipped backend with a process watchdog, no concurrent query threads."""
def __init__(self, deadline, question_deadline, checkpoint): def __init__(self, deadline, question_deadline, checkpoint):
super().__init__(build_timeout=deadline, query_timeout=deadline, session_transport=False) super().__init__(build_timeout=deadline, query_timeout=deadline, session_transport=False, heap_mb=HEAP_MB)
self.deadline = time.monotonic() + deadline self.deadline = time.monotonic() + deadline
self.question_deadline = question_deadline self.question_deadline = question_deadline
self.checkpoint = checkpoint self.checkpoint = checkpoint
@@ -127,7 +131,15 @@ class MeasuredBackend(JoernBackend):
process.stdout.close() process.stdout.close()
elapsed = time.monotonic() - started elapsed = time.monotonic() - started
if frontend or query: if frontend or query:
stage = {"command": binary, "seconds": elapsed, "exit": process.returncode} stage = {
"command": binary,
"argv": command,
"heap_mb": self._heap_mb(),
"java_tool_options": self._jvm_env()["JAVA_TOOL_OPTIONS"],
"seconds": elapsed,
"exit": process.returncode,
"oom": b"OutOfMemoryError" in output,
}
self.stages.append(stage) self.stages.append(stage)
if frontend: if frontend:
# Capture the exact frontend output before build failure cleanup or overlays. # Capture the exact frontend output before build failure cleanup or overlays.
@@ -139,6 +151,8 @@ class MeasuredBackend(JoernBackend):
self.fatal = "frontend wrote no CPG" self.fatal = "frontend wrote no CPG"
elif elapsed < 5: elif elapsed < 5:
self.fatal = f"implausibly fast JVM step: {command[0]} in {elapsed:.3f}s (<5s)" 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"
self.checkpoint() self.checkpoint()
return subprocess.CompletedProcess(command, process.returncode if not self.fatal else 1, output.decode(errors="replace"), "") return subprocess.CompletedProcess(command, process.returncode if not self.fatal else 1, output.decode(errors="replace"), "")
@@ -158,8 +172,17 @@ def unit_record(unit, status, reason="", **extra):
} }
def worker(pin, root, out, deadline, question_deadline): def worker_language(pin, root, out, deadline, question_deadline, persist=write_json):
proof = {"files": [], "bytes": 0, "cpg_path": None, "cpg_bytes": 0, "jvm": []} proof = {
"files": [],
"bytes": 0,
"cpg_path": None,
"cpg_bytes": 0,
"jvm": [],
"heap_mb": HEAP_MB,
"excludes": sorted(EXCLUDED_DIRS),
"materialization": pin.get("materialization", {}),
}
record = { record = {
**pin, **pin,
"done": False, "done": False,
@@ -181,7 +204,7 @@ def worker(pin, root, out, deadline, question_deadline):
if rid in backend.answers: if rid in backend.answers:
row["questions_completed"] = [rid] row["questions_completed"] = [rid]
row["witness_rows"] = [r for r in backend.answers[rid] if isinstance(r, dict) and "kind" not in r and "sink" in r] row["witness_rows"] = [r for r in backend.answers[rid] if isinstance(r, dict) and "kind" not in r and "sink" in r]
write_json(out, record) persist(out, record)
backend = MeasuredBackend(deadline, question_deadline, checkpoint) backend = MeasuredBackend(deadline, question_deadline, checkpoint)
proof["jvm"] = backend.stages proof["jvm"] = backend.stages
@@ -205,7 +228,7 @@ def worker(pin, root, out, deadline, question_deadline):
record["questions"] = requests record["questions"] = requests
for unit_row in record["units"]: for unit_row in record["units"]:
unit_row["questions"] = {unit_row["unit"]: requests[unit_row["unit"]]} if unit_row["unit"] in requests else {} unit_row["questions"] = {unit_row["unit"]: requests[unit_row["unit"]]} if unit_row["unit"] in requests else {}
write_json(out, record) persist(out, record)
cpg = backend.build(root, language=language) cpg = backend.build(root, language=language)
if cpg is None: if cpg is None:
raise ValueError(backend.fatal or backend.last_failure or ("pin deadline" if backend.timed_out else "CPG build failed")) raise ValueError(backend.fatal or backend.last_failure or ("pin deadline" if backend.timed_out else "CPG build failed"))
@@ -284,7 +307,46 @@ def worker(pin, root, out, deadline, question_deadline):
record["questions_asked"] = list(dict.fromkeys(backend.asked)) record["questions_asked"] = list(dict.fromkeys(backend.asked))
record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]] record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]]
record["done"] = True record["done"] = True
persist(out, record)
return record
def worker(pin, root, out, deadline, question_deadline):
"""Run language partitions sequentially under one shared container deadline."""
languages = sorted({"javascript" if u["language"] == "typescript" else u["language"] for u in pin["units"] if u["supported"]})
if len(languages) <= 1:
return worker_language(pin, root, out, deadline, question_deadline)
end = time.monotonic() + deadline
record = {
**pin,
"done": False,
"units": [
unit_record(u, "timeout" if u["supported"] else "unsupported", "pin deadline" if u["supported"] else u["reason"])
for u in pin["units"]
],
"instruments": {},
}
def persist(_, partial):
updates = {u["unit"]: u for u in partial["units"]}
record["units"] = [updates.get(u["unit"], u) for u in record["units"]]
record["instruments"][language] = partial["instrument"]
record["questions_asked"] = [rid for u in record["units"] for rid in u["questions_asked"]]
record["questions_completed"] = [rid for u in record["units"] for rid in u["questions_completed"]]
write_json(out, record) write_json(out, record)
write_json(out, record)
for language in languages:
remaining = end - time.monotonic()
if remaining <= 0:
break
units = [
u for u in pin["units"] if u["supported"] and ("javascript" if u["language"] == "typescript" else u["language"]) == language
]
questions = {u["unit"]: pin["questions"][u["unit"]] for u in units}
worker_language({**pin, "units": units, "questions": questions}, root, out, remaining, question_deadline, persist)
record["done"] = True
write_json(out, record)
return record return record
+247 -5
View File
@@ -313,7 +313,7 @@ def test_dry_run_makes_no_subprocess_calls(tmp_path, monkeypatch, capsys):
import sys import sys
u = unit() u = unit()
examples = [{**u, "id": "a", "candidate": "app.py::run", "source": "population-v1"}] examples = [{**u, "id": "a", "candidate": "app.py::run", "source": "population-v1", "excerpt": False}]
units = tmp_path / "units.jsonl" units = tmp_path / "units.jsonl"
units.write_text(json.dumps(u) + "\n") units.write_text(json.dumps(u) + "\n")
example_file = tmp_path / "examples.jsonl" example_file = tmp_path / "examples.jsonl"
@@ -337,13 +337,14 @@ def test_family_filtered_results_merge_without_losing_prior_units(tmp_path):
assert trace.make_plan([unit(), unit("b", family="path")], tmp_path) == [] assert trace.make_plan([unit(), unit("b", family="path")], tmp_path) == []
def test_failed_launch_persists_failed_pin_and_resume_skips_it(tmp_path, monkeypatch): def test_failed_launch_persists_failed_pin_and_resume_replans_it(tmp_path, monkeypatch):
from types import SimpleNamespace from types import SimpleNamespace
class Inputs: class Inputs:
def materialize(self, pin, target): def materialize(self, pin, target):
target.mkdir() target.mkdir()
(target / "app.py").write_text("def run():\n pass\n") (target / "app.py").write_text("def run():\n pass\n")
return {"mode": "source"}
commands = [] commands = []
@@ -359,7 +360,8 @@ def test_failed_launch_persists_failed_pin_and_resume_skips_it(tmp_path, monkeyp
assert result["units"][0]["status"] == "failed" assert result["units"][0]["status"] == "failed"
assert "produced no result" in result["units"][0]["reason"] assert "produced no result" in result["units"][0]["reason"]
assert [c[1] for c in commands if c[0] == "docker"] == ["run", "rm"] assert [c[1] for c in commands if c[0] == "docker"] == ["run", "rm"]
assert trace.make_plan([unit()], tmp_path) == [] assert len(trace.make_plan([unit()], tmp_path)) == 1
assert trace.make_plan([unit()], tmp_path, rerun_status=()) == []
def import_container_worker(): def import_container_worker():
@@ -410,7 +412,7 @@ def test_trace_dry_run_unsupported_units_write_nothing(tmp_path, monkeypatch, ca
(out / "previous.json").write_text('{"done": true, "units": []}') (out / "previous.json").write_text('{"done": true, "units": []}')
before = {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in out.glob("*")} before = {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in out.glob("*")}
units = [unit("ts", file="app.ts"), unit("py", pin="python")] units = [unit("ts", file="app.ts"), unit("py", pin="python")]
examples = [{**u, "id": u["unit"], "candidate": u["file"] + "::run", "source": "population-v1"} for u in units] examples = [{**u, "id": u["unit"], "candidate": u["file"] + "::run", "source": "population-v1", "excerpt": False} for u in units]
units_file, examples_file = tmp_path / "units.jsonl", tmp_path / "examples.jsonl" units_file, examples_file = tmp_path / "units.jsonl", tmp_path / "examples.jsonl"
units_file.write_text("\n".join(map(json.dumps, units))) units_file.write_text("\n".join(map(json.dumps, units)))
examples_file.write_text("\n".join(map(json.dumps, examples))) examples_file.write_text("\n".join(map(json.dumps, examples)))
@@ -428,7 +430,7 @@ def test_trace_dry_run_unsupported_units_write_nothing(tmp_path, monkeypatch, ca
def test_trace_real_unsupported_run_then_typescript_opt_in_replans(tmp_path, monkeypatch, capsys): def test_trace_real_unsupported_run_then_typescript_opt_in_replans(tmp_path, monkeypatch, capsys):
u = unit(file="app.ts") u = unit(file="app.ts")
example = {**u, "id": "a", "candidate": "app.ts::run", "source": "population-v1"} example = {**u, "id": "a", "candidate": "app.ts::run", "source": "population-v1", "excerpt": False}
units_file, examples_file, out = tmp_path / "units.jsonl", tmp_path / "examples.jsonl", tmp_path / "results" units_file, examples_file, out = tmp_path / "units.jsonl", tmp_path / "examples.jsonl", tmp_path / "results"
units_file.write_text(json.dumps(u) + "\n") units_file.write_text(json.dumps(u) + "\n")
examples_file.write_text(json.dumps(example) + "\n") examples_file.write_text(json.dumps(example) + "\n")
@@ -469,6 +471,7 @@ def test_trace_resume_identity_includes_selection_even_if_support_unchanged(tmp_
first = trace.make_plan([unit()], tmp_path)[0] first = trace.make_plan([unit()], tmp_path)[0]
trace.save_pin(tmp_path / trace.pin_name(first), {**first, "done": True}) trace.save_pin(tmp_path / trace.pin_name(first), {**first, "done": True})
assert trace.make_plan([unit()], tmp_path) == [] assert trace.make_plan([unit()], tmp_path) == []
assert trace.make_plan([unit()], tmp_path, rerun_status=()) == []
assert len(trace.make_plan([unit()], tmp_path, include_typescript=True)) == 1 assert len(trace.make_plan([unit()], tmp_path, include_typescript=True)) == 1
@@ -567,3 +570,242 @@ def test_frontend_measurement_survives_failed_build_cleanup(tmp_path, monkeypatc
assert result["instrument"]["cpg_path"] == str(tmp_path / "actual.graph") assert result["instrument"]["cpg_path"] == str(tmp_path / "actual.graph")
assert result["instrument"]["cpg_bytes"] == 34000 assert result["instrument"]["cpg_bytes"] == 34000
assert not (tmp_path / "actual.graph").exists() assert not (tmp_path / "actual.graph").exists()
def pair_inputs(tmp_path):
from types import SimpleNamespace
inputs = trace.Inputs(trace.ROOT, tmp_path / "cache")
excerpt = tmp_path / "excerpt.py"
excerpt.write_text("# commit: " + "f" * 40 + "\n# parent: " + "f" * 40 + "\ndef run(): pass\n")
case = SimpleNamespace(name="fixture", repo="owner/repo", relpath="pkg/app.py", vuln_file=excerpt, fixed_file=excerpt, context_files=())
inputs.catalog_rows[case.name] = [
("advisory-fixes-2", {"repo": case.repo, "relpath": case.relpath, "parent": "a" * 40, "commit": "b" * 40})
]
return inputs, case
@pytest.mark.parametrize("side,sha", [("vuln", "a" * 40), ("fixed", "b" * 40)])
def test_pair_materializes_catalog_side_not_header(tmp_path, monkeypatch, side, sha):
inputs, case = pair_inputs(tmp_path)
fetched = tmp_path / "fetched"
(fetched / "pkg").mkdir(parents=True)
(fetched / case.relpath).write_text("class Handler:\n def run(self): pass\n")
calls = []
def fetch(repo, commit, roots):
calls.append((repo, commit, roots))
return fetched
monkeypatch.setattr(inputs, "fetch_source", fetch)
monkeypatch.setattr(inputs, "pair_for", lambda row: (case, side))
target = tmp_path / "export"
proof = inputs.materialize({"units": [unit(file=case.relpath)]}, target)
assert calls == [("owner/repo", sha, ["pkg"])]
assert proof["commit"] == sha and proof["mode"] == "source"
assert (target / case.relpath).read_text().startswith("class Handler:")
@pytest.mark.parametrize("change", [{"parent": ""}, {"parent": "b" * 40}, {"commit": "HEAD"}])
def test_catalog_ambiguous_commits_refused(tmp_path, change):
inputs, case = pair_inputs(tmp_path)
inputs.catalog_rows[case.name][0][1].update(change)
with pytest.raises(ValueError, match="ambiguous catalog commits"):
inputs.source_identity(case, "vuln")
def test_ambiguous_catalog_and_side_refused(tmp_path):
inputs, case = pair_inputs(tmp_path)
inputs.catalog_rows[case.name] *= 2
with pytest.raises(ValueError, match="ambiguous catalog source"):
inputs.source_identity(case, "fixed")
with pytest.raises(ValueError, match="ambiguous pair side"):
inputs.pair_for(unit(pin_role="unknown"))
def test_sparse_export_filters_languages_dirs_and_caps_context(tmp_path):
source, target = tmp_path / "source", tmp_path / "export"
contents = {
"pkg/app.py": "def run(): pass\n",
"pkg/context.py": "x" * 200,
"pkg/other.js": "function run() {}",
"other/app.py": "other",
}
contents.update({f"pkg/{directory}/a.py": "excluded" for directory in worker.EXCLUDED_DIRS})
for rel, data in contents.items():
path = source / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(data)
proof = trace.export_sources(source, target, [unit(file="pkg/app.py")], ["pkg"], cap=50)
assert [p.relative_to(target).as_posix() for p in target.rglob("*") if p.is_file()] == ["pkg/app.py"]
assert proof["cap_hit"] and proof["bytes"] == len(contents["pkg/app.py"])
assert proof["excludes"] == sorted(worker.EXCLUDED_DIRS)
assert trace.source_root("module/src/main/java/org/Handler.java", "java") == "module/src/main/java"
assert trace.source_root("pkg/sub/app.ts", "typescript") == "pkg"
assert trace.source_root("app.js", "javascript") == "."
def test_wrong_fetched_function_fails_without_fallback(tmp_path, monkeypatch):
inputs, case = pair_inputs(tmp_path)
source = tmp_path / "source"
(source / "pkg").mkdir(parents=True)
(source / case.relpath).write_text("def runner(): pass\n")
inputs.allow_excerpt_fallback = True
monkeypatch.setattr(inputs, "pair_for", lambda row: (case, "vuln"))
monkeypatch.setattr(inputs, "fetch_source", lambda *args: source)
with pytest.raises(ValueError, match="labelled function not found in fetched source"):
inputs.materialize({"units": [unit(file=case.relpath)]}, tmp_path / "export")
def test_excerpt_fallback_requires_explicit_flag(tmp_path, monkeypatch):
inputs, case = pair_inputs(tmp_path)
monkeypatch.setattr(inputs, "pair_for", lambda row: (case, "vuln"))
def unavailable(*args):
raise ValueError("fetch failed")
monkeypatch.setattr(inputs, "fetch_source", unavailable)
pin = {"units": [unit(file=case.relpath)]}
with pytest.raises(ValueError, match="fetch failed"):
inputs.materialize(pin, tmp_path / "export")
inputs.allow_excerpt_fallback = True
proof = inputs.materialize(pin, tmp_path / "fallback")
assert proof["mode"] == "excerpt-fallback" and proof["reason"] == "fetch failed"
def test_fetch_renders_sparse_pinned_host_commands(tmp_path, monkeypatch):
inputs, _ = pair_inputs(tmp_path)
commands = []
def git(cmd, **kwargs):
commands.append(cmd[3:])
assert kwargs["env"]["GIT_TERMINAL_PROMPT"] == "0"
assert "GIT_NO_LAZY_FETCH" not in kwargs["env"]
return subprocess.CompletedProcess(cmd, 0, "a" * 40 + "\n", "")
monkeypatch.setattr(trace.subprocess, "run", git)
clone = inputs.fetch_source("owner/repo", "a" * 40, ["module/src/main/java"])
assert clone.is_relative_to(inputs.cache)
assert ["fetch", "--depth", "1", "--filter=blob:none", "https://github.com/owner/repo.git", "a" * 40] in commands
assert ["sparse-checkout", "set", "--", "module/src/main/java"] in commands
assert ["checkout", "--detach", "--force", "a" * 40] in commands
def test_explicit_heap_in_frontend_and_query_commands(tmp_path, monkeypatch):
backend = worker.MeasuredBackend(900, 120, lambda: None)
commands = []
def run(command, **kwargs):
commands.append(command)
return subprocess.CompletedProcess(command, 0, BEGIN + '\n{"r": []}\n' + END, "")
monkeypatch.setattr(backend, "_run", run)
monkeypatch.setattr("openultrasast.cpg.backend.shutil.which", lambda name: "/opt/joern-cli/" + name)
graph = tmp_path / "cpg.bin"
graph.write_bytes(b"x" * 2048)
backend._build_with_frontend(tmp_path, graph, tmp_path, "python")
backend._batch_once(graph, "taint", {"r": {}})
assert {Path(cmd[0]).name for cmd in commands} == {"pysrc2cpg", "joern"}
assert all("-J-Xmx2560m" in cmd for cmd in commands)
assert "-Xmx2560m" in backend._jvm_env()["JAVA_TOOL_OPTIONS"]
@pytest.mark.parametrize("binary", ["pysrc2cpg", "joern"])
def test_oom_output_overrides_missing_graph_or_markers(tmp_path, binary):
launcher = tmp_path / binary
launcher.write_text(f"#!{sys.executable}\nprint('java.lang.OutOfMemoryError: Java heap space')\n")
launcher.chmod(0o755)
backend = worker.MeasuredBackend(30, 10, lambda: None)
done = backend._run([str(launcher), "--script", "query.sc"], timeout=10)
assert done.returncode == 1
assert backend.fatal == "JVM out of memory"
assert worker.instrument_failure({"jvm": backend.stages}) == "JVM out of memory"
def test_two_language_pin_builds_sequential_frontends_and_routes_questions(tmp_path, monkeypatch):
events = []
class LanguagesBackend(FakeBackend):
def build(self, root, language):
events.append(("build", language))
graph = root / (language + ".bin")
graph.write_bytes(b"x" * 2048)
self.stages = [
{"command": {"python": "pysrc2cpg", "javascript": "jssrc2cpg"}[language], "seconds": 20, "exit": 0, "cpg_bytes": 2048}
]
def batch(query, requests):
events.append(("query", language, sorted(requests)))
self.asked.extend(requests)
return {
**dict.fromkeys(requests, []),
"__census__": [{"files": "1", "file_names": ["app.py" if language == "python" else "app.js"]}],
}
return CpgResult(graph, lambda q, p: None, run_batch_once=batch, cleanup=lambda: events.append(("cleanup", language)))
monkeypatch.setattr(worker, "MeasuredBackend", LanguagesBackend)
(tmp_path / "app.py").write_text("def run(): pass\n")
(tmp_path / "app.js").write_text("function run() {}\n")
pin = {"units": [unit(), unit("js", language="javascript", file="app.js")]}
pin["questions"] = trace.prepare_questions(pin, tmp_path, 120)
assert all(params["callDepth"] == "3" for params in pin["questions"].values())
result = worker.worker(pin, tmp_path, tmp_path / "result.json", 900, 120)
assert events == [
("build", "javascript"),
("query", "javascript", ["js"]),
("cleanup", "javascript"),
("build", "python"),
("query", "python", ["a"]),
("cleanup", "python"),
]
assert all(u["status"] == "asked-nothing" for u in result["units"])
assert set(result["instruments"]) == {"python", "javascript"}
assert all(u["instrument"]["heap_mb"] == 2560 for u in result["units"])
def test_resume_materialization_version_and_status_selection(tmp_path):
units = [unit(source="advisory-fixes-2"), unit("other", pin="other", source="population-v1")]
pin = trace.make_plan(units, tmp_path, only_source={"advisory-fixes-2"})[0]
record = {**pin, "done": True, "units": [trace.unit_record(pin["units"][0], "asked-nothing")]}
path = tmp_path / trace.pin_name(pin)
trace.write_json(path, record)
assert trace.make_plan(units, tmp_path, only_source={"advisory-fixes-2"}) == []
assert len(trace.make_plan(units, tmp_path, only_source={"advisory-fixes-2"}, rerun_status={"failed", "asked-nothing"})) == 1
trace.write_json(path, {**record, "materialization_version": trace.MATERIALIZATION_VERSION - 1})
assert len(trace.make_plan(units, tmp_path, only_source={"advisory-fixes-2"})) == 1
def test_sparse_fetch_from_local_git_without_network(tmp_path, monkeypatch):
origin = tmp_path / "origin"
origin.mkdir()
real_run = subprocess.run
def git(*args):
return real_run(["git", "-C", str(origin), *args], capture_output=True, text=True, check=True).stdout.strip()
git("init")
git("config", "user.name", "Offline Test")
git("config", "user.email", "[email protected]")
for rel in ("module/src/main/java/Handler.java", "module/src/main/java/docs/Other.java", "elsewhere/Other.java"):
path = origin / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("class Handler { void run() {} }\n")
git("add", ".")
git("commit", "-m", "local fixture")
sha = git("rev-parse", "HEAD")
inputs, _ = pair_inputs(tmp_path)
def local_only(cmd, **kwargs):
# Substitute a local repository for the one URL; no network-capable command runs.
cmd = [str(origin) if arg == "https://github.com/owner/repo.git" else arg for arg in cmd]
return real_run(cmd, **kwargs)
monkeypatch.setattr(trace.subprocess, "run", local_only)
clone = inputs.fetch_source("owner/repo", sha, ["module/src/main/java"])
assert not (clone / "elsewhere/Other.java").exists()
target = tmp_path / "export"
labelled = unit(file="module/src/main/java/Handler.java", language="java")
proof = trace.export_sources(clone, target, [labelled], ["module/src/main/java"])
trace.verify_functions(target, [labelled])
assert proof["files"] == 1 and proof["bytes"] > 0