1792 words
9 minutes
Patch 11 — Renaming the `frida-gum` Thread Pool

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

CAUTION

#FreePalestine

WARNING

This is just for educational purposes.


Patch 11 — Changing g_set_prgname("frida") at the Source in frida-gum#

Introduction#

Patch 8 added a g_set_prgname("ggbond") call in frida-core’s frida_init_with_runtime() to override the program name before GLib thread pool workers are created. That patch works — but it is a downstream override applied after the name "frida" has already been set by a deeper layer. Patch 11 goes to the actual origin: gum/gum.c inside the frida-gum subproject, which explicitly sets the program name to "frida" during its own embedded initialisation. Changing it here eliminates the root cause rather than patching over it.

This patch operates on a different repository from all previous patches. The first 10 patches target frida-core. This one targets frida-gum — the lower-level native instrumentation library that frida-core depends on. It is consequently numbered 0001 in its own patch series for the frida-gum subproject.


The Original Function / String#

Repository: frida-gum
File: gum/gum.c
Function: gum_init_embedded(void)

gum_init_embedded is the initialisation entry point for GumJS when Frida-Gum is being used in an embedded context — i.e., injected into a host process rather than running as a standalone application. It sets up GLib’s type system, logging, and core Gum internals. Near the end of this initialisation sequence it explicitly registers the program name:

void
gum_init_embedded (void)
{
  /* ... GLib and Gum setup ... */

  g_log_set_default_handler (gum_on_log_message, NULL);
  gum_do_init ();

  g_set_prgname ("frida");    // ← the root source of the "frida" prgname

#if defined (HAVE_LINUX) && defined (HAVE_GLIBC)
  gum_libdl_prevent_unload ();
#endif
}

This call is the definitive registration of "frida" as GLib’s program name in the context of an injected Frida agent. Every subsequent GLib subsystem that reads g_get_prgname() — in particular the GThreadPool worker thread naming logic (pool-<prgname>-<N>) — derives its value from this single line.

The relationship between Patch 8 and Patch 11:

PatchFileCallRuns when
Patch 8src/frida-glue.c (frida-core)g_set_prgname("ggbond")frida_init_with_runtime() — called from the agent entrypoint after gum_init_embedded
Patch 11gum/gum.c (frida-gum)g_set_prgname("frida")g_set_prgname("ggbond")gum_init_embedded() — called first, sets the name to "frida"

Without Patch 11, the sequence is:

  1. gum_init_embedded()g_set_prgname("frida") — name is now "frida"
  2. GLib thread pool workers may be spawned here, picking up pool-frida-N
  3. frida_init_with_runtime()g_set_prgname("ggbond") — name is now "ggbond"
  4. Later thread pool workers pick up pool-ggbond-N

There is a race window between steps 1 and 3 where threads created during gum_init_embedded itself get the name pool-frida-N. With Patch 11, "frida" is never set at all — step 1 sets "ggbond" directly, making Patch 8 a redundant-but-harmless defense-in-depth layer.

Why it is a detection surface:

  • GLib’s thread pool may create threads during gum_init_embedded itself (e.g., for the GLib worker thread pool used by GTask and async I/O operations that Gum initialises).
  • Any such threads, created during the window when prgname is still "frida", would be named pool-frida-N and persist even after Patch 8’s override runs, because pthread_setname_np is called once at thread creation time and the name is not retroactively updated when g_set_prgname is called again.
  • A detector that scans thread names immediately after injection — before frida_init_with_runtime completes — will see pool-frida-N entries created during gum_init_embedded, even if Patch 8 is applied.

Before Patching — Behavior on Android#

Thread names captured during the injection windows — leaked pool-frida entries:

If a detector polls /proc/<pid>/task/<tid>/comm during the brief window between gum_init_embedded returning and frida_init_with_runtime completing Patch 8’s override:

android:/ # while true; do
>   ls /proc/12345/task/ | xargs -I{} sh -c 'printf "{}: "; cat /proc/12345/task/{}/comm' 2>/dev/null | grep "pool-frida"
>   sleep 0.01
> done
12356: pool-frida-1
12357: pool-frida-2

