1455 words
7 minutes
Patch 9 — Renaming the `jit-cache` memfd Descriptor

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

CAUTION

#FreePalestine

WARNING

This is just for educational purposes.


Patch 9 — Replacing the memfd_create Name with "jit-cache"#

Introduction#

Frida’s JIT compiler (via Frida-Gum’s code generation pipeline) needs executable, writable memory regions to compile and store trampolines, inline hooks, and generated stubs at runtime. On Linux and Android, Frida allocates this memory using memfd_create — a Linux syscall that creates an anonymous, memory-backed file descriptor. Like all file descriptors, the one created by memfd_create appears in /proc/<pid>/fd/ as a symbolic link, and its name is visible in that symlink’s target string as /memfd:<name> (deleted).

Before this patch, the name argument passed to memfd_create is whatever the calling code supplies — in practice a Frida-branded string. The name appears verbatim in /proc/<pid>/fd/ and /proc/<pid>/maps, making it yet another static fingerprint readable from inside the process without any privileges.

This patch hardcodes "jit-cache" as the memfd_create name regardless of what the caller passes. jit-cache is the exact name the Android Runtime (ART) uses for its own JIT compilation memory regions — making Frida’s allocation indistinguishable from a legitimate ART artifact.


The Original Function / String#

File: lib/base/linux.vala
Namespace: Frida
Function: memfd_create(string name, uint flags) — a private wrapper method

The function is a thin Vala wrapper around the raw Linux memfd_create(2) syscall:

private int memfd_create (string name, uint flags) {
    return Linux.syscall (LinuxSyscall.MEMFD_CREATE, name, flags);
}

It is called from the surrounding class whenever Frida needs to allocate anonymous executable memory. The name parameter is passed straight through to the kernel, which stores it internally and exposes it through the /proc filesystem.

How the kernel exposes memfd_create names:

When memfd_create("frida-<something>", MFD_CLOEXEC) is called (the exact name varies by Frida version — common examples include frida-jit-code or similar descriptive labels):

  1. The kernel creates an anonymous inode in tmpfs.
  2. The caller-supplied name is attached to that inode.
  3. A file descriptor is returned and appears in /proc/<pid>/fd/<n> as a symlink pointing to /memfd:<name> (deleted).
  4. Once the memory is mapped (via mmap on the fd), the mapping appears in /proc/<pid>/maps with the pathname /memfd:<name> (deleted).

Both /proc/<pid>/fd and /proc/<pid>/maps are readable by any thread in the process, and by a privileged companion process scanning from outside.

Why this is a detection surface:

  • The string frida-jit-code (or whatever variant Frida uses) is visible in /proc/<pid>/maps for as long as the JIT-compiled memory region is mapped — which is the entire duration of any active instrumentation session.
  • It is also visible in /proc/<pid>/fd as a live file descriptor entry.
  • Neither of these paths requires the agent .so to be on disk — they exist purely in kernel memory. All previous filename-based patches (Patch 2) cannot affect this surface.
  • strings on the binary also finds the name literals passed to memfd_create since they are string arguments compiled into the Vala/C code.

Before Patching — Behavior on Android#

memfd-backed mappings visible in /proc/<pid>/maps:

$ adb shell cat /proc/$(pidof com.example.targetapp)/maps | grep memfd
7a1c000000-7a1c100000 rwxp 00000000 00:05 131073  /memfd:frida-jit-code (deleted)
7a1c100000-7a1c200000 rwxp 00000000 00:05 131074  /memfd:frida-jit-code (deleted)

The rwxp permissions (read, write, execute, private) are themselves a secondary heuristic — few legitimate Android libraries map RWX anonymous regions. But the frida-jit-code name removes all ambiguity.

memfd file descriptors visible in /proc/<pid>/fd:

$ adb shell ls -la /proc/$(pidof com.example.targetapp)/fd | grep memfd
lrwxrwxrwx 1 u0_a123 u0_a123 64 /proc/12345/fd/41 -> /memfd:frida-jit-code (deleted)
lrwxrwxrwx 1 u0_a123 u0_a123 64 /proc/12345/fd/42 -> /memfd:frida-jit-code (deleted)

String visible in the compiled binary:

$ adb shell strings /data/local/tmp/frida-agent-64.so | grep "jit"
frida-jit-code

How Apps Detect It#

Method 1 — Scanning /proc/self/maps for memfd:frida entries (C++)#

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

bool detectFridaMemfd() {
    FILE *maps = fopen("/proc/self/maps", "r");
    if (!maps) return false;

    char line[512];
    while (fgets(line, sizeof(line), maps)) {
        // memfd entries appear as: address perms ... /memfd:<name> (deleted)
        if (strstr(line, "/memfd:frida") != NULL) {
            fclose(maps);
            return true;
        }
    }
    fclose(maps);
    return false;
}
#include <dirent.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

bool detectFridaMemfdFd() {
    DIR *fd_dir = opendir("/proc/self/fd");
    if (!fd_dir) return false;

    struct dirent *entry;
    char target[256];

    while ((entry = readdir(fd_dir)) != NULL) {
        char fd_path[64];
        snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%s", entry->d_name);

        ssize_t len = readlink(fd_path, target, sizeof(target) - 1);
        if (len <= 0) continue;
        target[len] = '\0';

        if (strstr(target, "/memfd:frida") != NULL) {
            closedir(fd_dir);
            return true;
        }
    }
    closedir(fd_dir);
    return false;
}

Method 3 — Heuristic: RWX anonymous mappings combined with name check (C++)#

Some detectors use a two-stage check: first flag any RWX memfd mapping (unusual in legitimate Android code), then confirm by name:

