Skip to content

Darwin symbolicator deadlocks when used concurrently with module enumeration #1090

@schirrmacher

Description

@schirrmacher

Summary

On macOS, calling gum_process_enumerate_modules() concurrently with
gum_darwin_symbolicator_new_with_task() (or gum_symbol_details_from_address())
causes a deadlock. The deadlock occurs immediately and is 100% reproducible.

Environment

  • OS: macOS (tested on arm64, Darwin 25.1.0)
  • Frida version: 17.5.2
  • Architecture: arm64

Reproduction

Minimal C reproducer

#include <frida-gum.h>
#include <pthread.h>
#include <mach/mach.h>

static GumAddress g_addr;

static gboolean module_cb(GumModule *m, gpointer d) { return TRUE; }

static void *enum_thread(void *arg) {
    while (1) gum_process_enumerate_modules(module_cb, NULL);
    return NULL;
}

static void *resolve_thread(void *arg) {
    while (1) {
        GumDarwinSymbolicator *s =
gum_darwin_symbolicator_new_with_task(mach_task_self(), NULL);
        if (s) {
            GumDebugSymbolDetails details;
            gum_darwin_symbolicator_details_from_address(s, g_addr, &details);
            g_object_unref(s);
        }
    }
    return NULL;
}

int main(void) {
    gum_init_embedded();
    g_addr = gum_module_find_global_export_by_name("malloc");

    pthread_t t1, t2;
    pthread_create(&t1, NULL, enum_thread, NULL);
    pthread_create(&t2, NULL, resolve_thread, NULL);

    pthread_join(t1, NULL);  // Never returns - deadlock
    pthread_join(t2, NULL);
    return 0;
}

Observed behavior

Both threads immediately deadlock. Neither thread completes a single iteration:

  [Thread A] Starting enumerate_modules loop...
  [Thread B] Starting resolve_symbol loop...
  [Watchdog] 1s: enum=0 (+0), resolve=0 (+0)
  [Watchdog] 2s: enum=0 (+0), resolve=0 (+0)
  [Watchdog] 3s: enum=0 (+0), resolve=0 (+0)
  *** DEADLOCK DETECTED ***

Expected behavior

Both operations should be able to run concurrently without deadlocking.

Analysis

This appears to be a lock ordering issue. The likely scenario:

Thread A (enumerate_modules):
1. Acquires LOCK_A
2. Needs LOCK_B → blocked

Thread B (symbolicator):
1. Acquires LOCK_B
2. Needs LOCK_A → blocked

→ Classic deadlock

Note: Even creating a fresh symbolicator via gum_darwin_symbolicator_new_with_task()
triggers the deadlock - it's not limited to the cached symbolicator used by
gum_symbol_details_from_address().

Impact

This affects any application that:

  • Enumerates modules (e.g., for hooking, introspection)
  • Resolves symbols (e.g., for stack traces, debugging)
  • Does both operations from different threads

Workaround

Currently, the only workaround is to serialize access - never call these functions
concurrently. This is difficult in practice since module enumeration may happen during
initialization while symbol resolution happens during runtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions