Compare commits

...
2 Commits
Author SHA1 Message Date
Ole André Vadla Ravnås a6b853d490 Add PHARO as a script runtime
ScriptInstance becomes the abstract peer of JSScriptInstance and
PharoScriptInstance, so Pharo scripts ride the same engine without
going through the GumJS interfaces.
2026-08-14 20:01:56 +02:00
Ole André Vadla Ravnås c3ba7814d3 Link the Pharo bindings when Gum has them
Gum answers frida-gumpharo-1.0 only where the bindings were built,
so ask for it and go without when it is absent.
2026-08-14 18:54:27 +02:00
5 changed files with 171 additions and 20 deletions

No files matched your search

+1 -1
View File
@@ -63,7 +63,7 @@ if build_agent
link_depends: extra_link_depends,
vs_module_defs: 'frida-agent.def',
override_options: ['b_asneeded=true'],
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
)
operations = ['--move', 'constructor', 'frida_libc_shim_init', 'first']
+5 -1
View File
@@ -2274,7 +2274,11 @@ namespace Frida {
/**
* The V8 runtime.
*/
V8;
V8,
/**
* The Pharo runtime.
*/
PHARO;
public static ScriptRuntime from_nick (string nick) throws Error {
return Marshal.enum_from_nick<ScriptRuntime> (nick);
+1 -1
View File
@@ -49,7 +49,7 @@ payload = static_library('frida-payload-' + api_version, payload_sources,
],
vala_header: 'frida-payload.h',
vala_vapi: f'frida-payload-@[email protected]',
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, base_dep],
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep],
install: true,
install_dir: [true, header_install_dir, true],
)
+155 -17
View File
@@ -91,7 +91,27 @@ namespace Frida {
if (name == null)
name = "script%u".printf (script_id.handle);
Gum.ScriptBackend backend = pick_backend (options.runtime);
ScriptRuntime runtime = options.runtime;
if (runtime == DEFAULT)
runtime = preferred_runtime;
#if HAVE_GUMPHARO
if (runtime == PHARO) {
if (source == null)
throw new Error.NOT_SUPPORTED ("The Pharo runtime cannot load precompiled scripts");
var instance = new PharoScriptInstance (script_id, new Gum.PharoScript (name, source));
instances[script_id] = instance;
instance.closed.connect (on_instance_closed);
instance.message.connect (on_instance_message);
instance.debug_message.connect (on_instance_debug_message);
return instance;
}
#endif
Gum.ScriptBackend backend = pick_backend (runtime);
Gum.Script script;
try {
@@ -109,7 +129,8 @@ namespace Frida {
return Source.REMOVE;
});
var instance = new ScriptInstance (script_id, script);
var js_instance = new JSScriptInstance (script_id, script);
ScriptInstance instance = js_instance;
instances[script_id] = instance;
instance.closed.connect (on_instance_closed);
@@ -175,7 +196,9 @@ namespace Frida {
}
public Gum.Script eternalize_script (AgentScriptId script_id) throws Error {
var instance = get_instance (script_id);
var instance = get_instance (script_id) as JSScriptInstance;
if (instance == null)
throw new Error.NOT_SUPPORTED ("Only JavaScript scripts may be eternalized");
var script = instance.eternalize ();
@@ -231,7 +254,7 @@ namespace Frida {
source.attach (Gum.ScriptBackend.get_scheduler ().get_js_context ());
}
public sealed class ScriptInstance : Object, RpcPeer {
public abstract class ScriptInstance : Object, RpcPeer {
public signal void closed ();
public signal void message (string json, Bytes? data);
public signal void debug_message (string message);
@@ -241,6 +264,120 @@ namespace Frida {
construct;
}
public abstract async void close ();
public abstract async void flush ();
public abstract async void load () throws Error;
public abstract void post (string json, Bytes? data) throws Error;
public abstract async void prepare_for_termination (TerminationReason reason);
public abstract void unprepare_for_termination ();
public abstract async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError;
public virtual void interrupt () throws Error {
throw new Error.NOT_SUPPORTED ("Interrupt is not available for this runtime");
}
public virtual async void terminate () {
yield close ();
}
public virtual void enable_debugger () throws Error {
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
}
public virtual void disable_debugger () {
}
public virtual void post_debug_message (string message) throws Error {
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
}
}
#if HAVE_GUMPHARO
public sealed class PharoScriptInstance : ScriptInstance {
private Gum.PharoScript? script;
private State state = CREATED;
private enum State {
CREATED,
LOADED,
UNLOADED,
DESTROYED
}
private RpcClient rpc_client;
public PharoScriptInstance (AgentScriptId script_id, Gum.PharoScript script) {
Object (script_id: script_id);
this.script = script;
script.set_message_handler (on_message);
}
construct {
rpc_client = new RpcClient (this);
}
public override async void close () {
if (state == DESTROYED)
return;
if (state == LOADED) {
script.unload ();
state = UNLOADED;
}
script = null;
state = DESTROYED;
closed ();
}
public override async void flush () {
yield close ();
}
public override async void load () throws Error {
if (state != CREATED)
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
script.load ();
state = LOADED;
}
public override void post (string json, Bytes? data) throws Error {
if (state != LOADED)
throw new Error.INVALID_OPERATION ("Only active scripts may be posted to");
script.post (json, data);
}
public override async void prepare_for_termination (TerminationReason reason) {
}
public override void unprepare_for_termination () {
}
private void on_message (Gum.PharoScript script, string json, Bytes? data) {
bool handled = rpc_client.try_handle_message (json);
if (!handled)
this.message (json, data);
}
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError {
if (script == null)
throw new Error.INVALID_OPERATION ("Script is destroyed");
script.post (json, data);
}
}
#endif
public sealed class JSScriptInstance : ScriptInstance {
public Gum.Script? script {
get {
return _script;
@@ -274,7 +411,7 @@ namespace Frida {
private RpcClient rpc_client;
public ScriptInstance (AgentScriptId script_id, Gum.Script script) {
public JSScriptInstance (AgentScriptId script_id, Gum.Script script) {
Object (script_id: script_id, script: script);
}
@@ -282,7 +419,7 @@ namespace Frida {
rpc_client = new RpcClient (this);
}
public async void close () {
public override async void close () {
if (close_request != null) {
try {
yield close_request.future.wait_async (null);
@@ -338,7 +475,7 @@ namespace Frida {
close_request.resolve (true);
}
public async void flush () {
public override async void flush () {
if (close_request == null)
close.begin ();
@@ -349,7 +486,7 @@ namespace Frida {
}
}
public async void load () throws Error {
public override async void load () throws Error {
if (state != CREATED)
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
@@ -362,11 +499,11 @@ namespace Frida {
load_request.resolve (true);
}
public void interrupt () {
public override void interrupt () throws Error {
script.interrupt ();
}
public async void terminate () {
public override async void terminate () {
script.interrupt ();
yield close ();
@@ -397,7 +534,7 @@ namespace Frida {
return result;
}
public async void prepare_for_termination (TerminationReason reason) {
public override async void prepare_for_termination (TerminationReason reason) {
if (state == LOADED) {
schedule_on_js_thread (() => {
script.get_stalker ().flush ();
@@ -408,7 +545,7 @@ namespace Frida {
yield ensure_dispose_called (reason);
}
public void unprepare_for_termination () {
public override void unprepare_for_termination () {
if (state == DISPOSED) {
state = LOADED;
dispose_request = null;
@@ -448,7 +585,7 @@ namespace Frida {
dispose_request.resolve (true);
}
public void post (string json, Bytes? data) throws Error {
public override void post (string json, Bytes? data) throws Error {
switch (state) {
case LOADING:
case LOADED:
@@ -460,17 +597,17 @@ namespace Frida {
}
}
public void enable_debugger () throws Error {
public override void enable_debugger () throws Error {
if (_script != null)
_script.set_debug_message_handler (on_debug_message);
}
public void disable_debugger () {
public override void disable_debugger () {
if (_script != null)
_script.set_debug_message_handler (null);
}
public void post_debug_message (string message) {
public override void post_debug_message (string message) throws Error {
if (_script != null)
_script.post_debug_message (message);
}
@@ -485,7 +622,8 @@ namespace Frida {
this.debug_message (message);
}
private async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable) throws Error, IOError {
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError {
if (script == null)
throw new Error.INVALID_OPERATION ("Script is destroyed");
script.post (json, data);
+9
View File
@@ -382,6 +382,12 @@ if have_in_tree_payloads
endif
gumjs_inspector_dep = dependency('frida-gumjs-inspector-1.0', default_options: ['default_library=static'])
gumpharo_dep = dependency('frida-gumpharo-' + api_version, required: false)
have_gumpharo = gumpharo_dep.found()
if have_gumpharo
vala_flags += '--define=HAVE_GUMPHARO'
endif
gee_options = [
'default_library=static',
'disable-introspection=true',
@@ -669,6 +675,9 @@ gumjs_inspector_vala_args = ['--pkg=frida-gumjs-inspector-1.0', gum_vala_args]
if have_in_tree_payloads
gumjs_vala_args = ['--pkg=frida-gumjs-1.0', '--pkg=gio-2.0', gum_vala_args]
if have_gumpharo
gumjs_vala_args += ['--pkg=frida-gumpharo-' + api_version]
endif
have_v8 = gumjs_dep.get_variable('gumjs_v8') == 'enabled'
if have_v8