Files
frida-python/tests/test_bindgen.py
T
Ole André Vadla Ravnås 3353755177 Fix attach() linker_notifier_offsets kwarg
Its customization hardcoded an empty selector map instead of the one
computed from the options type, so the plural kwarg was rejected. Let
codegen build the options argument for custom facade methods that take
**kwargs.
2026-08-02 18:19:14 +02:00

989 lines
36 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"
CERTIFICATE = """\
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIUEoCZX+FRWNTzcvvrCE2chFvE0EgwDQYJKoZIhvcNAQEL
BQAwHDEaMBgGA1UEAwwRZnJpZGEtcHl0aG9uLXRlc3QwIBcNMjYwNzMwMjIyNjAy
WhgPMjEyNjA3MDYyMjI2MDJaMBwxGjAYBgNVBAMMEWZyaWRhLXB5dGhvbi10ZXN0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtKTIUbHLhkqRKdKqXuMc
u7q8OZD2eKdykp4Qvv+I5JcmNW7nlW+58zTCLaqUCsSrojDb8T2qvtRJbaJYAofE
P7bhb5ROOVwsyOjhvP7rZ4zRXIDAUlB7zE15yNkWL+Rh4l3wRxMOMIB2HQlurtDk
PIqPOITwRvrADKZt/73iMASwrkkqKnl/F3rLT0JQrd907sBdGGZNbJM9L4TUBT9t
MBBl6qXtDEFyBfeoqlnrLUTN/Jwyka+/6n6eZCN8g2g5tS1Lc7LFjX4iBG9ibgaH
hrNSTM+haLImFS3dtXFr4rOCAHnDYYvkCY1nuZOXannCvZowv0xczqpGLbj5IYJK
MQIDAQABo1MwUTAdBgNVHQ4EFgQUi83cV5yMTdWaVeRpgNO3vyeitMMwHwYDVR0j
BBgwFoAUi83cV5yMTdWaVeRpgNO3vyeitMMwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEADScSFuasB4mgOb/kktq3O09rf/uUIa6OEjdFRec/i2+E
ZAUd5onsWQTHm4yhjqQwqVBte58tTqZB7S1HMCy5sEgEVUFnTrMUqtrf/WAnsWA6
SdjNr7h/aRnLHHysLkbl0Zo4fOIWIYSJpLn39e0OzcNBjEHN5U9HE/m/2LFku1Kb
KMTYXrhBKvVPpZLxZyoLz00EM7euaNx3juAgfQdSDfKGahf5CqqMJ/VmITISDJDR
jtN3wcv0/nZA5x/b73QCj0jqIEWqguYf4GLWitxGsSdT2dBEpT88Kz3Ibwb8uRT+
Ls3eRsdoqxiOT0E7Z44O8gjbqsYOr/jdMShwEytDwQ==
-----END CERTIFICATE-----
"""
@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_certificate_property_accepts_pem_and_path(self):
import tempfile
options = self._frida.RemoteDeviceOptions()
self.assertIsNone(options.certificate)
options.certificate = CERTIFICATE
self.assertEqual(options.certificate, CERTIFICATE)
with tempfile.NamedTemporaryFile("w", suffix=".pem") as f:
f.write(CERTIFICATE)
f.flush()
options.certificate = f.name
self.assertEqual(options.certificate, CERTIFICATE)
options.certificate = None
self.assertIsNone(options.certificate)
def test_certificate_property_rejects_missing_file(self):
options = self._frida.RemoteDeviceOptions()
with self.assertRaises(self._frida.InvalidArgumentError):
options.certificate = "/nonexistent.pem"
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_attach_accepts_linker_notifier_offsets(self):
session = self.frida.get_local_device().attach(0, linker_notifier_offsets=[0x100])
session.detach()
def test_enable_debugger_defaults_to_any_port(self):
import inspect
for script in [self.frida.Script, self.frida.aio.Script]:
self.assertEqual(inspect.signature(script.enable_debugger).parameters["port"].default, 0)
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()