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
PRETTY_NAME="Ubuntu Resolute Raccoon (development branch)"
VERSION_ID="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 
cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_setgid,cap_setuid,cap_sys_chroot,cap_sys_ptrace,cap_sys_admin,cap_sys_resource=p
------------------------------------------------------------------------

------------------------------------------------------------------------
$ cat /etc/os-release
PRETTY_NAME="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 
cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_sys_chroot,cap_sys_ptrace,cap_sys_admin=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.rootfs_XXXXXX 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 sc_bootstrap_mount_namespace(const struct sc_mount_config 
*config) {
 508     char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX";
 ...
 511     if (mkdtemp(scratch_dir) == NULL) {
 ...
 523     sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL);
 ...
 530     sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL);
 ...
 535         sc_do_mount("none", scratch_dir, "tmpfs", 0, "uid=0,gid=0");
 536         sc_replicate_base_rootfs(scratch_dir, config->rootfs_dir, 
config->mounts);
------------------------------------------------------------------------
 354 static void sc_replicate_base_rootfs(const char *scratch_dir, const char 
*rootfs_dir,
 355                                      const struct sc_mount *root_mounts) {
 ...
 362     if (chown(scratch_dir, 0, 0) < 0) {
 ...
 397         sc_must_snprintf(full_path, sizeof(full_path), "%s/%s", 
scratch_dir, ent->d_name);
 ...
 446         } else if (ent->d_type == DT_REG) {
 ...
 450             int fd = open(full_path, O_CREAT | O_TRUNC, 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.rootfs_XXXXXX
  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 O_NOFOLLOW,
  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.rootfs_XXXXXX directory at line
511, but before the creation of our arbitrary file at line 450, snap-
confine mounts a temporary filesystem onto /tmp/snap.rootfs_XXXXXX 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.rootfs_XXXXXX 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.rootfs_XXXXXX
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
PRETTY_NAME="Ubuntu Resolute Raccoon (development branch)"
VERSION_ID="26.04"

$ id
uid=1001(jane) gid=1001(jane) groups=1001(jane),100(users)

$ ./exploit
scratch directory for constructing namespace: /tmp/snap.rootfs_ZW0eZT
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
PRETTY_NAME="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.rootfs_AkYmf6
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).

Reply via email to