Skip to content
All posts
eBPFdetectionLinuxATT&CK

Fileless execution on Linux, and why your FIM will never see it

A walk through memfd_create-based execution, what it looks like from the kernel, and the exact eBPF signal that catches it.

Mohamed Genina

Co-Founder & COO · 11 August 2026 · 4 min read

There is a class of Linux payload that never touches a filesystem. Not "deleted itself after running" — never written at all. File integrity monitoring cannot see it, on-disk antivirus cannot scan it, and a forensic image taken afterwards contains no artefact.

It is also about eleven lines of Python.

The primitive

memfd_create(2) creates an anonymous file backed by memory. It behaves like a file — you can write to it, seek in it, and pass its descriptor around — but it has no path on any filesystem. It exists in the process's descriptor table and nowhere else.

Linux will happily execute one.

import ctypes, os
 
libc = ctypes.CDLL("libc.so.6", use_errno=True)
fd = libc.memfd_create(b"payload", 0)
 
with open("/bin/echo", "rb") as f:
    os.write(fd, f.read())
 
os.execv(f"/proc/self/fd/{fd}", ["memfd:payload", "hello"])

Substitute a real payload for /bin/echo and you have a stage-two loader whose binary never exists as a file. Fetch it over HTTPS straight into the descriptor and it never exists on disk at any point in its lifetime.

This is MITRE ATT&CK T1620, Reflective Code Loading. It is used by commodity Linux malware and by every competent red team.

What each layer sees

File integrity monitoring — nothing. No file was created, modified or deleted.

On-disk antivirus — nothing to scan.

Auditd with the default rules — an execve with a path of /proc/self/fd/3, which is unhelpful in a way that is easy to filter out as noise, and frequently is.

Container runtime logs — nothing. The container did not start, stop or change.

Application logs — whatever the application chose to say, which for a compromised application is generally nothing useful.

The kernel — everything. It knows exactly what inode it is about to execute, and it knows that inode lives on tmpfs under an anonymous mount with no path.

The eBPF signal

At sched_process_exec, the kernel hands us a linux_binprm. From it we reach the file, its inode, and the superblock the inode belongs to:

SEC("tracepoint/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx) {
    struct task_struct *task = (void *)bpf_get_current_task();
    struct file *exe = BPF_CORE_READ(task, mm, exe_file);
    struct inode *inode = BPF_CORE_READ(exe, f_inode);
    struct super_block *sb = BPF_CORE_READ(inode, i_sb);
 
    __u64 magic = BPF_CORE_READ(sb, s_magic);
    struct dentry *dentry = BPF_CORE_READ(exe, f_path.dentry);
    bool unhashed = BPF_CORE_READ(dentry, d_flags) & DCACHE_DISCONNECTED;
 
    /* TMPFS_MAGIC with a disconnected dentry and no mount point:
       an anonymous memfd, not a file on a real tmpfs mount. */
    bool from_memfd = (magic == TMPFS_MAGIC) && unhashed;
    ...
}

That is the whole detection. One boolean, computed at exec time, on a code path that runs a few hundred times a second on a busy host.

It becomes exec_from_memfd on the OCSF 1007 Process Activity event:

{
  "class_uid": 1007,
  "activity_id": 1,
  "process": { "name": "memfd:payload", "cmd_line": "/proc/self/fd/3 --stage2" },
  "exec_from_memfd": true
}

Why it is done in-kernel

We could ship every exec event and compute this in the backend. We do not, for two reasons.

The first is cost. Resolving the superblock and dentry flags is a handful of pointer dereferences with no allocation. Doing it in userspace means shipping exe_file metadata on every exec on every host, which is a large volume increase for a fact that is false 99.99% of the time.

The second is more important: the information may not survive the trip. A process that execs from a memfd and exits within milliseconds — which is the common shape — may be gone before a userspace agent gets around to inspecting /proc/<pid>/exe. The race is real and attackers know about it. Computing it inside the exec path means there is no window.

False positives, honestly

Three, and they are worth knowing before you enable the rule at critical severity.

JVM code caches. Some JVM configurations and some Java agents use memfd for generated code. In practice they rarely execve from it, so filtering on the parent process name being a JVM is usually sufficient.

Hermetic build sandboxes. Nix and Bazel occasionally exec from memfd during hermetic builds. This shows up on build fleets and almost nowhere else, which is a good argument for a separate policy on build hosts.

Some container runtime internals. runc historically used memfd to protect its own binary against a container-escape vector — the fix for CVE-2019-5736. A modern runc does this differently, but you may see it on older stacks.

All three are filterable on parent process name. None of them look like a payload fetched over HTTPS into a descriptor and executed with an argv the application has never used.

Catching the rest of the chain

The exec is the signal, not the story. In practice it sits inside a chain, and the chain is what makes it actionable:

detection:
  memfd_exec:
    class_uid: 1007
    activity_id: 1
    exec_from_memfd: true
  egress:
    class_uid: 4001
    connection_info.direction_id: 2
    network_activity.egress_to_internet: true
  condition: memfd_exec and egress within 30s

A memfd exec on its own is worth investigating. A memfd exec followed by an outbound connection to an address the workload has never spoken to is worth waking someone up for.

Try it

The quickstart ends with exactly this test, using /bin/echo as a harmless payload. It takes about five minutes from nothing to a critical finding with a full process tree, and it is a reasonable way to check whether any runtime tool — ours or otherwise — is watching the kernel or reading a log.

Related reading