diff --git a/gum/backend-arm/gumstalker-arm.c b/gum/backend-arm/gumstalker-arm.c index c3981b239..dd6875b15 100644 --- a/gum/backend-arm/gumstalker-arm.c +++ b/gum/backend-arm/gumstalker-arm.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2023 Håvard Sørbø * * Licence: wxWindows Library Licence, Version 3.1 @@ -16,6 +16,7 @@ #include "gummemory.h" #include "gummetalhash.h" #include "gumspinlock.h" +#include "gumstalker-priv.h" #include "gumthumbreader.h" #include "gumthumbrelocator.h" #include "gumthumbwriter.h" @@ -1588,6 +1589,64 @@ gum_call_probe_unref (GumCallProbe * probe) } } +void +_gum_stalker_modify_to_run_on_thread (GumStalker * self, + GumThreadId thread_id, + GumCpuContext * cpu_context, + GumStalkerRunOnThreadFunc func, + gpointer data) +{ + GumExecCtx * ctx; + guint32 pc; + GumArmWriter * cw; + GumAddress cpu_context_copy, infect_body; + + 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); + + cpu_context_copy = GUM_ADDRESS (gum_arm_writer_cur (cw)); + gum_arm_writer_put_bytes (cw, (guint8 *) cpu_context, sizeof (GumCpuContext)); + + infect_body = GUM_ADDRESS (gum_arm_writer_cur (cw)); + + gum_exec_ctx_write_arm_prolog (ctx, cw); + + gum_arm_writer_put_call_address_with_arguments (cw, + GUM_ADDRESS (func), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (cpu_context_copy), + GUM_ARG_ADDRESS, GUM_ADDRESS (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, 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; +} + static GumExecCtx * gum_stalker_create_exec_ctx (GumStalker * self, GumThreadId thread_id, diff --git a/gum/backend-arm64/gumstalker-arm64.c b/gum/backend-arm64/gumstalker-arm64.c index b734f923d..a0c79c61f 100644 --- a/gum/backend-arm64/gumstalker-arm64.c +++ b/gum/backend-arm64/gumstalker-arm64.c @@ -20,6 +20,7 @@ #include "gummemory.h" #include "gummetalhash.h" #include "gumspinlock.h" +#include "gumstalker-priv.h" #ifdef HAVE_LINUX # include "gum-init.h" # include "guminterceptor.h" @@ -1928,6 +1929,74 @@ gum_call_probe_unref (GumCallProbe * probe) } } +void +_gum_stalker_modify_to_run_on_thread (GumStalker * self, + GumThreadId thread_id, + GumCpuContext * cpu_context, + GumStalkerRunOnThreadFunc func, + gpointer data) +{ + GumExecCtx * ctx; + GumAddress pc; + GumArm64Writer * cw; + GumAddress cpu_context_copy; + + ctx = gum_stalker_create_exec_ctx (self, thread_id, NULL, NULL); + + pc = 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); + + cpu_context_copy = GUM_ADDRESS (gum_arm64_writer_cur (cw)); + gum_arm64_writer_put_bytes (cw, (guint8 *) 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 (func), 2, + GUM_ARG_ADDRESS, cpu_context_copy, + GUM_ARG_ADDRESS, GUM_ADDRESS (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, 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 arm64. + */ + gum_arm64_writer_put_ldr_reg_address (cw, ARM64_REG_X17, 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; +} + static GumExecCtx * gum_stalker_create_exec_ctx (GumStalker * self, GumThreadId thread_id, diff --git a/gum/backend-mips/gumstalker-mips.c b/gum/backend-mips/gumstalker-mips.c index 782ab1a95..7dc064c08 100644 --- a/gum/backend-mips/gumstalker-mips.c +++ b/gum/backend-mips/gumstalker-mips.c @@ -1,11 +1,13 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * * Licence: wxWindows Library Licence, Version 3.1 */ #include "gumstalker.h" +#include "gumstalker-priv.h" + struct _GumStalker { GObject parent; @@ -183,6 +185,15 @@ gum_stalker_remove_call_probe (GumStalker * self, { } +void +_gum_stalker_modify_to_run_on_thread (GumStalker * self, + GumThreadId thread_id, + GumCpuContext * cpu_context, + GumStalkerRunOnThreadFunc func, + gpointer data) +{ +} + gboolean gum_stalker_iterator_next (GumStalkerIterator * self, const cs_insn ** insn) diff --git a/gum/backend-x86/gumstalker-x86.c b/gum/backend-x86/gumstalker-x86.c index a7e04c923..45a348ede 100644 --- a/gum/backend-x86/gumstalker-x86.c +++ b/gum/backend-x86/gumstalker-x86.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2010-2013 Karl Trygve Kalleberg * Copyright (C) 2020 Duy Phan Thanh * @@ -16,6 +16,7 @@ #include "gummemory.h" #include "gumx86relocator.h" #include "gumspinlock.h" +#include "gumstalker-priv.h" #ifdef HAVE_WINDOWS # include "gumexceptor.h" #endif @@ -2172,6 +2173,82 @@ gum_call_probe_unref (GumCallProbe * probe) } } +void +_gum_stalker_modify_to_run_on_thread (GumStalker * self, + GumThreadId thread_id, + GumCpuContext * cpu_context, + GumStalkerRunOnThreadFunc func, + gpointer data) +{ + GumExecCtx * ctx; + guint8 * pc; + GumX86Writer * cw; + GumAddress cpu_context_copy; + + 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); + + cpu_context_copy = GUM_ADDRESS (gum_x86_writer_cur (cw)); + gum_x86_writer_put_bytes (cw, (guint8 *) 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 (func), 2, + GUM_ARG_ADDRESS, GUM_ADDRESS (cpu_context_copy), + GUM_ARG_ADDRESS, GUM_ADDRESS (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; +} + static GumExecCtx * gum_stalker_create_exec_ctx (GumStalker * self, GumThreadId thread_id, diff --git a/gum/gumstalker-priv.h b/gum/gumstalker-priv.h new file mode 100644 index 000000000..6279f7f15 --- /dev/null +++ b/gum/gumstalker-priv.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 Ole André Vadla Ravnås + * + * Licence: wxWindows Library Licence, Version 3.1 + */ + +#ifndef __GUM_STALKER_PRIV_H__ +#define __GUM_STALKER_PRIV_H__ + +#include "gumstalker.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL void _gum_stalker_modify_to_run_on_thread (GumStalker * self, + GumThreadId thread_id, GumCpuContext * cpu_context, + GumStalkerRunOnThreadFunc func, gpointer data); + +G_END_DECLS + +#endif diff --git a/gum/gumstalker.c b/gum/gumstalker.c index a5732c56e..c7083d112 100644 --- a/gum/gumstalker.c +++ b/gum/gumstalker.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2022 Ole André Vadla Ravnås + * Copyright (C) 2017-2024 Ole André Vadla Ravnås * * Licence: wxWindows Library Licence, Version 3.1 */ @@ -8,6 +8,28 @@ #include "gumstalker.h" +#include "gumstalker-priv.h" + +typedef struct _GumRunOnThreadCtx GumRunOnThreadCtx; +typedef struct _GumRunOnThreadSyncCtx GumRunOnThreadSyncCtx; + +struct _GumRunOnThreadCtx +{ + GumStalker * stalker; + GumStalkerRunOnThreadFunc func; + gpointer data; + GDestroyNotify data_destroy; +}; + +struct _GumRunOnThreadSyncCtx +{ + GMutex mutex; + GCond cond; + gboolean done; + GumStalkerRunOnThreadFunc func; + gpointer data; +}; + struct _GumDefaultStalkerTransformer { GObject parent; @@ -22,6 +44,13 @@ struct _GumCallbackStalkerTransformer GDestroyNotify data_destroy; }; +static void gum_modify_to_run_on_thread (GumThreadId thread_id, + GumCpuContext * cpu_context, gpointer user_data); +static void gum_do_run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data); +static void gum_do_run_on_thread_sync (const GumCpuContext * cpu_context, + 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 ( @@ -170,6 +199,126 @@ G_DEFINE_INTERFACE (GumStalkerObserver, gum_stalker_observer, G_TYPE_OBJECT) * END LOOP: */ +gboolean +gum_stalker_run_on_thread (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer data, + GDestroyNotify data_destroy) +{ + gboolean accepted = TRUE; + gboolean finished = TRUE; + + if (thread_id == gum_process_get_current_thread_id ()) + { + func (NULL, data); + } + else + { + GumRunOnThreadCtx * rc; + + rc = g_slice_new (GumRunOnThreadCtx); + rc->stalker = self; + rc->func = func; + rc->data = data; + rc->data_destroy = data_destroy; + + accepted = gum_process_modify_thread (thread_id, + gum_modify_to_run_on_thread, rc, GUM_MODIFY_THREAD_FLAGS_NONE); + if (accepted) + finished = FALSE; + else + g_slice_free (GumRunOnThreadCtx, rc); + } + + if (finished && data_destroy != NULL) + data_destroy (data); + + return accepted; +} + +static void +gum_modify_to_run_on_thread (GumThreadId thread_id, + GumCpuContext * cpu_context, + gpointer user_data) +{ + GumRunOnThreadCtx * rc = user_data; + + _gum_stalker_modify_to_run_on_thread (rc->stalker, thread_id, cpu_context, + gum_do_run_on_thread, rc); +} + +static void +gum_do_run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + GumRunOnThreadCtx * rc = user_data; + + rc->func (cpu_context, rc->data); + + if (rc->data_destroy != NULL) + rc->data_destroy (rc->data); + g_slice_free (GumRunOnThreadCtx, rc); +} + +gboolean +gum_stalker_run_on_thread_sync (GumStalker * self, + GumThreadId thread_id, + GumStalkerRunOnThreadFunc func, + gpointer data) +{ + gboolean success = TRUE; + + if (thread_id == gum_process_get_current_thread_id ()) + { + func (NULL, data); + } + else + { + GumRunOnThreadSyncCtx rc; + + g_mutex_init (&rc.mutex); + g_cond_init (&rc.cond); + rc.done = FALSE; + rc.func = func; + rc.data = data; + + g_mutex_lock (&rc.mutex); + + if (gum_stalker_run_on_thread (self, thread_id, gum_do_run_on_thread_sync, + &rc, NULL)) + { + while (!rc.done) + g_cond_wait (&rc.cond, &rc.mutex); + } + else + { + success = FALSE; + } + + g_mutex_unlock (&rc.mutex); + + g_cond_clear (&rc.cond); + g_mutex_clear (&rc.mutex); + } + + return success; +} + +static void +gum_do_run_on_thread_sync (const GumCpuContext * cpu_context, + gpointer user_data) +{ + GumRunOnThreadSyncCtx * rc = user_data; + + rc->func (cpu_context, rc->data); + + g_mutex_lock (&rc->mutex); + rc->done = TRUE; + g_cond_signal (&rc->cond); + g_mutex_unlock (&rc->mutex); +} + static void gum_stalker_transformer_default_init (GumStalkerTransformerInterface * iface) { diff --git a/gum/gumstalker.h b/gum/gumstalker.h index 11735d2e2..17b796e8a 100644 --- a/gum/gumstalker.h +++ b/gum/gumstalker.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2010 Karl Trygve Kalleberg * Copyright (C) 2023 Håvard Sørbø * @@ -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 @@ -211,6 +213,12 @@ GUM_API GumProbeId gum_stalker_add_call_probe (GumStalker * self, GUM_API void gum_stalker_remove_call_probe (GumStalker * self, GumProbeId id); +GUM_API gboolean gum_stalker_run_on_thread (GumStalker * self, + GumThreadId thread_id, GumStalkerRunOnThreadFunc func, gpointer data, + GDestroyNotify data_destroy); +GUM_API gboolean gum_stalker_run_on_thread_sync (GumStalker * self, + GumThreadId thread_id, GumStalkerRunOnThreadFunc func, gpointer data); + GUM_API GumStalkerTransformer * gum_stalker_transformer_make_default (void); GUM_API GumStalkerTransformer * gum_stalker_transformer_make_from_callback ( GumStalkerTransformerCallback callback, gpointer data, diff --git a/tests/core/arch-arm/stalker-arm.c b/tests/core/arch-arm/stalker-arm.c index 3e2cc1c8f..0a8343c80 100644 --- a/tests/core/arch-arm/stalker-arm.c +++ b/tests/core/arch-arm/stalker-arm.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2017 Antonio Ken Iannillo * Copyright (C) 2023 Håvard Sørbø * @@ -126,8 +126,33 @@ TESTLIST_BEGIN (stalker) #ifdef HAVE_LINUX TESTENTRY (prefetch) #endif + + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_current) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + +struct _RunOnThreadCtx +{ + GumThreadId caller_id; + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + gboolean started; + GumThreadId thread_id; + gboolean * done; +}; + static gboolean store_range_of_test_runner (const GumModuleDetails * details, gpointer user_data); static guint32 pretend_workload (GumMemoryRange * runner_range); @@ -193,6 +218,12 @@ 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 (gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); + TESTCASE (trust_should_be_one_by_default) { g_assert_cmpuint (gum_stalker_get_trust_threshold (fixture->stalker), ==, 1); @@ -3772,3 +3803,144 @@ prefetch_read_blocks (int fd, } #endif + +TESTCASE (run_on_thread_current) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, thread_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + g_assert_cmpuint (ctx.thread_id, ==, thread_id); +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + g_assert_true (accepted); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +} + +static void +run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + RunOnThreadCtx * ctx = user_data; + + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); + + if (ctx->thread_id == ctx->caller_id) + g_assert_null (cpu_context); + else + g_assert_nonnull (cpu_context); +} + +TESTCASE (run_on_thread_other) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, other_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmphex (ctx.thread_id, ==, other_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, other_id, + run_on_thread, &ctx); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (ctx.thread_id, ==, other_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (gboolean * done, + GumThreadId * thread_id) +{ + GThread * thread; + TestThreadSyncData sync_data; + + 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); + + *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; + 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 d73ac9bb8..e0007557e 100644 --- a/tests/core/arch-arm64/stalker-arm64.c +++ b/tests/core/arch-arm64/stalker-arm64.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2017 Antonio Ken Iannillo * Copyright (C) 2023 Håvard Sørbø * @@ -80,6 +80,13 @@ TESTLIST_BEGIN (stalker) TESTENTRY (prefetch) TESTENTRY (observer) #endif + + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_current) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () #ifdef HAVE_LINUX @@ -93,6 +100,24 @@ struct _GumTestStalkerObserver #endif +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + +struct _RunOnThreadCtx +{ + GumThreadId caller_id; + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + gboolean started; + GumThreadId thread_id; + gboolean * 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); @@ -164,6 +189,12 @@ G_DEFINE_TYPE_EXTENDED (GumTestStalkerObserver, 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 (gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); + static const guint32 flat_code[] = { 0xcb000000, /* sub w0, w0, w0 */ 0x91000400, /* add w0, w0, #1 */ @@ -2391,3 +2422,144 @@ gum_test_stalker_observer_increment_total (GumStalkerObserver * observer) } #endif + +TESTCASE (run_on_thread_current) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, thread_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + g_assert_cmpuint (ctx.thread_id, ==, thread_id); +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + g_assert_true (accepted); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +} + +static void +run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + RunOnThreadCtx * ctx = user_data; + + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); + + if (ctx->thread_id == ctx->caller_id) + g_assert_null (cpu_context); + else + g_assert_nonnull (cpu_context); +} + +TESTCASE (run_on_thread_other) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, other_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmphex (ctx.thread_id, ==, other_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, other_id, + run_on_thread, &ctx); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (ctx.thread_id, ==, other_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (gboolean * done, + GumThreadId * thread_id) +{ + GThread * thread; + TestThreadSyncData sync_data; + + 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); + + *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; + 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 01c0c3ba0..683afaca6 100644 --- a/tests/core/arch-x86/stalker-x86.c +++ b/tests/core/arch-x86/stalker-x86.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2023 Ole André Vadla Ravnås + * Copyright (C) 2009-2024 Ole André Vadla Ravnås * Copyright (C) 2010-2013 Karl Trygve Kalleberg * * Licence: wxWindows Library Licence, Version 3.1 @@ -120,6 +120,13 @@ TESTLIST_BEGIN (stalker) TESTENTRY (try_and_dont_catch_excluded) TESTGROUP_END () #endif + + TESTGROUP_BEGIN ("RunOnThread") + TESTENTRY (run_on_thread_current) + TESTENTRY (run_on_thread_current_sync) + TESTENTRY (run_on_thread_other) + TESTENTRY (run_on_thread_other_sync) + TESTGROUP_END () TESTLIST_END () #ifdef HAVE_LINUX @@ -150,6 +157,24 @@ struct _PrefetchBackpatchContext #endif +typedef struct _RunOnThreadCtx RunOnThreadCtx; +typedef struct _TestThreadSyncData TestThreadSyncData; + +struct _RunOnThreadCtx +{ + GumThreadId caller_id; + GumThreadId thread_id; +}; + +struct _TestThreadSyncData +{ + GMutex mutex; + GCond cond; + gboolean started; + GumThreadId thread_id; + gboolean * done; +}; + static gpointer run_stalked_briefly (gpointer data); #ifdef HAVE_LINUX static gpointer run_spawned_thread (gpointer data); @@ -239,6 +264,12 @@ 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 (gboolean * done, + GumThreadId * thread_id); +static gpointer sleeping_dummy (gpointer data); + static const guint8 flat_code[] = { 0x33, 0xc0, /* xor eax, eax */ 0xff, 0xc0, /* inc eax */ @@ -3482,3 +3513,144 @@ test_check_followed (void) } #endif + +TESTCASE (run_on_thread_current) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, thread_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + g_assert_cmpuint (ctx.thread_id, ==, thread_id); +} + +TESTCASE (run_on_thread_current_sync) +{ + GumThreadId thread_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread_id = gum_process_get_current_thread_id (); + ctx.caller_id = thread_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, thread_id, + run_on_thread, &ctx); + g_assert_true (accepted); + g_assert_cmpuint (thread_id, ==, ctx.thread_id); +} + +static void +run_on_thread (const GumCpuContext * cpu_context, + gpointer user_data) +{ + RunOnThreadCtx * ctx = user_data; + + g_usleep (250000); + ctx->thread_id = gum_process_get_current_thread_id (); + + if (ctx->thread_id == ctx->caller_id) + g_assert_null (cpu_context); + else + g_assert_nonnull (cpu_context); +} + +TESTCASE (run_on_thread_other) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread (fixture->stalker, other_id, + run_on_thread, &ctx, NULL); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmphex (ctx.thread_id, ==, other_id); +} + +TESTCASE (run_on_thread_other_sync) +{ + GThread * thread; + gboolean done = FALSE; + GumThreadId other_id, this_id; + RunOnThreadCtx ctx; + gboolean accepted; + + thread = create_sleeping_dummy_thread_sync (&done, &other_id); + + this_id = gum_process_get_current_thread_id (); + g_assert_cmphex (this_id, !=, other_id); + ctx.caller_id = this_id; + ctx.thread_id = G_MAXSIZE; + + accepted = gum_stalker_run_on_thread_sync (fixture->stalker, other_id, + run_on_thread, &ctx); + g_assert_true (accepted); + done = TRUE; + g_thread_join (thread); + g_assert_cmpuint (ctx.thread_id, ==, other_id); +} + +static GThread * +create_sleeping_dummy_thread_sync (gboolean * done, + GumThreadId * thread_id) +{ + GThread * thread; + TestThreadSyncData sync_data; + + 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); + + *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; + 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; +}