Qualys Security Advisory
Local Privilege Escalation in set-capabilities versions of snap-confine (CVE-2026-8933)
======================================================================== Contents ========================================================================
Summary Analysis Exploitation Acknowledgments Timeline
======================================================================== Summary ========================================================================
We discovered a vulnerability (a Local Privilege Escalation from any user to full root) in snap-confine, "a program used internally by snapd to construct the execution environment for snap applications."
Counter-intuitively, only set-capabilities versions of snap-confine are vulnerable, set-uid-root versions are not; in particular:
- default installations of Ubuntu Desktop 26.04 and 25.10 are vulnerable and exploitable (they ship a set-capabilities /usr/lib/snapd/snap-confine);
- default, up-to-date installations of Ubuntu Desktop 24.04 are also vulnerable and exploitable (they ship a set-capabilities /snap/snapd/current/usr/lib/snapd/snap-confine).
======================================================================== Analysis ========================================================================
After the publication of CVE-2026-3888 and the release of Ubuntu 26.04 Beta, we decided to re-assess the security of snap-confine. One of the most fundamental changes between Ubuntu 24.04 and Ubuntu 26.04 is that /usr/lib/snapd/snap-confine is not set-uid-root anymore; instead, it is set-capabilities now, undoubtedly for security reasons (the Principle of Least Privilege):
------------------------------------------------------------------------ $ cat /etc/os-release PRETTYNAME="Ubuntu Resolute Raccoon (development branch)" VERSIONID="26.04"
$ stat /usr/lib/snapd/snap-confine Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
$ getcap /usr/lib/snapd/snap-confine /usr/lib/snapd/snap-confine capchown,capdacoverride,capdacreadsearch,capfowner,capsetgid,capsetuid,capsyschroot,capsysptrace,capsysadmin,capsysresource=p ------------------------------------------------------------------------
------------------------------------------------------------------------ $ cat /etc/os-release PRETTYNAME="Ubuntu 24.04.3 LTS"
$ stat /usr/lib/snapd/snap-confine Access: (4755/-rwsr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
$ stat /snap/snapd/current/usr/lib/snapd/snap-confine Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
$ getcap /snap/snapd/current/usr/lib/snapd/snap-confine /snap/snapd/current/usr/lib/snapd/snap-confine capchown,capdacoverride,capdacreadsearch,capfowner,capsyschroot,capsysptrace,capsysadmin=p ------------------------------------------------------------------------
Unfortunately, belatedly we realized that this change has a potentially dangerous consequence: because snap-confine is not set-uid-root anymore, it runs with the effective uid of our unprivileged user (but with near- root capabilities), and all the files and directories that it creates therefore do not belong to root anymore, but to our unprivileged user.
For example, snap-confine creates a /tmp/snap.rootfsXXXXXX directory at line 511, and a file inside this directory at line 450, and even though snap-confine quickly changes the ownership of this directory and file to root at lines 362 and 454, a small window of time exists when they are under the complete control of our unprivileged user (between lines 511 and 362, and between lines 450 and 454):
------------------------------------------------------------------------ 507 static void scbootstrapmountnamespace(const struct scmountconfig config) { 508 char scratchdir[] = "/tmp/snap.rootfsXXXXXX"; ... 511 if (mkdtemp(scratchdir) == NULL) { ... 523 scdomount(scratchdir, scratchdir, NULL, MSBIND, NULL); ... 530 scdomount("none", scratchdir, NULL, MSUNBINDABLE, NULL); ... 535 scdomount("none", scratchdir, "tmpfs", 0, "uid=0,gid=0"); 536 screplicatebaserootfs(scratchdir, config->rootfsdir, config->mounts); ------------------------------------------------------------------------ 354 static void screplicatebaserootfs(const char scratchdir, const char rootfsdir, 355 const struct scmount rootmounts) { ... 362 if (chown(scratchdir, 0, 0) < 0) { ... 397 scmustsnprintf(fullpath, sizeof(fullpath), "%s/%s", scratchdir, ent->dname); ... 446 } else if (ent->dtype == DTREG) { ... 450 int fd = open(fullpath, OCREAT | OTRUNC, 0644); ... 454 if (fchown(fd, 0, 0) < 0) { ------------------------------------------------------------------------
======================================================================== Exploitation ========================================================================
Our basic idea for exploiting this vulnerability is to transform it into an arbitrary file creation, by winning these two race conditions:
- first, immediately after the creation of the /tmp/snap.rootfsXXXXXX directory at line 511, and while this directory still belongs to our unprivileged user, we quickly create a symlink inside this directory, whose target is an arbitrary file in the filesystem, and whose name is the name of one of the files that snap-confine will create at line 450 (for example, when setting up the sandbox for the firefox snap, snap-confine will create a file named default256.png);
- second, immediately after the creation of our arbitrary file at line 450 (which succeeds because this open() does not specify ONOFOLLOW, and because snap-confine runs with near-root capabilities), and while this file still belongs to our unprivileged user, we quickly change its permissions to 0666 so that we can still write to it even after snap-confine changes its ownership to root at line 454.
However, to implement this exploit in practice, we must overcome two major problems:
1/ After the creation of the /tmp/snap.rootfsXXXXXX directory at line 511, but before the creation of our arbitrary file at line 450, snap- confine mounts a temporary filesystem onto /tmp/snap.rootfsXXXXXX at line 535; but we cannot create the symlink to our arbitrary file inside this directory because it is not visible from outside the sandbox that snap-confine is setting up (because of the two mounts at lines 523 and 530, which make this temporary filesystem private).
The solution to this first problem is surprisingly simple: immediately after the creation of the /tmp/snap.rootfsXXXXXX directory, and while this directory still belongs to our unprivileged user, we quickly mount a FUSE filesystem onto it (with fusermount), which allows us to unmount (with fusermount -u -z) every filesystem that snap-confine later mounts onto it, at lines 523 and 535.
As a result, when snap-confine reaches line 450, /tmp/snap.rootfsXXXXXX is simply back to the directory that was originally created at line 511 and that still belongs to our unprivileged user: inside this directory we can create the symlink to our arbitrary file, and finally snap-confine creates this arbitrary file at line 450.
2/ In reality, this arbitrary file creation is not entirely arbitrary, because snap-confine itself is confined by a highly restrictive AppArmor profile; for example, we cannot create or write to any file in /etc. To solve this second problem we inspected snap-confine's AppArmor profile, and one of the few allow-read-write rules caught our attention:
/run/udev/ rw,
We therefore exploit our arbitrary file creation to create a .rules file in /run/udev/rules.d, write PROGRAM="/bin/sh -c id>>/tmp/pwned" into it, and mount and unmount a FUSE filesystem (any FUSE filesystem) to trigger the execution of this arbitrary command with full root privileges (via the systemd-udevd daemon):
------------------------------------------------------------------------ $ cat /etc/os-release PRETTYNAME="Ubuntu Resolute Raccoon (development branch)" VERSIONID="26.04"
$ id uid=1001(jane) gid=1001(jane) groups=1001(jane),100(users)
$ ./exploit scratch directory for constructing namespace: /tmp/snap.rootfsZW0eZT ndirs 1 nregs 0 -rw-r--r-- 1 root root 78 Apr 21 21:00 /tmp/pwned /run/udev/rules.d: total 12 -rw-rw-rw- 1 root root 36 Apr 21 21:00 2064396357-default256.png.rules -rw-r----- 1 root root 265 Apr 21 20:20 90-netplan.rules -rw-r----- 1 root root 98 Apr 21 20:20 99-netplan-enp0s3.rules uid=0(root) gid=0(root) groups=0(root) uid=0(root) gid=0(root) groups=0(root) ------------------------------------------------------------------------
------------------------------------------------------------------------ $ cat /etc/os-release PRETTYNAME="Ubuntu 24.04.3 LTS"
$ id uid=1001(jane) gid=1001(jane) groups=1001(jane),100(users)
$ ./exploit scratch directory for constructing namespace: /tmp/snap.rootfsAkYmf6 ndirs 1 nregs 0 -rw-r--r-- 1 root root 78 Apr 21 13:09 /tmp/pwned /run/udev/rules.d: total 8 -rw-rw-rw- 1 root root 36 Apr 21 13:09 429214770-default256.png.rules -rw-r----- 1 root root 162 Apr 21 12:47 90-netplan.rules uid=0(root) gid=0(root) groups=0(root) uid=0(root) gid=0(root) groups=0(root) ------------------------------------------------------------------------
======================================================================== Acknowledgments ========================================================================
We thank everyone at Canonical who worked on this release (Eduardo Barretto and Zygmunt Krynicki in particular). We also thank the members of the linux-distros list (Caryl Takvorian in particular).
======================================================================== Timeline ========================================================================
2026-04-22: We sent our advisory and exploit to the Ubuntu Security Team.
2026-07-13: The Ubuntu Security Team sent their patches to the linux-distros list.
2026-07-14: We sent our advisory to the linux-distros list.
2026-07-21: Coordinated Release Date (14:00 UTC).