mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(cpg): a timed-out query must kill the JVM, not just the script
`joern` and `joern-parse` are shell scripts that launch a JVM as a separate process. `subprocess.run(timeout=)` kills the script and leaves the JVM resident, holding its heap, indefinitely. Every `query_failed: timeout` leaked about a gigabyte. Found by running out of memory. After a day of 300s and 2,400s taint timeouts, four orphaned JVMs -- three hours old, ~2GB between them -- were still resident on a 7GB machine, and background tasks started being killed. On a CI runner doing several scans in sequence this is an OOM rather than a degradation, and it would present as "the scan needs more memory", which is the wrong lesson entirely: the scan's own peak is a flat ~1.5GB whether its query runs for five minutes or forty. The command now starts in its own session, and a timeout SIGTERMs the process group and SIGKILLs what survives. An injected test runner is left alone, because those spawn nothing. Tested with a real shell script that backgrounds a child, since the bug is specifically about grandchildren -- and the test reaps that child itself if the assertion fails, rather than leaking the process it is complaining about. 855 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
a895bceefc
commit
5abc6cd6f6
@@ -7,6 +7,7 @@ Nothing here imports a JVM binding, and every test runs with Joern absent.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
@@ -51,6 +52,7 @@ def test_the_seam_imports_nothing_outside_the_standard_library() -> None:
|
||||
"os",
|
||||
"re",
|
||||
"shutil",
|
||||
"signal",
|
||||
"subprocess",
|
||||
"tempfile",
|
||||
"dataclasses",
|
||||
@@ -684,3 +686,42 @@ def test_a_file_php2cpg_miscompiles_is_excluded_and_named(tmp_path: Path, monkey
|
||||
build = next(c for c in commands if "--script" not in c)
|
||||
assert "--exclude" in build and any("bad.php" in part for part in build), "and excluded from the build"
|
||||
assert not any("ok.php" in part for part in build if part != str(tmp_path)), "a global at function scope is fine"
|
||||
|
||||
|
||||
def test_a_timed_out_query_kills_the_jvm_not_just_the_script(tmp_path: Path) -> None:
|
||||
"""`joern` is a shell script that launches a JVM as a separate process.
|
||||
|
||||
`subprocess.run(timeout=)` kills the script and leaves the JVM resident, holding its heap. Measured the
|
||||
hard way: after a day of 300s and 2,400s taint timeouts, four orphaned JVMs three hours old were still
|
||||
using about a gigabyte each and the machine ran out of memory. On CI that is an OOM rather than a
|
||||
degradation, and it would look like the scan needing more memory -- the wrong lesson entirely.
|
||||
|
||||
Uses a real shell script that backgrounds a child, because the whole bug is about grandchildren.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
|
||||
from openultrasast.cpg.backend import JoernBackend
|
||||
|
||||
marker = tmp_path / "child-alive"
|
||||
script = tmp_path / "fake-joern.sh"
|
||||
script.write_text(
|
||||
f"#!/bin/sh\n( while true; do echo x > '{marker}'; sleep 0.2; done ) &\necho $! > " + str(tmp_path / "child.pid") + "\nsleep 60\n"
|
||||
)
|
||||
script.chmod(0o755)
|
||||
|
||||
started = time.monotonic()
|
||||
assert JoernBackend()._run([str(script)], timeout=2) is None, "a timeout reports nothing, never a verdict"
|
||||
assert time.monotonic() - started < 30, "and it does not wait for the script's own sleep"
|
||||
|
||||
child = int((tmp_path / "child.pid").read_text().strip())
|
||||
time.sleep(0.5)
|
||||
alive = True
|
||||
try:
|
||||
os.kill(child, 0)
|
||||
except ProcessLookupError:
|
||||
alive = False
|
||||
if alive: # cleanup, so a failure here does not leak the process it is complaining about
|
||||
with contextlib.suppress(ProcessLookupError, PermissionError):
|
||||
os.kill(child, 9)
|
||||
assert not alive, "the grandchild JVM must die with the script, or every timeout leaks a gigabyte"
|
||||
|
||||
Reference in New Issue
Block a user