writing Linux, Kernel, shellcode

Cacheghost: Executing code from the kernel page cache

July 30, 2026 · updated · 5 min read · 48 views · Linux, Kernel, shellcode linux-kernel page-cache mmap memory-management proc-maps edr-evasion memory-forensics shellcode-execution stealth-execution red-teaming c poc shared-memory system-programming offensive-security

The technique

MAP_SHARED file mappings don't go to disk immediately. Writes go to the page cache. The file gets updated later, when the kernel feels like writing back dirty pages.

If you write shellcode to a MAP_SHARED mapping and then another process mmaps the same file MAP_SHARED at the same offset, the kernel gives it the dirty page cache page, not the clean file from disk. If the second mapping is PROT_EXEC, the shellcode executes.

The file on disk is never touched by the injection. /proc/pid/maps shows a normal file-backed mapping.


The PoC

static const unsigned char shellcode[] = {
    0x6a, 0x2a,             /* push    42            */
    0x5f,                   /* pop     rdi           */
    0x48, 0x31, 0xc0,       /* xor     rax, rax      */
    0xb0, 0x3c,             /* mov     al, 60        */
    0x0f, 0x05              /* syscall               */
};

int fd = open(path, O_CREAT|O_RDWR|O_TRUNC, 0644); write(fd, benign, 4096); fsync(fd);

/* seeder: mmap + write shellcode to page cache */ void *map = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); memcpy(map, shellcode, sizeof(shellcode));

/* executor: fork + mmap + execute */ pid_t pid = fork(); if (pid == 0) { void *emap = mmap(NULL, 4096, PROT_READ|PROT_EXEC, MAP_SHARED, fd, 0); /* jump to the shellcode in the page cache */ goto *emap; // or asm volatile("call *%0" : : "r"(emap)) }

waitpid(pid, &status, 0); // status should be 42

Output:

=== cacheghost ===

[1] seeder mmap MAP_SHARED + memcpy(shellcode) [2] executor pid=102367 /proc/self/maps: 7ff325ade000-7ff325adf000 r-xs ... /home/user/.cacheghost 7ff325adf000-7ff325ae0000 rw-s ... /home/user/.cacheghost [3] call shellcode... [4] exit code: 42

Exit code 42 confirms the child executed code from the page cache. The mapping is r-xs (read-execute-shared, file-backed). The other rw-s mapping is inherited from the parent.


What's Actually Happening

On the seeder's memcpy, the store hits the page cache page. The kernel marks it dirty and will eventually write it back, but not yet. The file on disk still has benign data.

On the executor's mmap, the kernel calls filemap_fault(), which looks up the page in the file's address_space xarray. The page is already there, dirty, with the shellcode. find_get_page() returns it. The fault handler installs a PTE pointing to that physical page. No disk read. The child calls into it and the shellcode runs.

The file on disk isn't involved at any point.


What This Is

This is a demonstration that MAP_SHARED writeback semantics can produce an executable mapping whose content doesn't match the backing file. It's a curiosity about how the page cache works.

It's not a new execution primitive. The seeder already has arbitrary code execution (it just wrote to memory). The technique doesn't give you execution in a process you don't control.

It's not a kernel vulnerability. MAP_SHARED is supposed to work this way. Dirty pages are supposed to propagate to new mappers. That's the entire point of shared mappings.

What it is: a narrow observation about forensic visibility. Instead of executing shellcode from an anonymous rwxp mapping (which every EDR looks for), you can execute it from a file-backed r-xs mapping. The mapping looks normal. Whether that matters depends on what your specific EDR checks.


The Detection Angle

This is the part I'm least sure about, so I'll lay out what I know and what I don't.

The technique produces a file-backed r-xs mapping where the inode is valid and the path is real. An EDR that only scans for rwxp anonymous mappings won't flag it. An EDR that checks whether the content of a mapped page matches the file's disk content would need to bypass the page cache to do so, most don't.

But:

  • An EDR that hashed the file at install time and hashes again on demand would see a mismatch after writeback commits the shellcode to disk. The file changed. That's detectable.
  • An EDR that monitors mmap syscalls for PROT_EXEC on non-standard files could flag this immediately.
  • Volatility plugins like linux_pagecache can enumerate page cache pages. Comparing them against disk is possible, just uncommon.
  • The r-xs mapping on a .cacheghost_demo file is itself suspicious. Rename it to libsomething.so and it blends in better.

I'm not claiming this bypasses all detection. I'm claiming it bypasses a specific class of detection, the kind that only looks for rwxp anonymous memory and assumes file-backed mappings are safe. That covers a lot of Linux EDR agents, but far from all.


The Writeback Problem

The dirty page cache page gets written to disk eventually. The default dirty_expire_centisecs is 3000 (30 seconds). After writeback:

  • The file on disk now has the shellcode
  • The sha256 doesn't match the original
  • If an EDR checks the file hash, it sees a change

So the stealth window is at most 30 seconds without writeback prevention. If you write to the page periodically (a byte every second), the page stays young and writeback doesn't claim it. On tmpfs, there's no writeback at all.

But this is a limitation, not a feature. The technique is for short-lived operations where the shellcode runs once and exits. It's not for persistent stealth, maybe could be used for stealers? idk.


Comparison with Other Approaches

technique file modified exec memory maps entry
memfd_create + execve no no /proc/self/fd/N
ptrace injection no yes rwxp
process_vm_writev no yes rwxp
LD_PRELOAD yes no .so mapping
memfd + fexecve no no memfd (tmpfs)
Dirty Pipe conditional depends depends
cacheghost no no r-xs file-backed

The closest comparison is memfd_create. Both leave the disk clean. The difference: memfd mappings are anonymous (tmpfs, no persistent path). cacheghost mappings point to a real file with a real inode. Depending on your threat model, that's either better (blends with other file mappings) or worse (the file path is visible).


Assessment

The technique is 80 lines of C that demonstrates a known kernel behavior being used in an unusual way. The interesting part is the observation that this behavior creates a blind spot in some detection tooling. The execution primitive itself isn't new.

I think that's fine.


The PoC is at https://github.com/klydz/cacheghost.

← older Bypassing XDP and TC: Stealthy Connection Interception via BPF SK_LOOKUP newer → I Spent a Week Fuzzing the BPF Verifier's New Circular Number System