The PR_SET_MM_EXE_FILE prctl: How CRIU Rewrites /proc/self/exe Without execve()

2026-07-15

Every process has a kernel-tracked pointer to the file it was execve()'d from. That pointer is what /proc/self/exe resolves to, what readlink on /proc/<pid>/exe returns, and what appears in /proc/<pid>/maps as the main binary. It lives in mm_struct.exe_file — a reference-counted struct file* stashed on the process's memory descriptor. Normally, only execve() can change it, because normally the identity of your binary is fixed at exec time.

But sometimes it isn't. CRIU (Checkpoint/Restore In Userspace) restores a process from a saved image without doing an execve(): it forks a clone, maps the original binary back in with mmap(), and reconstructs the address space page by page. Everything works — except /proc/self/exe, which still points to the CRIU restorer binary, not the original. That leak breaks anything that reads /proc/self/exe to find its own binary (Java, Go runtimes, dynamic linkers doing self-relocation).

The fix is prctl(PR_SET_MM_EXE_FILE) (added in Linux 3.18). You open the target binary, pass the fd, and the kernel swaps mm->exe_file to point at it. From that instant, /proc/self/exe resolves to the new file.

It's not a free lunch. The kernel enforces several rules:

Concrete example: a Go binary calls os.Executable(), which reads /proc/self/exe. Under CRIU restore without PR_SET_MM_EXE_FILE, it returns /usr/sbin/criu. With the prctl, it correctly returns /opt/myapp/server. Same PID, same address space, but the kernel's "what am I?" pointer got rewritten mid-flight.

Rule of thumb: anything you find in /proc/<pid>/ that looks immutable — exe, cmdline, auxv, arg_start/env_start — has a corresponding PR_SET_MM_* subcommand. There are ~15 of them. They exist entirely so that userspace restore tools can lie to the kernel about identity without going through execve().

The clever bit: because mm->exe_file is just a refcounted struct file*, the swap is atomic from the reader's perspective. A concurrent readlink("/proc/self/exe") sees either the old or new path, never a torn result.

Key Takeaway: The kernel's idea of "which binary is this process" is a mutable pointer, and prctl(PR_SET_MM_EXE_FILE) is the only supported way to change it without execve() — which is exactly what CRIU needs to restore a process without re-exec'ing it.

All newsletters