These threads were created during gum_init_embedded with the name pool-frida-N. Even after Patch 8 calls g_set_prgname("ggbond"), the kernel’s stored name for TIDs 12356 and 12357 remains pool-frida-1 and pool-frida-2 — thread names are set once at creation and are not retroactively updated.

Confirming the literal "frida" string in frida-gum’s compiled output:

$ strings libfrida-gum.a | grep -w "frida"
frida
frida_agent_main
...

The string "frida" as a standalone word — the argument to g_set_prgname — is a literal in gum.c’s compiled object and ends up in the final frida-agent.so which statically links frida-gum.


How Apps Detect It#

Method 1 — Race-condition-aware polling at injection time (C++)#

An injected SDK that runs detection code early in its own JNI_OnLoad can catch the pool-frida-N threads created during Gum’s initialisation window if it polls immediately:

// Runs in JNI_OnLoad, very early in process startup
static void earlyThreadScan() {
    // Poll repeatedly for the first 500ms to catch names set during injection window
    for (int attempt = 0; attempt < 50; attempt++) {
        if (detectPoolFridaThreads()) {
            triggerTamperResponse();
            return;
        }
        usleep(10000);  // 10ms between polls
    }
}

Where detectPoolFridaThreads() uses strstr(comm, "pool-frida") as shown in Patch 8’s writeup. This polling loop catches threads that are created and named during Gum init even if they are later renamed or if the prgname is changed after the fact.

Method 2 — Thread name history via /proc/<pid>/task snapshot (shell)#

A watchdog process running from outside the target app (as a privileged service or via ADB) can enumerate thread names continuously:

#!/bin/bash
## Continuously monitor target process thread names
PID=$(pidof com.example.targetapp)
while true; do
    for tid in $(ls /proc/$PID/task 2>/dev/null); do
        name=$(cat /proc/$PID/task/$tid/comm 2>/dev/null)
        if echo "$name" | grep -q "pool-frida"; then
            echo "[ALERT] Found Frida pool thread: $tid -> $name"
        fi
    done
    sleep 0.05
done

Method 3 — Static analysis of frida-gum blob for the prgname call (Python)#

A static scanner looking for the pattern of g_set_prgname being called with "frida" in any loaded library can find the specific bytes representing the call sequence in the compiled agent:

import subprocess

def scan_for_frida_prgname(binary_path: str) -> bool:
    result = subprocess.run(["strings", binary_path], capture_output=True, text=True)
    lines = result.stdout.splitlines()
    # Look for standalone "frida" as a string (argument to g_set_prgname)
    return "frida" in lines  # exact match on a full line from strings output

Any occurrence of the standalone string "frida" in the agent binary is suspicious — it is not a substring of a longer word in this context, it is the bare program name argument.


The Patch#

What changed: The argument to g_set_prgname in gum_init_embedded() is changed from "frida" to "ggbond":

--- a/gum/gum.c
+++ b/gum/gum.c
@@ -304,7 +304,7 @@ gum_init_embedded (void)
   g_log_set_default_handler (gum_on_log_message, NULL);
   gum_do_init ();
 
-  g_set_prgname ("frida");
+  g_set_prgname ("ggbond");
 
 #if defined (HAVE_LINUX) && defined (HAVE_GLIBC)
   gum_libdl_prevent_unload ();

Why "ggbond" again (same as Patch 8):

Both patches converge on the same value intentionally. Using "ggbond" in both gum_init_embedded and frida_init_with_runtime means:

  • The prgname is "ggbond" from the very first moment Gum initialises — no window where it is "frida".
  • All thread pool threads created at any point in the initialisation sequence — whether during Gum init, during frida-core init, or after a script is loaded — are uniformly named pool-ggbond-N.
  • There is no observable moment where any thread carries pool-frida-N.

Why this patch lives in frida-gum rather than frida-core:

frida-gum is a git submodule of frida-core. Its gum/gum.c is compiled as part of the frida-gum static library that is linked into frida-agent.so. Changing it requires a separate patch applied to the frida-gum submodule repository — hence the separate patch series numbering (0001 in the frida-gum series vs 0008 in the frida-core series). This is why the Florida patch set maintains two parallel patch series: nine patches for frida-core and this one patch for frida-gum.

