Auto-generate the bindings from Frida's .gir

This commit is contained in:
Ole André Vadla Ravnås authored and Ole André Vadla Ravnås committed 2026-07-16 17:07:24 +02:00
1 parent 0988dca83e
commit 2d5f9b0f7a
62 files changed
+6656 -9545

No files matched your search

+2 -3
View File
@@ -11,6 +11,5 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.8
- uses: jamescurtin/isort-action@master
- run: pip install isort
- run: isort --check-only --diff .
+3 -2
View File
@@ -5,11 +5,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: lgeiger/pyflakes-action@master
- run: pip install pyflakes
- run: pyflakes $(git ls-files '*.py' | grep -v '^frida/frida_bindgen/assets/')
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: jpetrucciani/mypy-check@master
with:
mypy_flags: '--exclude examples --exclude setup'
mypy_flags: '--exclude examples --exclude setup --exclude frida/frida_bindgen/assets'
+19
View File
@@ -0,0 +1,19 @@
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
run: pip install setuptools
- name: Build the extension
run: make
- name: Run the test suite
run: python3 -m unittest tests.test_bindgen -v
+2
View File
@@ -1,5 +1,7 @@
*.pyc
*.abi3.so
/*.egg-info/
/.cache/
/build/
/deps/
/dist/
+3
View File
@@ -1,3 +1,6 @@
[submodule "releng"]
path = releng
url = https://github.com/frida/releng.git
[submodule "frida-bindgen"]
path = frida-bindgen
url = https://github.com/frida/frida-bindgen.git
+3 -3
View File
@@ -1,8 +1,8 @@
include meson.build
include frida/meson.build
include frida/_frida/extension.c
include frida/_frida/extension.version
include frida/_frida/meson.build
include frida/extension.version
recursive-include frida/frida_bindgen *.py *.c *.h
recursive-include frida-bindgen *.py
include subprojects/frida-core.wrap
include BSDmakefile
+1 -1
View File
@@ -8,7 +8,7 @@ all $(MAKECMDGOALS):
$(MAKECMDGOALS)
git-submodules:
@if [ ! -f releng/meson/meson.py ]; then \
@if [ ! -f releng/meson/meson.py ] || [ ! -f frida-bindgen/frida_bindgen_core/model.py ]; then \
git submodule update --init --recursive --depth 1; \
fi
-include git-submodules
Submodule
+1
Submodule frida-bindgen added at df17879d92.
-189
View File
@@ -1,189 +0,0 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
try:
from . import _frida
except Exception as ex:
print("")
print("***")
if str(ex).startswith("No module named "):
print("Frida native extension not found")
print("Please check your PYTHONPATH.")
else:
print(f"Failed to load the Frida native extension: {ex}")
print("Please ensure that the extension was compiled correctly")
print("***")
print("")
raise ex
from . import core
__version__: str = _frida.__version__
get_device_manager = core.get_device_manager
Relay = _frida.Relay
PortalService = core.PortalService
EndpointParameters = core.EndpointParameters
Compiler = core.Compiler
PackageManager = core.PackageManager
FileMonitor = _frida.FileMonitor
Cancellable = core.Cancellable
ServerNotRunningError = _frida.ServerNotRunningError
ExecutableNotFoundError = _frida.ExecutableNotFoundError
ExecutableNotSupportedError = _frida.ExecutableNotSupportedError
ProcessNotFoundError = _frida.ProcessNotFoundError
ProcessNotRespondingError = _frida.ProcessNotRespondingError
InvalidArgumentError = _frida.InvalidArgumentError
InvalidOperationError = _frida.InvalidOperationError
PermissionDeniedError = _frida.PermissionDeniedError
AddressInUseError = _frida.AddressInUseError
TimedOutError = _frida.TimedOutError
NotSupportedError = _frida.NotSupportedError
ProtocolError = _frida.ProtocolError
TransportError = _frida.TransportError
OperationCancelledError = _frida.OperationCancelledError
def query_system_parameters() -> Dict[str, Any]:
"""
Returns a dictionary of information about the host system
"""
return get_local_device().query_system_parameters()
def spawn(
program: Union[str, List[Union[str, bytes]], Tuple[Union[str, bytes]]],
argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None,
envp: Optional[Dict[str, str]] = None,
env: Optional[Dict[str, str]] = None,
cwd: Optional[str] = None,
stdio: Optional[str] = None,
**kwargs: Any,
) -> int:
"""
Spawn a process into an attachable state
"""
return get_local_device().spawn(program=program, argv=argv, envp=envp, env=env, cwd=cwd, stdio=stdio, **kwargs)
def resume(target: core.ProcessTarget) -> None:
"""
Resume a process from the attachable state
:param target: the PID or name of the process
"""
get_local_device().resume(target)
def kill(target: core.ProcessTarget) -> None:
"""
Kill a process
:param target: the PID or name of the process
"""
get_local_device().kill(target)
def attach(
target: core.ProcessTarget,
realm: Optional[str] = None,
persist_timeout: Optional[int] = None,
exceptor: Optional[str] = None,
unwind_broker: Optional[bool] = None,
exit_monitor: Optional[bool] = None,
thread_suspend_monitor: Optional[bool] = None,
linker_notifier_offsets: Optional[Sequence[int]] = None,
) -> core.Session:
"""
Attach to a process
:param target: the PID or name of the process
"""
return get_local_device().attach(
target,
realm=realm,
persist_timeout=persist_timeout,
exceptor=exceptor,
unwind_broker=unwind_broker,
exit_monitor=exit_monitor,
thread_suspend_monitor=thread_suspend_monitor,
linker_notifier_offsets=linker_notifier_offsets,
)
def inject_library_file(target: core.ProcessTarget, path: str, entrypoint: str, data: str) -> int:
"""
Inject a library file to a process.
:param target: the PID or name of the process
"""
return get_local_device().inject_library_file(target, path, entrypoint, data)
def inject_library_blob(target: core.ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int:
"""
Inject a library blob to a process
:param target: the PID or name of the process
"""
return get_local_device().inject_library_blob(target, blob, entrypoint, data)
def get_local_device() -> core.Device:
"""
Get the local device
"""
return get_device_manager().get_local_device()
def get_remote_device() -> core.Device:
"""
Get the first remote device in the devices list
"""
return get_device_manager().get_remote_device()
def get_usb_device(timeout: int = 0) -> core.Device:
"""
Get the first device connected over USB in the devices list
"""
return get_device_manager().get_usb_device(timeout)
def get_device(id: Optional[str], timeout: int = 0) -> core.Device:
"""
Get a device by its id
"""
return get_device_manager().get_device(id, timeout)
def get_device_matching(predicate: Callable[[core.Device], bool], timeout: int = 0) -> core.Device:
"""
Get device matching predicate.
:param predicate: a function to filter the devices
:param timeout: operation timeout in seconds
"""
return get_device_manager().get_device_matching(predicate, timeout)
def enumerate_devices() -> List[core.Device]:
"""
Enumerate all the devices from the device manager
"""
return get_device_manager().enumerate_devices()
@core.cancellable
def shutdown() -> None:
"""
Shutdown the main device manager
"""
get_device_manager()._impl.close()
-905
View File
@@ -1,905 +0,0 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
# Exceptions
class AddressInUseError(Exception): ...
class ExecutableNotFoundError(Exception): ...
class ExecutableNotSupportedError(Exception): ...
class ServerNotRunningError(Exception): ...
class TimedOutError(Exception): ...
class TransportError(Exception): ...
class ProcessNotFoundError(Exception): ...
class ProcessNotRespondingError(Exception): ...
class ProtocolError(Exception): ...
class InvalidArgumentError(Exception): ...
class InvalidOperationError(Exception): ...
class NotSupportedError(Exception): ...
class OperationCancelledError(Exception): ...
class PermissionDeniedError(Exception): ...
class Object:
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler.
"""
...
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler.
"""
...
class Application(Object):
@property
def identifier(self) -> str:
"""
Application identifier.
"""
...
@property
def name(self) -> str:
"""
Human-readable application name.
"""
...
@property
def parameters(self) -> Dict[str, Any]:
"""
Parameters.
"""
...
@property
def pid(self) -> int:
"""
Process ID, or 0 if not running.
"""
...
class Bus(Object):
def attach(self) -> None:
"""
Attach to the bus.
"""
...
def post(self, message: str, data: Optional[Union[bytes, str]]) -> None:
"""
Post a JSON-encoded message to the bus.
"""
...
class Cancellable(Object):
def cancel(self) -> None:
"""
Set cancellable to cancelled.
"""
...
def connect(self, callback: Callable[..., Any]) -> int:
"""
Register notification callback.
"""
...
def disconnect(self, handler_id: int) -> None:
"""
Unregister notification callback.
"""
...
@classmethod
def get_current(cls) -> "Cancellable":
"""
Get the top cancellable from the stack.
"""
...
def get_fd(self) -> int:
"""
Get file descriptor for integrating with an event loop.
"""
...
def is_cancelled(self) -> bool:
"""
Query whether cancellable has been cancelled.
"""
...
def pop_current(self) -> None:
"""
Pop cancellable off the cancellable stack.
"""
...
def push_current(self) -> None:
"""
Push cancellable onto the cancellable stack.
"""
...
def raise_if_cancelled(self) -> None:
"""
Raise an exception if cancelled.
"""
...
def release_fd(self) -> None:
"""
Release a resource previously allocated by get_fd().
"""
...
class Child(Object):
@property
def argv(self) -> List[str]:
"""
Argument vector.
"""
...
@property
def envp(self) -> Dict[str, str]:
"""
Environment vector.
"""
...
@property
def identifier(self) -> str:
"""
Application identifier.
"""
...
@property
def origin(self) -> str:
"""
Origin.
"""
...
@property
def parent_pid(self) -> int:
"""
Parent Process ID.
"""
...
@property
def path(self) -> str:
"""
Path of executable.
"""
...
@property
def pid(self) -> int:
"""
Process ID.
"""
...
class Crash(Object):
@property
def parameters(self) -> Dict[str, Any]:
"""
Parameters.
"""
...
@property
def pid(self) -> int:
"""
Process ID.
"""
...
@property
def process_name(self) -> str:
"""
Process name.
"""
...
@property
def report(self) -> str:
"""
Human-readable crash report.
"""
...
@property
def summary(self) -> str:
"""
Human-readable crash summary.
"""
...
class Device(Object):
@property
def id(self) -> Optional[str]:
"""
Device ID.
"""
...
@property
def name(self) -> Optional[str]:
"""
Human-readable device name.
"""
...
@property
def icon(self) -> Optional[Any]:
"""
Icon.
"""
...
@property
def type(self) -> Optional[str]:
"""
Device type. One of: local, remote, usb.
"""
...
@property
def bus(self) -> Optional[Bus]:
"""
Message bus.
"""
...
def attach(
self,
pid: int,
realm: Optional[str] = None,
persist_timeout: Optional[int] = None,
exceptor: Optional[str] = None,
unwind_broker: Optional[bool] = None,
exit_monitor: Optional[bool] = None,
thread_suspend_monitor: Optional[bool] = None,
linker_notifier_offsets: Optional[Sequence[int]] = None,
) -> "Session":
"""
Attach to a PID.
"""
...
def disable_spawn_gating(self) -> None:
"""
Disable spawn gating.
"""
...
def enable_spawn_gating(self) -> None:
"""
Enable spawn gating.
"""
...
def enumerate_applications(
self, identifiers: Optional[Sequence[str]] = None, scope: Optional[str] = None
) -> List[Application]:
"""
Enumerate applications.
"""
...
def enumerate_pending_children(self) -> List[Child]:
"""
Enumerate pending children.
"""
...
def enumerate_pending_spawn(self) -> List["Spawn"]:
"""
Enumerate pending spawn.
"""
...
def enumerate_processes(self, pids: Optional[Sequence[int]] = None, scope: Optional[str] = None) -> List[Process]:
"""
Enumerate processes.
"""
...
def get_frontmost_application(self, scope: Optional[str] = None) -> Optional[Application]:
"""
Get details about the frontmost application.
"""
...
def inject_library_blob(self, pid: int, blob_buffer: bytes, entrypoint: str, data: str) -> int:
"""
Inject a library blob to a PID.
"""
...
def inject_library_file(self, pid: int, path: str, entrypoint: str, data: str) -> int:
"""
Inject a library file to a PID.
"""
...
def input(self, pid: int, data: bytes) -> None:
"""
Input data on stdin of a spawned process.
"""
...
def is_lost(self) -> bool:
"""
Query whether the device has been lost.
"""
...
def kill(self, pid: int) -> None:
"""
Kill a PID.
"""
...
def open_channel(self, address: str) -> "IOStream":
"""
Open a device-specific communication channel.
"""
...
def open_service(self, address: str) -> Service:
"""
Open a device-specific service.
"""
...
def unpair(self) -> None:
"""
Unpair device.
"""
...
def override_option(self, name: str, value: Any) -> None:
"""
Override a backend-specific option.
"""
...
def query_system_parameters(self) -> Dict[str, Any]:
"""
Returns a dictionary of information about the host system.
"""
...
def resume(self, pid: int) -> None:
"""
Resume a process from the attachable state.
"""
...
def spawn(
self,
program: str,
argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None,
envp: Optional[Dict[str, str]] = None,
env: Optional[Dict[str, str]] = None,
cwd: Optional[str] = None,
stdio: Optional[str] = None,
**kwargs: Any,
) -> int:
"""
Spawn a process into an attachable state.
"""
...
class DeviceManager(Object):
def add_remote_device(
self,
address: str,
certificate: Optional[str] = None,
origin: Optional[str] = None,
token: Optional[str] = None,
keepalive_interval: Optional[int] = None,
) -> Device:
"""
Add a remote device.
"""
...
def close(self) -> None:
"""
Close the device manager.
"""
...
def enumerate_devices(self) -> List[Device]:
"""
Enumerate devices.
"""
...
def get_device_matching(self, predicate: Callable[[Device], bool], timeout: int) -> Device:
"""
Get device matching predicate.
"""
...
def remove_remote_device(self, address: str) -> None:
"""
Remove a remote device.
"""
...
class EndpointParameters(Object): ...
class FileMonitor(Object):
def disable(self) -> None:
"""
Disable the file monitor.
"""
...
def enable(self) -> None:
"""
Enable the file monitor.
"""
...
class IOStream(Object):
def close(self) -> None:
"""
Close the stream.
"""
...
def is_closed(self) -> bool:
"""
Query whether the stream is closed.
"""
...
def read(self, size: int) -> bytes:
"""
Read up to the specified number of bytes from the stream.
"""
...
def read_all(self, size: int) -> bytes:
"""
Read exactly the specified number of bytes from the stream.
"""
...
def write(self, data: bytes) -> int:
"""
Write as much as possible of the provided data to the stream.
"""
...
def write_all(self, data: bytes) -> None:
"""
Write all of the provided data to the stream.
"""
...
class PortalMembership(Object):
def terminate(self) -> None:
"""
Terminate the membership.
"""
...
class PortalService(Object):
@property
def device(self) -> Device:
"""
Device for in-process control.
"""
...
def broadcast(self, message: str, data: Optional[Union[str, bytes]] = None) -> None:
"""
Broadcast a message to all control channels.
"""
...
def enumerate_tags(self, connection_id: int) -> List[str]:
"""
Enumerate tags of a specific connection.
"""
...
def kick(self, connection_id: int) -> None:
"""
Kick out a specific connection.
"""
...
def narrowcast(self, tag: str, message: str, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a message to control channels with a specific tag.
"""
...
def post(self, connection_id: int, message: str, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a message to a specific control channel.
"""
...
def start(self) -> None:
"""
Start listening for incoming connections.
"""
...
def stop(self) -> None:
"""
Stop listening for incoming connections, and kick any connected clients.
"""
...
def tag(self, connection_id: int, tag: str) -> None:
"""
Tag a specific control channel.
"""
...
def untag(self, connection_id: int, tag: str) -> None:
"""
Untag a specific control channel.
"""
...
class Process(Object):
@property
def pid(self) -> int:
"""
Process ID.
"""
...
@property
def name(self) -> str:
"""
Human-readable process name.
"""
...
@property
def parameters(self) -> Dict[str, Any]:
"""
Parameters.
"""
...
class Relay(Object):
def __init__(self, address: str, username: str, password: str, kind: str) -> None: ...
@property
def address(self) -> str:
"""
Network address or address:port of the TURN server.
"""
...
@property
def kind(self) -> str:
"""
Relay kind. One of: turn-udp, turn-tcp, turn-tls.
"""
...
@property
def password(self) -> str:
"""
The TURN password to use for the allocate request.
"""
...
@property
def username(self) -> str:
"""
The TURN username to use for the allocate request.
"""
...
class Script(Object):
def eternalize(self) -> None:
"""
Eternalize the script.
"""
...
def is_destroyed(self) -> bool:
"""
Query whether the script has been destroyed.
"""
...
def load(self) -> None:
"""
Load the script.
"""
...
def interrupt(self) -> None:
"""
Interrupt any JavaScript currently executing in the script.
"""
...
def post(self, message: str, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a JSON-encoded message to the script.
"""
...
def unload(self) -> None:
"""
Unload the script.
"""
...
def terminate(self) -> None:
"""
Interrupt execution and unload the script.
"""
...
def enable_debugger(self, port: Optional[int]) -> None:
"""
Enable the Node.js compatible script debugger
"""
...
def disable_debugger(self) -> None:
"""
Disable the Node.js compatible script debugger
"""
...
class Service(Object):
def activate(self) -> None:
"""
Activate the service.
"""
...
def cancel(self) -> None:
"""
Cancel the service.
"""
...
def request(self, parameters: Any) -> Any:
"""
Perform a request.
"""
...
class Session(Object):
@property
def pid(self) -> int:
"""
Process ID.
"""
...
def compile_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> bytes:
"""
Compile script source code to bytecode.
"""
...
def create_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> Script:
"""
Create a new script.
"""
...
def create_script_from_bytes(
self, data: bytes, name: Optional[str] = None, runtime: Optional[str] = None
) -> Script:
"""
Create a new script from bytecode.
"""
...
def snapshot_script(self, embed_script: str, warmup_script: Optional[str], runtime: Optional[str] = None) -> bytes:
"""
Evaluate script and snapshot the resulting VM state
"""
...
def detach(self) -> None:
"""
Detach session from the process.
"""
...
def disable_child_gating(self) -> None:
"""
Disable child gating.
"""
...
def enable_child_gating(self) -> None:
"""
Enable child gating.
"""
...
def is_detached(self) -> bool:
"""
Query whether the session is detached.
"""
...
def join_portal(
self, address: str, certificate: Optional[str] = None, token: Optional[str] = None, acl: Optional[Any] = None
) -> PortalMembership:
"""
Join a portal.
"""
...
def resume(self) -> None:
"""
Resume session after network error.
"""
...
def setup_peer_connection(
self, stun_server: Optional[str] = None, relays: Optional[Sequence[Relay]] = None
) -> None:
"""
Set up a peer connection with the target process.
"""
...
class Spawn(Object):
@property
def identifier(self) -> str:
"""
Application identifier.
"""
...
@property
def pid(self) -> int:
"""
Process ID.
"""
...
class Compiler(Object):
def build(
self,
entrypoint: str,
project_root: Optional[str] = None,
output_format: Optional[str] = None,
bundle_format: Optional[str] = None,
type_check: Optional[str] = None,
source_maps: Optional[str] = None,
compression: Optional[str] = None,
platform: Optional[str] = None,
externals: Optional[Sequence[str]] = None,
) -> str:
"""
Build an agent.
"""
...
def watch(
self,
entrypoint: str,
project_root: Optional[str] = None,
output_format: Optional[str] = None,
bundle_format: Optional[str] = None,
type_check: Optional[str] = None,
source_maps: Optional[str] = None,
compression: Optional[str] = None,
platform: Optional[str] = None,
externals: Optional[Sequence[str]] = None,
) -> None:
"""
Continuously build an agent.
"""
...
class PackageManager(Object):
@property
def registry(self) -> str:
"""
The registry being used.
"""
...
@registry.setter
def registry(self, value: str) -> None:
"""
Change the registry to use.
"""
...
def search(
self,
query: str,
offset: Optional[int] = None,
limit: Optional[int] = None,
) -> PackageSearchResult:
"""
Search for packages to install.
"""
...
def install(
self,
project_root: Optional[str] = None,
role: Optional[str] = None,
specs: Optional[Sequence[str]] = None,
omits: Optional[Sequence[str]] = None,
) -> PackageInstallResult:
"""
Install one or more packages.
"""
...
class Package(Object):
@property
def name(self) -> str:
"""
Package name.
"""
...
@property
def version(self) -> str:
"""
Package version.
"""
...
@property
def description(self) -> Optional[str]:
"""
Package description.
"""
...
@property
def url(self) -> Optional[str]:
"""
Package URL.
"""
...
class PackageSearchResult(Object):
@property
def packages(self) -> List[Package]:
"""
Batch of matching packages.
"""
...
@property
def total(self) -> int:
"""
Total matching packages.
"""
...
class PackageInstallResult(Object):
@property
def packages(self) -> List[Package]:
"""
The toplevel packages that are installed.
"""
...
__version__: str
File diff suppressed because it is too large. Load diff
-21
View File
@@ -1,21 +0,0 @@
py_sources = [
'__init__.pyi',
'py.typed',
]
python.install_sources(py_sources, subdir: 'frida' / '_frida', pure: false)
extra_link_args = []
if host_os_family == 'darwin'
extra_link_args += '-Wl,-exported_symbol,_PyInit__frida'
elif host_os_family != 'windows'
extra_link_args += '-Wl,--version-script,' + meson.current_source_dir() / 'extension.version'
endif
extension = python.extension_module('_frida', 'extension.c',
limited_api: '3.7',
c_args: frida_component_cflags,
link_args: extra_link_args,
dependencies: [python_dep, frida_core_dep, os_deps],
install: true,
subdir: 'frida',
)
-1861
View File
File diff suppressed because it is too large. Load diff
File renamed without changes.
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()
@@ -0,0 +1,81 @@
static gboolean
PyGObject_unmarshal_certificate (const gchar * str,
GTlsCertificate ** certificate)
{
GError * error = NULL;
if (strchr (str, '\n') != NULL)
*certificate = g_tls_certificate_new_from_pem (str, -1, &error);
else
*certificate = g_tls_certificate_new_from_file (str, &error);
if (error != NULL)
goto propagate_error;
return TRUE;
propagate_error:
{
PyFrida_raise (g_error_new_literal (FRIDA_ERROR, FRIDA_ERROR_INVALID_ARGUMENT, error->message));
g_error_free (error);
return FALSE;
}
}
static int
PyEndpointParameters_init (PyEndpointParameters * self,
PyObject * args,
PyObject * kw)
{
int result = -1;
static char * keywords[] = { "address", "port", "certificate", "origin", "auth_service", "asset_root", NULL };
char * address = NULL;
unsigned short int port = 0;
char * certificate_value = NULL;
char * origin = NULL;
PyObject * auth_service_obj = NULL;
char * asset_root_value = NULL;
GTlsCertificate * certificate = NULL;
FridaAuthenticationService * auth_service = NULL;
GFile * asset_root = NULL;
FridaEndpointParameters * handle;
if (PyGObject_tp_init ((PyObject *) self, args, kw) < 0)
return -1;
if (!PyArg_ParseTupleAndKeywords (args, kw, "|esHesesOes", keywords,
"utf-8", &address,
&port,
"utf-8", &certificate_value,
"utf-8", &origin,
&auth_service_obj,
"utf-8", &asset_root_value))
return -1;
if (certificate_value != NULL && !PyGObject_unmarshal_certificate (certificate_value, &certificate))
goto beach;
if (auth_service_obj != NULL && auth_service_obj != Py_None)
auth_service = g_object_ref (PY_GOBJECT_HANDLE (auth_service_obj));
if (asset_root_value != NULL)
asset_root = g_file_new_for_path (asset_root_value);
handle = frida_endpoint_parameters_new (address, port, certificate, origin, auth_service, asset_root);
PyGObject_take_handle ((PyGObject *) self, handle, PYFRIDA_TYPE (EndpointParameters));
result = 0;
beach:
g_clear_object (&asset_root);
g_clear_object (&auth_service);
g_clear_object (&certificate);
PyMem_Free (asset_root_value);
PyMem_Free (origin);
PyMem_Free (certificate_value);
PyMem_Free (address);
return result;
}
@@ -0,0 +1,5 @@
static GHashTable * pygobject_type_spec_by_type;
static GHashTable * frida_exception_by_error_code;
static PyObject * cancelled_exception;
static PyObject * inspect_getargspec;
static PyObject * inspect_ismethod;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
static void PyGObject_class_init (void);
static int PyGObject_init (PyGObject * self);
static void PyGObject_dealloc (PyGObject * self);
static void PyGObject_gc_dealloc (PyGObject * self);
static gpointer PyGObject_steal_handle (PyGObject * self);
static void PyGObject_register_type (GType instance_type, PyGObjectType * python_type);
static PyObject * PyGObject_marshal_object (gpointer handle, GType type);
static PyObject * PyGObject_new_take_handle (gpointer handle, const PyGObjectType * pytype);
static PyObject * PyGObject_try_get_from_handle (gpointer handle);
static PyObject * PyGObject_on (PyGObject * self, PyObject * args);
static PyObject * PyGObject_off (PyGObject * self, PyObject * args);
static gboolean PyGObject_parse_signal_method_args (PyObject * args, GType instance_type, guint * signal_id, PyObject ** callback);
static gint PyGObject_compare_signal_closure_callback (GClosure * closure, PyObject * callback);
static GClosure * PyGObject_make_closure_for_signal (PyObject * callback, guint max_arg_count);
static void PyGObjectSignalClosure_finalize (PyObject * callback);
static void PyGObjectSignalClosure_marshal (GClosure * closure, GValue * return_gvalue, guint n_param_values, const GValue * param_values, gpointer invocation_hint, gpointer marshal_data);
static PyObject * PyGObjectSignalClosure_marshal_params (const GValue * params, guint params_length);
static PyObject * PyGObject_marshal_value (const GValue * value);
static const gchar * PyGObject_class_name_from_c (const gchar * cname);
static guint PyFrida_get_max_argument_count (PyObject * callable);
static gboolean PyGObject_unmarshal_enum (const gchar * str, GType type, gpointer value);
static PyObject * PyGObject_marshal_enum (gint value, GType type);
static gboolean PyGObject_unmarshal_string (PyObject * value, gchar ** str);
static PyObject * PyGObject_marshal_string (const gchar * str);
static gboolean PyGObject_unmarshal_strv (PyObject * value, gchar *** strv, gint * length);
static PyObject * PyGObject_marshal_strv (gchar * const * strv, gint length);
static PyObject * PyGObject_marshal_bytes (GBytes * bytes);
static gboolean PyGObject_unmarshal_vardict (PyObject * value, GHashTable ** dict);
static PyObject * PyGObject_marshal_vardict (GHashTable * dict);
static gboolean PyGObject_unmarshal_variant (PyObject * value, GVariant ** variant);
static gboolean PyGObject_unmarshal_variant_from_sequence (PyObject * sequence, GVariant ** variant);
static gboolean PyGObject_unmarshal_variant_from_mapping (PyObject * mapping, GVariant ** variant);
static PyObject * PyGObject_marshal_variant (GVariant * variant);
static PyObject * PyGObject_marshal_variant_byte_array (GVariant * variant);
static PyObject * PyGObject_marshal_variant_dict (GVariant * variant);
static PyObject * PyGObject_marshal_variant_array (GVariant * variant);
static PyObject * PyGObject_marshal_variant_tuple (GVariant * variant);
static PyObject * PyFrida_raise (GError * error);
static PyObject * PyFrida_marshal_error (GError * error);
static void PyFrida_object_decref (gpointer obj);
static void PyFrida_deliver (PyObject * callback, PyObject * result, PyObject * error);
static PyObject * PyFrida_make_completion (GTask * task, PyFridaCompleteFunc complete);
static PyObject * PyFrida_complete_request (PyObject * module, PyObject * args);
static gboolean PyFrida_return_error (GTask * task, PyObject * error);
@@ -0,0 +1,81 @@
#define PYFRIDA_TYPE(name) \
(&_PYFRIDA_TYPE_VAR (name, type))
#define PYFRIDA_TYPE_OBJECT(name) \
PYFRIDA_TYPE (name)->object
#define _PYFRIDA_TYPE_VAR(name, var) \
G_PASTE (G_PASTE (G_PASTE (Py, name), _), var)
#define PYFRIDA_DEFINE_BASETYPE(pyname, cname, destroy_func, ...) \
_PYFRIDA_DEFINE_TYPE_SLOTS (cname, __VA_ARGS__); \
_PYFRIDA_DEFINE_TYPE_SPEC (cname, pyname, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE); \
static PyGObjectType _PYFRIDA_TYPE_VAR (cname, type) = \
{ \
.parent = NULL, \
.object = NULL, \
.destroy = destroy_func, \
}
#define PYFRIDA_DEFINE_TYPE(pyname, cname, parent_cname, destroy_func, ...) \
_PYFRIDA_DEFINE_TYPE_SLOTS (cname, __VA_ARGS__); \
_PYFRIDA_DEFINE_TYPE_SPEC (cname, pyname, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE); \
static PyGObjectType _PYFRIDA_TYPE_VAR (cname, type) = \
{ \
.parent = PYFRIDA_TYPE (parent_cname), \
.object = NULL, \
.destroy = destroy_func, \
}
#define PYFRIDA_DEFINE_GC_TYPE(pyname, cname, parent_cname, destroy_func, ...) \
_PYFRIDA_DEFINE_TYPE_SLOTS (cname, __VA_ARGS__); \
_PYFRIDA_DEFINE_TYPE_SPEC (cname, pyname, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC); \
static PyGObjectType _PYFRIDA_TYPE_VAR (cname, type) = \
{ \
.parent = PYFRIDA_TYPE (parent_cname), \
.object = NULL, \
.destroy = destroy_func, \
}
#define PYFRIDA_REGISTER_TYPE(cname, gtype) \
G_BEGIN_DECLS \
{ \
PyGObjectType * t = PYFRIDA_TYPE (cname); \
t->object = PyType_FromSpecWithBases (&_PYFRIDA_TYPE_VAR (cname, spec), \
(t->parent != NULL) ? PyTuple_Pack (1, t->parent->object) : NULL); \
PyGObject_register_type (gtype, t); \
Py_IncRef (t->object); \
PyModule_AddObject (module, G_STRINGIFY (cname), t->object); \
} \
G_END_DECLS
#define _PYFRIDA_DEFINE_TYPE_SPEC(cname, pyname, type_flags) \
static PyType_Spec _PYFRIDA_TYPE_VAR (cname, spec) = \
{ \
.name = pyname, \
.basicsize = sizeof (G_PASTE (Py, cname)), \
.itemsize = 0, \
.flags = type_flags, \
.slots = _PYFRIDA_TYPE_VAR (cname, slots), \
}
#define _PYFRIDA_DEFINE_TYPE_SLOTS(cname, ...) \
static PyType_Slot _PYFRIDA_TYPE_VAR (cname, slots)[] = \
{ \
__VA_ARGS__ \
{ 0 }, \
}
#define PYFRIDA_DECLARE_EXCEPTION(error_code, name) \
G_STMT_START \
{ \
PyObject * exception = PyErr_NewException ("frida." name "Error", NULL, NULL); \
g_hash_table_insert (frida_exception_by_error_code, GINT_TO_POINTER (error_code), exception); \
Py_IncRef (exception); \
PyModule_AddObject (module, name "Error", exception); \
} \
G_STMT_END
#define PY_GOBJECT(o) ((PyGObject *) (o))
#define PY_GOBJECT_HANDLE(o) (PY_GOBJECT (o)->handle)
#define PY_GOBJECT_SIGNAL_CLOSURE(o) ((PyGObjectSignalClosure *) (o))
#define PyFrida_RETURN_NONE \
G_STMT_START \
{ \
Py_IncRef (Py_None); \
return Py_None; \
} \
G_STMT_END
@@ -0,0 +1,29 @@
struct _PyGObject
{
PyObject_HEAD
gpointer handle;
const PyGObjectType * type;
GSList * signal_closures;
};
struct _PyGObjectType
{
PyGObjectType * parent;
PyObject * object;
PyGObjectInitFromHandleFunc init_from_handle;
GDestroyNotify destroy;
};
struct _PyGObjectSignalClosure
{
GClosure parent;
guint max_arg_count;
};
struct _PyFridaRequest
{
GTask * task;
PyFridaCompleteFunc complete;
};
@@ -0,0 +1,6 @@
typedef struct _PyGObject PyGObject;
typedef void (* PyGObjectInitFromHandleFunc) (PyObject * self, gpointer handle);
typedef struct _PyGObjectType PyGObjectType;
typedef struct _PyGObjectSignalClosure PyGObjectSignalClosure;
typedef struct _PyFridaRequest PyFridaRequest;
typedef void (* PyFridaCompleteFunc) (GTask * task, PyObject * value, PyObject * error);
+23
View File
@@ -0,0 +1,23 @@
def _setup(self):
self._message_handlers = []
self._impl.on("message", self._on_message)
def on(self, signal, callback):
if signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def _on_message(self, raw_message, data):
message = json.loads(raw_message)
for handler in list(self._message_handlers):
try:
handler(message, data)
except Exception:
traceback.print_exc()
@@ -0,0 +1,27 @@
def _setup(self):
self._message_handlers = []
self._loop = asyncio.get_event_loop()
self._impl.on("message", self._on_message)
def on(self, signal, callback):
if signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def _on_message(self, raw_message, data):
message = json.loads(raw_message)
for handler in list(self._message_handlers):
_dispatch(self._loop, self._deliver_message, handler, message, data)
def _deliver_message(self, handler, message, data):
try:
handler(message, data)
except Exception:
traceback.print_exc()
@@ -0,0 +1,22 @@
def __enter__(self):
stack = getattr(_current_cancellable, "stack", None)
if stack is None:
stack = []
_current_cancellable.stack = stack
stack.append(self)
return self
def __exit__(self, *exc):
_current_cancellable.stack.pop()
return False
@classmethod
def get_current(cls):
return _current_cancellable_get()
def raise_if_cancelled(self):
if self._impl.is_cancelled():
raise _frida.OperationCancelledError("operation was cancelled")
def get_pollfd(self):
return CancellablePollFD(self._impl)
@@ -0,0 +1,18 @@
def __enter__(self):
self._token = _current_cancellable.set(self)
return self
def __exit__(self, *exc):
_current_cancellable.reset(self._token)
return False
@classmethod
def get_current(cls):
return _current_cancellable.get()
def raise_if_cancelled(self):
if self._impl.is_cancelled():
raise _frida.OperationCancelledError("operation was cancelled")
def get_pollfd(self):
return CancellablePollFD(self._impl)
@@ -0,0 +1,23 @@
class CancellablePollFD:
def __init__(self, cancellable):
self.handle = cancellable.get_fd()
self._cancellable = cancellable
def __del__(self):
self.release()
def release(self):
if self._cancellable is not None:
if self.handle != -1:
self._cancellable.release_fd()
self.handle = -1
self._cancellable = None
def __repr__(self):
return repr(self.handle)
def __enter__(self):
return self.handle
def __exit__(self, *exc):
self.release()
@@ -0,0 +1,23 @@
class CancellablePollFD:
def __init__(self, cancellable):
self.handle = cancellable.get_fd()
self._cancellable = cancellable
def __del__(self):
self.release()
def release(self):
if self._cancellable is not None:
if self.handle != -1:
self._cancellable.release_fd()
self.handle = -1
self._cancellable = None
def __repr__(self):
return repr(self.handle)
def __enter__(self):
return self.handle
def __exit__(self, *exc):
self.release()
@@ -0,0 +1,9 @@
def get_device_matching(self, predicate, timeout=0):
deadline = time.monotonic() + timeout
while True:
for device in self.enumerate_devices():
if predicate(device):
return device
if time.monotonic() >= deadline:
raise _frida.InvalidArgumentError("no matching device found")
time.sleep(0.05)
@@ -0,0 +1,9 @@
async def get_device_matching(self, predicate, timeout=0):
deadline = time.monotonic() + timeout
while True:
for device in await self.enumerate_devices():
if predicate(device):
return device
if time.monotonic() >= deadline:
raise _frida.InvalidArgumentError("no matching device found")
await asyncio.sleep(0.05)
@@ -0,0 +1,25 @@
@property
def type(self):
return self.dtype
def get_bus(self):
return self.bus
def get_process(self, process_name):
process_name_lc = process_name.lower()
matching = [
process
for process in self.enumerate_processes()
if fnmatch.fnmatchcase(process.name.lower(), process_name_lc)
]
if len(matching) == 1:
return matching[0]
if len(matching) > 1:
matches = ", ".join(f"{p.name} (pid: {p.pid})" for p in matching)
raise _frida.ProcessNotFoundError(f"ambiguous name; it matches: {matches}")
raise _frida.ProcessNotFoundError(f"unable to find process with name '{process_name}'")
def _pid_of(self, target):
if isinstance(target, str):
return self.get_process(target).pid
return target
@@ -0,0 +1,25 @@
@property
def type(self):
return self.dtype
def get_bus(self):
return self.bus
async def get_process(self, process_name):
process_name_lc = process_name.lower()
matching = [
process
for process in await self.enumerate_processes()
if fnmatch.fnmatchcase(process.name.lower(), process_name_lc)
]
if len(matching) == 1:
return matching[0]
if len(matching) > 1:
matches = ", ".join(f"{p.name} (pid: {p.pid})" for p in matching)
raise _frida.ProcessNotFoundError(f"ambiguous name; it matches: {matches}")
raise _frida.ProcessNotFoundError(f"unable to find process with name '{process_name}'")
async def _pid_of(self, target):
if isinstance(target, str):
return (await self.get_process(target)).pid
return target
@@ -0,0 +1,12 @@
class _Implementation:
def _frida_dispatch(self, name, args, completion):
def run():
try:
result = _unwrap(getattr(self, name)(*[_wrap(a) for a in args]))
error = None
except Exception as e:
result = None
error = e
_frida._complete_request(completion, result, error)
threading.Thread(target=run, daemon=True).start()
@@ -0,0 +1,17 @@
class _Implementation:
def _frida_dispatch(self, name, args, completion):
loop = self._loop
async def run():
try:
result = getattr(self, name)(*[_wrap(a) for a in args])
if asyncio.iscoroutine(result):
result = await result
result = _unwrap(result)
error = None
except Exception as e:
result = None
error = e
_frida._complete_request(completion, result, error)
_dispatch(loop, lambda: loop.create_task(run()))
@@ -0,0 +1,25 @@
def close(self):
_invoke(self._impl.close_async, (0,))
def read(self, count):
return _invoke(self._impl.input_stream.read_bytes_async, (count, 0))
def read_all(self, count):
chunks = []
remaining = count
while remaining > 0:
chunk = _invoke(self._impl.input_stream.read_bytes_async, (remaining, 0))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def write(self, data):
return _invoke(self._impl.output_stream.write_bytes_async, (data, 0))
def write_all(self, data):
view = memoryview(data)
while len(view) != 0:
written = _invoke(self._impl.output_stream.write_bytes_async, (bytes(view), 0))
view = view[written:]
@@ -0,0 +1,25 @@
async def close(self):
await _invoke(self._impl.close_async, (0,))
async def read(self, count):
return await _invoke(self._impl.input_stream.read_bytes_async, (count, 0))
async def read_all(self, count):
chunks = []
remaining = count
while remaining > 0:
chunk = await _invoke(self._impl.input_stream.read_bytes_async, (remaining, 0))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
async def write(self, data):
return await _invoke(self._impl.output_stream.write_bytes_async, (data, 0))
async def write_all(self, data):
view = memoryview(data)
while len(view) != 0:
written = await _invoke(self._impl.output_stream.write_bytes_async, (bytes(view), 0))
view = view[written:]
@@ -0,0 +1,37 @@
def _setup(self):
self._authenticated_handlers = []
self._message_handlers = []
self._impl.on("authenticated", self._on_authenticated)
self._impl.on("message", self._on_message)
def on(self, signal, callback):
if signal == "authenticated":
self._authenticated_handlers.append(callback)
elif signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "authenticated":
self._authenticated_handlers.remove(callback)
elif signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def _on_authenticated(self, connection_id, raw_session_info):
session_info = json.loads(raw_session_info)
for handler in list(self._authenticated_handlers):
try:
handler(connection_id, session_info)
except Exception:
traceback.print_exc()
def _on_message(self, connection_id, raw_message, data):
message = json.loads(raw_message)
for handler in list(self._message_handlers):
try:
handler(connection_id, message, data)
except Exception:
traceback.print_exc()
@@ -0,0 +1,38 @@
def _setup(self):
self._authenticated_handlers = []
self._message_handlers = []
self._loop = asyncio.get_event_loop()
self._impl.on("authenticated", self._on_authenticated)
self._impl.on("message", self._on_message)
def on(self, signal, callback):
if signal == "authenticated":
self._authenticated_handlers.append(callback)
elif signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "authenticated":
self._authenticated_handlers.remove(callback)
elif signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def _on_authenticated(self, connection_id, raw_session_info):
session_info = json.loads(raw_session_info)
for handler in list(self._authenticated_handlers):
_dispatch(self._loop, self._deliver, handler, connection_id, session_info)
def _on_message(self, connection_id, raw_message, data):
message = json.loads(raw_message)
for handler in list(self._message_handlers):
_dispatch(self._loop, self._deliver, handler, connection_id, message, data)
def _deliver(self, handler, *args):
try:
handler(*args)
except Exception:
traceback.print_exc()
@@ -0,0 +1,38 @@
def _to_camel_case(name):
result = ""
capitalize = False
for ch in name:
if ch == "_":
capitalize = True
elif capitalize:
result += ch.upper()
capitalize = False
else:
result += ch
return result
class RPCException(Exception):
def __str__(self):
return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0])
class _ScriptExports:
def __init__(self, script):
self._script = script
def __getattr__(self, name):
script = self._script
js_name = _to_camel_case(name)
def method(*args):
if args and isinstance(args[-1], bytes):
params, data = list(args[:-1]), args[-1]
else:
params, data = list(args), None
return script._rpc_request(["call", js_name, params], data)
return method
def __dir__(self):
return self._script._list_exports()
@@ -0,0 +1,35 @@
def _to_camel_case(name):
result = ""
capitalize = False
for ch in name:
if ch == "_":
capitalize = True
elif capitalize:
result += ch.upper()
capitalize = False
else:
result += ch
return result
class RPCException(Exception):
def __str__(self):
return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0])
class _ScriptExports:
def __init__(self, script):
self._script = script
def __getattr__(self, name):
script = self._script
js_name = _to_camel_case(name)
def method(*args):
if args and isinstance(args[-1], bytes):
params, data = list(args[:-1]), args[-1]
else:
params, data = list(args), None
return script._rpc_request(["call", js_name, params], data)
return method
@@ -0,0 +1,111 @@
def _setup(self):
self.exports_sync = _ScriptExports(self)
self._message_handlers = []
self._log_handler = self.default_log_handler
self._pending = {}
self._next_request_id = 1
self._cond = threading.Condition()
self._impl.on("message", self._on_message)
self._impl.on("destroyed", self._on_destroyed)
@property
def exports(self):
return self.exports_sync
def on(self, signal, callback):
if signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def get_log_handler(self):
return self._log_handler
def set_log_handler(self, handler):
self._log_handler = handler
def default_log_handler(self, level, text):
if level == "info":
print(text, file=sys.stdout)
else:
print(text, file=sys.stderr)
def list_exports_sync(self):
return self._rpc_request(["list"])
def list_exports(self):
return self.list_exports_sync()
def _list_exports(self):
return self.list_exports_sync()
def _rpc_request(self, args, data=None):
outcome = {}
cond = self._cond
def complete(value, error):
with cond:
outcome["value"] = value
outcome["error"] = error
cond.notify_all()
with cond:
request_id = self._next_request_id
self._next_request_id += 1
self._pending[request_id] = complete
if self._impl.is_destroyed():
self._on_destroyed()
else:
self.post(["frida:rpc", request_id] + list(args), data)
with cond:
while not outcome:
cond.wait()
if outcome["error"] is not None:
raise outcome["error"]
return outcome["value"]
def _on_message(self, raw_message, data):
message = json.loads(raw_message)
mtype = message["type"]
payload = message.get("payload")
if mtype == "log":
self._log_handler(message["level"], payload)
elif mtype == "send" and isinstance(payload, list) and payload[:1] == ["frida:rpc"]:
self._on_rpc_message(payload[1], payload[2], payload[3:], data)
else:
for handler in list(self._message_handlers):
try:
handler(message, data)
except Exception:
traceback.print_exc()
def _on_destroyed(self):
while True:
with self._cond:
pending_ids = list(self._pending.keys())
complete = self._pending.pop(pending_ids[0]) if pending_ids else None
if complete is None:
break
complete(None, _frida.InvalidOperationError("script has been destroyed"))
def _on_rpc_message(self, request_id, operation, params, data):
if operation not in ("ok", "error"):
return
complete = self._pending.pop(request_id, None)
if complete is None:
return
if operation == "error":
complete(None, RPCException(*params[0:3]))
elif data is not None:
complete((params[1], data) if len(params) > 1 else data, None)
else:
complete(params[0], None)
@@ -0,0 +1,98 @@
def _setup(self):
self.exports = _ScriptExports(self)
self._message_handlers = []
self._log_handler = self.default_log_handler
self._pending = {}
self._next_request_id = 1
self._loop = asyncio.get_event_loop()
self._impl.on("message", self._on_message)
self._impl.on("destroyed", self._on_destroyed)
def on(self, signal, callback):
if signal == "message":
self._message_handlers.append(callback)
else:
self._impl.on(signal, _make_signal_handler(callback))
def off(self, signal, callback):
if signal == "message":
self._message_handlers.remove(callback)
else:
self._impl.off(signal, callback)
def get_log_handler(self):
return self._log_handler
def set_log_handler(self, handler):
self._log_handler = handler
def default_log_handler(self, level, text):
if level == "info":
print(text, file=sys.stdout)
else:
print(text, file=sys.stderr)
async def list_exports(self):
return await self._rpc_request(["list"])
def _rpc_request(self, args, data=None):
loop = asyncio.get_running_loop()
future = loop.create_future()
def complete(value, error):
if future.done():
return
if error is not None:
future.set_exception(error)
else:
future.set_result(value)
request_id = self._next_request_id
self._next_request_id += 1
self._pending[request_id] = lambda value, error: _dispatch(loop, complete, value, error)
if self._impl.is_destroyed():
self._on_destroyed()
else:
self.post(["frida:rpc", request_id] + list(args), data)
return future
def _on_message(self, raw_message, data):
message = json.loads(raw_message)
mtype = message["type"]
payload = message.get("payload")
if mtype == "log":
_dispatch(self._loop, self._log_handler, message["level"], payload)
elif mtype == "send" and isinstance(payload, list) and payload[:1] == ["frida:rpc"]:
self._on_rpc_message(payload[1], payload[2], payload[3:], data)
else:
for handler in list(self._message_handlers):
_dispatch(self._loop, self._deliver_message, handler, message, data)
def _deliver_message(self, handler, message, data):
try:
handler(message, data)
except Exception:
traceback.print_exc()
def _on_destroyed(self):
while True:
pending_ids = list(self._pending.keys())
complete = self._pending.pop(pending_ids[0]) if pending_ids else None
if complete is None:
break
complete(None, _frida.InvalidOperationError("script has been destroyed"))
def _on_rpc_message(self, request_id, operation, params, data):
if operation not in ("ok", "error"):
return
complete = self._pending.pop(request_id, None)
if complete is None:
return
if operation == "error":
complete(None, RPCException(*params[0:3]))
elif data is not None:
complete((params[1], data) if len(params) > 1 else data, None)
else:
complete(params[0], None)
@@ -0,0 +1,63 @@
__version__ = _frida.__version__
def get_device_manager():
return _wrap(_frida.get_device_manager())
def get_device(id, timeout=0):
return get_device_manager().get_device_by_id(id, int(timeout * 1000))
def get_device_matching(predicate, timeout=0):
return get_device_manager().get_device_matching(predicate, timeout)
def get_local_device():
return get_device_manager().get_device_by_type("local", 0)
def get_remote_device():
return get_device_manager().get_device_by_type("remote", 0)
def get_usb_device(timeout=0):
return get_device_manager().get_device_by_type("usb", int(timeout * 1000))
def enumerate_devices():
return get_device_manager().enumerate_devices()
def query_system_parameters():
return get_local_device().query_system_parameters()
def spawn(program, argv=None, envp=None, env=None, cwd=None, stdio=None, **aux):
return get_local_device().spawn(
program, argv=argv, envp=envp, env=env, cwd=cwd, stdio=stdio, **aux
)
def resume(target):
return get_local_device().resume(target)
def kill(target):
return get_local_device().kill(target)
def attach(target, **kwargs):
return get_local_device().attach(target, **kwargs)
def inject_library_file(target, path, entrypoint, data):
return get_local_device().inject_library_file(target, path, entrypoint, data)
def inject_library_blob(target, blob, entrypoint, data):
return get_local_device().inject_library_blob(target, blob, entrypoint, data)
def shutdown():
get_device_manager().close()
@@ -0,0 +1,64 @@
__version__ = _frida.__version__
def get_device_manager():
return _wrap(_frida.get_device_manager())
async def get_device(id, timeout=0):
return await get_device_manager().get_device_by_id(id, int(timeout * 1000))
async def get_device_matching(predicate, timeout=0):
return await get_device_manager().get_device_matching(predicate, timeout)
async def get_local_device():
return await get_device_manager().get_device_by_type("local", 0)
async def get_remote_device():
return await get_device_manager().get_device_by_type("remote", 0)
async def get_usb_device(timeout=0):
return await get_device_manager().get_device_by_type("usb", int(timeout * 1000))
async def enumerate_devices():
return await get_device_manager().enumerate_devices()
async def query_system_parameters():
return await (await get_local_device()).query_system_parameters()
async def spawn(program, argv=None, envp=None, env=None, cwd=None, stdio=None, **aux):
device = await get_local_device()
return await device.spawn(
program, argv=argv, envp=envp, env=env, cwd=cwd, stdio=stdio, **aux
)
async def resume(target):
return await (await get_local_device()).resume(target)
async def kill(target):
return await (await get_local_device()).kill(target)
async def attach(target, **kwargs):
return await (await get_local_device()).attach(target, **kwargs)
async def inject_library_file(target, path, entrypoint, data):
return await (await get_local_device()).inject_library_file(target, path, entrypoint, data)
async def inject_library_blob(target, blob, entrypoint, data):
return await (await get_local_device()).inject_library_blob(target, blob, entrypoint, data)
async def shutdown():
await get_device_manager().close()
@@ -0,0 +1,13 @@
static PyObject *
PyFrida_get_device_manager (PyObject * module,
PyObject * args)
{
static PyObject * manager = NULL;
if (manager == NULL)
manager = PyObject_CallFunction ((PyObject *) PYFRIDA_TYPE_OBJECT (DeviceManager), NULL);
Py_IncRef (manager);
return manager;
}
+100
View File
@@ -0,0 +1,100 @@
from __future__ import annotations
import argparse
from io import StringIO
from pathlib import Path
from . import codegen
from .customization import load_customizations
from .loader import compute_model
def main():
run(build_arguments())
def build_arguments() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Generate the Python bindings for Frida.")
p.add_argument(
"--frida-gir",
required=True,
type=Path,
help="Path to the Frida .gir file.",
)
p.add_argument(
"--glib-gir",
required=True,
type=Path,
help="Path to the GLib .gir file.",
)
p.add_argument(
"--gobject-gir",
required=True,
type=Path,
help="Path to the GObject .gir file.",
)
p.add_argument(
"--gio-gir",
required=True,
type=Path,
help="Path to the GIO .gir file.",
)
p.add_argument(
"--output-py",
required=True,
type=Path,
help="Path to the output .py file.",
)
p.add_argument(
"--output-pyi",
required=True,
type=Path,
help="Path to the output .pyi file.",
)
p.add_argument(
"--output-aio",
required=True,
type=Path,
help="Path to the output asyncio facade .py file.",
)
p.add_argument(
"--output-c",
required=True,
type=Path,
help="Path to the output C file for the Python extension.",
)
return p.parse_args()
def run(args: argparse.Namespace) -> None:
customizations = load_customizations()
model = compute_model(args.frida_gir, args.glib_gir, args.gobject_gir, args.gio_gir, customizations)
artefacts = codegen.generate_all(model)
with OutputFile(args.output_py) as f:
f.write(artefacts["py"])
with OutputFile(args.output_aio) as f:
f.write(artefacts["aio"])
with OutputFile(args.output_pyi) as f:
f.write(artefacts["pyi"])
with OutputFile(args.output_c) as f:
f.write(artefacts["c"])
class OutputFile:
def __init__(self, output_path):
self._output_path = output_path
self._io = StringIO()
def __enter__(self):
return self._io
def __exit__(self, *exc):
result = self._io.getvalue()
if self._output_path.exists():
existing_contents = self._output_path.read_text(encoding="utf-8")
if existing_contents == result:
return False
self._output_path.write_text(result, encoding="utf-8")
return False
File diff suppressed because it is too large. Load diff
+384
View File
@@ -0,0 +1,384 @@
from __future__ import annotations
from typing import Mapping
from .model import (
ConstructorCustomizations,
CustomCode,
Customizations,
ModuleFunction,
EnumerationCustomizations,
EnumerationMemberCustomizations,
MethodCustomizations,
ObjectTypeCustomizations,
PropertyCustomizations,
SignalCustomizations,
TypeCustomizations,
)
_RESOLVE_PID = ("pid = self._pid_of(target)", "pid = await self._pid_of(target)")
def load_customizations() -> Customizations:
type_customizations: Mapping[str, TypeCustomizations] = {
"DeviceManager": ObjectTypeCustomizations(
custom_code=CustomCode(
members=(
"facade_device_manager_members.py",
"facade_device_manager_members_aio.py",
),
helpers=("facade_toplevel.py", "facade_toplevel_aio.py"),
module_functions=(
ModuleFunction(
py_name="get_device_manager",
c_symbol="PyFrida_get_device_manager",
asset="module_get_device_manager.c",
),
),
),
),
"Bus": ObjectTypeCustomizations(
provides_signals=True,
custom_code=CustomCode(
members=("facade_bus.py", "facade_bus_aio.py"),
),
methods={
"post": MethodCustomizations(
param_typings=["message", "data=None"],
custom_logic="json = _to_json(message)",
),
},
),
"PortalService": ObjectTypeCustomizations(
provides_signals=True,
custom_code=CustomCode(
members=("facade_portal.py", "facade_portal_aio.py"),
),
methods={
"post": MethodCustomizations(
param_typings=["connection_id", "message", "data=None"],
custom_logic="json = _to_json(message)",
),
"narrowcast": MethodCustomizations(
param_typings=["tag", "message", "data=None"],
custom_logic="json = _to_json(message)",
),
"broadcast": MethodCustomizations(
param_typings=["message", "data=None"],
custom_logic="json = _to_json(message)",
),
},
),
"Device": ObjectTypeCustomizations(
methods={
"spawn": MethodCustomizations(
param_typings=[
"program",
"argv=None",
"envp=None",
"env=None",
"cwd=None",
"stdio=None",
"**aux",
],
custom_logic="""\
if not isinstance(program, str):
argv = program
program = argv[0]
if len(argv) == 1:
argv = None
if isinstance(program, bytes):
program = program.decode()
options = _frida.SpawnOptions()
if argv is not None:
options.argv = [arg.decode() if isinstance(arg, bytes) else arg for arg in argv]
if envp is not None:
options.envp = _to_envp(envp)
if env is not None:
options.env = _to_envp(env)
if cwd is not None:
options.cwd = cwd
if stdio is not None:
options.stdio = stdio
if aux:
options.aux = aux
""",
),
"is_lost": MethodCustomizations(as_property=True),
"resume": MethodCustomizations(param_typings=["target"], custom_logic=_RESOLVE_PID),
"kill": MethodCustomizations(param_typings=["target"], custom_logic=_RESOLVE_PID),
"input": MethodCustomizations(param_typings=["target", "data"], custom_logic=_RESOLVE_PID),
"inject_library_file": MethodCustomizations(
param_typings=["target", "path", "entrypoint", "data"],
custom_logic=_RESOLVE_PID,
),
"inject_library_blob": MethodCustomizations(
param_typings=["target", "blob", "entrypoint", "data"],
custom_logic=_RESOLVE_PID,
),
"attach": MethodCustomizations(
param_typings=["target", "**kwargs"],
custom_logic=(
"pid = self._pid_of(target)\n" "options = _make_options(_frida.SessionOptions, kwargs)",
"pid = await self._pid_of(target)\n" "options = _make_options(_frida.SessionOptions, kwargs)",
),
),
},
custom_code=CustomCode(
members=("facade_device_members.py", "facade_device_members_aio.py"),
),
),
"Script": ObjectTypeCustomizations(
provides_signals=True,
custom_code=CustomCode(
members=("facade_rpc_members.py", "facade_rpc_members_aio.py"),
helpers=("facade_rpc_helpers.py", "facade_rpc_helpers_aio.py"),
),
methods={
"post": MethodCustomizations(
param_typings=["message", "data=None"],
custom_logic="json = _to_json(message)",
),
"is_destroyed": MethodCustomizations(as_property=True),
},
),
"Session": ObjectTypeCustomizations(
methods={
"is_detached": MethodCustomizations(as_property=True),
"create_script": MethodCustomizations(
param_typings=["source", "name=None", "snapshot=None", "runtime=None"],
custom_logic="""\
options = _frida.ScriptOptions()
if name is not None:
options.name = name
if snapshot is not None:
options.snapshot = snapshot
if runtime is not None:
options.runtime = runtime""",
),
"create_script_from_bytes": MethodCustomizations(
param_typings=["data", "name=None", "snapshot=None", "runtime=None"],
custom_logic="""\
bytes = data
options = _frida.ScriptOptions()
if name is not None:
options.name = name
if snapshot is not None:
options.snapshot = snapshot
if runtime is not None:
options.runtime = runtime""",
),
"compile_script": MethodCustomizations(
param_typings=["source", "name=None", "runtime=None"],
custom_logic="""\
options = _frida.ScriptOptions()
if name is not None:
options.name = name
if runtime is not None:
options.runtime = runtime""",
),
"snapshot_script": MethodCustomizations(
param_typings=["embed_script", "warmup_script=None", "runtime=None"],
custom_logic="""\
options = _frida.SnapshotOptions()
if warmup_script is not None:
options.warmup_script = warmup_script
if runtime is not None:
options.runtime = runtime""",
),
},
),
"Service": ObjectTypeCustomizations(
methods={
"is_closed": MethodCustomizations(as_property=True),
},
),
"EndpointParameters": ObjectTypeCustomizations(
custom_constructor="codegen_endpoint_parameters.c",
constructor=ConstructorCustomizations(
param_typings=[
"address=None",
"port=None",
"certificate=None",
"origin=None",
"authentication=None",
"asset_root=None",
"request_handler=None",
],
custom_logic="""\
auth_service = None
if authentication is not None:
if isinstance(authentication, tuple):
scheme, data = authentication
if scheme == "token":
auth_service = _frida.StaticAuthenticationService(data)
elif scheme == "callback":
if not callable(data):
raise ValueError("authentication data must be callable for the callback scheme")
service = AuthenticationService.__new__(AuthenticationService)
service.authenticate = lambda token: json.dumps(data(token))
AuthenticationService.__init__(service)
auth_service = service._impl
else:
raise ValueError("invalid authentication scheme")
else:
auth_service = _unwrap(authentication)
kwargs = {}
if address is not None:
kwargs["address"] = address
if port is not None:
kwargs["port"] = port
if certificate is not None:
kwargs["certificate"] = certificate
if origin is not None:
kwargs["origin"] = origin
if auth_service is not None:
kwargs["auth_service"] = auth_service
if asset_root is not None:
kwargs["asset_root"] = str(asset_root)
self._impl = _frida.EndpointParameters(**kwargs)
if request_handler is not None:
self._impl.request_handler = _unwrap(request_handler)
""",
),
),
"RelayKind": EnumerationCustomizations(
members={
"turn_udp": EnumerationMemberCustomizations(js_name="TurnUDP"),
"turn_tcp": EnumerationMemberCustomizations(js_name="TurnTCP"),
"turn_tls": EnumerationMemberCustomizations(js_name="TurnTLS"),
},
),
"ScriptRuntime": EnumerationCustomizations(
members={
"qjs": EnumerationMemberCustomizations(js_name="QJS"),
},
),
"ControlService": ObjectTypeCustomizations(
methods={
"get_endpoint_params": MethodCustomizations(drop=True),
},
properties={
"endpoint-params": PropertyCustomizations(drop=True),
},
),
"Injector": ObjectTypeCustomizations(drop=True),
"RpcClient": ObjectTypeCustomizations(drop=True),
"RpcPeer": ObjectTypeCustomizations(drop=True),
"Cancellable": ObjectTypeCustomizations(
custom_code=CustomCode(
members=("facade_cancellable.py", "facade_cancellable_aio.py"),
helpers=("facade_cancellable_helpers.py", "facade_cancellable_helpers_aio.py"),
),
methods={
"is_cancelled": MethodCustomizations(as_property=True),
"make_pollfd": MethodCustomizations(drop=True),
"source_new": MethodCustomizations(drop=True),
},
),
"IOStream": ObjectTypeCustomizations(
custom_code=CustomCode(
members=("facade_iostream.py", "facade_iostream_aio.py"),
),
methods={
"close": MethodCustomizations(drop=True),
"close_async": MethodCustomizations(suppress_facade=True),
"splice_async": MethodCustomizations(drop=True),
"has_pending": MethodCustomizations(drop=True),
"set_pending": MethodCustomizations(drop=True),
"clear_pending": MethodCustomizations(drop=True),
},
),
"InputStream": ObjectTypeCustomizations(
methods={
"close": MethodCustomizations(drop=True),
"read": MethodCustomizations(drop=True),
"read_async": MethodCustomizations(drop=True),
"read_all": MethodCustomizations(drop=True),
"read_all_async": MethodCustomizations(drop=True),
"read_bytes": MethodCustomizations(drop=True),
"skip": MethodCustomizations(drop=True),
"is_closed": MethodCustomizations(drop=True),
"has_pending": MethodCustomizations(drop=True),
"set_pending": MethodCustomizations(drop=True),
"clear_pending": MethodCustomizations(drop=True),
},
),
"OutputStream": ObjectTypeCustomizations(
methods={
"close": MethodCustomizations(drop=True),
"flush": MethodCustomizations(drop=True),
"write": MethodCustomizations(drop=True),
"write_async": MethodCustomizations(drop=True),
"write_all": MethodCustomizations(drop=True),
"write_all_async": MethodCustomizations(drop=True),
"write_bytes": MethodCustomizations(drop=True),
"writev": MethodCustomizations(drop=True),
"writev_async": MethodCustomizations(drop=True),
"writev_all": MethodCustomizations(drop=True),
"writev_all_async": MethodCustomizations(drop=True),
"splice": MethodCustomizations(drop=True),
"is_closing": MethodCustomizations(drop=True),
"is_closed": MethodCustomizations(drop=True),
"has_pending": MethodCustomizations(drop=True),
"set_pending": MethodCustomizations(drop=True),
"clear_pending": MethodCustomizations(drop=True),
},
),
"UnixSocketAddress": ObjectTypeCustomizations(
methods={
"get_path_len": MethodCustomizations(drop=True),
"get_is_abstract": MethodCustomizations(drop=True),
},
properties={
"abstract": PropertyCustomizations(drop=True),
"path-as-array": PropertyCustomizations(drop=True),
},
),
"SocketAddress": ObjectTypeCustomizations(
constructor=ConstructorCustomizations(drop=True),
methods={
"to_native": MethodCustomizations(drop=True),
},
),
"SocketConnectable": ObjectTypeCustomizations(drop_abstract_base=True),
"InetAddress": ObjectTypeCustomizations(
properties={
"bytes": PropertyCustomizations(drop=True),
},
),
"Object": ObjectTypeCustomizations(
methods={
"is_floating": MethodCustomizations(drop=True),
"ref": MethodCustomizations(drop=True),
"ref_sink": MethodCustomizations(drop=True),
"unref": MethodCustomizations(drop=True),
"getv": MethodCustomizations(drop=True),
"get_property": MethodCustomizations(drop=True),
"set_property": MethodCustomizations(drop=True),
"notify_by_pspec": MethodCustomizations(drop=True),
"freeze_notify": MethodCustomizations(drop=True),
"thaw_notify": MethodCustomizations(drop=True),
"bind_property": MethodCustomizations(drop=True),
"bind_property_full": MethodCustomizations(drop=True),
"bind_property_with_closures": MethodCustomizations(drop=True),
"force_floating": MethodCustomizations(drop=True),
"get_data": MethodCustomizations(drop=True),
"get_qdata": MethodCustomizations(drop=True),
"run_dispose": MethodCustomizations(drop=True),
"set_data": MethodCustomizations(drop=True),
"steal_data": MethodCustomizations(drop=True),
"steal_qdata": MethodCustomizations(drop=True),
"watch_closure": MethodCustomizations(drop=True),
},
signals={
"notify": SignalCustomizations(drop=True),
},
),
}
return Customizations(type_customizations=type_customizations)
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
from pathlib import Path
import frida_bindgen_core as core
from .model import FACTORY, Customizations, Model
INCLUDED_GIO_OBJECT_TYPES = [
"Cancellable",
"IOStream",
"InputStream",
"OutputStream",
"SocketAddress",
"SocketAddressEnumerator",
"SocketConnectable",
"InetSocketAddress",
"InetAddress",
"UnixSocketAddress",
]
INCLUDED_GIO_ENUMERATIONS = [
"FileMonitorEvent",
"SocketFamily",
"UnixSocketAddressType",
]
def compute_model(
frida_gir: Path,
glib_gir: Path,
gobject_gir: Path,
gio_gir: Path,
customizations: Customizations,
) -> Model:
return core.compute_model(
frida_gir,
glib_gir,
gobject_gir,
gio_gir,
customizations,
FACTORY,
INCLUDED_GIO_OBJECT_TYPES,
INCLUDED_GIO_ENUMERATIONS,
seed_object_first=True,
)
+341
View File
@@ -0,0 +1,341 @@
from __future__ import annotations
from collections import OrderedDict
from dataclasses import dataclass, field
from functools import cached_property
from typing import List, Mapping, Optional, Sequence, Tuple, Union
import frida_bindgen_core as core
from frida_bindgen_core import TransferOwnership
from frida_bindgen_core.naming import to_pascal_case, to_snake_case
class Model(core.Model):
@cached_property
def regular_object_types(self) -> List[ObjectType]:
return [t for t in self.object_types.values() if not t.is_frida_list]
class ObjectType(core.ObjectType):
@cached_property
def py_name(self) -> str:
custom = self.customizations
if custom is not None and custom.py_name is not None:
return custom.py_name
return "GObject" if self.name == "Object" else self.name
@property
def custom_code(self) -> Optional[CustomCode]:
custom = self.customizations
return custom.custom_code if custom is not None else None
@property
def provides_signals(self) -> bool:
custom = self.customizations
return custom is not None and custom.provides_signals
@property
def custom_constructor(self) -> Optional[str]:
custom = self.customizations
return custom.custom_constructor if custom is not None else None
@property
def constructor_custom_params(self) -> List[str]:
custom = self.customizations
if custom is None or custom.constructor is None:
return []
return custom.constructor.param_typings or []
@property
def constructor_custom_logic(self) -> Optional[str]:
custom = self.customizations
if custom is None or custom.constructor is None:
return None
return custom.constructor.custom_logic
@cached_property
def c_symbol_prefix(self) -> str:
return f"Py{self.py_name}"
@cached_property
def parent_c_symbol_prefix(self) -> str:
parent = self.parent
return parent.c_symbol_prefix if parent is not None else "PyGObject"
class ClassObjectType(ObjectType):
pass
class InterfaceObjectType(ObjectType):
@cached_property
def has_abstract_base(self) -> bool:
custom = self.customizations
if custom is None:
return True
return not custom.drop_abstract_base
class Constructor(core.Constructor):
pass
class Method(core.Method):
@cached_property
def cself_name(self) -> str:
return to_snake_case(self.object_type.name).split("_")[-1]
@cached_property
def param_ctypings(self) -> List[str]:
result = [f"{self.object_type.c_type} * {self.cself_name}"]
result += [param.ctyping for param in self.parameters]
if self.is_async:
result += ["GAsyncReadyCallback callback", "gpointer user_data"]
return result
@cached_property
def finish_param_ctypings(self) -> List[str]:
result = [
f"{self.object_type.c_type} * {self.cself_name}",
"GAsyncResult * result",
]
if self.throws:
result.append("GError ** error")
return result
@cached_property
def operation_type_name(self) -> str:
return f"Py{self.object_type.name}{to_pascal_case(self.name)}Operation"
@cached_property
def customizations(self) -> Optional[MethodCustomizations]:
custom = self.object_type.customizations
if custom is None:
return None
return custom.methods.get(self.name)
@property
def suppress_facade(self) -> bool:
custom = self.customizations
return custom is not None and custom.suppress_facade
@property
def as_property(self) -> bool:
custom = self.customizations
return custom is not None and custom.as_property
@property
def custom_facade_params(self) -> List[str]:
custom = self.customizations
if custom is None:
return []
return custom.param_typings or []
@property
def custom_logic(self) -> Optional[Union[str, Tuple[str, str]]]:
custom = self.customizations
return custom.custom_logic if custom is not None else None
@cached_property
def facade_call_args(self) -> str:
return "".join(f"{param.name}, " for param in self.input_parameters if param.type.name != "Gio.Cancellable")
class Property(core.Property):
pass
class Signal(core.Signal):
pass
class Parameter(core.Parameter):
@cached_property
def ctyping(self) -> str:
return f"{self.type.c} {self.name}"
class ReturnValue(core.ReturnValue):
@cached_property
def destroy_func(self) -> Optional[str]:
if self.transfer_ownership == TransferOwnership.none:
return None
return self.type.destroy_func
class Enumeration(core.Enumeration):
@cached_property
def c_symbol_prefix(self) -> str:
return f"Py{self.name}"
class EnumerationMember(core.EnumerationMember):
@cached_property
def js_name(self) -> str:
custom = self.customizations
if custom is not None and custom.js_name is not None:
return custom.js_name
return to_pascal_case(self.name)
@cached_property
def nick(self) -> str:
return self.name.replace("_", "-")
@cached_property
def customizations(self) -> Optional[EnumerationMemberCustomizations]:
custom = self.enumeration.customizations
if custom is None:
return None
return custom.members.get(self.name)
@dataclass
class Customizations:
type_customizations: Mapping[str, TypeCustomizations] = field(default_factory=OrderedDict)
@dataclass
class TypeCustomizations:
pass
@dataclass
class ModuleFunction:
py_name: str
c_symbol: str
asset: str
@dataclass
class CustomCode:
members: Optional[Tuple[str, str]] = None
helpers: Optional[Tuple[str, str]] = None
module_functions: Optional[Tuple[ModuleFunction, ...]] = None
@dataclass
class ObjectTypeCustomizations(TypeCustomizations):
py_name: Optional[str] = None
drop: bool = False
drop_abstract_base: bool = False
custom_code: Optional[CustomCode] = None
provides_signals: bool = False
custom_constructor: Optional[str] = None
constructor: Optional[ConstructorCustomizations] = None
methods: Mapping[str, MethodCustomizations] = field(default_factory=dict)
properties: Mapping[str, PropertyCustomizations] = field(default_factory=dict)
signals: Mapping[str, SignalCustomizations] = field(default_factory=dict)
@dataclass
class ConstructorCustomizations:
drop: bool = False
param_typings: Optional[List[str]] = None
custom_logic: Optional[str] = None
@dataclass
class MethodCustomizations:
drop: bool = False
suppress_facade: bool = False
as_property: bool = False
param_typings: Optional[List[str]] = None
custom_logic: Optional[Union[str, Tuple[str, str]]] = None
@dataclass
class PropertyCustomizations:
drop: bool = False
@dataclass
class SignalCustomizations:
drop: bool = False
@dataclass
class EnumerationCustomizations(TypeCustomizations):
members: Mapping[str, EnumerationMemberCustomizations] = field(default_factory=dict)
@dataclass
class EnumerationMemberCustomizations:
js_name: Optional[str] = None
def _make_class(
*,
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
implements,
resolve_type,
model,
):
return ClassObjectType(
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
resolve_type,
model,
)
def _make_interface(
*,
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
resolve_type,
model,
):
return InterfaceObjectType(
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
resolve_type,
model,
)
FACTORY = core.Factory(
class_object_type=_make_class,
interface_object_type=_make_interface,
constructor=Constructor,
method=Method,
parameter=Parameter,
return_value=ReturnValue,
signal=Signal,
property_=Property,
enumeration=Enumeration,
enumeration_member=EnumerationMember,
model=Model,
)
def parse_gir(file_path: str, dependencies: Sequence[Model]) -> Model:
return core.parse_gir(file_path, dependencies, FACTORY)
+110 -3
View File
@@ -1,8 +1,115 @@
subdir('_frida')
if frida_core_dep.type_name() == 'internal'
frida_core_subprj = subproject('frida-core')
frida_gir = frida_core_subprj.get_variable('core_public_gir')
glib_gir = frida_core_subprj.get_variable('glib_gir')
gobject_gir = frida_core_subprj.get_variable('gobject_gir')
gio_gir = frida_core_subprj.get_variable('gio_gir')
else
girdir = frida_core_dep.get_variable('frida_girdir')
frida_gir = girdir / 'Frida-1.0.gir'
glib_gir = girdir / 'GLib-2.0.gir'
gobject_gir = girdir / 'GObject-2.0.gir'
gio_gir = girdir / 'Gio-2.0.gir'
endif
env = environment()
env.set('PYTHONPATH', [meson.current_source_dir(), meson.project_source_root() / 'frida-bindgen'])
code = custom_target('binding-code',
output: [
'__init__.py',
'_frida.pyi',
'extension.c',
'aio.py',
],
input: [
frida_gir,
glib_gir,
gobject_gir,
gio_gir,
files(
'frida_bindgen' / '__init__.py',
'frida_bindgen' / '__main__.py',
'frida_bindgen' / 'cli.py',
'frida_bindgen' / 'codegen.py',
'frida_bindgen' / 'customization.py',
'frida_bindgen' / 'loader.py',
'frida_bindgen' / 'model.py',
'frida_bindgen' / 'assets' / 'codegen_gobject_globals.c',
'frida_bindgen' / 'assets' / 'codegen_gobject_methods.c',
'frida_bindgen' / 'assets' / 'codegen_gobject_prototypes.h',
'frida_bindgen' / 'assets' / 'codegen_macros.h',
'frida_bindgen' / 'assets' / 'codegen_structs.h',
'frida_bindgen' / 'assets' / 'codegen_typedefs.h',
'frida_bindgen' / 'assets' / 'facade_toplevel.py',
'frida_bindgen' / 'assets' / 'facade_toplevel_aio.py',
'frida_bindgen' / 'assets' / 'facade_cancellable.py',
'frida_bindgen' / 'assets' / 'facade_cancellable_aio.py',
'frida_bindgen' / 'assets' / 'facade_cancellable_helpers.py',
'frida_bindgen' / 'assets' / 'facade_cancellable_helpers_aio.py',
'frida_bindgen' / 'assets' / 'facade_device_members.py',
'frida_bindgen' / 'assets' / 'facade_device_members_aio.py',
'frida_bindgen' / 'assets' / 'facade_device_manager_members.py',
'frida_bindgen' / 'assets' / 'facade_device_manager_members_aio.py',
'frida_bindgen' / 'assets' / 'facade_bus.py',
'frida_bindgen' / 'assets' / 'facade_bus_aio.py',
'frida_bindgen' / 'assets' / 'facade_portal.py',
'frida_bindgen' / 'assets' / 'facade_portal_aio.py',
'frida_bindgen' / 'assets' / 'facade_rpc_helpers.py',
'frida_bindgen' / 'assets' / 'facade_rpc_helpers_aio.py',
'frida_bindgen' / 'assets' / 'facade_rpc_members.py',
'frida_bindgen' / 'assets' / 'facade_rpc_members_aio.py',
'frida_bindgen' / 'assets' / 'facade_iostream.py',
'frida_bindgen' / 'assets' / 'facade_iostream_aio.py',
'frida_bindgen' / 'assets' / 'facade_interface.py',
'frida_bindgen' / 'assets' / 'facade_interface_aio.py',
'frida_bindgen' / 'assets' / 'codegen_endpoint_parameters.c',
),
files(
meson.project_source_root() / 'frida-bindgen' / 'frida_bindgen_core' / '__init__.py',
meson.project_source_root() / 'frida-bindgen' / 'frida_bindgen_core' / 'loader.py',
meson.project_source_root() / 'frida-bindgen' / 'frida_bindgen_core' / 'model.py',
meson.project_source_root() / 'frida-bindgen' / 'frida_bindgen_core' / 'naming.py',
),
],
command: [
python, '-m', 'frida_bindgen',
'--frida-gir=@INPUT0@',
'--glib-gir=@INPUT1@',
'--gobject-gir=@INPUT2@',
'--gio-gir=@INPUT3@',
'--output-py=@OUTPUT0@',
'--output-pyi=@OUTPUT1@',
'--output-c=@OUTPUT2@',
'--output-aio=@OUTPUT3@',
],
env: env,
install: true,
install_dir: [
python.get_install_dir() / 'frida',
python.get_install_dir() / 'frida',
false,
python.get_install_dir() / 'frida',
]
)
py_sources = [
'__init__.py',
'core.py',
'py.typed',
]
python.install_sources(py_sources, subdir: 'frida', pure: false)
extra_link_args = []
if host_os_family == 'darwin'
extra_link_args += '-Wl,-exported_symbol,_PyInit__frida'
elif host_os_family != 'windows'
extra_link_args += '-Wl,--version-script,' + meson.current_source_dir() / 'extension.version'
endif
extension = python.extension_module('_frida', code[2],
limited_api: '3.7',
c_args: frida_component_cflags,
link_args: extra_link_args,
dependencies: [python_dep, frida_core_dep, os_deps],
install: true,
subdir: 'frida',
)
+6 -6
View File
@@ -32,9 +32,9 @@ endif
subdir('frida')
test('frida-python', python,
args: ['-m', 'unittest', 'discover'],
workdir: meson.current_source_dir(),
env: {'PYTHONPATH': meson.current_build_dir() / 'src'},
timeout: 30,
)
#test('frida-python', python,
# args: ['-m', 'unittest', 'discover'],
# workdir: meson.current_source_dir(),
# env: {'PYTHONPATH': meson.current_build_dir() / 'src'},
# timeout: 30,
#)
+14
View File
@@ -4,7 +4,21 @@ build-backend = "setuptools.build_meta"
[tool.black]
line-length = 120
extend-exclude = "frida/frida_bindgen/assets/"
[tool.isort]
profile = "black"
line_length = 120
extend_skip_glob = ["frida/frida_bindgen/assets/*"]
known_first_party = ["frida", "frida_bindgen_core"]
[tool.mypy]
exclude = ["frida/frida_bindgen/assets/"]
[[tool.mypy.overrides]]
module = "frida_bindgen_core.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = ["_frida", "frida._frida", "frida.aio", "frida.core"]
ignore_missing_imports = true
+20 -4
View File
@@ -13,6 +13,8 @@ from setuptools.extension import Extension
SOURCE_ROOT = Path(__file__).resolve().parent
FRIDA_EXTENSION = os.environ.get("FRIDA_EXTENSION", None)
GENERATED_PACKAGE_FILES = ["__init__.py", "aio.py", "_frida.pyi"]
def main():
setup(
@@ -50,12 +52,12 @@ def main():
"Topic :: Software Development :: Debuggers",
"Topic :: Software Development :: Libraries :: Python Modules",
],
packages=["frida", "frida._frida"],
package_data={"frida": ["py.typed"], "frida._frida": ["py.typed", "__init__.pyi"]},
packages=["frida"],
package_data={"frida": ["py.typed"]},
ext_modules=[
Extension(
name="frida._frida",
sources=["frida/_frida/extension.c"],
sources=[],
py_limited_api=True,
)
],
@@ -112,6 +114,7 @@ class FridaPrebuiltExt(build_ext):
target = self.get_ext_fullpath(ext.name)
Path(target).parent.mkdir(parents=True, exist_ok=True)
shutil.copy(FRIDA_EXTENSION, target)
copy_generated_package(self.build_lib)
class FridaDemandBuiltExt(build_ext):
@@ -119,11 +122,24 @@ class FridaDemandBuiltExt(build_ext):
make = SOURCE_ROOT / "make.bat" if platform.system() == "Windows" else "make"
subprocess.run([make], check=True)
outputs = [entry for entry in (SOURCE_ROOT / "build" / "frida" / "_frida").glob("_frida.*") if entry.is_file()]
outputs = [
entry
for entry in (SOURCE_ROOT / "build" / "frida").glob("_frida.*")
if entry.is_file() and entry.suffix != ".pyi"
]
assert len(outputs) == 1
target = self.get_ext_fullpath(ext.name)
Path(target).parent.mkdir(parents=True, exist_ok=True)
shutil.copy(outputs[0], target)
copy_generated_package(self.build_lib)
def copy_generated_package(build_lib):
generated = SOURCE_ROOT / "build" / "frida"
destination = Path(build_lib) / "frida"
destination.mkdir(parents=True, exist_ok=True)
for name in GENERATED_PACKAGE_FILES:
shutil.copy(generated / name, destination / name)
if __name__ == "__main__":
-4
View File
@@ -1,4 +0,0 @@
from .test_core import TestCore
from .test_rpc import TestRpc
__all__ = ["TestCore", "TestRpc"]
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# Quick feedback loop: regenerate bindings via run_bindgen.sh, then compile the
# generated extension.c with the exact flags ninja would use, without linking.
set -e
repo=$(cd "$(dirname "$0")/.." && pwd)
"$repo/tests/run_bindgen.sh"
python3 - "$repo" "$@" <<'EOF'
import json
import shlex
import subprocess
import sys
repo = sys.argv[1]
extra = sys.argv[2:]
cdb = json.load(open(f"{repo}/build/compile_commands.json"))
entry = next(e for e in cdb if e["file"].endswith("extension.c"))
args = shlex.split(entry["command"])
cmd = []
i = 0
while i < len(args):
a = args[i]
if a in ("-o", "-MF", "-MQ", "-c"):
i += 2
continue
if a == "-MD":
i += 1
continue
cmd.append(a)
i += 1
cmd += ["-fsyntax-only", f"{repo}/build/bindgen-out/extension.c"] + extra
sys.exit(subprocess.run(cmd, cwd=entry["directory"]).returncode)
EOF
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# Quick feedback loop: run the bindgen against the .gir files from the build
# tree, without going through ninja. Outputs land in tests/bindgen-out/.
set -e
repo=$(dirname "$0")/..
girdir=$repo/build/subprojects/frida-core/src/api
outdir=$repo/build/bindgen-out
mkdir -p "$outdir"
PYTHONPATH=$repo/frida:$repo/frida-bindgen exec python3 -m frida_bindgen \
--frida-gir="$girdir/Frida-1.0.gir" \
--glib-gir="$girdir/GLib-2.0.gir" \
--gobject-gir="$girdir/GObject-2.0.gir" \
--gio-gir="$girdir/Gio-2.0.gir" \
--output-py="$outdir/__init__.py" \
--output-aio="$outdir/aio.py" \
--output-pyi="$outdir/_frida.pyi" \
--output-c="$outdir/extension.c" \
"$@"
+934
View File
@@ -0,0 +1,934 @@
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()
-49
View File
@@ -1,49 +0,0 @@
import threading
import time
import unittest
import frida
class TestCore(unittest.TestCase):
def test_enumerate_devices(self):
devices = frida.get_device_manager().enumerate_devices()
self.assertTrue(len(devices) > 0)
def test_get_existing_device(self):
device = frida.get_device_matching(lambda d: d.id == "local")
self.assertEqual(device.name, "Local System")
device = frida.get_device_manager().get_device_matching(lambda d: d.id == "local")
self.assertEqual(device.name, "Local System")
def test_get_nonexistent_device(self):
def get_nonexistent():
frida.get_device_manager().get_device_matching(lambda device: device.type == "lol")
self.assertRaisesRegex(frida.InvalidArgumentError, "device not found", get_nonexistent)
def test_wait_for_nonexistent_device(self):
def wait_for_nonexistent():
frida.get_device_manager().get_device_matching(lambda device: device.type == "lol", timeout=0.1)
self.assertRaisesRegex(frida.InvalidArgumentError, "device not found", wait_for_nonexistent)
def test_cancel_wait_for_nonexistent_device(self):
cancellable = frida.Cancellable()
def wait_for_nonexistent():
frida.get_device_manager().get_device_matching(
lambda device: device.type == "lol", timeout=-1, cancellable=cancellable
)
def cancel_after_100ms():
time.sleep(0.1)
cancellable.cancel()
threading.Thread(target=cancel_after_100ms).start()
self.assertRaisesRegex(frida.OperationCancelledError, "operation was cancelled", wait_for_nonexistent)
if __name__ == "__main__":
unittest.main()
View File
Whitespace-only changes.
-171
View File
@@ -1,171 +0,0 @@
import subprocess
import threading
import time
import unittest
import frida
from .data import target_program
class TestRpc(unittest.TestCase):
target: subprocess.Popen
session: frida.core.Session
@classmethod
def setUp(cls):
cls.target = subprocess.Popen([target_program], stdin=subprocess.PIPE)
# TODO: improve injectors to handle injection into a process that hasn't yet finished initializing
time.sleep(0.05)
cls.session = frida.attach(cls.target.pid)
@classmethod
def tearDown(cls):
cls.session.detach()
cls.target.terminate()
cls.target.stdin.close()
cls.target.wait()
def test_basics(self):
script = self.session.create_script(
name="test-rpc",
source="""\
rpc.exports = {
add(a, b) {
const result = a + b;
if (result < 0)
throw new Error("No");
return result;
},
sub(a, b) {
return a - b;
},
speak() {
const buf = Memory.allocUtf8String("Yo");
return Memory.readByteArray(buf, 2);
},
speakWithMetadata() {
const buf = Memory.allocUtf8String("Yo");
return ['soft', Memory.readByteArray(buf, 2)];
},
processData(val, data) {
return { val, dump: hexdump(data, { header: false }) };
},
};
""",
)
script.load()
agent = script.exports_sync
self.assertEqual(agent.add(2, 3), 5)
self.assertEqual(agent.sub(5, 3), 2)
self.assertRaises(Exception, lambda: agent.add(1, -2))
self.assertEqual(agent.speak(), b"\x59\x6f")
meta, data = agent.speak_with_metadata()
self.assertEqual(meta, "soft")
self.assertEqual(data, b"\x59\x6f")
result = agent.process_data(1337, b"\x13\x37")
self.assertEqual(result["val"], 1337)
self.assertEqual(result["dump"], "00000000 13 37 .7")
def test_post_failure(self):
script = self.session.create_script(
name="test-rpc",
source="""\
rpc.exports = {
init: function () {
},
};
""",
)
script.load()
agent = script.exports_sync
self.session.detach()
self.assertRaisesScriptDestroyed(lambda: agent.init())
self.assertEqual(script._pending, {})
def test_unload_mid_request(self):
script = self.session.create_script(
name="test-rpc",
source="""\
rpc.exports = {
waitForever: function () {
return new Promise(function () {});
},
};
""",
)
script.load()
agent = script.exports_sync
def unload_script_after_100ms():
time.sleep(0.1)
script.unload()
threading.Thread(target=unload_script_after_100ms).start()
self.assertRaisesScriptDestroyed(lambda: agent.wait_forever())
self.assertEqual(script._pending, {})
def test_detach_mid_request(self):
script = self.session.create_script(
name="test-rpc",
source="""\
rpc.exports = {
waitForever: function () {
return new Promise(function () {});
},
};
""",
)
script.load()
agent = script.exports_sync
def terminate_target_after_100ms():
time.sleep(0.1)
self.target.terminate()
threading.Thread(target=terminate_target_after_100ms).start()
self.assertRaisesScriptDestroyed(lambda: agent.wait_forever())
self.assertEqual(script._pending, {})
def test_cancellation_mid_request(self):
script = self.session.create_script(
name="test-rpc",
source="""\
rpc.exports = {
waitForever: function () {
return new Promise(function () {});
},
};
""",
)
script.load()
agent = script.exports_sync
def cancel_after_100ms():
time.sleep(0.1)
cancellable.cancel()
cancellable = frida.Cancellable()
threading.Thread(target=cancel_after_100ms).start()
self.assertRaisesOperationCancelled(lambda: agent.wait_forever(cancellable=cancellable))
self.assertEqual(script._pending, {})
def call_wait_forever_with_cancellable():
with cancellable:
agent.wait_forever()
cancellable = frida.Cancellable()
threading.Thread(target=cancel_after_100ms).start()
self.assertRaisesOperationCancelled(call_wait_forever_with_cancellable)
self.assertEqual(script._pending, {})
def assertRaisesScriptDestroyed(self, operation):
self.assertRaisesRegex(frida.InvalidOperationError, "script has been destroyed", operation)
def assertRaisesOperationCancelled(self, operation):
self.assertRaisesRegex(frida.OperationCancelledError, "operation was cancelled", operation)
if __name__ == "__main__":
unittest.main()