diff --git a/bindings/gumjs/gumquickprocess.c b/bindings/gumjs/gumquickprocess.c index ed2c99687..b7cc88592 100644 --- a/bindings/gumjs/gumquickprocess.c +++ b/bindings/gumjs/gumquickprocess.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2022 Ole André Vadla Ravnås + * Copyright (C) 2020-2024 Ole André Vadla Ravnås * Copyright (C) 2020-2023 Francesco Tamagni * Copyright (C) 2023 Grant Douglas * @@ -40,6 +40,7 @@ #endif typedef struct _GumQuickMatchContext GumQuickMatchContext; +typedef struct _GumQuickRunOnThreadContext GumQuickRunOnThreadContext; typedef struct _GumQuickFindModuleByNameContext GumQuickFindModuleByNameContext; typedef struct _GumQuickFindRangeByAddressContext GumQuickFindRangeByAddressContext; @@ -60,6 +61,12 @@ struct _GumQuickMatchContext GumQuickProcess * parent; }; +struct _GumQuickRunOnThreadContext +{ + JSValue user_func; + GumQuickCore * core; +}; + struct _GumQuickFindModuleByNameContext { const gchar * name; @@ -89,6 +96,15 @@ GUMJS_DECLARE_FUNCTION (gumjs_process_get_current_thread_id) GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_threads) static gboolean gum_emit_thread (const GumThreadDetails * details, GumQuickMatchContext * mc); +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread) +static void gum_quick_run_on_thread_context_free ( + GumQuickRunOnThreadContext * rc); +static void gum_do_call_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static void gum_quick_process_maybe_start_stalker_gc_timer ( + GumQuickProcess * self, GumQuickScope * scope); +static gboolean gum_quick_process_on_stalker_gc_timer_tick ( + GumQuickProcess * self); GUMJS_DECLARE_FUNCTION (gumjs_process_find_module_by_name) static gboolean gum_store_module_if_name_matches ( const GumModuleDetails * details, GumQuickFindModuleByNameContext * fc); @@ -124,6 +140,7 @@ static const JSCFunctionListEntry gumjs_process_entries[] = JS_CFUNC_DEF ("isDebuggerAttached", 0, gumjs_process_is_debugger_attached), JS_CFUNC_DEF ("getCurrentThreadId", 0, gumjs_process_get_current_thread_id), JS_CFUNC_DEF ("_enumerateThreads", 0, gumjs_process_enumerate_threads), + JS_CFUNC_DEF ("_runOnThread", 0, gumjs_process_run_on_thread), JS_CFUNC_DEF ("findModuleByName", 0, gumjs_process_find_module_by_name), JS_CFUNC_DEF ("_enumerateModules", 0, gumjs_process_enumerate_modules), JS_CFUNC_DEF ("findRangeByAddress", 0, gumjs_process_find_range_by_address), @@ -146,8 +163,12 @@ _gum_quick_process_init (GumQuickProcess * self, self->module = module; self->core = core; + self->main_module_value = JS_UNINITIALIZED; + self->stalker = NULL; + self->stalker_gc_timer = NULL; + _gum_quick_core_store_module_data (core, "process", self); obj = JS_NewObject (ctx); @@ -174,6 +195,8 @@ _gum_quick_process_flush (GumQuickProcess * self) void _gum_quick_process_dispose (GumQuickProcess * self) { + g_assert (self->stalker_gc_timer == NULL); + g_clear_pointer (&self->exception_handler, gum_quick_exception_handler_free); gumjs_free_main_module_value (self); } @@ -316,6 +339,129 @@ gum_emit_thread (const GumThreadDetails * details, return _gum_quick_process_match_result (ctx, &result, &mc->result); } +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread) +{ + GumQuickProcess * self; + GumThreadId thread_id; + JSValue user_func; + GumQuickScope scope = GUM_QUICK_SCOPE_INIT (core); + GumQuickRunOnThreadContext * rc; + gboolean success; + + self = gumjs_get_parent_module (core); + + if (!_gum_quick_args_parse (args, "ZF", &thread_id, &user_func)) + return JS_EXCEPTION; + + if (self->stalker == NULL) + self->stalker = gum_stalker_new (); + + rc = g_slice_new (GumQuickRunOnThreadContext); + rc->user_func = JS_DupValue (core->ctx, user_func); + rc->core = core; + + _gum_quick_scope_suspend (&scope); + + success = gum_stalker_run_on_thread (self->stalker, thread_id, + gum_do_call_on_thread, rc, + (GDestroyNotify) gum_quick_run_on_thread_context_free); + + _gum_quick_scope_resume (&scope); + + gum_quick_process_maybe_start_stalker_gc_timer (self, &scope); + + if (!success) + goto run_failed; + + return JS_UNDEFINED; + +run_failed: + { + _gum_quick_throw_literal (ctx, "failed to run on thread"); + + return JS_EXCEPTION; + } +} + +static void +gum_quick_run_on_thread_context_free (GumQuickRunOnThreadContext * rc) +{ + GumQuickCore * core = rc->core; + GumQuickScope scope; + + _gum_quick_scope_enter (&scope, core); + JS_FreeValue (core->ctx, rc->user_func); + _gum_quick_scope_leave (&scope); + + g_slice_free (GumQuickRunOnThreadContext, rc); +} + +static void +gum_do_call_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + GumQuickRunOnThreadContext * rc = user_data; + GumQuickScope scope; + + _gum_quick_scope_enter (&scope, rc->core); + _gum_quick_scope_call (&scope, rc->user_func, JS_UNDEFINED, 0, NULL); + _gum_quick_scope_leave (&scope); +} + +static void +gum_quick_process_maybe_start_stalker_gc_timer (GumQuickProcess * self, + GumQuickScope * scope) +{ + GumQuickCore * core = self->core; + GSource * source; + + if (self->stalker_gc_timer != NULL) + return; + + if (!gum_stalker_garbage_collect (self->stalker)) + { + g_object_unref (self->stalker); + self->stalker = NULL; + return; + } + + source = g_timeout_source_new (10); + g_source_set_callback (source, + (GSourceFunc) gum_quick_process_on_stalker_gc_timer_tick, self, NULL); + self->stalker_gc_timer = source; + + _gum_quick_core_pin (core); + _gum_quick_scope_suspend (scope); + + g_source_attach (source, + gum_script_scheduler_get_js_context (core->scheduler)); + g_source_unref (source); + + _gum_quick_scope_resume (scope); +} + +static gboolean +gum_quick_process_on_stalker_gc_timer_tick (GumQuickProcess * self) +{ + gboolean pending_garbage; + + pending_garbage = gum_stalker_garbage_collect (self->stalker); + if (!pending_garbage) + { + GumQuickCore * core = self->core; + GumQuickScope scope; + + _gum_quick_scope_enter (&scope, core); + + _gum_quick_core_unpin (core); + self->stalker_gc_timer = NULL; + + _gum_quick_scope_leave (&scope); + } + + return pending_garbage ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE; +} + GUMJS_DEFINE_FUNCTION (gumjs_process_find_module_by_name) { GumQuickFindModuleByNameContext fc; diff --git a/bindings/gumjs/gumquickprocess.h b/bindings/gumjs/gumquickprocess.h index e8811e756..dbe7ddfee 100644 --- a/bindings/gumjs/gumquickprocess.h +++ b/bindings/gumjs/gumquickprocess.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Ole André Vadla Ravnås + * Copyright (C) 2020-2024 Ole André Vadla Ravnås * Copyright (C) 2023 Francesco Tamagni * * Licence: wxWindows Library Licence, Version 3.1 @@ -22,6 +22,10 @@ struct _GumQuickProcess GumQuickCore * core; JSValue main_module_value; + + GumStalker * stalker; + GSource * stalker_gc_timer; + GumQuickExceptionHandler * exception_handler; }; diff --git a/bindings/gumjs/gumv8process.cpp b/bindings/gumjs/gumv8process.cpp index 569427903..3729a4601 100644 --- a/bindings/gumjs/gumv8process.cpp +++ b/bindings/gumjs/gumv8process.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2022 Ole André Vadla Ravnås + * Copyright (C) 2010-2024 Ole André Vadla Ravnås * Copyright (C) 2020-2023 Francesco Tamagni * Copyright (C) 2023 Grant Douglas * @@ -54,6 +54,13 @@ struct GumV8ExceptionHandler GumV8Core * core; }; +struct GumV8RunOnThreadContext +{ + Global * user_func; + + GumV8Core * core; +}; + struct GumV8FindModuleByNameContext { gchar * name; @@ -73,6 +80,12 @@ GUMJS_DECLARE_FUNCTION (gumjs_process_get_current_thread_id) GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_threads) static gboolean gum_emit_thread (const GumThreadDetails * details, GumV8MatchContext * mc); +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread) +static void gum_v8_run_on_thread_context_free (GumV8RunOnThreadContext * rc); +static void gum_do_call_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static void gum_v8_process_maybe_start_stalker_gc_timer (GumV8Process * self); +static gboolean gum_v8_process_on_stalker_gc_timer_tick (GumV8Process * self); GUMJS_DECLARE_FUNCTION (gumjs_process_find_module_by_name) static gboolean gum_store_module_if_name_matches ( const GumModuleDetails * details, GumV8FindModuleByNameContext * fc); @@ -110,13 +123,13 @@ static const GumV8Function gumjs_process_functions[] = { "isDebuggerAttached", gumjs_process_is_debugger_attached }, { "getCurrentThreadId", gumjs_process_get_current_thread_id }, { "_enumerateThreads", gumjs_process_enumerate_threads }, + { "_runOnThread", gumjs_process_run_on_thread }, { "findModuleByName", gumjs_process_find_module_by_name }, { "_enumerateModules", gumjs_process_enumerate_modules }, { "_enumerateRanges", gumjs_process_enumerate_ranges }, { "enumerateSystemRanges", gumjs_process_enumerate_system_ranges }, { "_enumerateMallocRanges", gumjs_process_enumerate_malloc_ranges }, { "setExceptionHandler", gumjs_process_set_exception_handler }, - { NULL, NULL } }; @@ -131,6 +144,8 @@ _gum_v8_process_init (GumV8Process * self, self->module = module; self->core = core; + self->stalker = NULL; + auto process_module = External::New (isolate, self); auto process = _gum_v8_create_module ("Process", scope, isolate); @@ -169,6 +184,8 @@ _gum_v8_process_flush (GumV8Process * self) void _gum_v8_process_dispose (GumV8Process * self) { + g_assert (self->stalker_gc_timer == NULL); + g_clear_pointer (&self->exception_handler, gum_v8_exception_handler_free); delete self->main_module_value; @@ -268,6 +285,112 @@ gum_emit_thread (const GumThreadDetails * details, return proceed; } +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread) +{ + GumThreadId thread_id; + Local user_func; + if (!_gum_v8_args_parse (args, "ZF", &thread_id, &user_func)) + return; + + if (module->stalker == NULL) + module->stalker = gum_stalker_new (); + + auto rc = g_slice_new (GumV8RunOnThreadContext); + rc->user_func = new Global (isolate, user_func); + rc->core = core; + + gboolean success; + { + ScriptUnlocker unlocker (core); + + success = gum_stalker_run_on_thread (module->stalker, thread_id, + gum_do_call_on_thread, rc, + (GDestroyNotify) gum_v8_run_on_thread_context_free); + } + + gum_v8_process_maybe_start_stalker_gc_timer (module); + + if (!success) + _gum_v8_throw_ascii_literal (isolate, "failed to run on thread"); + + return; +} + +static void +gum_v8_run_on_thread_context_free (GumV8RunOnThreadContext * rc) +{ + ScriptScope scope (rc->core->script); + delete rc->user_func; + + g_slice_free (GumV8RunOnThreadContext, rc); +} + +static void +gum_do_call_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + auto rc = (GumV8RunOnThreadContext *) user_data; + auto core = rc->core; + + ScriptScope scope (core->script); + auto isolate = core->isolate; + + auto user_func = Local::New (isolate, *rc->user_func); + auto result = user_func->Call (isolate->GetCurrentContext (), + Undefined (isolate), 0, nullptr); + _gum_v8_ignore_result (result); +} + +static void +gum_v8_process_maybe_start_stalker_gc_timer (GumV8Process * self) +{ + GumV8Core * core = self->core; + + if (self->stalker_gc_timer != NULL) + return; + + if (!gum_stalker_garbage_collect (self->stalker)) + { + g_object_unref (self->stalker); + self->stalker = NULL; + return; + } + + auto source = g_timeout_source_new (10); + g_source_set_callback (source, + (GSourceFunc) gum_v8_process_on_stalker_gc_timer_tick, self, NULL); + self->stalker_gc_timer = source; + + _gum_v8_core_pin (core); + + { + ScriptUnlocker unlocker (core); + + g_source_attach (source, + gum_script_scheduler_get_js_context (core->scheduler)); + g_source_unref (source); + } +} + +static gboolean +gum_v8_process_on_stalker_gc_timer_tick (GumV8Process * self) +{ + gboolean pending_garbage; + + pending_garbage = gum_stalker_garbage_collect (self->stalker); + if (!pending_garbage) + { + GumV8Core * core = self->core; + + ScriptScope scope (core->script); + + _gum_v8_core_unpin (core); + self->stalker_gc_timer = NULL; + } + + return pending_garbage ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE; +} + GUMJS_DEFINE_FUNCTION (gumjs_process_find_module_by_name) { GumV8FindModuleByNameContext fc; diff --git a/bindings/gumjs/gumv8process.h b/bindings/gumjs/gumv8process.h index 03547e96d..3182d8e26 100644 --- a/bindings/gumjs/gumv8process.h +++ b/bindings/gumjs/gumv8process.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2020 Ole André Vadla Ravnås + * Copyright (C) 2010-2024 Ole André Vadla Ravnås * Copyright (C) 2023 Francesco Tamagni * * Licence: wxWindows Library Licence, Version 3.1 @@ -19,6 +19,10 @@ struct GumV8Process GumV8Core * core; v8::Global * main_module_value; + + GumStalker * stalker; + GSource * stalker_gc_timer; + GumV8ExceptionHandler * exception_handler; }; diff --git a/bindings/gumjs/runtime/core.js b/bindings/gumjs/runtime/core.js index caeb3fbc7..b17fdc166 100644 --- a/bindings/gumjs/runtime/core.js +++ b/bindings/gumjs/runtime/core.js @@ -403,6 +403,20 @@ makeEnumerateRanges(Process); makeEnumerateApi(Process, 'enumerateMallocRanges', 0); Object.defineProperties(Process, { + runOnThread: { + enumerable: true, + value: function (threadId, callback) { + return new Promise((resolve, reject) => { + Process._runOnThread(threadId, () => { + try { + resolve(callback()); + } catch (e) { + reject(e); + } + }); + }); + }, + }, findModuleByAddress: { enumerable: true, value: function (address) { diff --git a/tests/gumjs/script.c b/tests/gumjs/script.c index 1aa0b69cf..c74bd77bf 100644 --- a/tests/gumjs/script.c +++ b/tests/gumjs/script.c @@ -213,6 +213,11 @@ TESTLIST_BEGIN (script) #endif TESTGROUP_END () + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (process_can_run_on_thread_with_success) + TESTENTRY (process_can_run_on_thread_with_failure) + TESTGROUP_END () + TESTGROUP_BEGIN ("Module") TESTENTRY (module_imports_can_be_enumerated) TESTENTRY (module_imports_can_be_enumerated_legacy_style) @@ -500,6 +505,7 @@ TESTLIST_END () typedef int (* TargetFunctionInt) (int arg); typedef struct _GumInvokeTargetContext GumInvokeTargetContext; typedef struct _GumNamedSleeperContext GumNamedSleeperContext; +typedef struct _TestRunOnThreadSyncContext TestRunOnThreadSyncContext; typedef struct _GumCrashExceptorContext GumCrashExceptorContext; typedef struct _TestTrigger TestTrigger; @@ -517,6 +523,15 @@ struct _GumNamedSleeperContext GAsyncQueue * sleeper_messages; }; +struct _TestRunOnThreadSyncContext +{ + GMutex mutex; + GCond cond; + gboolean started; + GumThreadId thread_id; + gboolean * done; +}; + struct _GumCrashExceptorContext { gboolean called; @@ -570,6 +585,10 @@ static gpointer run_stalked_through_target_function (gpointer data); static gpointer sleeping_dummy (gpointer data); G_GNUC_UNUSED static gpointer named_sleeper (gpointer data); +static GThread * create_sleeping_dummy_thread_sync (gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy_func (gpointer data); +static const gchar * get_local_thread_string_value (void); static gpointer invoke_target_function_int_worker (gpointer data); static gpointer invoke_target_function_trigger (gpointer data); @@ -622,6 +641,7 @@ static int target_function_nested_b (int arg); static int target_function_nested_c (int arg); static TargetFunctionInt target_function_original = NULL; +static GPrivate target_thread_string_value = G_PRIVATE_INIT (g_free); gint gum_script_dummy_global_to_trick_optimizer = 0; @@ -5380,6 +5400,113 @@ TESTCASE (process_malloc_ranges_can_be_enumerated_legacy_style) #endif +TESTCASE (process_can_run_on_thread_with_success) +{ + GThread * thread; + GumThreadId thread_id; + gboolean done = FALSE; + + thread = create_sleeping_dummy_thread_sync (&done, &thread_id); + + COMPILE_AND_LOAD_SCRIPT ( + "const getLocalThreadStringValue = new NativeFunction(" GUM_PTR_CONST ", " + "'pointer', []);" + "Process.runOnThread(0x%" G_GSIZE_MODIFIER "x, () => {" + " return getLocalThreadStringValue().readUtf8String();" + "})" + ".then(str => { send(str); });", + get_local_thread_string_value, + thread_id); + + EXPECT_SEND_MESSAGE_WITH ("\"53Cr3t\""); + + done = TRUE; + g_thread_join (thread); + + EXPECT_NO_MESSAGES (); +} + +TESTCASE (process_can_run_on_thread_with_failure) +{ + GThread * thread; + GumThreadId thread_id; + gboolean done = FALSE; + + thread = create_sleeping_dummy_thread_sync (&done, &thread_id); + + COMPILE_AND_LOAD_SCRIPT ( + "Process.runOnThread(0x%" G_GSIZE_MODIFIER "x, () => {" + " throw new Error('epic fail');" + "})" + ".catch(e => { send(e.message); });", + thread_id); + + EXPECT_SEND_MESSAGE_WITH ("\"epic fail\""); + + done = TRUE; + g_thread_join (thread); + + EXPECT_NO_MESSAGES (); +} + +static GThread * +create_sleeping_dummy_thread_sync (gboolean * done, + GumThreadId * thread_id) +{ + TestRunOnThreadSyncContext sync_data; + GThread * thread; + + g_mutex_init (&sync_data.mutex); + g_cond_init (&sync_data.cond); + sync_data.started = FALSE; + sync_data.thread_id = 0; + sync_data.done = done; + + g_mutex_lock (&sync_data.mutex); + + thread = g_thread_new ("gumjs-test-sleeping-dummy-func", sleeping_dummy_func, + &sync_data); + + while (!sync_data.started) + g_cond_wait (&sync_data.cond, &sync_data.mutex); + + if (thread_id != NULL) + *thread_id = sync_data.thread_id; + + g_mutex_unlock (&sync_data.mutex); + + g_cond_clear (&sync_data.cond); + g_mutex_clear (&sync_data.mutex); + + return thread; +} + +static gpointer +sleeping_dummy_func (gpointer data) +{ + TestRunOnThreadSyncContext * sync_data = data; + gboolean * done = sync_data->done; + + g_private_replace (&target_thread_string_value, g_strdup ("53Cr3t")); + + g_mutex_lock (&sync_data->mutex); + sync_data->started = TRUE; + sync_data->thread_id = gum_process_get_current_thread_id (); + g_cond_signal (&sync_data->cond); + g_mutex_unlock (&sync_data->mutex); + + while (!(*done)) + g_thread_yield (); + + return NULL; +} + +static const gchar * +get_local_thread_string_value (void) +{ + return g_private_get (&target_thread_string_value); +} + TESTCASE (process_system_ranges_can_be_enumerated) { COMPILE_AND_LOAD_SCRIPT (