Why this does not break Frida’s functionality:

Identical reasoning to Patch 8 — g_get_prgname() is cosmetic metadata used only for thread names and log message prefixes. No Frida runtime behavior, communication protocol, or injection mechanism depends on the value being "frida". Changing it to "ggbond" makes no functional difference to any operation Frida performs.


After Patching — Behavior on Android#

Thread names from the very first moment of injection — no pool-frida at any point:

android:/ # while true; do
>   ls /proc/12345/task/ | xargs -I{} sh -c 'printf "{}: "; cat /proc/12345/task/{}/comm' 2>/dev/null | grep "pool"
>   sleep 0.01
> done
12356: pool-ggbond-1
12357: pool-ggbond-2
12356: pool-ggbond-1
12357: pool-ggbond-2

The poll loop that caught pool-frida-N in the injection window now finds only pool-ggbond-N — from thread creation time, with no transitional state.

Static strings scan of the patched frida-gum-linked agent — "frida" as a standalone word is gone:

$ strings frida-agent-patched-64.so | grep -w "frida"
(no output)

Complete thread name state at steady-state operation:

$ adb shell ps -T -p $(pidof com.example.targetapp)
USER           PID   TID PPID  VSZ   RSS WCHAN  PC S NAME
u0_a123      12345 12345 ...              S com.example.targetapp
u0_a123      12345 12346 ...              S Jit thread pool
u0_a123      12345 12347 ...              S RenderThread
u0_a123      12345 12351 ...              S xKbNmPqRjLa    ← gum-js-loop (Patch 4)
u0_a123      12345 12352 ...              S WrXcV          ← gmain (Patch 5)
u0_a123      12345 12353 ...              S BnZpQ          ← gdbus (Patch 7)
u0_a123      12345 12356 ...              S pool-ggbond-1  ← Patches 8+11
u0_a123      12345 12357 ...              S pool-ggbond-2  ← Patches 8+11

No thread name anywhere in the process carries a Frida-associated string at any point in the process lifecycle — from injection through teardown.


Noob Section (In Simple Words)#

The problem:

Patch 8 told GLib “your program name is ggbond” in frida-core, but there’s a deeper layer called frida-gum that runs first and explicitly sets the program name to "frida" before frida-core even gets a chance to override it. This creates a small time window: during Frida-Gum’s initialization, worker threads might be created with the name pool-frida-N. Once a thread is created with a name, that name is permanent, it doesn’t update retroactively. So even though Patch 8 changes the name to ggbond later, any threads born during that early window are stuck with pool-frida forever.

What this patch does:

It goes to the root of the problem in frida-gum’s source code (gum/gum.c) and changes g_set_prgname("frida") to g_set_prgname("ggbond"). Now the name is ggbond from the very first moment, with no window where it’s ever set to frida.

The result:

There is no longer any point in time where a thread could be created with pool-frida in its name. Every worker thread, from the very first one created during initialization to the last one created during a hooking session, is named pool-ggbond-N. Patch 8 still exists as a safety net, but this patch eliminates the root cause. Together they guarantee the word frida never appears in any thread name at any moment during the entire process lifecycle.

Wait, isn’t frida-gum on top of frida-core? Why does it run first?

It’s a common misconception. The layering is actually the other way around: frida-gum is the foundation, and frida-core is built on top of it. Think of it like this: frida-gum is the low-level engine that knows how to intercept functions, manage memory, and run JavaScript inside a process. It doesn’t know anything about devices, USB connections, or app sessions. frida-core is the higher-level layer that adds all of that: connecting to phones, managing sessions, injecting agents, handling the RPC protocol, etc. frida-core depends on frida-gum (it’s literally included as a git submodule), not the other way around. So when the agent starts up, frida-gum initializes first (gum_init_embedded()), sets up its internals and sets the program name to "frida", and only then does frida-core initialize on top of it (frida_init_with_runtime()). That’s why patching the name in frida-core (Patch 8) was too late, the name was already set by frida-gum before frida-core even started.