From 7fe6a0450b1b4d846b5483d03c22dff56904b291 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 27 Jul 2023 17:35:12 +0100 Subject: [PATCH] Changes to add ability to run a function on a given thread --- bindings/gumjs/gumquickprocess.c | 117 +++++++++++++++++ bindings/gumjs/gumv8process.cpp | 110 ++++++++++++++++ gum/backend-arm/gumstalker-arm.c | 100 +++++++++++++++ gum/backend-arm64/gumstalker-arm64.c | 106 ++++++++++++++++ gum/backend-mips/gumstalker-mips.c | 15 +++ gum/backend-x86/gumstalker-x86.c | 116 +++++++++++++++++ gum/gumstalker.c | 74 +++++++++++ gum/gumstalker.h | 10 ++ tests/core/arch-arm/stalker-arm.c | 173 ++++++++++++++++++++++++++ tests/core/arch-arm64/stalker-arm64.c | 172 +++++++++++++++++++++++++ tests/core/arch-x86/stalker-x86.c | 173 ++++++++++++++++++++++++++ tests/gumjs/script.c | 137 ++++++++++++++++++++ 12 files changed, 1303 insertions(+) diff --git a/bindings/gumjs/gumquickprocess.c b/bindings/gumjs/gumquickprocess.c index 5db951042a..e0b6599818 100644 --- a/bindings/gumjs/gumquickprocess.c +++ b/bindings/gumjs/gumquickprocess.c @@ -42,6 +42,7 @@ typedef struct _GumQuickMatchContext GumQuickMatchContext; typedef struct _GumQuickFindModuleByNameContext GumQuickFindModuleByNameContext; typedef struct _GumQuickFindRangeByAddressContext GumQuickFindRangeByAddressContext; +typedef struct _GumQuickRunOnThreadContext GumQuickRunOnThreadContext; struct _GumQuickExceptionHandler { @@ -78,6 +79,14 @@ struct _GumQuickFindRangeByAddressContext GumQuickCore * core; }; +struct _GumQuickRunOnThreadContext +{ + GumQuickCore * core; + GumQuickScope scope; + JSValue user_func; + gboolean sync; +}; + static void gumjs_free_main_module_value (GumQuickProcess * self); GUMJS_DECLARE_GETTER (gumjs_process_get_main_module) GUMJS_DECLARE_FUNCTION (gumjs_process_get_current_dir) @@ -103,6 +112,8 @@ static gboolean gum_emit_range (const GumRangeDetails * details, GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_system_ranges) GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_malloc_ranges) GUMJS_DECLARE_FUNCTION (gumjs_process_set_exception_handler) +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread_sync) +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread_async) static GumQuickExceptionHandler * gum_quick_exception_handler_new ( JSValue callback, GumQuickCore * core); @@ -110,6 +121,8 @@ static void gum_quick_exception_handler_free ( GumQuickExceptionHandler * handler); static gboolean gum_quick_exception_handler_on_exception ( GumExceptionDetails * details, GumQuickExceptionHandler * handler); +static void gum_js_process_run_cb (const GumCpuContext * cpu_context, + gpointer user_data); static const JSCFunctionListEntry gumjs_process_entries[] = { @@ -132,6 +145,8 @@ static const JSCFunctionListEntry gumjs_process_entries[] = JS_CFUNC_DEF ("_enumerateMallocRanges", 0, gumjs_process_enumerate_malloc_ranges), JS_CFUNC_DEF ("setExceptionHandler", 0, gumjs_process_set_exception_handler), + JS_CFUNC_DEF ("runOnThreadSync", 0, gumjs_process_run_on_thread_sync), + JS_CFUNC_DEF ("runOnThreadAsync", 0, gumjs_process_run_on_thread_async), }; void @@ -622,3 +637,105 @@ gum_quick_exception_handler_on_exception (GumExceptionDetails * details, return handled; } + +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread_sync) +{ + GumQuickScope scope = GUM_QUICK_SCOPE_INIT (core); + GumThreadId thread_id; + JSValue user_func; + GumQuickRunOnThreadContext sync_ctx; + GumStalker * stalker; + gboolean success; + + if (!_gum_quick_args_parse (args, "ZF", &thread_id, &user_func)) + return JS_EXCEPTION; + + if (thread_id == 0) + return JS_UNDEFINED; + + _gum_quick_scope_suspend (&scope); + + sync_ctx.core = core; + sync_ctx.scope = scope; + sync_ctx.user_func = user_func; + sync_ctx.sync = TRUE; + + stalker = gum_stalker_new (); + + success = gum_stalker_run_on_thread_sync (stalker, thread_id, + gum_js_process_run_cb, &sync_ctx); + _gum_quick_scope_resume (&scope); + + while (gum_stalker_garbage_collect (stalker)) + g_usleep (10000); + + g_object_unref (stalker); + + if (success) + { + return JS_UNDEFINED; + } + else + { + _gum_quick_throw_literal (ctx, "Failed to run on thread"); + return JS_EXCEPTION; + } +} + +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread_async) +{ + GumQuickScope scope = GUM_QUICK_SCOPE_INIT (core); + GumThreadId thread_id; + JSValue user_func; + GumQuickRunOnThreadContext sync_ctx; + GumStalker * stalker; + gboolean success; + + if (!_gum_quick_args_parse (args, "ZF", &thread_id, &user_func)) + return JS_EXCEPTION; + + if (thread_id == 0) + return JS_UNDEFINED; + + _gum_quick_scope_suspend (&scope); + + sync_ctx.core = core; + sync_ctx.scope = scope; + sync_ctx.user_func = JS_DupValue (core->ctx, user_func); + sync_ctx.sync = FALSE; + + stalker = gum_stalker_new (); + + success = gum_stalker_run_on_thread_async (stalker, thread_id, + gum_js_process_run_cb, &sync_ctx); + _gum_quick_scope_resume (&scope); + + while (gum_stalker_garbage_collect (stalker)) + g_usleep (10000); + + g_object_unref (stalker); + + if (success) + { + return JS_UNDEFINED; + } + else + { + _gum_quick_throw_literal (ctx, "Failed to run on thread"); + return JS_EXCEPTION; + } +} + +static void +gum_js_process_run_cb (const GumCpuContext * cpu_context, + gpointer user_data) +{ + GumQuickRunOnThreadContext * sync_ctx = + (GumQuickRunOnThreadContext *) user_data; + + _gum_quick_scope_call (&sync_ctx->scope, sync_ctx->user_func, JS_UNDEFINED, 0, + NULL); + + if (!sync_ctx->sync) + JS_FreeValue (sync_ctx->core->ctx, sync_ctx->user_func); +} diff --git a/bindings/gumjs/gumv8process.cpp b/bindings/gumjs/gumv8process.cpp index 19eaf57171..dcaba4f830 100644 --- a/bindings/gumjs/gumv8process.cpp +++ b/bindings/gumjs/gumv8process.cpp @@ -63,6 +63,15 @@ struct GumV8FindModuleByNameContext GumV8Process * parent; }; +struct GumV8RunOnThreadContext +{ + GumV8Core * core; + Isolate * isolate; + Local context; + Local user_func; + MaybeLocal ret; +}; + GUMJS_DECLARE_GETTER (gumjs_process_get_main_module) GUMJS_DECLARE_FUNCTION (gumjs_process_get_current_dir) GUMJS_DECLARE_FUNCTION (gumjs_process_get_home_dir) @@ -84,6 +93,8 @@ static gboolean gum_emit_range (const GumRangeDetails * details, GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_system_ranges) GUMJS_DECLARE_FUNCTION (gumjs_process_enumerate_malloc_ranges) GUMJS_DECLARE_FUNCTION (gumjs_process_set_exception_handler) +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread_sync) +GUMJS_DECLARE_FUNCTION (gumjs_process_run_on_thread_async) static GumV8ExceptionHandler * gum_v8_exception_handler_new ( Local callback, GumV8Core * core); @@ -91,6 +102,8 @@ static void gum_v8_exception_handler_free ( GumV8ExceptionHandler * handler); static gboolean gum_v8_exception_handler_on_exception ( GumExceptionDetails * details, GumV8ExceptionHandler * handler); +static void gum_js_process_run_cb (const GumCpuContext * cpu_context, + gpointer user_data); const gchar * gum_v8_script_exception_type_to_string (GumExceptionType type); @@ -115,6 +128,8 @@ static const GumV8Function gumjs_process_functions[] = { "enumerateSystemRanges", gumjs_process_enumerate_system_ranges }, { "_enumerateMallocRanges", gumjs_process_enumerate_malloc_ranges }, { "setExceptionHandler", gumjs_process_set_exception_handler }, + { "runOnThreadSync", gumjs_process_run_on_thread_sync }, + { "runOnThreadAsync", gumjs_process_run_on_thread_async }, { NULL, NULL } }; @@ -513,3 +528,98 @@ gum_v8_exception_handler_on_exception (GumExceptionDetails * details, return handled; } + + +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread_sync) +{ + GumThreadId thread_id; + Local user_func; + GumV8RunOnThreadContext sync_ctx; + GumStalker * stalker; + gboolean success; + + auto isolate = core->isolate; + auto context = isolate->GetCurrentContext (); + + if (!_gum_v8_args_parse (args, "ZF", &thread_id, &user_func)) + return; + + if (thread_id == 0) + return; + + stalker = gum_stalker_new (); + + { + ScriptUnlocker unlocker (core); + sync_ctx.core = core; + sync_ctx.isolate = isolate; + sync_ctx.context = context; + sync_ctx.user_func = user_func; + + success = gum_stalker_run_on_thread_sync (stalker, thread_id, + gum_js_process_run_cb, &sync_ctx); + } + + while (gum_stalker_garbage_collect (stalker)) + g_usleep (10000); + + g_object_unref (stalker); + + if (success) + info.GetReturnValue ().Set (sync_ctx.ret.ToLocalChecked ()); + else + _gum_v8_throw_ascii_literal (isolate, "Failed to run on thread"); +} + +GUMJS_DEFINE_FUNCTION (gumjs_process_run_on_thread_async) +{ + GumThreadId thread_id; + Local user_func; + GumV8RunOnThreadContext sync_ctx; + GumStalker * stalker; + gboolean success; + + auto isolate = core->isolate; + auto context = isolate->GetCurrentContext (); + + if (!_gum_v8_args_parse (args, "ZF", &thread_id, &user_func)) + return; + + if (thread_id == 0) + return; + + stalker = gum_stalker_new (); + + { + ScriptUnlocker unlocker (core); + sync_ctx.core = core; + sync_ctx.isolate = isolate; + sync_ctx.context = context; + sync_ctx.user_func = Local::New (isolate, user_func); + + success = gum_stalker_run_on_thread_async (stalker, thread_id, + gum_js_process_run_cb, &sync_ctx); + } + + while (gum_stalker_garbage_collect (stalker)) + g_usleep (10000); + + g_object_unref (stalker); + + if (!success) + _gum_v8_throw_ascii_literal (isolate, "Failed to run on thread"); +} + +static void +gum_js_process_run_cb (const GumCpuContext * cpu_context, + gpointer user_data) +{ + GumV8RunOnThreadContext * sync_ctx = (GumV8RunOnThreadContext *) user_data; + + ScriptScope scope (sync_ctx->core->script); + auto isolate = sync_ctx->isolate; + auto context = sync_ctx->context; + auto recv = Undefined (isolate); + + sync_ctx->ret = sync_ctx->user_func->Call (context, recv, 0, nullptr); +} diff --git a/gum/backend-arm/gumstalker-arm.c b/gum/backend-arm/gumstalker-arm.c index c3981b239f..3082698581 100644 --- a/gum/backend-arm/gumstalker-arm.c +++ b/gum/backend-arm/gumstalker-arm.c @@ -82,6 +82,8 @@ typedef guint GumBackpatchType; typedef gboolean (* GumCheckExcludedFunc) (GumExecCtx * ctx, gconstpointer address); +typedef struct _GumStalkerRunOnThreadCtx GumStalkerRunOnThreadCtx; + struct _GumStalker { GObject parent; @@ -414,6 +416,13 @@ struct _GumBackpatch GumPrologState opened_prolog; }; +struct _GumStalkerRunOnThreadCtx +{ + GumStalker * stalker; + GumStalkerRunOnThreadFunc func; + gpointer user_data; +}; + static void gum_stalker_finalize (GObject * object); G_GNUC_INTERNAL gpointer _gum_stalker_do_follow_me (GumStalker * self, @@ -705,6 +714,9 @@ static gboolean gum_is_exclusive_store_insn (const cs_insn * insn); static guint gum_count_bits_set (guint16 value); static guint gum_count_trailing_zeros (guint16 value); +static void gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, gpointer user_data); + G_DEFINE_TYPE (GumStalker, gum_stalker, G_TYPE_OBJECT) static GPrivate gum_stalker_exec_ctx_private; @@ -6200,3 +6212,91 @@ gum_count_trailing_zeros (guint16 value) } #endif + + +gboolean +gum_stalker_is_run_on_thread_supported (void) +{ + return TRUE; +} + +gboolean +gum_stalker_run_on_thread_async (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx ctx; + ctx.stalker = self; + ctx.func = func; + ctx.user_data = user_data; + return gum_process_modify_thread (thread_id, + gum_stalker_do_run_on_thread_async, &ctx, GUM_MODIFY_THREAD_FLAGS_NONE); +} + +static void +gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx * run_ctx = (GumStalkerRunOnThreadCtx *) user_data; + GumStalker * self = run_ctx->stalker; + GumExecCtx * ctx; + guint32 pc; + GumArmWriter * cw; + GumAddress cpu_ctx; + guint32 infect_body; + + if (gum_process_get_current_thread_id () == thread_id) + { + run_ctx->func (cpu_context, run_ctx->user_data); + } + else + { + ctx = gum_stalker_create_exec_ctx (self, thread_id, NULL, NULL); + if ((cpu_context->cpsr & GUM_PSR_T_BIT) == 0) + pc = cpu_context->pc; + else + pc = cpu_context->pc + 1; + + gum_spinlock_acquire (&ctx->code_lock); + + gum_stalker_thaw (self, ctx->thunks, self->thunks_size); + cw = &ctx->arm_writer; + gum_arm_writer_reset (cw, ctx->infect_thunk); + + /* Copy the cpu context for the target thread. */ + cpu_ctx = GUM_ADDRESS (gum_arm_writer_cur (cw)); + gum_arm_writer_put_bytes (cw, (gpointer) cpu_context, + sizeof (GumCpuContext)); + + infect_body = GPOINTER_TO_SIZE (gum_arm_writer_cur (cw)); + + gum_exec_ctx_write_arm_prolog (ctx, cw); + + gum_arm_writer_put_call_address_with_arguments (cw, + GUM_ADDRESS (run_ctx->func), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (cpu_ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (run_ctx->user_data)); + + gum_arm_writer_put_call_address_with_arguments (cw, + GUM_ADDRESS (gum_exec_ctx_unfollow), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (pc)); + + gum_exec_ctx_write_arm_epilog (ctx, cw); + + gum_arm_writer_put_push_regs (cw, 2, ARM_REG_R0, ARM_REG_PC); + gum_arm_writer_put_ldr_reg_address (cw, ARM_REG_R0, GUM_ADDRESS (pc)); + gum_arm_writer_put_str_reg_reg_offset (cw, ARM_REG_R0, ARM_REG_SP, 4); + gum_arm_writer_put_pop_regs (cw, 2, ARM_REG_R0, ARM_REG_PC); + + gum_arm_writer_flush (cw); + gum_stalker_freeze (self, cw->base, gum_arm_writer_offset (cw)); + + gum_spinlock_release (&ctx->code_lock); + + cpu_context->cpsr &= ~GUM_PSR_T_BIT; + cpu_context->pc = infect_body; + } +} diff --git a/gum/backend-arm64/gumstalker-arm64.c b/gum/backend-arm64/gumstalker-arm64.c index 3c8bba7c53..25868c784c 100644 --- a/gum/backend-arm64/gumstalker-arm64.c +++ b/gum/backend-arm64/gumstalker-arm64.c @@ -86,6 +86,7 @@ typedef struct _GumBackpatchCall GumBackpatchCall; typedef struct _GumBackpatchJmp GumBackpatchJmp; typedef struct _GumBackpatchInlineCache GumBackpatchInlineCache; typedef struct _GumBackpatchExcludedCall GumBackpatchExcludedCall; +typedef struct _GumStalkerRunOnThreadCtx GumStalkerRunOnThreadCtx; #ifdef HAVE_LINUX typedef struct _Unwind_Exception _Unwind_Exception; @@ -479,6 +480,13 @@ struct _GumBackpatch }; }; +struct _GumStalkerRunOnThreadCtx +{ + GumStalker * stalker; + GumStalkerRunOnThreadFunc func; + gpointer user_data; +}; + #ifdef HAVE_LINUX extern _Unwind_Reason_Code __gxx_personality_v0 (int version, @@ -761,6 +769,9 @@ static gpointer gum_find_thread_exit_implementation (void); static gboolean gum_is_bl_imm (guint32 insn) G_GNUC_UNUSED; +static void gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, gpointer user_data); + G_DEFINE_TYPE (GumStalker, gum_stalker, G_TYPE_OBJECT) static GPrivate gum_stalker_exec_ctx_private; @@ -5886,3 +5897,98 @@ gum_is_bl_imm (guint32 insn) } #endif + +gboolean +gum_stalker_is_run_on_thread_supported (void) +{ + return TRUE; +} + +gboolean +gum_stalker_run_on_thread_async (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx ctx; + ctx.stalker = self; + ctx.func = func; + ctx.user_data = user_data; + return gum_process_modify_thread (thread_id, + gum_stalker_do_run_on_thread_async, &ctx, GUM_MODIFY_THREAD_FLAGS_NONE); +} + +static void +gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx * run_ctx = (GumStalkerRunOnThreadCtx *) user_data; + GumStalker * self = run_ctx->stalker; + GumExecCtx * ctx; + guint8 * pc; + GumArm64Writer * cw; + GumAddress cpu_ctx; + + if (gum_process_get_current_thread_id () == thread_id) + { + run_ctx->func (cpu_context, run_ctx->user_data); + } + else + { + ctx = gum_stalker_create_exec_ctx (self, thread_id, NULL, NULL); + + pc = GSIZE_TO_POINTER (gum_strip_code_address (cpu_context->pc)); + + gum_spinlock_acquire (&ctx->code_lock); + + gum_stalker_thaw (self, ctx->thunks, self->thunks_size); + cw = &ctx->code_writer; + gum_arm64_writer_reset (cw, ctx->infect_thunk); + + /* Copy the cpu context for the target thread. */ + cpu_ctx = GUM_ADDRESS (gum_arm64_writer_cur (cw)); + gum_arm64_writer_put_bytes (cw, (gpointer) cpu_context, + sizeof (GumCpuContext)); + + ctx->infect_body = GUM_ADDRESS (gum_arm64_writer_cur (cw)); + +#ifdef HAVE_PTRAUTH + ctx->infect_body = GPOINTER_TO_SIZE (ptrauth_sign_unauthenticated ( + GSIZE_TO_POINTER (ctx->infect_body), ptrauth_key_process_independent_code, + ptrauth_string_discriminator ("pc"))); +#endif + gum_exec_ctx_write_prolog (ctx, GUM_PROLOG_MINIMAL, cw); + + gum_arm64_writer_put_call_address_with_arguments (cw, + GUM_ADDRESS (run_ctx->func), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (cpu_ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (run_ctx->user_data)); + + gum_arm64_writer_put_call_address_with_arguments (cw, + GUM_ADDRESS (gum_exec_ctx_unfollow), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (pc)); + + gum_exec_ctx_write_epilog (ctx, GUM_PROLOG_MINIMAL, cw); + + /* + * Here we spoil x17 since this is a necessity of the AARCH64 architecture + * when performing long branches. However, the documentation states... + * + * "Registers r16 (IP0) and r17 (IP1) may be used by a linker as a scratch + * register between a routine and any subroutine it calls." + * + * This same approach is used elsewhere in Stalker for aarch64. + */ + gum_arm64_writer_put_ldr_reg_address (cw, ARM64_REG_X17, GUM_ADDRESS (pc)); + gum_arm64_writer_put_br_reg_no_auth (cw, ARM64_REG_X17); + + gum_arm64_writer_flush (cw); + gum_stalker_freeze (self, cw->base, gum_arm64_writer_offset (cw)); + + gum_spinlock_release (&ctx->code_lock); + + cpu_context->pc = ctx->infect_body; + } +} diff --git a/gum/backend-mips/gumstalker-mips.c b/gum/backend-mips/gumstalker-mips.c index 782ab1a959..b168286b94 100644 --- a/gum/backend-mips/gumstalker-mips.c +++ b/gum/backend-mips/gumstalker-mips.c @@ -209,6 +209,21 @@ gum_stalker_iterator_put_callout (GumStalkerIterator * self, { } +gboolean +gum_stalker_is_run_on_thread_supported (void) +{ + return FALSE; +} + +gboolean +gum_stalker_run_on_thread_async (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer user_data) +{ + return FALSE; +} + csh gum_stalker_iterator_get_capstone (GumStalkerIterator * self) { diff --git a/gum/backend-x86/gumstalker-x86.c b/gum/backend-x86/gumstalker-x86.c index a7e04c9238..1d790ec26d 100644 --- a/gum/backend-x86/gumstalker-x86.c +++ b/gum/backend-x86/gumstalker-x86.c @@ -110,6 +110,7 @@ typedef struct _GumBackpatchRet GumBackpatchRet; typedef struct _GumBackpatchJmp GumBackpatchJmp; typedef struct _GumBackpatchInlineCache GumBackpatchInlineCache; typedef struct _GumIcEntry GumIcEntry; +typedef struct _GumStalkerRunOnThreadCtx GumStalkerRunOnThreadCtx; typedef guint GumVirtualizationRequirements; @@ -544,6 +545,13 @@ struct _GumCheckElfSection #endif +struct _GumStalkerRunOnThreadCtx +{ + GumStalker * stalker; + GumStalkerRunOnThreadFunc func; + gpointer user_data; +}; + #if defined (HAVE_LINUX) && !defined (HAVE_ANDROID) extern _Unwind_Reason_Code __gxx_personality_v0 (int version, @@ -866,6 +874,9 @@ static GumInterceptor * gum_exec_ctx_interceptor = NULL; # endif #endif +static void gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, gpointer user_data); + gboolean gum_stalker_is_supported (void) { @@ -6642,3 +6653,108 @@ gum_store_thread_exit_match (GumAddress address, #endif #endif + +gboolean +gum_stalker_is_run_on_thread_supported (void) +{ + return TRUE; +} + +gboolean +gum_stalker_run_on_thread_async (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx ctx; + ctx.stalker = self; + ctx.func = func; + ctx.user_data = user_data; + return gum_process_modify_thread (thread_id, + gum_stalker_do_run_on_thread_async, &ctx, GUM_MODIFY_THREAD_FLAGS_NONE); +} + +static void +gum_stalker_do_run_on_thread_async (GumThreadId thread_id, + GumCpuContext * cpu_context, + gpointer user_data) +{ + GumStalkerRunOnThreadCtx * run_ctx = (GumStalkerRunOnThreadCtx *) user_data; + GumStalker * self = run_ctx->stalker; + GumExecCtx * ctx; + guint8 * pc; + GumX86Writer * cw; + GumAddress cpu_ctx; + + if (gum_process_get_current_thread_id () == thread_id) + { + run_ctx->func (cpu_context, run_ctx->user_data); + } + else + { + ctx = gum_stalker_create_exec_ctx (self, thread_id, NULL, NULL); + + pc = GSIZE_TO_POINTER (GUM_CPU_CONTEXT_XIP (cpu_context)); + + gum_spinlock_acquire (&ctx->code_lock); + + gum_stalker_thaw (self, ctx->thunks, self->thunks_size); + cw = &ctx->code_writer; + gum_x86_writer_reset (cw, ctx->infect_thunk); + + /* Copy the cpu context for the target thread. */ + cpu_ctx = GUM_ADDRESS (gum_x86_writer_cur (cw)); + gum_x86_writer_put_bytes (cw, (gpointer) cpu_context, + sizeof (GumCpuContext)); + +#ifdef HAVE_LINUX + /* + * In case the thread is in a Linux system call we prefix with a couple of + * NOPs so that when we restart, we don't re-attempt the syscall. We will + * drop ourselves back to the syscall once we are done. + */ + gum_x86_writer_put_nop_padding (cw, MAX (sizeof (gum_int80_code), + sizeof (gum_syscall_code))); +#endif + + ctx->infect_body = GUM_ADDRESS (gum_x86_writer_cur (cw)); + gum_exec_ctx_write_prolog (ctx, GUM_PROLOG_MINIMAL, cw); + gum_x86_writer_put_call_address_with_aligned_arguments (cw, GUM_CALL_CAPI, + GUM_ADDRESS (run_ctx->func), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (cpu_ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (run_ctx->user_data)); + gum_x86_writer_put_call_address_with_aligned_arguments (cw, GUM_CALL_CAPI, + GUM_ADDRESS (gum_exec_ctx_unfollow), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (ctx), + GUM_ARG_ADDRESS, GUM_ADDRESS (pc)); + gum_exec_ctx_write_epilog (ctx, GUM_PROLOG_MINIMAL, cw); + +#ifdef HAVE_LINUX + if (memcmp (&pc[-sizeof (gum_int80_code)], gum_int80_code, + sizeof (gum_int80_code)) == 0) + { + gum_x86_writer_put_jmp_address (cw, + GUM_ADDRESS (&pc[-sizeof (gum_int80_code)])); + } + else if (memcmp (&pc[-sizeof (gum_syscall_code)], gum_syscall_code, + sizeof (gum_syscall_code)) == 0) + { + gum_x86_writer_put_jmp_address (cw, + GUM_ADDRESS (&pc[-sizeof (gum_syscall_code)])); + } + else + { + gum_x86_writer_put_jmp_address (cw, GUM_ADDRESS (pc)); + } +#else + gum_x86_writer_put_jmp_address (cw, GUM_ADDRESS (pc)); +#endif + + gum_x86_writer_flush (cw); + gum_stalker_freeze (self, cw->base, gum_x86_writer_offset (cw)); + + gum_spinlock_release (&ctx->code_lock); + + GUM_CPU_CONTEXT_XIP (cpu_context) = ctx->infect_body; + } +} diff --git a/gum/gumstalker.c b/gum/gumstalker.c index a5732c56ec..707a7e8bd2 100644 --- a/gum/gumstalker.c +++ b/gum/gumstalker.c @@ -8,6 +8,8 @@ #include "gumstalker.h" +typedef struct _RunOnThreadSyncCtx RunOnThreadSyncCtx; + struct _GumDefaultStalkerTransformer { GObject parent; @@ -22,6 +24,15 @@ struct _GumCallbackStalkerTransformer GDestroyNotify data_destroy; }; +struct _RunOnThreadSyncCtx +{ + GMutex mutex; + GCond cond; + volatile gboolean done; + GumStalkerRunOnThreadFunc func; + gpointer user_data; +}; + static void gum_default_stalker_transformer_iface_init (gpointer g_iface, gpointer iface_data); static void gum_default_stalker_transformer_transform_block ( @@ -38,6 +49,9 @@ static void gum_callback_stalker_transformer_transform_block ( static void gum_stalker_observer_default_init ( GumStalkerObserverInterface * iface); +static void gum_stalker_do_run_on_thread_sync ( + const GumCpuContext * cpu_context, gpointer user_data); + G_DEFINE_INTERFACE (GumStalkerTransformer, gum_stalker_transformer, G_TYPE_OBJECT) @@ -410,4 +424,64 @@ gum_stalker_observer_switch_callback (GumStalkerObserver * observer, target); } +gboolean +gum_stalker_run_on_thread_sync (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer user_data) +{ + RunOnThreadSyncCtx ctx; + + if (!gum_stalker_is_run_on_thread_supported ()) + { + return FALSE; + } + + if (gum_process_get_current_thread_id () == thread_id) + { + return gum_stalker_run_on_thread_async (self, thread_id, func, user_data); + } + else + { + g_mutex_init (&ctx.mutex); + g_cond_init (&ctx.cond); + ctx.done = FALSE; + ctx.func = func; + ctx.user_data = user_data; + + g_mutex_lock (&ctx.mutex); + if (!gum_stalker_run_on_thread_async (self, thread_id, + gum_stalker_do_run_on_thread_sync, &ctx)) + { + return FALSE; + } + + while (!ctx.done) + g_cond_wait (&ctx.cond, &ctx.mutex); + + g_mutex_unlock (&ctx.mutex); + + g_cond_clear (&ctx.cond); + g_mutex_clear (&ctx.mutex); + + return TRUE; + } + +} + +static void +gum_stalker_do_run_on_thread_sync (const GumCpuContext * cpu_context, + gpointer user_data) +{ + RunOnThreadSyncCtx * ctx = (RunOnThreadSyncCtx *) user_data; + + ctx->func (cpu_context, ctx->user_data); + + g_mutex_lock (&ctx->mutex); + ctx->done = TRUE; + g_cond_signal (&ctx->cond); + g_mutex_unlock (&ctx->mutex); +} + #endif + diff --git a/gum/gumstalker.h b/gum/gumstalker.h index 11735d2e2c..450888896e 100644 --- a/gum/gumstalker.h +++ b/gum/gumstalker.h @@ -66,6 +66,8 @@ typedef guint GumProbeId; typedef struct _GumCallDetails GumCallDetails; typedef void (* GumCallProbeCallback) (GumCallDetails * details, gpointer user_data); +typedef void (* GumStalkerRunOnThreadFunc) (const GumCpuContext * cpu_context, + gpointer user_data); #ifndef GUM_DIET @@ -275,6 +277,14 @@ GUM_API void gum_stalker_observer_switch_callback ( GumStalkerObserver * observer, gpointer from_address, gpointer start_address, gpointer from_insn, gpointer * target); +GUM_API gboolean gum_stalker_is_run_on_thread_supported (void); + +GUM_API gboolean gum_stalker_run_on_thread_async (GumStalker * self, + GumThreadId thread_id, GumStalkerRunOnThreadFunc func, gpointer user_data); + +GUM_API gboolean gum_stalker_run_on_thread_sync (GumStalker * self, + GumThreadId thread_id, GumStalkerRunOnThreadFunc func, gpointer user_data); + G_END_DECLS #endif diff --git a/tests/core/arch-arm/stalker-arm.c b/tests/core/arch-arm/stalker-arm.c index 3e2cc1c8f6..8f1e9a5b47 100644 --- a/tests/core/arch-arm/stalker-arm.c +++ b/tests/core/arch-arm/stalker-arm.c @@ -126,8 +126,32 @@ TESTLIST_BEGIN (stalker) #ifdef HAVE_LINUX TESTENTRY (prefetch) #endif + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_support) + TESTENTRY (run_on_thread_current_async) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other_async) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + +struct _RunOnThreadCtx +{ + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + volatile gboolean started; + volatile GumThreadId thread_id; + volatile gboolean * volatile done; +}; + static gboolean store_range_of_test_runner (const GumModuleDetails * details, gpointer user_data); static guint32 pretend_workload (GumMemoryRange * runner_range); @@ -192,6 +216,12 @@ static void prefetch_read_blocks (int fd, GHashTable * table); static GHashTable * prefetch_compiled = NULL; static GHashTable * prefetch_executed = NULL; #endif +static void run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static GThread * create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); + TESTCASE (trust_should_be_one_by_default) { @@ -3772,3 +3802,146 @@ prefetch_read_blocks (int fd, } #endif + +TESTCASE (run_on_thread_support) +{ + gboolean supported = gum_stalker_is_run_on_thread_supported (); + g_assert_true (supported); +} + +TESTCASE (run_on_thread_current_async) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_async (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +static void +run_on_thread (const GumCpuContext * cpu_context, gpointer user_data) +{ + RunOnThreadCtx * ctx = (RunOnThreadCtx *) user_data; + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); +} + +TESTCASE (run_on_thread_other_async) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_async (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_sync (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id) +{ + TestThreadSyncData 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 ("sleepy", sleeping_dummy, &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 (gpointer data) +{ + TestThreadSyncData * sync_data = data; + volatile gboolean * done = sync_data->done; + + 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; +} diff --git a/tests/core/arch-arm64/stalker-arm64.c b/tests/core/arch-arm64/stalker-arm64.c index d73ac9bb82..b20e496895 100644 --- a/tests/core/arch-arm64/stalker-arm64.c +++ b/tests/core/arch-arm64/stalker-arm64.c @@ -80,8 +80,18 @@ TESTLIST_BEGIN (stalker) TESTENTRY (prefetch) TESTENTRY (observer) #endif + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_support) + TESTENTRY (run_on_thread_current_async) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other_async) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + #ifdef HAVE_LINUX struct _GumTestStalkerObserver @@ -93,6 +103,20 @@ struct _GumTestStalkerObserver #endif +struct _RunOnThreadCtx +{ + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + volatile gboolean started; + volatile GumThreadId thread_id; + volatile gboolean * volatile done; +}; + static void insert_extra_add_after_sub (GumStalkerIterator * iterator, GumStalkerOutput * output, gpointer user_data); static void store_x0 (GumCpuContext * cpu_context, gpointer user_data); @@ -163,6 +187,11 @@ G_DEFINE_TYPE_EXTENDED (GumTestStalkerObserver, G_IMPLEMENT_INTERFACE (GUM_TYPE_STALKER_OBSERVER, gum_test_stalker_observer_iface_init)) #endif +static void run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static GThread * create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); static const guint32 flat_code[] = { 0xcb000000, /* sub w0, w0, w0 */ @@ -2391,3 +2420,146 @@ gum_test_stalker_observer_increment_total (GumStalkerObserver * observer) } #endif + +TESTCASE (run_on_thread_support) +{ + gboolean supported = gum_stalker_is_run_on_thread_supported (); + g_assert_true (supported); +} + +TESTCASE (run_on_thread_current_async) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_async (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +static void +run_on_thread (const GumCpuContext * cpu_context, gpointer user_data) +{ + RunOnThreadCtx * ctx = (RunOnThreadCtx *) user_data; + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); +} + +TESTCASE (run_on_thread_other_async) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_async (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_sync (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id) +{ + TestThreadSyncData 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 ("sleepy", sleeping_dummy, &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 (gpointer data) +{ + TestThreadSyncData * sync_data = data; + volatile gboolean * done = sync_data->done; + + 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; +} diff --git a/tests/core/arch-x86/stalker-x86.c b/tests/core/arch-x86/stalker-x86.c index 00431e487d..5f97b7f2d9 100644 --- a/tests/core/arch-x86/stalker-x86.c +++ b/tests/core/arch-x86/stalker-x86.c @@ -120,6 +120,13 @@ TESTLIST_BEGIN (stalker) TESTENTRY (try_and_dont_catch_excluded) TESTGROUP_END () #endif + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_support) + TESTENTRY (run_on_thread_current_async) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other_async) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () #ifdef HAVE_LINUX @@ -150,6 +157,23 @@ struct _PrefetchBackpatchContext #endif +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + +struct _RunOnThreadCtx +{ + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + volatile gboolean started; + volatile GumThreadId thread_id; + volatile gboolean * volatile done; +}; + static gpointer run_stalked_briefly (gpointer data); #ifdef HAVE_LINUX static gpointer run_spawned_thread (gpointer data); @@ -238,6 +262,12 @@ void test_check_bit (guint32 * val, guint8 bit); void test_try_and_catch (guint32 * val); void test_try_and_dont_catch (guint32 * val); #endif +static void run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static GThread * create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); + static const guint8 flat_code[] = { 0x33, 0xc0, /* xor eax, eax */ @@ -3481,3 +3511,146 @@ test_check_followed (void) } #endif + +TESTCASE (run_on_thread_support) +{ + gboolean supported = gum_stalker_is_run_on_thread_supported (); + g_assert_true (supported); +} + +TESTCASE (run_on_thread_current_async) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_async (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id = gum_process_get_current_thread_id (); + RunOnThreadCtx ctx; + gboolean success; + + success = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + +#if defined (HAVE_ANDROID) + /* + * getcontext/setcontext is not supported on the musl C-runtime (or Android), + * therefore `gum_process_modify_thread` fails. + */ + g_assert_false (success); +#else + g_assert_true (success); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +#endif +} + +static void +run_on_thread (const GumCpuContext * cpu_context, gpointer user_data) +{ + RunOnThreadCtx * ctx = (RunOnThreadCtx *) user_data; + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); +} + +TESTCASE (run_on_thread_other_async) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_async (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GumThreadId this_id, other_id; + volatile gboolean done = FALSE; + GThread * thread; + RunOnThreadCtx ctx; + + this_id = gum_process_get_current_thread_id (); + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + gum_stalker_run_on_thread_sync (fixture->stalker, other_id, run_on_thread, + &ctx); + + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (this_id, !=, other_id); + g_assert_cmpuint (other_id, ==, ctx.thread_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id) +{ + TestThreadSyncData 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 ("sleepy", sleeping_dummy, &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 (gpointer data) +{ + TestThreadSyncData * sync_data = data; + volatile gboolean * done = sync_data->done; + + 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; +} diff --git a/tests/gumjs/script.c b/tests/gumjs/script.c index 5240ea8b49..939d347205 100644 --- a/tests/gumjs/script.c +++ b/tests/gumjs/script.c @@ -200,6 +200,11 @@ TESTLIST_BEGIN (script) #endif TESTGROUP_END () + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (process_can_run_on_thread_sync) + TESTENTRY (process_can_run_on_thread_async) + TESTGROUP_END () + TESTGROUP_BEGIN ("Module") TESTENTRY (module_imports_can_be_enumerated) TESTENTRY (module_imports_can_be_enumerated_legacy_style) @@ -486,6 +491,7 @@ TESTLIST_END () typedef struct _GumInvokeTargetContext GumInvokeTargetContext; typedef struct _GumCrashExceptorContext GumCrashExceptorContext; typedef struct _TestTrigger TestTrigger; +typedef struct _TestRunOnThreadSyncContext TestRunOnThreadSyncContext; struct _GumInvokeTargetContext { @@ -509,6 +515,15 @@ struct _TestTrigger GCond cond; }; +struct _TestRunOnThreadSyncContext +{ + GMutex mutex; + GCond cond; + volatile gboolean started; + volatile GumThreadId thread_id; + volatile gboolean * volatile done; +}; + static size_t gum_get_size_max (void); static gboolean gum_test_size_max (size_t sz); static size_t gum_add_size (size_t sz); @@ -547,6 +562,9 @@ static gpointer run_stalked_through_target_function (gpointer data); #endif static gpointer sleeping_dummy (gpointer data); +static GThread * create_sleeping_dummy_thread_sync (volatile gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy_func (gpointer data); static gpointer invoke_target_function_int_worker (gpointer data); static gpointer invoke_target_function_trigger (gpointer data); @@ -5291,6 +5309,125 @@ TESTCASE (process_malloc_ranges_can_be_enumerated_legacy_style) #endif +TESTCASE (process_can_run_on_thread_sync) +{ + GThread * thread; + GumThreadId thread_id; + volatile gboolean done = FALSE; + + thread = create_sleeping_dummy_thread_sync (&done, &thread_id); + +#if !defined (HAVE_I386) + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Stalker Run-On-Thread Unsupported"); +#endif + + COMPILE_AND_LOAD_SCRIPT ( + "const threads = Process.enumerateThreads();" + "const thread = threads.find(t => t.id == " GUM_PTR_CONST ");" + "const data = 1338;" + "var out_val = 0;" + "const ret = Process.runOnThreadSync(thread.id, function (ctx) {" + " send (data);" + " out_val = 1339;" + "});" + "send (out_val)", + thread_id); + + EXPECT_SEND_MESSAGE_WITH ("1338"); + EXPECT_SEND_MESSAGE_WITH ("1339"); + + done = TRUE; + g_thread_join (thread); +} + +TESTCASE (process_can_run_on_thread_async) +{ + GThread * thread; + GumThreadId thread_id; + volatile gboolean done = FALSE; + + thread = create_sleeping_dummy_thread_sync (&done, &thread_id); + +#if !defined (HAVE_I386) + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Stalker Run-On-Thread Unsupported"); +#endif + + COMPILE_AND_LOAD_SCRIPT ( + "async function run () {" + " const threads = Process.enumerateThreads();" + " const thread = threads.find(t => t.id == " GUM_PTR_CONST ");" + " const data = 1338;" + " let res;" + " const prom = new Promise (function (resolve, reject) {" + " res = resolve;" + " });" + " const ret = Process.runOnThreadAsync(thread.id, function (ctx) {" + " send (data);" + " res();" + " });" + " await prom;" + "};" + "run();", + thread_id); + + EXPECT_SEND_MESSAGE_WITH ("1338"); + + done = TRUE; + g_thread_join (thread); +} + +static GThread * +create_sleeping_dummy_thread_sync (volatile 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 ("process-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; + volatile gboolean * done = sync_data->done; + + 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; +} + TESTCASE (process_system_ranges_can_be_enumerated) { COMPILE_AND_LOAD_SCRIPT (