CVE-2026-31431: Copy Fail vs. rootless containers

A technical deep dive into CVE-2026-31431 (Copy Fail), analyzing how rootless Podman architecture successfully contains this kernel-level privilege escalation exploit.
CVE-2026-31431: Copy Fail vs. rootless containers
04 May 2026
Table of Contents
- Introduction
- The vulnerability
- Analyzing the shellcode
- Setting up the lab
- Setting up rootless Podman
- Running the exploit inside a container
- Tracing the exploit mechanism
- Why rootless containers stopped the escalation
- Conclusions
Introduction
In the previous post about SELinux MCS and GitLab runners, I briefly mentioned CVE-2026-31431 (“Copy Fail”) as a motivating example for per-job VM isolation. After that post went out I spent the weekend setting up a lab to actually run the exploit, trace it at the syscall level, and verify that the rootless Podman architecture we deploy on GNOME’s runners would contain it. This post documents the entire process: from disassembling the shellcode to watching the kernel reject the privilege escalation in real time.
The vulnerability
For a full technical breakdown of the root cause, the scatterlist mechanics, and the disclosure timeline, read Theori’s excellent writeup at xint.io/blog/copy-fail-linux-distributions. In this blog post we’ll initially analyze the shellcode embedded in the public exploit, then set up a lab to run it inside a rootless container and subsequently trace what happens at the kernel level.
Analyzing the shellcode
In the days following the disclosure I noticed a lot of people running the exploit on their systems without bothering to check what the shellcode actually does. Executing a compressed binary blob from a GitHub repository you have never audited is not a great security practice — for all you know it could be exfiltrating data or dropping a backdoor alongside the privilege escalation. So before running anything, let’s look at what the actual shellcode contains.
The shellcode is embedded in the Python exploit as a compressed and hex-encoded string. The script uses zlib.decompress() to turn this into raw bytes. Running file on the extracted binary confirms it is an ELF 64-bit LSB executable, x86-64, statically linked.
This is not raw shellcode — it is a fully formed ELF executable. The exploit overwrites the beginning of /usr/bin/su with this tiny binary. When the OS executes su, it loads the corrupted pages from the page cache and runs the malicious ELF instead of the legitimate utility.
The actual code performs a setuid(0) syscall followed by an execve("/bin/sh"). If execve fails, the payload calls exit(0) rather than crashing.
Setting up the lab
To reproduce the vulnerability I provisioned a Fedora 43 VM using virt-install. The kernel I had installed was 6.17.1-300.fc43.x86_64, which predates the fix entirely — the patch was backported into the stable 6.19.x tree starting with 6.19.12, so the entire 6.17.x line is vulnerable.
Setting up rootless Podman
On the Fedora VM, I configured rootless Podman following the same patterns we use on GNOME’s GitLab runners — a dedicated podman system user with linger enabled, pasta for networking, and a large Sub-UID/Sub-GID allocation.
Running the exploit inside a container
Running strace inside a container requires two overrides: --cap-add=SYS_PTRACE and --security-opt seccomp=unconfined. Without both, strace will fail immediately. Inside the container, I installed strace, created an unprivileged test user, and ran the exploit script.
Tracing the exploit mechanism
The strace output captured the exact mechanism by which the vulnerability corrupts the page cache. The exploit leverages socket, bind, setsockopt, and splice to trigger the kernel bug, allowing it to write arbitrary data into the memory pages associated with system binaries.
Why rootless containers stopped the escalation
Even though the exploit successfully triggers the kernel bug, the rootless architecture provides a critical layer of defense. Because the container runs inside a User Namespace, the "root" user (UID 0) inside the container is mapped to a non-privileged UID on the host.
When the shellcode attempts to gain root privileges, it is only gaining privileges within that specific namespace. It cannot modify the actual /usr/bin/su on the host filesystem because the underlying host user lacks the necessary permissions. The kernel's UID mapping effectively traps the exploit within the container's boundaries.
Conclusions
CVE-2026-31431 is a powerful reminder of how kernel vulnerabilities can bypass traditional security boundaries. However, it also highlights the effectiveness of defense-in-depth strategies. By utilizing rootless containers and User Namespaces, we can significantly mitigate the impact of even the most severe privilege escalation exploits.
Source: Hacker News















