1605 words
8 minutes
Patch 8 — Renaming the `pool-frida` Thread Pool

( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )

CAUTION

#FreePalestine

WARNING

This is just for educational purposes.


Patch 8 — Renaming the pool-frida GLib Thread Pool Threads#

Introduction#

GLib’s thread pool implementation names every worker thread it spawns using the pattern pool-<progname>-<N>, where <progname> is whatever the application has registered as its program name via g_set_prgname(). In an unpatched Frida agent, g_set_prgname() is never called explicitly during agent initialisation — so GLib falls back to deriving the program name from the process’s argv[0], or in the context of an injected agent, it can default to a name derived from the library path. In practice, because Frida’s build registers itself as "frida" internally, GLib thread pool workers end up named pool-frida-1, pool-frida-2, etc.

These thread names appear in /proc/<pid>/task/<tid>/comm just like gum-js-loop and gmain, giving detectors yet another static Frida-branded string to search for.

This patch adds a single call — g_set_prgname("ggbond") — inside frida_init_with_runtime() in src/frida-glue.c. This forces GLib to use "ggbond" as the program name before any thread pool workers are created, so all pool threads are subsequently named pool-ggbond-1, pool-ggbond-2, etc. — clean of any Frida branding.


The Original Function / String#

File: src/frida-glue.c
Function: frida_init_with_runtime(FridaRuntime rt)

frida-glue.c is the C-level glue layer that bootstraps Frida’s runtime environment. It is called once, early in agent startup, before GLib’s event loop, thread scheduler, or any subsystem that spawns threads is initialised. This makes it the correct place to set the program name — setting it here ensures it is in place before the first GThreadPool is created.

How GLib names thread pool threads:

GLib’s GThreadPool worker threads are created via an internal function that calls g_thread_new() with a name constructed as:

gchar *thread_name = g_strdup_printf ("pool-%s-%u", g_get_prgname (), pool_id);

g_get_prgname() returns whatever was last set by g_set_prgname(), or a value derived from argv[0] if it was never called. In a dlopen-injected shared library like frida-agent.so, argv[0] belongs to the host process (e.g., com.example.targetapp), but GLib may have already been initialised with a program name from a previous call within the Frida codebase. The Frida build system registers the name "frida" in various places, causing GLib to produce pool-frida-<N> names for its thread pool workers.

Why this is a detection surface:

  • pool-frida-1 (or pool-frida-2, pool-frida-3) are kernel-visible thread names in /proc/<pid>/task/<tid>/comm.
  • They contain the literal substring frida, which any thread-name detector scanning for Frida artifacts will catch even if gum-js-loop and gmain have been randomized.
  • GLib thread pools are created for multiple internal operations: the script scheduler, the D-Bus connection manager, and the injector transport layer — so multiple pool-frida-N threads may exist simultaneously, each a separate hit.
  • The pattern pool-frida- is distinct enough that a substring match (not just exact strcmp) catches all variants regardless of the numeric suffix.

Before Patching — Behavior on Android#

Full thread listing showing all Frida-named threads:

$ adb shell ps -T -p $(pidof com.example.targetapp)
USER           PID   TID PPID  VSZ   RSS WCHAN         PC S NAME
u0_a123      12345 12345 1234  2.1G  45M futex_wait    0  S com.example.targetapp
u0_a123      12345 12346 1234  2.1G  45M futex_wait    0  S Jit thread pool
u0_a123      12345 12347 1234  2.1G  45M futex_wait    0  S RenderThread
u0_a123      12345 12351 1234  2.1G  45M futex_wait    0  S gum-js-loop
u0_a123      12345 12352 1234  2.1G  45M futex_wait    0  S gmain
u0_a123      12345 12353 1234  2.1G  45M futex_wait    0  S gdbus
u0_a123      12345 12354 1234  2.1G  45M futex_wait    0  S pool-frida-1
u0_a123      12345 12355 1234  2.1G  45M futex_wait    0  S pool-frida-2

Reading /proc comm files directly:

android:/ # cat /proc/12345/task/12354/comm
pool-frida-1
android:/ # cat /proc/12345/task/12355/comm
pool-frida-2

