Files
frida-python/tests/test_bindgen.py

935 lines
33 KiB
Python

import os
import socket
import subprocess
import sys
import unittest
from pathlib import Path
def free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
REPO = Path(__file__).resolve().parent.parent
BINDGEN_CORE = REPO / "frida-bindgen"
GIRDIR = REPO / "build" / "subprojects" / "frida-core" / "src" / "api"
OUTDIR = REPO / "build" / "bindgen-out"
BUILT_EXTENSION = REPO / "build" / "frida" / "_frida.abi3.so"
@unittest.skipUnless(
(GIRDIR / "Frida-1.0.gir").exists(),
"requires a configured build tree with generated .gir files",
)
class TestBindgen(unittest.TestCase):
def test_generates_all_outputs(self):
OUTDIR.mkdir(exist_ok=True)
subprocess.run(
[
sys.executable,
"-m",
"frida_bindgen",
f"--frida-gir={GIRDIR / 'Frida-1.0.gir'}",
f"--glib-gir={GIRDIR / 'GLib-2.0.gir'}",
f"--gobject-gir={GIRDIR / 'GObject-2.0.gir'}",
f"--gio-gir={GIRDIR / 'Gio-2.0.gir'}",
f"--output-py={OUTDIR / '__init__.py'}",
f"--output-aio={OUTDIR / 'aio.py'}",
f"--output-pyi={OUTDIR / '_frida.pyi'}",
f"--output-c={OUTDIR / 'extension.c'}",
],
check=True,
env={"PYTHONPATH": os.pathsep.join([str(REPO / "frida"), str(BINDGEN_CORE)])},
)
for name in ("__init__.py", "aio.py", "_frida.pyi", "extension.c"):
self.assertGreater((OUTDIR / name).stat().st_size, 0)
def test_generated_python_compiles(self):
for name in ("__init__.py", "aio.py"):
path = OUTDIR / name
if not path.exists():
self.skipTest("run test_generates_all_outputs first")
compile(path.read_text(), str(path), "exec")
@unittest.skipUnless(
BUILT_EXTENSION.exists(),
"requires a built _frida extension (run `make`)",
)
class TestExtension(unittest.TestCase):
def setUp(self):
sys.path.insert(0, str(BUILT_EXTENSION.parent))
import _frida
self._frida = _frida
def tearDown(self):
sys.path.remove(str(BUILT_EXTENSION.parent))
def test_exposes_version(self):
self.assertRegex(self._frida.__version__, r"\d+\.\d+")
def test_constructs_object_with_string_and_enum_parameters(self):
relay = self._frida.Relay("1.2.3.4:5", "user", "pass", "turn-udp")
self.assertIsInstance(relay, self._frida.Relay)
def test_constructs_object_without_parameters(self):
self.assertIsInstance(self._frida.DeviceManager(), self._frida.DeviceManager)
def test_rejects_invalid_enum_value(self):
with self.assertRaises(ValueError):
self._frida.Relay("1.2.3.4:5", "user", "pass", "bogus-kind")
def test_reads_back_string_and_enum_properties(self):
relay = self._frida.Relay("1.2.3.4:5", "bob", "secret", "turn-tcp")
self.assertEqual(relay.address, "1.2.3.4:5")
self.assertEqual(relay.username, "bob")
self.assertEqual(relay.password, "secret")
self.assertEqual(relay.kind, "turn-tcp")
def test_invokes_synchronous_methods(self):
cancellable = self._frida.Cancellable()
self.assertFalse(cancellable.is_cancelled())
cancellable.cancel()
self.assertTrue(cancellable.is_cancelled())
def test_registers_frida_exceptions(self):
for name in (
"ServerNotRunningError",
"ProcessNotFoundError",
"InvalidArgumentError",
"TransportError",
"OperationCancelledError",
):
exception = getattr(self._frida, name)
self.assertTrue(issubclass(exception, Exception))
self.assertEqual(exception.__module__, "frida")
def test_throwing_method_raises_mapped_exception(self):
cancellable = self._frida.Cancellable()
cancellable.cancel()
with self.assertRaises(self._frida.OperationCancelledError):
cancellable.set_error_if_cancelled()
def test_instantiates_handle_only_types(self):
# Types with no .gir constructor are created by the marshalling layer
# and must accept a bare, argument-free construction.
self.assertIsInstance(self._frida.Device(), self._frida.Device)
self.assertIsInstance(self._frida.Session(), self._frida.Session)
def test_exposes_object_typed_property(self):
self.assertTrue(hasattr(self._frida.IOStream, "input_stream"))
self.assertTrue(hasattr(self._frida.IOStream, "output_stream"))
def test_string_array_property_round_trips(self):
options = self._frida.SpawnOptions()
self.assertIsNone(options.argv)
options.argv = ["/bin/ls", "-la"]
self.assertEqual(options.argv, ["/bin/ls", "-la"])
options.argv = None
self.assertIsNone(options.argv)
def test_vardict_property_round_trips(self):
options = self._frida.SpawnOptions()
options.aux = {"name": "value", "count": 42, "flag": True}
self.assertEqual(options.aux, {"name": "value", "count": 42, "flag": True})
def test_variant_marshalling_types_and_casts(self):
options = self._frida.SpawnOptions()
options.aux = {
"int": 42,
"uint64": ("uint64", 0xFFFFFFFFFFFFFFFF),
"float": 3.5,
"string": "hi",
"bool": True,
"bytes": b"\x00\x01\x02",
"list": [1, 2, 3],
"nested": {"x": 1, "y": "z"},
"tuple": ("a", 1),
}
aux = options.aux
self.assertEqual(aux["int"], 42)
# the ("uint64", ...) cast preserves the value; as int64 it would be -1
self.assertEqual(aux["uint64"], 0xFFFFFFFFFFFFFFFF)
self.assertEqual(aux["float"], 3.5)
self.assertEqual(aux["string"], "hi")
self.assertIs(aux["bool"], True)
self.assertEqual(aux["bytes"], b"\x00\x01\x02")
self.assertEqual(aux["list"], [1, 2, 3])
self.assertEqual(aux["nested"], {"x": 1, "y": "z"})
self.assertEqual(aux["tuple"], ("a", 1))
def test_async_method_delivers_result_to_callback(self):
import threading
manager = self._frida.DeviceManager()
done = threading.Event()
box = {}
def on_complete(result, error):
box["result"] = result
box["error"] = error
done.set()
manager.close(on_complete)
self.assertTrue(done.wait(10), "callback was not invoked")
self.assertIsNone(box["error"])
self.assertIsNone(box["result"])
def test_raising_async_callback_does_not_corrupt_thread(self):
import contextlib
import io
import threading
raised = threading.Event()
def bad_callback(result, error):
raised.set()
raise RuntimeError("handler blew up")
with contextlib.redirect_stderr(io.StringIO()):
self._frida.DeviceManager().close(bad_callback)
self.assertTrue(raised.wait(10))
# A subsequent operation must still complete cleanly.
box = self._await_async(lambda cb: self._frida.DeviceManager().close(cb))
self.assertIsNone(box["error"])
def _await_async(self, invoke):
import threading
done = threading.Event()
box = {}
def on_complete(result, error):
box["result"] = result
box["error"] = error
done.set()
invoke(on_complete)
self.assertTrue(done.wait(10), "callback was not invoked")
return box
def test_async_method_passes_parameters_and_maps_errors(self):
manager = self._frida.DeviceManager()
box = self._await_async(lambda cb: manager.get_device_by_id("no-such-device", 1, cb))
self.assertIsNone(box["result"])
self.assertIsInstance(box["error"], self._frida.InvalidArgumentError)
def test_async_method_honors_cancellable(self):
manager = self._frida.DeviceManager()
cancellable = self._frida.Cancellable()
cancellable.cancel()
box = self._await_async(lambda cb: manager.get_device_by_id("anything", 5000, cb, cancellable))
self.assertIsInstance(box["error"], self._frida.OperationCancelledError)
@unittest.skipUnless(
(REPO / "build" / "frida" / "__init__.py").exists(),
"requires a built facade (run `make`)",
)
class TestFacade(unittest.TestCase):
def setUp(self):
sys.path.insert(0, str(REPO / "build"))
self._purge_frida()
import frida
self.frida = frida
def tearDown(self):
sys.path.remove(str(REPO / "build"))
self._purge_frida()
@staticmethod
def _purge_frida():
# The source tree's frida/ is a namespace package the stale test_core
# imports; drop it so the built facade under build/ is what loads.
for name in [n for n in sys.modules if n == "frida" or n.startswith("frida.")]:
del sys.modules[name]
def test_reexports_exceptions(self):
self.assertTrue(issubclass(self.frida.ServerNotRunningError, Exception))
def test_synchronous_method_blocks(self):
self.frida.DeviceManager().close()
def test_synchronous_method_raises_mapped_error(self):
with self.assertRaises(self.frida.InvalidArgumentError):
self.frida.DeviceManager().get_device_by_id("no-such-device", 1)
def test_aio_awaitable_completes(self):
import asyncio
import frida.aio
asyncio.run(frida.aio.DeviceManager().close())
def test_aio_awaitable_raises_mapped_error(self):
import asyncio
import frida.aio
async def scenario():
with self.assertRaises(self.frida.InvalidArgumentError):
await frida.aio.DeviceManager().get_device_by_id("no-such-device", 1)
asyncio.run(scenario())
def test_aio_cancellation_propagates(self):
import asyncio
import frida.aio
async def scenario():
manager = frida.aio.DeviceManager()
task = asyncio.ensure_future(manager.get_device_by_id("nope", 60000))
await asyncio.sleep(0.1)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task
asyncio.run(scenario())
def test_unreferenced_interface_impl_is_collected(self):
import gc
import weakref
class MyAuth(self.frida.AuthenticationService):
def authenticate(self, token):
return "{}"
refs = [weakref.ref(MyAuth()) for _ in range(3)]
gc.collect()
self.assertTrue(all(ref() is None for ref in refs))
def test_interface_impl_survives_gc_while_referenced_by_core(self):
import gc
class MyAuth(self.frida.AuthenticationService):
def authenticate(self, token):
return "{}"
params = self.frida.EndpointParameters(address="127.0.0.1", port=0, authentication=MyAuth())
gc.collect()
gc.collect()
self.assertIsNotNone(params._impl.auth_service)
def test_interface_can_be_implemented(self):
import json
class MyAuth(self.frida.AuthenticationService):
def authenticate(self, token):
return json.dumps({"token": token})
service = MyAuth()
self.assertIsInstance(service, self.frida.AuthenticationService)
self.assertEqual(type(service._impl).__name__, "AuthenticationService")
def test_endpoint_parameters_authentication_schemes(self):
self.frida.EndpointParameters(address="::1", authentication=("token", "secret"))
self.frida.EndpointParameters(address="::1", authentication=("callback", lambda token: {"token": token}))
class MyAuth(self.frida.AuthenticationService):
def authenticate(self, token):
return "{}"
self.frida.EndpointParameters(address="::1", authentication=MyAuth())
with self.assertRaises(ValueError):
self.frida.EndpointParameters(authentication=("bogus", "x"))
def test_object_constructor_parameters(self):
cluster = self.frida.EndpointParameters(address="127.0.0.1", port=0)
service = self.frida.PortalService(cluster, None)
self.assertIsInstance(service.device, self.frida.Device)
def test_implemented_interface_receives_dispatch(self):
frida = self.frida
calls = []
def authenticate(token):
calls.append(token)
if token != "secret":
raise ValueError("wrong token")
return "{}"
control_port = free_port()
control = frida.EndpointParameters(
address="127.0.0.1", port=control_port, authentication=("callback", authenticate)
)
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
service = frida.PortalService(cluster, control)
service.start()
try:
with self.assertRaises(frida.InvalidArgumentError):
frida.DeviceManager().add_remote_device(
f"127.0.0.1:{control_port}", token="wrong"
).enumerate_processes()
frida.DeviceManager().add_remote_device(f"127.0.0.1:{control_port}", token="secret").enumerate_processes()
self.assertEqual(calls, ["wrong", "secret"])
finally:
service.stop()
def test_aio_implemented_interface_receives_dispatch(self):
import asyncio
import frida.aio
calls = []
async def scenario():
class MyAuth(frida.aio.AuthenticationService):
async def authenticate(self, token):
calls.append(token)
await asyncio.sleep(0)
if token != "secret":
raise ValueError("wrong token")
return "{}"
control_port = free_port()
control = frida.aio.EndpointParameters(address="127.0.0.1", port=control_port, authentication=MyAuth())
cluster = frida.aio.EndpointParameters(address="127.0.0.1", port=free_port())
service = frida.aio.PortalService(cluster, control)
await service.start()
try:
with self.assertRaises(self.frida.InvalidArgumentError):
manager = frida.aio.DeviceManager()
device = await manager.add_remote_device(f"127.0.0.1:{control_port}", token="wrong")
await device.enumerate_processes()
manager = frida.aio.DeviceManager()
device = await manager.add_remote_device(f"127.0.0.1:{control_port}", token="secret")
await device.enumerate_processes()
self.assertEqual(calls, ["wrong", "secret"])
finally:
await service.stop()
asyncio.run(scenario())
def test_web_request_handler_serves_response(self):
import urllib.request
frida = self.frida
seen = []
class Handler(frida.WebRequestHandler):
def handle_request(self, request):
seen.append(request.path)
return frida.WebResponse(200, b"pong")
control_port = free_port()
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
service = frida.PortalService(cluster, control)
service.start()
try:
with urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping") as response:
status = response.status
body = response.read()
finally:
service.stop()
self.assertEqual(status, 200)
self.assertEqual(body, b"pong")
self.assertEqual(seen, ["/ping"])
def test_web_request_handler_returning_none_yields_not_found(self):
import urllib.error
import urllib.request
frida = self.frida
class Handler(frida.WebRequestHandler):
def handle_request(self, request):
return None
control_port = free_port()
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
service = frida.PortalService(cluster, control)
service.start()
try:
with self.assertRaises(urllib.error.HTTPError) as raised:
urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping")
self.assertEqual(raised.exception.code, 404)
finally:
service.stop()
def test_web_request_handler_error_propagates(self):
import urllib.error
import urllib.request
frida = self.frida
class Handler(frida.WebRequestHandler):
def handle_request(self, request):
raise ValueError("boom")
control_port = free_port()
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
service = frida.PortalService(cluster, control)
service.start()
try:
with self.assertRaises(urllib.error.HTTPError) as raised:
urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping")
self.assertEqual(raised.exception.code, 500)
self.assertEqual(raised.exception.read(), b"boom")
finally:
service.stop()
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
def test_spawn_accepts_program_as_list(self):
import os
import tempfile
import time
device = self.frida.get_local_device()
fd, path = tempfile.mkstemp()
os.close(fd)
try:
pid = device.spawn(["/bin/sh", "-c", f'printf hi > "{path}"'])
device.resume(pid)
deadline = time.time() + 5
content = ""
while time.time() < deadline:
with open(path) as f:
content = f.read()
if content:
break
time.sleep(0.05)
finally:
os.unlink(path)
self.assertEqual(content, "hi")
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
def test_spawn_accepts_env_as_dict(self):
import os
import tempfile
import time
device = self.frida.get_local_device()
fd, path = tempfile.mkstemp()
os.close(fd)
try:
pid = device.spawn(
"/bin/sh",
argv=["/bin/sh", "-c", f'printf %s "$FRIDA_ENVP_TEST" > "{path}"'],
env={"FRIDA_ENVP_TEST": "envp-as-dict"},
)
device.resume(pid)
deadline = time.time() + 5
content = ""
while time.time() < deadline:
with open(path) as f:
content = f.read()
if content:
break
time.sleep(0.05)
finally:
os.unlink(path)
self.assertEqual(content, "envp-as-dict")
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
def test_spawn_accepts_envp_and_bytes_argv(self):
import os
import tempfile
import time
device = self.frida.get_local_device()
fd, path = tempfile.mkstemp()
os.close(fd)
try:
pid = device.spawn(
[b"/bin/sh", b"-c", f'printf %s "$FRIDA_ENVP2" > "{path}"'],
envp={"FRIDA_ENVP2": "envp-full"},
)
device.resume(pid)
deadline = time.time() + 5
content = ""
while time.time() < deadline:
with open(path) as f:
content = f.read()
if content:
break
time.sleep(0.05)
finally:
os.unlink(path)
self.assertEqual(content, "envp-full")
def test_toplevel_convenience_functions(self):
frida = self.frida
self.assertRegex(frida.__version__, r"\d+\.\d+")
for name in (
"spawn",
"resume",
"kill",
"attach",
"inject_library_file",
"inject_library_blob",
"enumerate_devices",
"get_device_matching",
"shutdown",
"query_system_parameters",
):
self.assertTrue(callable(getattr(frida, name)), name)
self.assertIn("local", [d.id for d in frida.enumerate_devices()])
self.assertEqual(frida.get_device_matching(lambda d: d.type == "local").id, "local")
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
def test_toplevel_attach_and_query(self):
frida = self.frida
pid = frida.spawn(["/bin/sh", "-c", "sleep 30"])
try:
session = frida.attach(pid)
session.detach()
finally:
frida.kill(pid)
params = frida.query_system_parameters()
self.assertIn("arch", params)
def test_device_type_property(self):
device = self.frida.get_local_device()
self.assertEqual(device.type, "local")
def test_bool_accessors_are_properties(self):
device = self.frida.get_local_device()
self.assertIs(device.is_lost, False)
session = device.attach(0)
self.assertIs(session.is_detached, False)
session.detach()
self.assertIs(session.is_detached, True)
def test_device_get_process_by_name(self):
device = self.frida.get_local_device()
with self.assertRaises(self.frida.ProcessNotFoundError):
device.get_process("frida-nonexistent-process-zzz")
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
def test_attach_accepts_pid(self):
device = self.frida.get_local_device()
pid = device.spawn(["/bin/sh", "-c", "sleep 30"])
try:
session = device.attach(pid)
session.detach()
finally:
device.kill(pid)
def test_facade_repr(self):
device = self.frida.get_local_device()
self.assertRegex(repr(device), r"Device\(id='local'")
process = self.frida.get_local_device().enumerate_processes()[0]
self.assertRegex(repr(process), r"Process\(pid=\d+")
def test_create_script_positional_name(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("rpc.exports={ping:()=>1};", "my-script")
script.load()
self.assertEqual(script.exports_sync.ping(), 1)
finally:
session.detach()
def test_create_script_from_bytes_positional_name(self):
session = self.frida.get_local_device().attach(0)
try:
bytecode = session.compile_script("rpc.exports={ping:()=>1};", "n")
script = session.create_script_from_bytes(bytecode, "my-script")
script.load()
self.assertEqual(script.exports_sync.ping(), 1)
finally:
session.detach()
def test_file_monitor_reports_changes(self):
import os
import tempfile
import time
directory = tempfile.mkdtemp()
target = os.path.join(directory, "watched.txt")
monitor = self.frida.FileMonitor(target)
events = []
monitor.on("change", lambda *args: events.append(args))
monitor.enable()
try:
with open(target, "w") as f:
f.write("hi")
deadline = time.time() + 3
while not events and time.time() < deadline:
time.sleep(0.05)
finally:
monitor.disable()
self.assertTrue(events)
self.assertEqual(events[0][0], target)
def test_frida_core_alias(self):
import frida.core
self.assertIs(frida.core.Session, self.frida.Session)
self.assertIs(frida.core.Device, self.frida.Device)
def test_device_bus_post_accepts_dict(self):
bus = self.frida.get_local_device().bus
self.assertTrue(callable(bus.post))
bus.post({"type": "ping"})
def test_script_options_snapshot_setter(self):
options = self._new_script_options()
options.snapshot = b"\x00\x01\x02"
self.assertEqual(options.snapshot, b"\x00\x01\x02")
def _new_script_options(self):
from frida import _frida
return _frida.ScriptOptions()
def test_iostream_exposes_old_api(self):
for name in ("is_closed", "close", "read", "read_all", "write", "write_all"):
self.assertTrue(hasattr(self.frida.IOStream, name))
def test_get_local_device_returns_wrapped_device(self):
device = self.frida.get_local_device()
self.assertIsInstance(device, self.frida.Device)
self.assertEqual(device.id, "local")
def test_device_manager_is_a_singleton(self):
import frida._frida as _frida
import frida.aio
underlying = _frida.get_device_manager()
self.assertIs(_frida.get_device_manager(), underlying)
self.assertIs(self.frida.get_device_manager()._impl, underlying)
self.assertIs(frida.aio.get_device_manager()._impl, underlying)
def test_aio_get_local_device(self):
import asyncio
import frida.aio
device = asyncio.run(frida.aio.get_local_device())
self.assertIsInstance(device, frida.aio.Device)
self.assertEqual(device.id, "local")
def test_method_with_options_parameter_returns_object(self):
import os
device = self.frida.get_local_device()
process = device.get_process_by_pid(os.getpid())
self.assertIsInstance(process, self.frida.Process)
self.assertEqual(process.pid, os.getpid())
def test_method_with_options_parameter_maps_errors(self):
device = self.frida.get_local_device()
with self.assertRaises(self.frida.ProcessNotFoundError):
device.attach(999999)
def test_list_return_marshals_to_wrapped_objects(self):
devices = self.frida.get_device_manager().enumerate_devices()
self.assertIsInstance(devices, list)
self.assertTrue(all(isinstance(d, self.frida.Device) for d in devices))
self.assertIn("local", [d.id for d in devices])
def test_vardict_return_marshals_to_dict(self):
params = self.frida.get_local_device().query_system_parameters()
self.assertIsInstance(params, dict)
self.assertIn("arch", params)
self.assertIsInstance(params["os"], dict)
def test_signal_handler_is_invoked(self):
cancellable = self.frida.Cancellable()
fired = []
cancellable.on("cancelled", lambda: fired.append(True))
cancellable.cancel()
self.assertEqual(fired, [True])
def test_removed_signal_handler_is_not_invoked(self):
cancellable = self.frida.Cancellable()
hits = []
def handler():
hits.append(True)
cancellable.on("cancelled", handler)
cancellable.off("cancelled", handler)
cancellable.cancel()
self.assertEqual(hits, [])
def test_custom_base_type_still_exposes_signals(self):
device = self.frida.get_local_device()
callback = lambda *args: None
device.on("spawn-added", callback)
device.off("spawn-added", callback)
def test_unknown_signal_name_raises(self):
with self.assertRaises(ValueError):
self.frida.get_device_manager().on("nonexistent", lambda: None)
def test_signal_handler_can_receive_the_emitter(self):
cancellable = self.frida.Cancellable()
received = []
cancellable.on("cancelled", lambda sender: received.append(sender))
cancellable.cancel()
self.assertEqual(len(received), 1)
self.assertIsInstance(received[0], self.frida.Cancellable)
def test_signal_handler_with_too_many_arguments_raises(self):
cancellable = self.frida.Cancellable()
with self.assertRaises(TypeError):
cancellable.on("cancelled", lambda a, b, c: None)
def test_cancellable_is_cancelled_is_a_property(self):
cancellable = self.frida.Cancellable()
self.assertFalse(cancellable.is_cancelled)
cancellable.cancel()
self.assertTrue(cancellable.is_cancelled)
def test_cancellable_pollfd_round_trips(self):
cancellable = self.frida.Cancellable()
pollfd = cancellable.get_pollfd()
with pollfd as fd:
self.assertIsInstance(fd, int)
pollfd.release()
pollfd.release()
def test_rpc_round_trip(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("rpc.exports = { add: (a, b) => a + b, echo: (x) => x };")
script.load()
self.assertEqual(sorted(dir(script.exports_sync)), ["add", "echo"])
self.assertEqual(script.exports_sync.add(3, 4), 7)
self.assertEqual(script.exports_sync.echo("hello"), "hello")
finally:
session.detach()
def test_script_rpc_error_propagates(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("rpc.exports = { boom: () => { throw new Error('kaboom'); } };")
script.load()
with self.assertRaises(self.frida.RPCException):
script.exports_sync.boom()
finally:
session.detach()
def test_script_log_handler_receives_console_log(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("console.log('hello from script');")
logs = []
script.set_log_handler(lambda level, text: logs.append((level, text)))
script.load()
self.assertIn(("info", "hello from script"), logs)
finally:
session.detach()
def test_script_list_exports(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("rpc.exports = { a: () => 1, b: () => 2 };")
script.load()
self.assertEqual(sorted(script.list_exports_sync()), ["a", "b"])
finally:
session.detach()
def test_script_message_round_trip(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("send({ hi: 1 });")
received = []
script.on("message", lambda message, data: received.append(message))
script.load()
self.assertEqual(received[0]["type"], "send")
self.assertEqual(received[0]["payload"], {"hi": 1})
finally:
session.detach()
def test_script_rpc_raises_after_destroy(self):
session = self.frida.get_local_device().attach(0)
try:
script = session.create_script("rpc.exports = { ping: () => 1 };")
script.load()
script.unload()
with self.assertRaises(self.frida.InvalidOperationError):
script.exports_sync.ping()
finally:
session.detach()
def test_rpc_exception_str_surfaces_message(self):
self.assertEqual(str(self.frida.RPCException("type", "name", "boom")), "boom")
self.assertEqual(str(self.frida.RPCException("boom")), "boom")
def test_sync_cancellable_context_cancels_blocking_call(self):
import threading
device = self.frida.get_local_device()
cancellable = self.frida.Cancellable()
threading.Timer(0.2, cancellable.cancel).start()
with self.assertRaises(self.frida.OperationCancelledError):
with cancellable:
device.get_process_by_name("frida-nonexistent-zzz", timeout=5000)
def test_aio_cancellable_context_cancels_awaitable(self):
import asyncio
import frida.aio
async def scenario():
device = await frida.aio.get_local_device()
cancellable = frida.aio.Cancellable()
async def cancel_soon():
await asyncio.sleep(0.2)
cancellable.cancel()
asyncio.ensure_future(cancel_soon())
with self.assertRaises(self.frida.OperationCancelledError):
with cancellable:
await device.get_process_by_name("frida-nonexistent-zzz", timeout=5000)
asyncio.run(scenario())
def test_attach_accepts_options_as_kwargs(self):
session = self.frida.get_local_device().attach(0, realm="native", persist_timeout=0)
session.detach()
def test_invalid_option_is_reported(self):
with self.assertRaises(AttributeError):
self.frida.get_local_device().attach(0, no_such_option=123)
def test_compile_script_returns_bytes(self):
session = self.frida.get_local_device().attach(0)
try:
data = session.compile_script("const x = 42;")
self.assertIsInstance(data, bytes)
self.assertGreater(len(data), 0)
finally:
session.detach()
def test_aio_rpc_round_trip(self):
import asyncio
import frida.aio
async def scenario():
device = await frida.aio.get_local_device()
session = await device.attach(0)
try:
script = await session.create_script("rpc.exports = { add: (a, b) => a + b };")
await script.load()
self.assertEqual(await script.exports.add(3, 4), 7)
finally:
await session.detach()
asyncio.run(scenario())
if __name__ == "__main__":
unittest.main()