bool detectSuspiciousRwxMemfd() {
    FILE *maps = fopen("/proc/self/maps", "r");
    if (!maps) return false;

    char line[512];
    while (fgets(line, sizeof(line), maps)) {
        // Check for rwxp (read+write+execute private) memfd mappings
        // format: addr-addr rwxp offset dev inode pathname
        if (strstr(line, "rwxp") == NULL) continue;
        if (strstr(line, "/memfd:") == NULL) continue;

        // Any non-ART memfd RWX mapping is suspicious
        if (strstr(line, "/memfd:jit-cache") != NULL) continue;   // legitimate ART
        if (strstr(line, "/memfd:dalvik-") != NULL) continue;      // legitimate ART

        fclose(maps);
        return true;  // Unrecognised RWX memfd — likely Frida
    }
    fclose(maps);
    return false;
}

This third method is notably more robust — it does not depend on knowing Frida’s exact name, but relies instead on the absence from a whitelist of known legitimate names. This is exactly why the patch targets "jit-cache" specifically: it is already on that whitelist.


The Patch#

What changed: The name parameter passed through to the kernel syscall is replaced with the hardcoded string "jit-cache":

--- a/lib/base/linux.vala
+++ b/lib/base/linux.vala
@@ -101,7 +101,7 @@ namespace Frida {
 
     private int memfd_create (string name, uint flags) {
-        return Linux.syscall (LinuxSyscall.MEMFD_CREATE, name, flags);
+        return Linux.syscall (LinuxSyscall.MEMFD_CREATE, "jit-cache", flags);
     }
 }

Why "jit-cache" specifically:

The Android Runtime (ART) allocates its own JIT-compiled code regions using memfd_create with the name "jit-cache". On any Android device running ART (Android 5.0+), the target application’s process already has mappings named /memfd:jit-cache (deleted) in its maps file before Frida is ever injected. This means:

  1. Frida’s new memfd:jit-cache entries are camouflaged among pre-existing legitimate ART entries — a detector cannot distinguish them.
  2. Any whitelist-based detector (Method 3 above) explicitly allows jit-cache because it is a known ART artifact.
  3. The name jit-cache appears in the binary as a string literal — innocuous and semantically appropriate for what the memory is actually used for (JIT-compiled code cache).

Why this does not break Frida’s functionality:

The name argument to memfd_create is purely cosmetic metadata attached to the kernel inode. The kernel does not use it for any access control, size limit, or behavioral decision. The file descriptor returned is functionally identical regardless of the name. All of Frida’s subsequent operations on the fd (mapping as executable memory, writing compiled stubs, etc.) work identically with "jit-cache" as the name.


After Patching — Behavior on Android#

/proc/<pid>/maps — Frida’s JIT regions blend into ART’s existing entries:

$ adb shell cat /proc/$(pidof com.example.targetapp)/maps | grep memfd
7a0c000000-7a0c080000 rwxp 00000000 00:05 98304   /memfd:jit-cache (deleted)
7a1c000000-7a1c100000 rwxp 00000000 00:05 131073  /memfd:jit-cache (deleted)
7a1c100000-7a1c200000 rwxp 00000000 00:05 131074  /memfd:jit-cache (deleted)

The first entry is ART’s own JIT cache. The second and third are Frida’s — but they are indistinguishable by name. A detector scanning for memfd:frida finds nothing. A whitelist detector sees only jit-cache entries, all of which it permits.

/proc/<pid>/fd — no frida in any symlink target:

$ adb shell ls -la /proc/$(pidof com.example.targetapp)/fd | grep memfd
lrwxrwxrwx ... /proc/12345/fd/38 -> /memfd:jit-cache (deleted)
lrwxrwxrwx ... /proc/12345/fd/41 -> /memfd:jit-cache (deleted)
lrwxrwxrwx ... /proc/12345/fd/42 -> /memfd:jit-cache (deleted)

$ adb shell ls -la /proc/$(pidof com.example.targetapp)/fd | grep "frida"
(no output)

Static strings scan — frida-jit-code is gone:

$ adb shell strings /data/local/tmp/frida-agent-patched-64.so | grep "jit"
jit-cache

The string jit-cache remains — but it is the ART-standard name, not a Frida-specific one. Any scanner that hits it will also hit every other legitimate Android process running ART, making it useless as a Frida-specific indicator.


Noob Section (In Simple Words)#

The problem:

When Frida compiles hooks on the fly, it needs a chunk of memory to store the compiled code. On Android/Linux it creates this memory using a system call called memfd_create, which takes a name tag. Frida passes something like frida-jit-code as that name. The problem is that the name shows up in two places any code in the app can read: the process’s memory map (/proc/<pid>/maps) and its file descriptor list (/proc/<pid>/fd). So even if you renamed the .so file and cleaned up all the thread names, a security tool can still find /memfd:frida-jit-code sitting right there.

What this patch does:

It hardcodes the name to "jit-cache" instead of whatever Frida-branded name was being used. jit-cache is the exact same name that Android’s own runtime (ART) uses for its JIT-compiled code regions. Every Android app already has jit-cache entries in its memory map, so adding a few more looks completely normal. You can learn more about JIT Cache from here.

The result:

Security tools scanning /proc/<pid>/maps or /proc/<pid>/fd for anything containing “frida” find nothing. The Frida memory regions now blend in perfectly with the app’s own ART JIT cache entries. Even smarter detectors that use a whitelist of “known safe” names will let jit-cache through because it’s a standard Android artifact.

What about AOT? ummmmmm? AOT Cache?