Confirming g_get_prgname value by extracting it from GLib internals (if needed post-mortem):

$ adb shell cat /proc/12345/cmdline | tr '\0' '\n' | head -1
com.example.targetapp
$ adb shell grep -a "frida" /proc/12345/comm 2>/dev/null
## Not directly readable but pool thread comms reveal it
$ adb shell ls /proc/12345/task/ | xargs -I{} sh -c 'echo -n "{}: "; cat /proc/12345/task/{}/comm' | grep frida
12354: pool-frida-1
12355: pool-frida-2

How Apps Detect It#

Method 1 — Substring scan of all task comm entries (C++)#

Unlike the exact-name checks for gum-js-loop and gmain, pool-frida-N requires a strstr (substring) check because of the numeric suffix:

#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <stdbool.h>

bool detectPoolFridaThreads() {
    DIR *task_dir = opendir("/proc/self/task");
    if (!task_dir) return false;

    struct dirent *entry;
    while ((entry = readdir(task_dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;

        char comm_path[64], comm[32] = {0};
        snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%s/comm", entry->d_name);

        FILE *f = fopen(comm_path, "r");
        if (!f) continue;
        fgets(comm, sizeof(comm), f);
        fclose(f);

        // Substring match — catches pool-frida-1, pool-frida-2, pool-frida-N, etc.
        if (strstr(comm, "pool-frida") != NULL) {
            closedir(task_dir);
            return true;
        }
    }
    closedir(task_dir);
    return false;
}

Method 2 — Comprehensive thread audit combining all known Frida thread patterns (C++)#

Production detection SDKs rarely check a single thread name in isolation. They run a combined scan covering all known Frida thread patterns together:

static const char *EXACT_FRIDA_THREADS[] = {
    "gum-js-loop",
    "gmain",
    "gdbus",
    NULL
};

static const char *SUBSTR_FRIDA_THREADS[] = {
    "pool-frida",   // matches pool-frida-1, pool-frida-2 ...
    "frida",        // broad catch-all
    NULL
};

bool comprehensiveFridaThreadScan() {
    DIR *task_dir = opendir("/proc/self/task");
    if (!task_dir) return false;

    struct dirent *entry;
    while ((entry = readdir(task_dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;

        char comm_path[64], comm[32] = {0};
        snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%s/comm", entry->d_name);
        FILE *f = fopen(comm_path, "r");
        if (!f) continue;
        fgets(comm, sizeof(comm), f);
        fclose(f);
        comm[strcspn(comm, "\n")] = '\0';

        for (int i = 0; EXACT_FRIDA_THREADS[i]; i++) {
            if (strcmp(comm, EXACT_FRIDA_THREADS[i]) == 0) {
                closedir(task_dir); return true;
            }
        }
        for (int i = 0; SUBSTR_FRIDA_THREADS[i]; i++) {
            if (strstr(comm, SUBSTR_FRIDA_THREADS[i]) != NULL) {
                closedir(task_dir); return true;
            }
        }
    }
    closedir(task_dir);
    return false;
}

Method 3 — GLib g_get_prgname() probe via hook (Frida-against-Frida scenario)#

In adversarial red team vs. blue team scenarios, a protected app may hook g_get_prgname in the loaded GLib to observe what program name the injected agent has registered, catching any call that tries to change the name post-injection. However this is an exotic technique; the simple /proc scan is far more commonly deployed.


The Patch#

What changed: A single g_set_prgname("ggbond") call is inserted into frida_init_with_runtime() before the existing runtime branching logic:

--- a/src/frida-glue.c
+++ b/src/frida-glue.c
@@ -40,6 +40,8 @@ frida_init_with_runtime (FridaRuntime rt)
     g_io_module_openssl_register ();
 #endif
 
+    g_set_prgname ("ggbond");
+
     if (runtime == FRIDA_RUNTIME_OTHER)
     {
       main_context = g_main_context_ref (g_main_context_default ());

Why "ggbond":

ggbond is a reference to GG Bond (猪猪侠), a popular Chinese animated pig superhero character from the show of the same name. It is a plausible-looking short program name, all lowercase, that contains no Frida-related substrings. Any detector scanning for frida as a substring in thread names will not match it. Any detector looking for pool-frida-N patterns will not match pool-ggbond-1.

Why g_set_prgname and not a symbol or binary patch:

g_set_prgname is a GLib API call that stores an internal global string. Thread pool names are constructed dynamically at thread creation time by reading g_get_prgname(). There is no single string literal in the binary that says pool-frida — the string is built at runtime from "pool-" + g_get_prgname() + "-" + thread_id. Therefore no sed replacement or LIEF section patch can address this: only intercepting the value before it is first read will work. Calling g_set_prgname("ggbond") early in frida_init_with_runtime — before any GThreadPool is created — ensures all subsequent pool thread names use the new value.

Why this does not break Frida’s functionality:

GLib’s program name is a cosmetic metadata value. It is used for thread naming, log message prefixes, and similar display purposes. No Frida control-flow logic, no IPC mechanism, and no protocol handshake depends on the value of g_get_prgname(). Changing it to "ggbond" has no effect on agent behavior, script execution, or communication with the host.


After Patching — Behavior on Android#

Thread listing — pool-frida-N is gone, replaced by pool-ggbond-N:

$ adb shell ps -T -p $(pidof com.example.targetapp)
USER           PID   TID PPID  VSZ   RSS WCHAN         PC S NAME
u0_a123      12345 12345 1234  2.1G  45M futex_wait    0  S com.example.targetapp
u0_a123      12345 12346 1234  2.1G  45M futex_wait    0  S Jit thread pool
u0_a123      12345 12347 1234  2.1G  45M futex_wait    0  S RenderThread
u0_a123      12345 12351 1234  2.1G  45M futex_wait    0  S xKbQmTzaRc1
u0_a123      12345 12352 1234  2.1G  45M futex_wait    0  S nLpWj
u0_a123      12345 12353 1234  2.1G  45M futex_wait    0  S jRmNb
u0_a123      12345 12354 1234  2.1G  45M futex_wait    0  S pool-ggbond-1
u0_a123      12345 12355 1234  2.1G  45M futex_wait    0  S pool-ggbond-2

/proc comm files:

android:/ # cat /proc/12345/task/12354/comm
pool-ggbond-1
android:/ # cat /proc/12345/task/12355/comm
pool-ggbond-2

All thread-name detection routines return false:

detectPoolFridaThreads();        // strstr(comm, "pool-frida") → no match on "pool-ggbond-1"
comprehensiveFridaThreadScan();  // no exact or substring match on any thread

Combined thread name coverage after patches 4, 5, 7, and 8:

ThreadUnpatched nameAfter patches
GumJS event loopgum-js-looprandom 11-char mixed-case
GLib main loopgmainrandom 5-char mixed-case
GLib D-Busgdbusrandom 5-char mixed-case
GLib thread pool workerspool-frida-Npool-ggbond-N

All Frida-branded thread names are eliminated. The process thread table is now clean of any Frida-specific strings.


Noob Section (In Simple Words)#

The problem:

GLib (a utility library Frida depends on) creates background worker threads and names them using the pattern pool-<program name>-1, pool-<program name>-2, etc. Since Frida registers itself as "frida" internally, these workers end up named pool-frida-1, pool-frida-2, and so on. These names are visible in the thread list just like gum-js-loop and gmain were, giving security tools yet another way to spot Frida.

What this patch does:

One line of code is added early in Frida’s startup that tells GLib “my program name is ggbond” instead of frida. Then the next question is what the in the world is ggbond? double games bond ? never mind I found it. Since GLib builds the thread names from whatever program name it’s been told, all worker threads now come out as pool-ggbond-1, pool-ggbond-2, etc. No binary patching or find-and-replace needed here, just a single API call that overrides the name before any threads are created.

The result:

The thread list no longer shows anything containing the word frida. The pool-ggbond-N names look like they belong to some random program. Combined with the earlier patches that randomized gum-js-loop, gmain, and gdbus, every single Frida-branded thread name is now gone from the process.