diff --git a/sample/tests/test_apk_decoder.py b/sample/tests/test_apk_decoder.py index 4a3a206..87ee404 100644 --- a/sample/tests/test_apk_decoder.py +++ b/sample/tests/test_apk_decoder.py @@ -983,6 +983,21 @@ def test_no_manifest_produced_raises(self): with self.assertRaises(ApktoolRunError): run_apktool_decode(apk, tdp / "out", ApktoolConfig(apktool_path=str(stub))) + def test_launch_os_error_is_wrapped(self): + with tempfile.TemporaryDirectory() as td: + tdp = Path(td) + apk = tdp / "fake.apk" + apk.write_bytes(b"PK") + with mock.patch( + "venomhook.apk_decoder.subprocess.run", + side_effect=PermissionError("permission denied"), + ): + with self.assertRaises(ApktoolRunError) as ctx: + run_apktool_decode( + apk, tdp / "out", ApktoolConfig(apktool_path="/bad/apktool") + ) + self.assertIn("could not exec apktool binary", str(ctx.exception)) + def test_default_command_includes_force_and_o(self): with tempfile.TemporaryDirectory() as td: tdp = Path(td) diff --git a/sample/tests/test_jadx_runner.py b/sample/tests/test_jadx_runner.py index 5d9e594..b822d62 100644 --- a/sample/tests/test_jadx_runner.py +++ b/sample/tests/test_jadx_runner.py @@ -457,6 +457,21 @@ def test_failed_invocation_with_no_output_raises(self): run_jadx(apk, tdp / "out", config=cfg) self.assertIn("exit=5", str(ctx.exception)) + def test_launch_os_error_is_wrapped(self): + with tempfile.TemporaryDirectory() as td: + tdp = Path(td) + apk = tdp / "fake.apk" + apk.write_bytes(b"PK") + with mock.patch( + "venomhook.jadx_runner.subprocess.run", + side_effect=PermissionError("permission denied"), + ): + with self.assertRaises(JadxRunError) as ctx: + run_jadx( + apk, tdp / "out", config=JadxConfig(jadx_path="/bad/jadx") + ) + self.assertIn("could not exec jadx binary", str(ctx.exception)) + def test_partial_failure_with_output_is_accepted(self): # jadx returns non-zero on partial decompile; if .java exists, succeed. with tempfile.TemporaryDirectory() as td: diff --git a/sample/tests/test_orchestrator.py b/sample/tests/test_orchestrator.py index 20f2a8b..4805b84 100644 --- a/sample/tests/test_orchestrator.py +++ b/sample/tests/test_orchestrator.py @@ -1,6 +1,7 @@ import sys import unittest from pathlib import Path +from unittest import mock ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "src")) @@ -24,6 +25,15 @@ def test_build_frida_command_attach_and_dry_run(self) -> None: cmd_str = run_frida("1234", Path("venomhook.js"), frida_path="frida", attach=True, dry_run=True) self.assertIn("-p 1234", cmd_str) + def test_run_frida_wraps_launch_os_error(self) -> None: + with mock.patch( + "venomhook.orchestrator.subprocess.run", + side_effect=PermissionError("permission denied"), + ): + with self.assertRaises(RuntimeError) as ctx: + run_frida("1234", Path("venomhook.js"), frida_path="/bad/frida", attach=True) + self.assertIn("could not exec frida binary", str(ctx.exception)) + if __name__ == "__main__": unittest.main() diff --git a/sample/tests/test_pipeline.py b/sample/tests/test_pipeline.py index a91fc4b..e8f309f 100644 --- a/sample/tests/test_pipeline.py +++ b/sample/tests/test_pipeline.py @@ -2,6 +2,7 @@ import tempfile import unittest from pathlib import Path +from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) @@ -119,6 +120,17 @@ def test_ghidra_runner_stub(self) -> None: runner.run(SAMPLE_STATIC_META, out_static) self.assertTrue(out_static.exists()) + def test_ghidra_runner_wraps_launch_os_error(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + runner = GhidraRunner(headless_cmd=["/bad/analyzeHeadless"], post_script=None) + with mock.patch( + "venomhook.ghidra_runner.subprocess.run", + side_effect=PermissionError("permission denied"), + ): + with self.assertRaises(RuntimeError) as ctx: + runner.run(SAMPLE_STATIC_META, Path(tmpdir) / "out.json") + self.assertIn("could not exec Ghidra headless command", str(ctx.exception)) + if __name__ == "__main__": unittest.main() diff --git a/src/venomhook/apk_decoder.py b/src/venomhook/apk_decoder.py index 68972c5..e806456 100644 --- a/src/venomhook/apk_decoder.py +++ b/src/venomhook/apk_decoder.py @@ -224,6 +224,10 @@ def run_apktool_decode( raise ApktoolNotFoundError( f"could not exec apktool binary at {binary!r}: {e}" ) from e + except OSError as e: + raise ApktoolRunError( + f"could not exec apktool binary at {binary!r}: {e}" + ) from e manifest = out / "AndroidManifest.xml" smali_present = any(out.glob("smali*")) diff --git a/src/venomhook/ghidra_runner.py b/src/venomhook/ghidra_runner.py index dffafef..94f3ec2 100644 --- a/src/venomhook/ghidra_runner.py +++ b/src/venomhook/ghidra_runner.py @@ -55,10 +55,15 @@ def run(self, binary_path: Path, out_static_meta: Path) -> None: logger.info("running Ghidra headless: %s", " ".join(cmd)) # Pin UTF-8 so Ghidra's i18n stdout/stderr doesn't crash decoding # under Windows cp949/cp1252 or non-UTF-8 POSIX locales. - result = subprocess.run( - cmd, capture_output=True, text=True, - encoding="utf-8", errors="replace", - ) + try: + result = subprocess.run( + cmd, capture_output=True, text=True, + encoding="utf-8", errors="replace", + ) + except OSError as e: + raise RuntimeError( + f"could not exec Ghidra headless command at {cmd[0]!r}: {e}" + ) from e if result.returncode != 0: logger.error("Ghidra headless failed: %s", result.stderr) raise RuntimeError(f"Ghidra headless failed (code {result.returncode})") diff --git a/src/venomhook/jadx_runner.py b/src/venomhook/jadx_runner.py index b35ccfa..792ce57 100644 --- a/src/venomhook/jadx_runner.py +++ b/src/venomhook/jadx_runner.py @@ -211,6 +211,10 @@ def run_jadx( raise JadxNotFoundError( f"could not exec jadx binary at {binary!r}: {e}" ) from e + except OSError as e: + raise JadxRunError( + f"could not exec jadx binary at {binary!r}: {e}" + ) from e java_files = sum(1 for _ in out.rglob("*.java")) stdout_tail = (completed.stdout or "")[-4096:] diff --git a/src/venomhook/orchestrator.py b/src/venomhook/orchestrator.py index 6b8b227..a49a8de 100644 --- a/src/venomhook/orchestrator.py +++ b/src/venomhook/orchestrator.py @@ -55,10 +55,13 @@ def run_frida( # Pin UTF-8 for both decoding of frida's child output and for writing # the captured log: the target process can emit non-ASCII strings, and # the system default codec is cp949/cp1252 on Korean/Western Windows. - proc = subprocess.run( - cmd, stdout=stdout_pipe, stderr=stderr_pipe, text=True, - encoding="utf-8", errors="replace", - ) + try: + proc = subprocess.run( + cmd, stdout=stdout_pipe, stderr=stderr_pipe, text=True, + encoding="utf-8", errors="replace", + ) + except OSError as e: + raise RuntimeError(f"could not exec frida binary at {frida_path!r}: {e}") from e if log_file and proc.stdout: log_file.parent.mkdir(parents=True, exist_ok=True) log_file.write_text(proc.stdout, encoding="utf-8")