Warning
The author does not maintain this report actively. Please see their reports on newer versions of Fedora Linux for up-to-date information and corrections of errors.
by OK Ryoko, revision 2024-11-28.1
Assumed audience: Linux system administrators, Linux utility authors and Fedora Linux package maintainers. Familiarity with process credentials, capabilities, syscalls, strace, Linux PAM and SELinux is assumed.
In this follow-up piece to SUID-root Binaries in Fedora Custom OS 38, I dive into the SUID-root binaries that are present in Fedora Workstation 38 but not in a minimal installation of Fedora Server 38 or that are present in both spins but behave differently under GNOME. I also discuss the use of file capabilities to limit the level of privilege attainable by those programs.
I provide a high-level summary of outcomes in the “The findings at a glance” section.
Appendix A expands the abbreviations that appear in this report.
Appendix B contains data supporting assertions made throughout the text about why each SUID-root binary is installed.
- Introduction
- Characterization of environments
- Conventions for presenting results
- The SUID-root binaries in detail
- /usr/bin/mount
- /usr/bin/su
- /usr/sbin/userhelper
- /usr/libexec/dbus-1/dbus-daemon-launch-helper
- /usr/libexec/Xorg.wrap
- /usr/libexec/libgtop_server2
- /usr/sbin/mount.nfs
- /usr/bin/fusermount
- /usr/bin/fusermount-glusterfs
- /usr/bin/fusermount3
- /usr/libexec/qemu-bridge-helper
- /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
- /usr/bin/vmware-user-suid-wrapper
- The findings at a glance
- Open questions
- Supporting information
- Feedback and support
- Funding sources
- Licensing
- Revision history
In Fedora Workstation 38, the GNOME desktop environment and default set of graphical user-space applications bring with them a number of new SUID-root binaries not seen in Fedora Custom OS 38. Furthermore, some SUID-root binaries that are also present on Fedora Custom OS 38 behave differently under GNOME.
For each new binary, I answer the following questions with respect to at least one reference use case:
- Why is it installed?
- Why does it need to be SUID-root?
- Can the SUID bit be substituted by zero or more file capabilities?
- If not, can the SUID bit be supplemented with zero or more file capabilities?
- Does the program drop privileges? If so, when?
- How does the default SELinux policy constrain the level of privilege of the program?
For each binary already present in Fedora Custom OS 38, I discuss only the changes to the program’s behavior under GNOME.
As before, the outcomes of this work aren’t comprehensive. Instead, they establish a baseline of understanding that members of the assumed audience can adapt to their needs.
My findings are for an installation of Fedora Workstation 38 1.6, virtualized using the libvirt 9.0.0 API with the QEMU/KVM driver from the image obtained via the following torrent:
https://torrent.fedoraproject.org/torrents/Fedora-Workstation-Live-x86_64-38.torrent
This image had SHA256 checksum 7a444a2e19012023bf0b015ae30135bafc5fd20f4f333310d42b118745093992.
When creating the virtual machine, I added a virtiofs shared file system so that I could extract traces and logs easily.
Here’s some basic system information:
[user@fedora ~]$ uname -a # a: all
Linux fedora 6.2.9-300.fc38.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Mar 30 22:32:58 UTC 2023 x86_64 GNU/Linux
SELinux is enabled and enforcing:
[user@fedora ~]$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33
The selinux-policy-targeted-38.8-2.fc38.noarch package provides the loaded policy.
On the first boot of the new system, I created a single unprivileged and unconfined user who was configured automatically:
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
By default, the Anaconda installer locks the root account:
[user@fedora ~]$ sudo passwd -S root # S: status
root LK 1969-12-30 0 99999 7 -1 (Password locked.)
My shell had a full bounding capability set but no other capabilities:
[user@fedora ~]$ cat /proc/$$/status | grep '^Cap'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
No securebits base or locked flags had been set.
I didn’t upgrade any packages on the system explicitly after the installation procedure.
I installed the following packages from the fedora repository to gain access to utilities and development headers for my analyses:
- bcc-0.25.0-3.fc38.x86_64 for the BPF Compiler Collection
- bcc-tools-0.25.0-3.fc38.x86_64 for the capable utility
- fuse-devel-2.9.9-16.fc38.x86_64 and fuse3-devel-3.13.1-2.fc38.x86_64 for FUSE development headers
- strace-6.6-1.fc38.x86_64 for strace(1)
- setools-console-4.4.3-1.fc38.x86_64 for sesearch(1) and seinfo(1)
I also installed the following packages from the fedora-debuginfo repository to help me interpret syscall stack traces:
- glib2-debuginfo-2.76.1-1.fc38.x86_64
- polkit-libs-debuginfo-122-3.fc38.x86_64
- spice-glib-debuginfo-0.42-1.fc38.x86_64
My first time using dnf(8) to install packages, I was asked whether to import the following GPG key:
Importing GPG key 0xEB10B464:
Userid : "Fedora (38) <[email protected]>"
Fingerprint: 6A51 BBAB BA3D 5467 B617 1221 809A 8D7C EB10 B464
From : /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-38-x86_64
Is this ok [y/N]:
I gave my assent after comparing the fingerprint to the value listed on https://fedoraproject.org/security/ for equality.
Finally, I disabled automatic software updates in GNOME Software to preserve the environment.
I examined the userhelper(8) program in the live environment, which differs from the system installed to a disk as follows:
- the host name is
localhost-live
; - the user name is
liveuser
, and - the root account is unlocked and has an empty password.
I obfuscated timestamps, IP addresses, UUIDs, and USB device names and IDs because they were irrelevant to the analyses.
I omitted all sudo(8) password prompts for brevity.
I normalized all hard tabs to spaces in command output.
I always had strace(1) write output to a file using the --output
option, which I omitted from most reported commands because it doesn’t change the content of the trace.
Likewise, I always redirected the output of journalctl(1) and the capable utility to a file.
In a virtual console (tty2), I searched the root file system for SUID-root binaries using find(1):
[user@fedora ~]$ sudo find / -ignore_readdir_race -perm /u=s -user root
/usr/bin/chage
/usr/bin/fusermount
/usr/bin/fusermount-glusterfs
/usr/bin/fusermount3
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/pkexec
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/bin/vmware-user-suid-wrapper
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/libexec/dbus-1/dbus-daemon-launch-helper
/usr/libexec/openssh/ssh-keysign
/usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
/usr/libexec/Xorg.wrap
/usr/libexec/libgtop_server2
/usr/libexec/qemu-bridge-helper
/usr/sbin/grub2-set-bootflag
/usr/sbin/mount.nfs
/usr/sbin/pam_timestamp_check
/usr/sbin/unix_chkpwd
/usr/sbin/userhelper
I counted 25 results. Of these, the following 11 binaries had not been present on a minimal installation of Fedora Custom OS 38:
/usr/bin/fusermount
/usr/bin/fusermount-glusterfs
/usr/bin/fusermount3
/usr/bin/vmware-user-suid-wrapper
/usr/libexec/dbus-1/dbus-daemon-launch-helper
/usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
/usr/libexec/Xorg.wrap
/usr/libexec/libgtop_server2
/usr/libexec/qemu-bridge-helper
/usr/sbin/mount.nfs
/usr/sbin/userhelper
Unless stated otherwise, all these binaries have owner root
, group root
and mode -rwsr-xr-x
with no extended attributes other than security.selinux
.
I start by discussing changes to the behavior of /usr/bin/mount and /usr/bin/su brought about by the introduction of the GNOME desktop environment. I then switch to the live environment to examine userhelper(8), a binary that supports the Anaconda installer. Finally, I return to the disk installation to look at binaries packaged with GNOME, binaries for (un)mounting file systems, and binaries that support guest desktop agents on virtualized systems.
mount(8) stores user-space mount options in the file at /run/mount/utab. Being in /run, this file doesn’t persist across reboots. The first time that a file system mount requires options to be written to /run/mount/utab, mount(8) will create /run/mount/utab with owner and group equal to the EUID and EGID, respectively, of mount(8). For example, if the first mount is done by an unprivileged user running mount(8) SUID-root, then /run/mount/utab will be created with owner root
and group equal to the primary group of the running user. If mount(8) needs to write to /run/mount/utab and this file already exists, then mount(8) will create a temporary replacement file. mount(8) will call fchown(2) on the temporary file to ensure that the owner and group match the owner and group, respectively, of the existing file at /run/mount/utab. However, if:
- /run/mount/utab was created with owner
root
and grouproot
; - mount(8) is running SUID-root, and
- /usr/bin/mount has only
CAP_SYS_ADMIN
in its permitted capability set (with the effective capability bit set)…
… then the operation to update /run/mount/utab will fail, although the mount(2) syscall will still succeed. To set myself up to demonstrate this, I added a VirtIO disk to the VM and linked the Fedora Workstation 38 Live ISO to the SATA CD-ROM device. I powered the VM on, logged into GNOME via GDM and checked for /run/mount/utab in GNOME Terminal:
[user@fedora ~]$ file /run/mount/utab
/run/mount/utab: ASCII text
[user@fedora ~]$ ls -l /run/mount/utab
-rw-r--r--. 1 root root 86 Jan 1 00:00 /run/mount/utab
[user@fedora ~]$ cat /run/mount/utab
SRC=/dev/sr0 TARGET=/run/media/user/Fedora-WS-Live-38-1-6 ROOT=/ OPTS=uhelper=udisks2
I inferred from the contents of this file that /run/mount/utab had been created automatically by udisksd(8) to mount the Fedora Workstation 38 Live ISO. This was confirmed by the following message in the systemd journal:
Jan 01 00:00:00 fedora udisksd[691]: Mounted /dev/sr0 at /run/media/user/Fedora-WS-Live-38-1-6 on behalf of uid 1000
Source:
sudo journalctl -b
, where-b
is short for--boot
I then reproduced the procedure outlined in SUID-root Binaries in Fedora Custom OS 38. In brief:
[user@fedora ~]$ (echo o; echo n; echo p; echo 1; echo; echo; echo w) | sudo fdisk /dev/vdb
[user@fedora ~]$ sudo mkfs.xfs -f /dev/vdb1 # f: force
[user@fedora ~]$ sudo mkdir -p /mnt/upart # p: parents
[user@fedora ~]$ echo '/dev/vdb1 /mnt/upart xfs defaults,noauto,owner 0 0' | sudo tee -a /etc/fstab > /dev/null
[user@fedora ~]$ sudo systemctl daemon-reload
[user@fedora ~]$ sudo chown user /dev/vdb1
[user@fedora ~]$ sudo chmod u+s /usr/bin/mount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/mount
I then tried to mount the file system:
[user@fedora ~]$ mount /mnt/upart
mount: /mnt/upart: filesystem was mounted, but any subsequent operation failed: Operation not permitted.
[user@fedora ~]$ findmnt /mnt/upart
TARGET SOURCE FSTYPE OPTIONS
/mnt/upart /dev/vdb1 xfs rw,nosuid,nodev,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
strace(1) revealed the failing syscall:
2974<mount> fchown(4</run/mount/utab.AxLs3I>, 0, 0) = -1 EPERM (Operation not permitted)
Source:
sudo strace -u user -fyY mount /mnt/upart
after runningsudo umount /mnt/upart
, where-u
is short for--user
and-fyY
for--follow-forks --decode-fds=path --decode-pids=comm
This syscall failed because I was running mount(8) with EGID 1000 and trying to set the file’s group to 0. As a result, mount(8) couldn’t record the mounting user in /run/mount/utab, meaning that only root could now unmount the file system:
[user@fedora ~]$ umount /mnt/upart
umount: /mnt/upart: must be superuser to unmount.
I was able to overcome this situation by conferring another capability:
[user@fedora ~]$ sudo setcap cap_chown,cap_sys_admin=ep /usr/bin/mount
Thus, I could take the mounting step to completion:
[user@fedora ~]$ sudo umount /mnt/upart
[user@fedora ~]$ findmnt /mnt/upart
[user@fedora ~]$ mount /mnt/upart
[user@fedora ~]$ findmnt /mnt/upart
TARGET SOURCE FSTYPE OPTIONS
/mnt/upart /dev/vdb1 xfs rw,nosuid,nodev,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
[user@fedora ~]$ umount /mnt/upart && findmnt /mnt/upart
[user@fedora ~]$
These considerations regarding /run/mount/utab also apply to mount(8) on Fedora Custom OS 38. I chose to document them here because they are more likely to apply under the default configuration of Fedora Workstation 38. The udisks2 service is enabled on both spins but is started automatically by systemd on only Fedora Workstation 38. Why? Because udisks2 depends on the graphical target, which is not reached on Fedora Custom OS 38:
[user@fedora ~]$ cat /usr/lib/systemd/system/udisks2.service | grep '^WantedBy'
WantedBy=graphical.target
When run from within a terminal emulator that uses X11, i.e., GNOME Terminal, su(1) runs the pam_xauth
module. To investigate potential behavioral changes, I fired up GNOME Terminal and unlocked the root account:
[user@fedora ~]$ sudo passwd -fu root # f: force, u: unlock
Unlocking password for user root.
passwd: Success
[user@fedora ~]$ sudo passwd root
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
I confirmed that the binary in question is SUID-root:
[user@fedora ~]$ ls -l /usr/bin/su
-rwsr-xr-x. 1 root root 58144 Jan 20 2023 /usr/bin/su
I then made the following changes to the binary in accordance with the outcomes of my previous work:
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_audit_write=ep /usr/bin/su
I could thus obtain a root shell in a pseudoterminal:
[user@fedora ~]$ su -lP # l: login, P: pty
Password:
[root@fedora ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[root@fedora ~]# logout
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Everything seemed to be working as expected. Upon inspecting the systemd journal, I found a new error message:
Jan 01 00:00:00 fedora su[2671]: pam_xauth(su-l:session): error creating temporary file `/root/.xauth12qJSL': Permission denied
The pam_xauth
module was trying to create a temporary X11 authentication file that would allow root to access the display of the user running su(1). su(1) is SUID-root and therefore runs with EUID 0. Why can’t it create a file in /root? Because root doesn’t have write permission on their own home directory:
[user@fedora ~]$ sudo ls -dl /root # d: directory
dr-xr-x---. 4 root root 4096 Jan 01 00:00 /root
Adding CAP_DAC_OVERRIDE
to the permitted capability set of /usr/bin/su resolves this error. Add this capability only if you have unlocked root and plan to run su(1) inside an X11-based terminal emulator. Be aware that, when su(1) has CAP_DAC_OVERRIDE
, the pam_unix
authentication module can read /etc/shadow and, consequently, will not delegate to the unix_chkpwd(8) helper.
- pam_xauth(8) man page
- Program to help other programs interface with PAM
- Provided by usermode-1.114-7.fc38.x86_64
- Installed with mode
-rws--x--x
- Has SHA256 checksum 20df73eff73096f516befe7a9e3d6cb2afe277fca269505300ff41bde4f240c6
- Has security context
system_u:object_r:userhelper_exec_t:s0
set by the userhelper 1.8.1 module
userhelper(8) is installed to enable Anaconda to perform live installations.
userhelper(8) expects to run with EUID 0 unconditionally.1
userhelper(8) can run commands as root (CAP_SETGID
and CAP_SETUID
).2 If invoked to do so, then userhelper(8) must be able to execute the PAM stack named after the command.3
userhelper(8) must be able to execute arbitrary PAM stacks, so there’s no general solution to this problem. To constrain the problem space, I decided to focus on userhelper(8) in the context of Anaconda. Thus, I performed the following steps in the live environment.
First, I installed strace(1):
[liveuser@localhost-live ~]$ sudo dnf install strace
Next, I inspected the desktop shortcut for clues:
[liveuser@localhost-live ~]$ cat /usr/share/applications/anaconda.desktop | grep '^Exec'
Exec=/usr/bin/liveinst
[liveuser@localhost-live ~]$ file /usr/bin/liveinst
/usr/bin/liveinst: symbolic link to consolehelper
[liveuser@localhost-live ~]$ command -v consolehelper
/usr/bin/consolehelper
consolehelper(8) turns out to be a wrapper for userhelper(8) and is provided by the same package:
usermode-1.114-7.fc38.x86_64 : Tools for certain user account management tasks
Repo : @System
Matched from:
Filename : /usr/bin/consolehelper
Source:
dnf -q provides /usr/bin/consolehelper
, where-q
is short for--quiet
, after omitting redundant results
In the case of the live installer, consolehelper(8) asks userhelper(8) to run /usr/sbin/liveinst as root:
2513<liveinst> execve("/usr/sbin/userhelper", ["/usr/sbin/userhelper", "-t", "-w", "liveinst"], 0x7ffddf542de8 /* 24 vars */) = 0
...
2513<userhelper> openat(AT_FDCWD</home/liveuser>, "/etc/security/console.apps/liveinst", O_RDWR) = 3</etc/security/console.apps/liveinst>
...
2513<userhelper> clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f90180f6b10) = 2520<userhelper>
...
2520<userhelper> setregid(0, 0) = 0
2520<userhelper> setreuid(0, 0) = 0
...
2520<userhelper> execve("/usr/sbin/liveinst", ["liveinst"], 0x5637dc43be10 /* 12 vars */) = 0
Source:
sudo strace -u liveuser -fyY -E DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" gtk-launch anaconda
, where-E
is short for--env
, after truncating whitespace for readability and omitting select syscalls
userhelper(8) knows how to run liveinst
because it reads the file at /etc/security/console.apps/liveinst, which has the following contents:
USER=root
PROGRAM=/usr/sbin/liveinst
SESSION=true
# has to be here otherwise consolehelper switches off the waiting cursor too early
STARTUP_NOTIFICATION_NAME="Starting Install to Hard Drive"
DOMAIN=anaconda
# DBus session connections fail when running setuid unless we pass DBUS_SESSION_BUS_ADDRESS through
# Keep the Gdk scale settings from the user environment
KEEP_ENV_VARS=DBUS_SESSION_BUS_ADDRESS,LIVECMD,GDK_SCALE
Source:
cat /etc/security/console.apps/liveinst
Next, I took a look at the PAM stack corresponding to liveinst
:
#%PAM-1.0
auth include config-util
account include config-util
session include config-util
Source:
cat /etc/pam.d/liveinst
after truncating whitespace for readability
What’s in the config-util stack?
#%PAM-1.0
auth sufficient pam_rootok.so
auth sufficient pam_timestamp.so
auth include system-auth
account required pam_permit.so
session required pam_permit.so
session optional pam_xauth.so
session optional pam_timestamp.so
Source:
cat /etc/pam.d/config-util
after truncating whitespace for readability
It looks like userhelper(8) must also be able to execute the authentication modules in the system-auth stack:
auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth [default=1 ignore=ignore success=ok] pam_localuser.so
auth sufficient pam_unix.so nullok
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth sufficient pam_sss.so forward_pass
auth required pam_deny.so
Source:
cat /etc/pam.d/system-auth | grep -E '^-?auth'
, where-E
is short for--extended-regexp
, after updating whitespace for readability
From previous work, I inferred that pam_xauth
requires CAP_DAC_OVERRIDE
to create a temporary X11 authentication file and pam_unix
requires CAP_AUDIT_WRITE
to write to the audit log.
After making the following change, I was able to run the installer through the “Install to Hard Drive” desktop shortcut without any seeing any warning or error messages in the systemd journal:
[liveuser@localhost-live ~]$ sudo setcap cap_dac_override,cap_setgid,cap_setuid,cap_audit_write=ep /usr/sbin/userhelper
userhelper(8) drops privileges via setregid(2) and setreuid(2) only when:4
- user authentication has failed;
- the user hasn’t cancelled the operation, and
- the application has been configured to run with fallback.
userhelper(8) doesn’t drop privileges in the context of the live installation.
The targeted SELinux policy doesn’t define any process transitions to the userhelper_t
domain through the userhelper_exec_t
type:
[user@fedora ~]$ sudo sesearch -T -t userhelper_exec_t # T: type_trans, t: target
[user@fedora ~]$
Thus, userhelper(8) runs unconfined.
If after installing Fedora Workstation 38 to a disk you have no other programs that depend on userhelper(8), then consider unsetting the SUID bit on /usr/sbin/userhelper.
All line numbers refer to usermode/[email protected].
The HAVE_FEXECVE
macro is assumed to be undefined per line 58 of src.fedoraproject.org/rpms/usermode/usermode.spec@d0c9ebd, which sets the --without-fexecve
configuration flag.
1 main
asserts that the value returned by geteuid(2) is 0 and otherwise exits with an error (lines 2340–2344)
2 main
calls wrap
(line 2517) to wrap the named program if invoked with the -w
option (line 2516 with w_flag
declared and described on line 2297). If a PAM session is needed (line 2066 with session
assigned on line 1867; this is the case for liveinst
), then wrap
calls become_super_supplementary_groups
(line 2089) before forking itself (line 2103). In the child process, wrap
calls become_super_other
(line 2130) before executing the wrapped program using execv(3) (line 2171). become_super_supplementary_groups
calls initgroups(3) to initialize the supplementary group list for root (line 1019); become_super_other
calls setregid(2) and setreuid(2) to change the GIDs and UIDs, respectively, to 0 (line 1031). If on the other hand a PAM session isn’t needed, then wrap
calls become_super
(line 2228), a thin wrapper for become_super_supplementary_groups
(line 1048) and become_super_other
(line 1049).
3 wrap
calls pam_start
(line 1931), providing the name of the program (declared as program
on line 1501) as the service name
4 wrap
tries to authenticate the user using PAM (lines 1944–1952). If authentication fails (line 1954), the operation hasn’t been cancelled (lines 1956 and 1963) and fallback is permitted (line 1964), then wrap
calls become_normal
(line 1970). In turn, become_normal
calls initgroups(3), setregid(2) and setreuid(2) to become the invoking user (lines 1055–1063).
- Program to help dbus-daemon(1) start a D-Bus service on demand as another user
- Provided by dbus-daemon-1:1.14.6-1.fc38.x86_64
- Installed with group
dbus
and mode-rwsr-x---
- Has SHA256 checksum 24b0af53f8ef22b736928e530d95a9a2063727743998ec3ac8eb81754487c339
- Has security context
system_u:object_r:dbusd_exec_t:s0
set by the dbus 1.19.0 module
dbus-daemon(1) and dbus-launch(1) are installed to enable GDM and Anaconda to start session buses. dbus-daemon-launch-helper is bundled with these programs.
dbus-daemon-launch-helper expects to run with EUID 0 unconditionally.1 It must be able to run programs as an arbitrary user (CAP_SETUID
) and group (CAP_SETGID
).2
To answer this question, I started by looking for a way to run dbus-daemon-launch-helper using the D-Bus API to more closely resemble real-world use cases. This helper is intended to support the system bus, so I first defined a simple D-Bus system service at /usr/share/dbus-1/system-services/org.me.test.service:
[D-BUS Service]
Name=org.me.test
Exec=/bin/true
User=user
I should have been able to start this service by name by messaging the system bus like so:
[user@fedora ~]$ dbus-send --system \
> --dest=org.freedesktop.DBus \
> /org/freedesktop/DBus \
> org.freedesktop.DBus.StartServiceByName \
> string:org.me.test uint32:0
Not so fast. On Fedora Workstation 38, dbus-broker(1) plays the role of the system bus, and dbus-broker-launch(1) activates all services as systemd units—no SUID-root helper needed. I could message the system bus to run my test service but not in a way that would get dbus-daemon-launch-helper to run.
Because there can be only one system bus binding the socket at /run/dbus/system_bus_socket, I reasoned that the simplest way to test this program was to run it directly as the dbus
user:
[user@fedora ~]$ sudo -u dbus /usr/libexec/dbus-1/dbus-daemon-launch-helper org.me.test # u: user
This did run my service as the generic user:
2660<dbus-daemon-lau> setgroups(2, [1000, 10]) = 0
2660<dbus-daemon-lau> setgid(1000) = 0
2660<dbus-daemon-lau> setuid(1000) = 0
2660<dbus-daemon-lau> execve("/bin/true", ["/bin/true"], 0x5582b6c712a0 /* 2 vars */) = 0
Source:
sudo strace -u dbus -fyY /usr/libexec/dbus-1/dbus-daemon-launch-helper org.me.test
after truncating whitespace for readability
I was then able to reproduce this step after making the following change:
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid=ep /usr/libexec/dbus-1/dbus-daemon-launch-helper
As far as I can tell from the source code and strace(1) output, dbus-daemon-launch-helper doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2) unless running a D-Bus system service as a nonroot user. This behavior is consistent with the purpose of the program.
dbus-daemon-launch-helper must be run as the dbus
user, who is unconfined:
[user@fedora ~]$ sudo -u dbus id -Z # Z: context
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
The type dbusd_exec_t
has an entrypoint to the unconfined_dbusd_t
application domain:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_dbusd_t -t dbusd_exec_t -c file -p entrypoint # s: source, c: class, p: perm
allow unconfined_dbusd_t dbusd_exec_t:file entrypoint;
The targeted SELinux policy defines a transition from processes in the unconfined_t
domain to the unconfined_dbusd_t
domain:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t dbusd_exec_t
type_transition unconfined_t dbusd_exec_t:process unconfined_dbusd_t;
This transition is allowed:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t unconfined_dbusd_t -c process -p transition
allow unconfined_t domain:process transition;
Processes in the unconfined_t
domain can read and execute files with the dbusd_exec_t
type:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t dbusd_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };
Finally, the unconfined_r
role is associated to the unconfined_dbusd_t
type:
[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o unconfined_dbusd_t # x: expand, r: role; o: only-matching
unconfined_dbusd_t
Thus, when the dbus
user runs /usr/libexec/dbus-1/dbus-daemon-launch-helper, the process should be subject to the constraints on the unconfined_dbusd_t
domain. I observed this transition using strace(1):
2737<strace> [unconfined_t] execve("/usr/libexec/dbus-1/dbus-daemon-launch-helper" [dbusd_exec_t], ["/usr/libexec/dbus-1/dbus-daemon-"..., "org.me.test"], 0x7fffbea92fc8 /* 19 vars */) = 0
2737<dbus-daemon-lau> [unconfined_dbusd_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
Source:
sudo strace -u dbus -fyY --secontext /usr/libexec/dbus-1/dbus-daemon-launch-helper org.me.test
The unconfined_dbusd_t
domain has the following permissions on the capability
and capability2
classes:
[user@fedora ~]$ sudo sesearch --allow -t unconfined_dbusd_t -c capability,capability2
allow unconfined_dbusd_t unconfined_dbusd_t:capability sys_module; [ secure_mode_insmod ]:False
allow unconfined_dbusd_t unconfined_dbusd_t:capability { audit_control audit_write chown dac_override dac_read_search fowner fsetid ipc_lock ipc_owner kill lease linux_immutable mknod net_admin net_bind_service net_broadcast net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_pacct sys_ptrace sys_rawio sys_resource sys_time sys_tty_config };
allow unconfined_dbusd_t unconfined_dbusd_t:capability2 { audit_read block_suspend bpf checkpoint_restore epolwakeup perfmon syslog wake_alarm };
Neither GDM nor Anaconda can run a system bus, so consider unsetting the SUID bit on /usr/libexec/dbus-1/dbus-daemon-launch-helper.
- D-BUS System Activation
- Fedora Project Wiki: Make dbus-broker the default DBus implementation
- alpine/aports issue #3313: dbus-daemon-launch-helper is installed setuid root as part of dbus package
The ACTIVATION_LAUNCHER_TEST
macro is assumed to be undefined.
1 In dbus/bus/[email protected], main
calls run_launch_helper
(line 94). In dbus/bus/[email protected], run_launch_helper
calls check_dbus_user
(line 542), which in turn calls check_permissions
(line 513). check_permissions
errors out if the EUID is nonzero (lines 196–203).
2 In dbus/bus/[email protected], run_launch_helper
calls launch_bus_name
(line 546), which in turn calls exec_for_correct_user
(line 487). exec_for_correct_user
then calls switch_user
(line 353). switch_user
calls initgroups(3) (line 310), setgid(2) (line 318) and setuid(2) (line 326) to become an arbitrary user.
- Program to run Xorg(1), the reference implementation of the X Window System, possibly as root
- Provided by xorg-x11-server-Xorg-1.20.14-21.fc38.x86_64
- Has SHA256 checksum d3629d7996ec24a9bd7a67d675a0f03a5561d6447796d700ac44562e71a1710d
- Has security context
system_u:object_r:xserver_exec_t:s0
set by the xserver 3.9.4 module
Xorg(1) is part of GNOME; Xorg.wrap(1) is bundled with Xorg(1).
Xorg.wrap(1) must execute Xorg(1) with EUID 0 conditionally so that Xorg(1) can run with full privileges if needed.
To answer this question, I first had to find a way to capture the execution of Xorg.wrap(1) reproducibly. I started by identifying the GDM process in a virtual console (tty2):
[user@fedora ~]$ pgrep -fu root /usr/sbin/gdm # f: full, u: euid
801
I then attached to this process with strace(1):
[user@fedora ~]$ sudo strace -fyY --secontext -p 801 # p: attach
Next, I logged in via GDM, choosing “GNOME on Xorg” from the session menu.
Once the desktop environment had loaded, I returned to tty2 and interrupted strace(1) with a timely Ctrl+C
.
I had successfully captured the execution of Xorg.wrap(1):
1741<X> [xserver_t] execve("/usr/libexec/Xorg.wrap" [xserver_exec_t], ["/usr/libexec/Xorg.wrap", "vt3", "-displayfd", "3", "-auth", "/run/user/1000/gdm/Xauthority", "-nolisten", "tcp", "-background", "none", "-noreset", "-keeptty", "-novtswitch", "-verbose", "3"], 0x5614b2ed1870 /* 22 vars */) = 0
1741<Xorg.wrap> [xserver_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
Source: See previous strace(1) command line
Back in GNOME on Xorg, I fired up GNOME Terminal and confirmed that Xorg(1) was running as the generic user:
[user@fedora ~]$ pgrep -fu user /usr/libexec/Xorg
1741
I then logged out of the graphical session.
Because GDM didn’t need to run Xorg(1) with root rights, I concluded that Xorg.wrap(1) doesn’t need its SUID bit to be set:
[user@fedora ~]$ sudo chmod u-s /usr/libexec/Xorg.wrap
After making this change, I was able to start another X.Org session successfully.
Xorg.wrap(1) calls setresgid(2) to change the EGID and saved SGID to the RGID and setresuid(2) to change the EUID and saved SUID to the RUID before running Xorg(1) when KMS graphics are available:1
1741<Xorg.wrap> [xserver_t] getgid() = 1000
1741<Xorg.wrap> [xserver_t] getuid() = 1000
1741<Xorg.wrap> [xserver_t] setresgid(-1, 1000, 1000) = 0
1741<Xorg.wrap> [xserver_t] setresuid(-1, 1000, 1000) = 0
1741<Xorg.wrap> [xserver_t] access("/usr/libexec/Xorg" [bin_t], X_OK) = 0
1741<Xorg.wrap> [xserver_t] getuid() = 1000
1741<Xorg.wrap> [xserver_t] geteuid() = 1000
1741<Xorg.wrap> [xserver_t] execve("/usr/libexec/Xorg" [bin_t], ["/usr/libexec/Xorg", "vt3", "-displayfd", "3", "-auth", "/run/user/1000/gdm/Xauthority", "-nolisten", "tcp", "-background", "none", "-noreset", "-keeptty", "-novtswitch", "-verbose", "3"], 0x7ffe51ac9b48 /* 22 vars */) = 0
Source: See previous strace(1) command line; whitespace has been truncated for readability
In tracing Xorg.wrap(1), I had confirmed that the process runs in the xserver_t
domain. The xserver_t
domain has the following permissions on the capability
and capability2
classes:
[user@fedora ~]$ sudo sesearch --allow -t xserver_t -c capability,capability2
allow xserver_t xserver_t:capability sys_module; [ secure_mode_insmod ]:False
allow xserver_t xserver_t:capability { audit_control audit_write chown dac_override dac_read_search fowner fsetid ipc_lock ipc_owner kill lease linux_immutable mknod net_admin net_bind_service net_broadcast net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_pacct sys_ptrace sys_rawio sys_resource sys_time sys_tty_config };
allow xserver_t xserver_t:capability2 { audit_read block_suspend bpf checkpoint_restore epolwakeup perfmon syslog wake_alarm };
Since GDM doesn’t run Xorg(1) with root rights, consider unsetting the SUID bit on /usr/libexec/Xorg.wrap whether or not you want to run GNOME on Xorg.
- Xorg.wrap(1) man page
1 Lines 257–272 of xserver/hw/xfree86/[email protected] after applying patches in src.fedoraproject.org/rpms/xorg-x11-server@21269fd
- Program to collect system monitoring data, e.g., resource usage
- Provided by libgtop2-2.41.1-1.fc38.x86_64
- Has SHA256 checksum 2fc203cd31489380f37b364979f6668804731fa172120d9d0fb26129a7eedeb7
- Has security context
system_u:object_r:bin_t:s0
LibGTop is installed to support GNOME Settings and GNOME System Monitor. libgtop_server2 is part of the libgtop2 package.
In general, libgtop_server2 needs privileges to read what is considered sensitive system data. For example, on the BSDs, libgtop_server2 must be able to open the kernel virtual memory device at /dev/kmem via the kvm(3) library functions,1 an operation that on Linux would require the CAP_SYS_RAWIO
capability.
Linux exposes all the information that libgtop_server2 supports reading through the proc file system. Some but not all files in this file system are protected by ptrace(2) access mode checks.
The LibGTop team began building the server for Linux in commit ec662d0 under the assumption that privileges are required to read the /proc/pid/smaps file containing the usage of each of a given process’s memory mappings. Indeed, permissions on /proc/pid/smaps are subject to ptrace(2) access mode checks, which are normally overcome with CAP_SYS_PTRACE
. However, any process can read any /proc/pid/smaps file with matching UIDs and GIDs. Therefore, even when running as a nonroot user, libgtop_server2 can report on the memory mappings of each of the user’s processes without obtaining data on processes owned by other users, e.g., root.
The next commit to LibGTop, e4335d4, dropped the requirement to run the server SUID-root in order to access /proc/pid/smaps and updated the behavior of the server accordingly but didn’t revert any of the changes to the build process. The first release that included the two commits was 2.39.90.
In effect, libgtop_server2 doesn’t need to be SUID-root on Fedora Workstation 38.
libgtop_server2 drops privileges reversibly via setreuid(2) and setregid(2) after storing its RUID, EUID, RGID and EGID.2
LibGTop defines the functions glibtop_suid_enter
and glibtop_suid_leave
to escalate and drop privileges, respectively.3 Library functions wanting to read sensitive system data must fence critical sections of code with these two functions. However, there are no Linux implementations of LibGTop functions that call glibtop_suid_enter
. Thus, even if libgtop_server2 were to run SUID-root on Linux, it would never escalate privileges after dropping them temporarily.
As far as I can tell from the source code, libgtop_server2 never drops privileges irreversibly by calling any of setuid(2), setreuid(2) or setresuid(2).
The targeted SELinux policy doesn’t restrict the permissions of the bin_t
domain on the capability
and capability2
classes, so libgtop_server2 runs unconfined.
Consider unsetting the SUID bit on /usr/libexec/libgtop_server2.
Neither GNOME Settings nor GNOME System Monitor launches the LibGTop server.4
1 See, for example, line 68 in libgtop/sysdeps/freebsd/[email protected] and FreeBSD’s kvm_open(3) man page
2 In libgtop/src/daemon/[email protected], main
calls glibtop_init_p
to initialize the server (line 81). In turn, glibtop_init_p
calls glibtop_open_p
to open the server on line 42 in libgtop/sysdeps/linux/[email protected], which stores the appropriate IDs (lines 59–62) and performs the appropriate ID transitions (lines 68–72). After glibtop_init_p
returns, main
drops privileges again on lines 85 and 87 in libgtop/src/daemon/[email protected].
3 See, for example, lines 26–37 in libgtop/sysdeps/linux/[email protected]
4 Clients of LibGTop start by calling a function to initialize the server. In gnome-control-center/panels/firmware-security/[email protected], get_cpu_model
calls glibtop_init
(line 197). Similarly, in gnome-system-monitor/src/[email protected], GsmApplication::on_startup
calls glibtop_init
(line 577). In libgtop/sysdeps/common/[email protected], glibtop_init
wraps glibtop_init_r
, passing &glibtop_global_server
for the server_ptr
parameter and 0
for both the features
and flags
parameters (line 45). glibtop_global_server
is initialized in libgtop/lib/[email protected] as the address of the uninitialized _glibtop_global_server
static global variable (lines 36 and 37). In the same file, glibtop_init_r
first sets the server
local variable to the address of _glibtop_global_server
(line 168). With the server not yet initialized, glibtop_init_r
calls glibtop_machine_new
(line 179) to initialize the server->machine
member (line 11 of libgtop/include/glibtop/[email protected]), then sets the features
variable to GLIBTOP_SYSDEPS_ALL
(line 185). GLIBTOP_SYSDEPS_ALL
is a macro that expands to the bit mask 11111111111111111111111111111
(line 64 in libgtop/include/glibtop/[email protected]). glibtop_init_r
then passes server
and features
to _init_server
(line 194). _init_server
sets server->server_command
to the LIBGTOP_SERVER
macro (lines 47–52) that, on Fedora Linux 38, expands to "/usr/libexec/libgtop_server2"
(line 301 of libgtop/[email protected]). To determine whether the server is needed, _init_server
computes a bitwise AND of features
and glibtop_server_features
(line 70). glibtop_server_features
is initialized as the sum of the values obtained by expanding all the macros prefixed with GLIBTOP_SUID_
(lines 28–54 of libgtop/lib/[email protected]). On Linux, all these macros expand to 0
(lines 25–50 of libgtop/sysdeps/linux/[email protected]). The bitwise AND expression therefore evalutes to 0
and _init_server
sets server->method
to GLIBTOP_METHOD_DIRECT
(line 76) before returning (line 79). glibtop_init_r
then calls glibtop_open_l
to open the server (lines 217 and 218). In libgtop/lib/[email protected], glibtop_open_l
performs a switch on server->method
(line 55). With this member set to GLIBTOP_METHOD_DIRECT
, glibtop_open_l
sets the server->features
member to 0
(line 57) and does not set the _GLIBTOP_INIT_STATE_SERVER
flag on server->flags
. Thus, glibtop_open_l
goes on to call glibtop_init_s
(line 165) without stopping to query the server for its features (line 118). We can see that on lines 85 and 101 that, had server->method
been GLIBTOP_METHOD_PIPE
, glibtop_open_l
would have proceeded to execl(3) the server at /usr/libexec/libgtop_server2. In libgtop/lib/[email protected], glibtop_init_s
concludes the initialization process by performing system-specific initialization, first by calling glibtop_open_s
(line 247) and then by calling a hook for each feature (lines 249 and 250). On Linux, glibtop_open_s
opens /proc/stat to count the number of CPUs being monitored (lines 70–104 in libgtop/sysdeps/linux/[email protected]). Finally, because the GLIBTOP_SUID_
-prefixed macros all expand to 0
on Linux, the array containing the initialization hooks, _glibtop_init_hook_s
, is never populated (lines 56–136 in libgtop/lib/[email protected]), so glibtop_init_s
has no hooks to run.
- Program to mount NFS file systems
- Provided by nfs-utils-1:2.6.2-2.rc6.fc38.1.x86_64
- Has SHA256 checksum 255e9803dd80592d3d4c44e582c5592063c390e41cb8ac65e94e845b3af94990
- Has security context
system_u:object_r:mount_exec_t:s0
set by the mount 1.16.1 module
nfs-utils is inherent to Fedora Workstation 38.
mount.nfs(8) expects to run with EUID 0 unconditionally.1 It must be able to mount file systems (CAP_SYS_ADMIN
).
To answer this question, I first prepared a NFSv4 file system in another Fedora Workstation 38 VM with IP address 192.0.2.100. In the working VM, I created the mount point:
[user@fedora ~]$ sudo mkdir -p /mnt/nfs4
I then added a corresponding entry to /etc/fstab:
192.0.2.100:/export/nfs4 /mnt/nfs4 nfs defaults,noauto,ro,user 0 0
… after which I reloaded the systemd manager configuration:
[user@fedora ~]$ sudo systemctl daemon-reload
I ensured that I could mount this file system:
[user@fedora ~]$ mount /mnt/nfs4 && findmnt /mnt/nfs4
TARGET SOURCE FSTYPE OPTIONS
/mnt/nfs4 192.0.2.100:/export/nfs4 nfs4 ro,nosuid,nodev,noexec,relatime,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.0.2.2,local_lock=none,addr=192.0.2.100
I used mount(8) in my test case because it runs mount.nfs(8) indirectly:
2794<mount> execve("/sbin/mount.nfs", ["/sbin/mount.nfs", "192.0.2.100:/export/nfs4", "/mnt/nfs4", "-o", "ro,noexec,nosuid,nodev,user"], 0x7ffd605d7cc0 /* 15 vars */) = 0
Source:
sudo strace -u user -fyY mount /mnt/nfs4
after runningumount /mnt/nfs4
After unmounting the file system, I successfully reproduced the mount operation after making the following change:
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/sbin/mount.nfs
In my previous work, I showed that substituting the SUID bit on /usr/bin/mount with the CAP_SYS_ADMIN
capability allowed for unrestricted mounting operations. Thus, I also confirmed that mount.nfs(8) won’t run without the SUID bit set despite having CAP_SYS_ADMIN
:
[user@fedora ~]$ sudo chmod u-s /usr/sbin/mount.nfs
[user@fedora ~]$ mount /mnt/nfs4
mount.nfs: not installed setuid - "user" NFS mounts not supported.
As far as I can tell from the source code and strace(1) output, mount.nfs(8) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
When the user
option is specified for the mount point, mount(8) drops privileges before executing the helper:2
2794<mount> getgid() = 1000
2794<mount> setgid(1000) = 0
2794<mount> getuid() = 1000
2794<mount> setuid(1000) = 0
2794<mount> execve("/sbin/mount.nfs", ["/sbin/mount.nfs", "192.0.2.100:/export/nfs4", "/mnt/nfs4", "-o", "ro,noexec,nosuid,nodev,user"], 0x7ffd605d7cc0 /* 15 vars */) = 0
Source:
sudo strace -u user -fyY mount /mnt/nfs4
after runningumount /mnt/nfs4
and truncating whitespace for readability
mount(8) and mount.nfs(8) both have the file type mount_exec_t
, through which there are no transitions from the unconfined_t
domain:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t mount_exec_t
[user@fedora ~]$
Thus, whether run indirectly via mount(8) or directly on the command line, mount.nfs(8) runs unconfined.
mount_t
is the only target domain for transitions through the mount_exec_t
type and was discussed in greater detail in SUID-root Binaries in Fedora Custom OS 38.
If you don’t need to mount NFSv4 file systems and have no programs that do so on your behalf, then consider unsetting the SUID bit on /usr/sbin/mount.nfs.
1 Lines 344–348 of nfs-utils/utils/mount/mount_libmount.c@nfs-utils-2-6-2-rc6. nfs-utils is compiled with the --enable-libmount-mount
flag for Fedora Linux 38 per line 157 of src.fedoraproject.org/rpms/nfs-utils/nfs-utils.spec@fc09626. Thus, the CONFIG_LIBMOUNT
macro is defined (line 411 of nfs-utils/configure.ac@nfs-utils-2-6-2-rc6), which causes the nfs-utils/utils/mount/mount_libmount.c file to be included in the mount.nfs(8) sources per lines 37–41 of nfs-utils/utils/mount/Makefile.am@nfs-utils-2-6-2-rc6, replacing the mount.c, fstab.c, nfsumount.c and fstab.h sources.
2 In util-linux/sys-utils/[email protected], main
calls mnt_context_mount
(lines 1032 and 1039). In util-linux/libmount/src/[email protected], mnt_context_mount
calls mnt_context_do_mount
(line 1328) which, in turn, calls do_mount
either directly (line 1134) or indirectly (lines 1132 and 1136). do_mount
calls exec_helper
if the cxt->helper
member is set (lines 807 and 808). exec_helper
creates a child process (line 641) in which it invokes drop_permissions
(line 648) ahead of invoking execv(3) to run the helper binary (line 687). In util-linux/include/[email protected], drop_permissions
sets the GIDs to the RGID via setgid(2) and the UIDs to the RUID via setuid(2) (lines 372 and 376).
- Program to mount and unmount FUSE v2 file systems
- Provided by fuse-2.9.9-16.fc38.x86_64
- Has SHA256 checksum e394ac0448368abc80d21e211f80edea47c87d2d0f7911b868018d406cf457b5
- Has security context
system_u:object_r:fusermount_exec_t:s0
set by the mount 1.16.1 module
fusermount(1) is installed to support GNOME Software (via Flatpak), GNOME Boxes and dracut modules for building an initramfs with live image capabilities.
fusermount(1) must enable unprivileged users to mount and unmount FUSE file systems (CAP_SYS_ADMIN
).
fusermount(1) executes /bin/mount or /bin/umount to update /etc/mtab by performing a fake mount or fake unmount, respectively, when running with EUID 0.1 However, fusermount(1) won’t update /etc/mtab when it’s a symbolic link,2 which is the case on Fedora Workstation 38:
[user@fedora ~]$ file /etc/mtab
/etc/mtab: symbolic link to ../proc/self/mounts
When unmounting a FUSE file system with EUID 0, fusermount(1) must be able to change the working directory to the parent directory of the mount point.3 This operation requires privileges (CAP_DAC_READ_SEARCH
) in the event that the parent directory isn’t otherwise accessible by the process.
I wanted to find the simplest way to test fusermount(1) without implementing my own file system. I opted to use the hello.c example included in the FUSE source code.
First, I cloned the libfuse repository:
[user@fedora ~]$ git clone -b fuse-2.9.9 'https://github.com/libfuse/libfuse' # b: branch
I also cloned the repository containing the corresponding RPM specification, checking out the appropriate commit:
[user@fedora ~]$ git clone 'https://src.fedoraproject.org/rpms/fuse'
[user@fedora ~]$ cd fuse
[user@fedora fuse]$ git checkout 268fdc7
I entered the libfuse repository and applied the patches:
[user@fedora fuse]$ cd ../libfuse
[user@fedora libfuse]$ git apply ../fuse/*.patch
Now, I could compile the example:
[user@fedora libfuse]$ gcc -Wall ./example/hello.c $(pkgconf --cflags --libs fuse) -o hello
[user@fedora libfuse]$ chmod 0770 hello
… where I linked against libfuse.so.2.9.9 provided by fuse-libs-2.9.9-16.fc38.x86_64.
I then created the mount point and mounted the file system:
[user@fedora libfuse]$ cd ~
[user@fedora ~]$ mkdir hello
[user@fedora ~]$ ./libfuse/hello /home/user/hello
[user@fedora ~]$ findmnt /home/user/hello
TARGET SOURCE FSTYPE OPTIONS
/home/user/hello hello fuse.hello rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
[user@fedora ~]$ cat /home/user/hello/hello
Hello World!
Success!
The hello program defines FUSE operations and hands them off to libfuse, which executes fusermount(1). fusermount(1) daemonizes:
[user@fedora ~]$ pgrep -u user hello
2777
Unmounting the file system terminates the loop:
[user@fedora ~]$ fusermount -u /home/user/hello && findmnt /home/user/hello
[user@fedora ~]$ pgrep -u user hello
[user@fedora ~]$
If I were to trace fusermount(1) normally, strace(1) would hang decoding a blocking read(2) of /dev/fuse. It is necessary to daemonize strace(1), running it as a child of the tracee so that it runs until the file system is unmounted:
[user@fedora ~]$ sudo strace -u user -fyY -D -o hello.strace ./libfuse/hello /home/user/hello # D: daemonize, o: output
[user@fedora ~]$ fusermount -u /home/user/hello
Thus, I could gracefully capture the execution of fusermount(1)…
2855<hello> execve("/usr/bin/fusermount", ["fusermount", "-o", "rw,nosuid,nodev,subtype=hello", "--", "/home/user/hello"], 0x182c520 /* 20 vars */) = 0
… as well as the mount(2) call:
2855<fusermount> mount("hello", ".", "fuse.hello", MS_NOSUID|MS_NODEV, "fd=4,rootmode=40000,user_id=1000"...) = 0
Having confirmed the (indirect) execution of fusermount(1), I could then reproduce the mount and unmount operations after making the following change:
[user@fedora ~]$ sudo chmod u-s /usr/bin/fusermount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/fusermount
Could I do anything I wasn’t supposed to be able to do with these capabilities? For example, could I mount somewhere I had no write access?
[user@fedora ~]$ sudo mkdir /mnt/hello
[user@fedora ~]$ ls -dl /mnt/hello
drwxr-xr-x. 1 root root 0 Jan 01 00:00 /mnt/hello
[user@fedora ~]$ ./libfuse/hello /mnt/hello
fusermount: user has no write access to mountpoint /mnt/hello
No. How about a world-writable sticky directory?
[user@fedora ~]$ sudo chmod o+wt /mnt/hello
[user@fedora ~]$ ./libfuse/hello /mnt/hello
fusermount: mountpoint /mnt/hello not owned by user
No dice.
fusermount(1) drops privileges reversibly before:
- opening the FUSE device at /dev/fuse;4
- reading the configuration file at /etc/fuse.conf,5 and
- resolving the path to the mount point.6
Strictly speaking, fusermount(1) drops only file system access privileges via setfsuid(2) and setfsgid(2), and only when the RUID is nonzero.7 As far as I can tell from the source code and strace(1) output, fusermount(1) doesn’t drop other privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
There are many domain transitions via fusermount_exec_t
…
[user@fedora ~]$ sudo sesearch -T -t fusermount_exec_t | wc -l # l: lines
38
… but none from unconfined_t
:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t fusermount_exec_t
[user@fedora ~]$
mount_t
is the only target domain for transitions through the fusermount_exec_t
type and was discussed in greater detail in SUID-root Binaries in Fedora Custom OS 38.
All line numbers refer to points in source code after applying the patches in src.fedoraproject.org/rpms/fuse@268fdc7 affecting libfuse/util/fusermount.c.
The __NETBSD__
and IGNORE_MTAB
macros are assumed to be undefined.
1 In libfuse/util/[email protected], main
calls mount_fuse
(line 1351) when not unmounting (lines 1341 and 1342). If the EUID is 0, then mount_fuse
calls add_mount
(lines 1171–1177). add_mount
calls fuse_mnt_add_mount
(line 148) which, in libfuse/lib/[email protected], calls a different add_mount
in the same file if the file at /etc/mtab needs to be updated (lines 125–128). This add_mount
calls setuid(2) to set the UIDs to the EUID, which is implicitly assumed to be 0, before calling execle(3) on /bin/mount (lines 102–104). When unmounting, main
in libfuse/util/[email protected] calls unmount_fuse
if the EUID is 0 (lines 1395 and 1396). unmount_fuse
calls unmount_fuse_locked
(line 468), which calls fuse_mnt_remove_mount
(line 460). In libfuse/lib/[email protected], fuse_mnt_remove_mount
calls remove_mount
if the file at /etc/mtab needs to be updated (lines 242–245). remove_mount
calls setuid(2) to change the UIDs to the EUID, which is implicitly assumed to be 0, before calling execle(3) on /bin/umount (lines 221–223).
2 In libfuse/lib/[email protected], mtab_needs_update
calls lstat(2) to determine whether the file exists (line 49) and, if so, returns if the file is a symbolic link (lines 57 and 58).
3 In libfuse/util/[email protected], unmount_fuse_locked
calls chdir_to_parent
(line 432), which calls chdir(2) on the path to which parent
points (line 377).
4 In libfuse/util/[email protected], mount_fuse
calls open_fuse_device
(line 1133) which, in turn, calls try_open_fuse_device
(line 1109). try_open_fuse_device
invokes drop_privs
(line 1093) before calling try_open
on FUSE_DEV_NEW
(line 1094), a macro that expands to "/dev/fuse"
(line 38).
5 In libfuse/util/[email protected], mount_fuse
calls drop_privs
(line 1137) before calling read_conf
(line 1138). read_conf
calls fopen(3) on FUSE_CONF
(line 545), a macro that expands to "/etc/fuse.conf"
(line 40).
6 In libfuse/util/[email protected], main
calls drop_privs
(line 1327) before calling fuse_mnt_resolve_path
(line 1328)
7 In libfuse/util/[email protected], drop_privs
sets the FSUID to the RUID via setfsuid(2) (line 86) and the FSGID to the RGID via setfsgid(2) (line 87) when the RUID is nonzero (line 85)
- Program to mount and unmount Gluster file systems
- Provided by glusterfs-fuse-11.0-1.fc38.x86_64
- Has SHA256 checksum 6a3fb94d81a5eb294a8914138af8991a20fb0f55c2da82407af351470a11845c
- Has security context
system_u:object_r:bin_t:s0
fusermount-glusterfs(8) is pulled in through a dependency of GNOME Boxes but isn’t involved in regular usage of said program.
Yes. fusermount-glusterfs(8) is compiled from source code that seems to originally have been based on libfuse 2.8.0. Although the code bases have since diverged, the findings for fusermount(1) hold.
To test this program, I first prepared a GlusterFS cluster using a pair of Fedora Workstation 38 VMs with IP addresses 192.0.2.101 and 192.0.2.102:
Volume Name: gv0
Type: Distributed-Replicate
Volume ID: 00000000-0000-0000-0000-000000000000
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 2 = 2
Transport-type: tcp
Bricks:
Brick1: 192.0.2.101:/exports/vdb1/brick
Brick2: 192.0.2.102:/exports/vdb1/brick
Options Reconfigured:
cluster.granular-entry-heal: on
storage.fips-mode-rchecksum: on
transport.address-family: inet
performance.client-io-threads: off
Source:
sudo gluster volume info gv0
on 192.0.2.101 after truncating the leading newline
In the working Fedora Workstation 38 VM, I configured FUSE to allow me to mount the GlusterFS volume as an unprivileged user:
[user@fedora ~]$ sudo sed -i -e 's/^# user_allow_other/user_allow_other/' /etc/fuse.conf # i: in-place, e: expression
Next, I prepared the mount point:
[user@fedora ~]$ mkdir gv0
I could then mount the GlusterFS volume like so:
[user@fedora ~]$ mount -t glusterfs 192.0.2.101:/gv0 -o log-file=/home/user/gv0.log /home/user/gv0
grep: warning: stray \ before -
[user@fedora ~]$ findmnt /home/user/gv0
TARGET SOURCE FSTYPE OPTIONS
/home/user/gv0 192.0.2.101:/gv0 fuse.glusterfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,default_permissions,allow_other,max_read=131072
strace(1) shows the execution of fusermount-glusterfs(8):
2851<glusterfs> execve("/usr/bin/fusermount-glusterfs", ["/usr/bin/fusermount-glusterfs", "-o", "default_permissions,allow_other,"..., "--", "/home/user/gv0"], 0x563fc6a200c0 /* 23 vars */) = 0
Source:
sudo strace -u user -fyY -D mount -t glusterfs -o log-file=/home/user/gv0.log 192.0.2.101:/gv0 /home/user/gv0
The unmounting step also succeeded:
[user@fedora ~]$ fusermount-glusterfs -u /home/user/gv0
[user@fedora ~]$ findmnt /home/user/gv0
[user@fedora ~]$
I was then able to reproduce these steps after making the following changes to the binary of interest:
[user@fedora ~]$ sudo chmod u-s /usr/bin/fusermount-glusterfs
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/fusermount-glusterfs
The targeted SELinux policy doesn’t restrict the permissions of the bin_t
domain on the capability
and capability2
classes, so fusermount-glusterfs(8) runs unconfined.
If you don’t need to mount GlusterFS volumes as an unprivileged user, then consider unsetting the SUID bit on /usr/bin/fusermount-glusterfs.
How can mount(8) know to call fusermount-glusterfs(8)? First, mount(8) runs the mount.glusterfs helper, a shell script:
2787<mount> execve("/sbin/mount.glusterfs", ["/sbin/mount.glusterfs", "192.0.2.101:/gv0", "/home/user/gv0", "-o", "rw,log-file=/home/user/gv0.log"], 0x5566e8e208e0 /* 19 vars */) = 0
Source: See previous strace(1) command line
In turn, the helper runs glusterfs(8):
2846<mount.glusterfs> execve("/usr/sbin/glusterfs", ["/usr/sbin/glusterfs", "--log-file=/home/user/gv0.log", "--process-name", "fuse", "--volfile-server=192.0.2.101", "--volfile-id=/gv0", "/home/user/gv0"], 0x55c2cd9154f0 /* 22 vars */) = 0
Source: See previous strace(1) command line
This is the libfuse client that executes fusermount-glusterfs(8) (see above).
- Program to mount and unmount FUSE v3 file systems
- Provided by fuse3-3.13.1-2.fc38.x86_64
- Has SHA256 checksum 3fd41301861398100eb921f983d93b15e30576a42f2ff28f4b1bc056a7b65e55
- Has security context
system_u:object_r:fusermount_exec_t:s0
set by the mount 1.16.1 module
fusermount3(1) is installed to support GNOME, Toolbox and Open VM Tools.
Yes, because fusermount3(1) is built from a later version of the same source code from which fusermount(1) is built. However, there is one key difference: fusermount3(1) drops privileges reversibly before changing to the mount point’s parent directory when unmounting with EUID 0.1 This means that CAP_DAC_READ_SEARCH
is no longer necessary when running fusermount3(1) SUID-root.
When validating the following changes…
[user@fedora ~]$ sudo chmod u-s /usr/bin/fusermount3
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/fusermount3
… I modified my fusermount(1) test procedure as follows:
- check out tag
fuse-3.13.1
in the libfuse repository; - apply patches from commit 86f284f of https://src.fedoraproject.org/rpms/fuse3, and
- call pkgconf(1) with
fuse3
to link against libfuse.so.3.13.1 provided by fuse3-libs-3.13.1-2.fc38.x86_64.
1 Lines 419–421 of libfuse/util/[email protected]
- QEMU helper to let unprivileged users connect a QEMU VM to a bridged network interface
- Provided by qemu-common-2:7.2.0-6.fc38.x86_64
- Has SHA256 checksum 549669a759506e8af5e7449cc530f2d62e455a147ec5f7291cfd34687370cc91
- Has security context
system_u:object_r:virt_bridgehelper_exec_t:s0
set by the virt 1.5.0 module
qemu-bridge-helper is installed to support GNOME Boxes.
qemu-bridge-helper must be able to configure and connect a host TAP network interface to a host bridge device (CAP_NET_ADMIN
).1
qemu-bridge-helper is capability-aware and drops all capabilities but CAP_NET_ADMIN
when run SUID-root.2 It should therefore be possible to replace the SUID bit like so:
[user@fedora ~]$ sudo chmod u-s /usr/libexec/qemu-bridge-helper
[user@fedora ~]$ sudo setcap cap_net_admin=ep /usr/libexec/qemu-bridge-helper
To test these changes, I started by creating a network bridge called br0
using nmcli(1). I updated the QEMU bridge helper configuration accordingly:
[user@fedora ~]$ sudo sed -i -e 's/^allow virbr0/allow br0/' /etc/qemu/bridge.conf
Next, I tried to start a blank VM:
[user@fedora ~]$ qemu-system-x86_64 \
> -netdev bridge,br=br0,id=hn0 \
> -device virtio-net-pci,netdev=hn0,id=nic \
> -nographic
Although the VM couldn’t boot, it was still able to acquire unique IP addresses on the LAN:
SeaBIOS (version 1.16.2-1.fc38)
iPXE (https://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+06FCC180+06F0C180 CA00
Booting from Hard Disk...
Boot failed: could not read the boot disk
Booting from Floppy...
Boot failed: could not read the boot disk
Booting from DVD/CD...
Boot failed: Could not read from CDROM (code 0003)
Booting from ROM...
iPXE (PCI 00:03.0) starting execution...ok
iPXE initialising devices...ok
iPXE 1.0.0+ (64113751) -- Open Source Network Boot Firmware -- https://ipxe.org
Features: DNS HTTP HTTPS iSCSI TFTP AoE ELF MBOOT PXE bzImage Menu PXEXT
net0: 52:54:00:12:34:56 using virtio-net on 0000:00:03.0 (Ethernet) [open]
[Link:up, TX:0 TXE:0 RX:0 RXE:0]
Configuring (net0 52:54:00:12:34:56).............. ok
net0: 192.0.2.103/255.255.255.0 gw 192.0.2.1
net0: 2001:db8::6742:1e24:5bc0:4ba4/32 gw 2001:db8::10ca:4764:93a5:364b
net0: 2001:db8::be6b:db29:d10d:e688/32 gw 2001:db8::10ca:4764:93a5:364b
net0: 2001:db8::839c:b5a4:6b3a:9b63/32
Nothing to boot: No such file or directory (https://ipxe.org/2d03e13b)
No more network devices
No bootable device.
To exit the VM, I pressed Ctrl+A
followed by X
.
The targeted SELinux policy defines only two type transitions through the virt_bridgehelper_exec_t
type:
[user@fedora ~]$ sudo sesearch -T -t virt_bridgehelper_exec_t
type_transition svirt_t virt_bridgehelper_exec_t:process virt_bridgehelper_t;
type_transition svirt_tcg_t virt_bridgehelper_exec_t:process virt_bridgehelper_t;
There are no transitions to either of these source domains:
[user@fedora ~]$ sudo sesearch -T -t svirt_t
[user@fedora ~]$ sudo sesearch -T -t svirt_tcg_t
[user@fedora ~]$
What about the context of qemu-system-x86_64(1)?
[user@fedora ~]$ ls -Z /usr/bin/qemu-system-x86_64 # Z: context
system_u:object_r:qemu_exec_t:s0 /usr/bin/qemu-system-x86_64
[user@fedora ~]$ sudo sesearch -T -t qemu_exec_t
[user@fedora ~]$
Nothing. So, qemu-bridge-helper runs unconfined.
Since /usr/libexec/qemu-bridge-helper is capability-aware, consider substituting the SUID bit with file capabilities if you plan to run VMs with networking as an unprivileged user. Otherwise, unset the SUID bit without setting any file capabilities.
All line numbers refer to qemu/[email protected].
The SIOCBRADDIF
and CONFIG_LIBCAP_NG
macros are assumed to be defined.
1 main
calls ioctl(2) on lines 354, 365, 378, 389, 396, 415, 426 and 434. On line 350, main
calls has_vnet_hdr
which, in turn, calls ioctl(2) to check for vnet header support (line 167).
2 main
calls drop_privileges
if the EUID is zero and the RUID is nonzero (lines 252–259). In turn, drop_privileges
drops all capabilities but CAP_NET_ADMIN
and resets the GIDs and UIDs of the process to the RGID and RUID, respectively, retaining supplementary groups (lines 212–229).
- SPICE helper for USB redirection
- Provided by spice-glib-0.42-1.fc38.x86_64
- Has SHA256 checksum e0a638c3e41eac39274541b33e53d81766ee9f1bdc472ce6a65a2b63974dac30
- Has security context
system_u:object_r:bin_t:s0
spice-client-glib-usb-acl-helper is installed to support GNOME Boxes.
spice-client-glib-usb-acl-helper must be able to set ACLs on device files under /dev/bus/usb (CAP_FOWNER
).1
spice-client-glib-usb-acl-helper is capability-aware and drops all capabilities but CAP_FOWNER
when run SUID-root.2 It should therefore be possible to replace the SUID bit like so:
[user@fedora ~]$ sudo chmod u-s /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
[user@fedora ~]$ sudo setcap cap_fowner=ep /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
To test this change, I first connected a trusted USB flash drive to my host machine.
In Virtual Machine Manager 4, I ensured the presence of a USB redirection device for the working VM and added a USB host device representing the real USB flash drive.
I powered the VM on and logged into GNOME via GDM.
I copied the ISO file I had used to install this VM to my home directory via virtiofs.
I launched GNOME Boxes and created a new VM from the ISO file using the default configuration, leaving the VM running.
I then launched GNOME Terminal and ran the capable utility:
[user@fedora ~]$ sudo /usr/share/bcc/tools/capable -KU --unique
… where -KU
is short for --kernel-stack --user-stack
.
I chose to use capable instead of strace(1) because I reasoned that the execution mode of capable is a better fit for an asynchronous program that runs as part of a virtualization stack. I also found that the output of capable had a higher signal-to-noise ratio for this particular use case.
Back in GNOME Boxes, I opened the preferences for the running VM and then navigated to the “Devices & Shares” tab. Under “USB Devices”, I toggled my USB flash drive.
In the nested VM, I waited for Fedora Workstation 38 Live to start before confirming the presence of my USB flash drive in GNOME Terminal:
Bus 002 Device 002: ID 0000:0000 USB Flash Drive
Source:
lsusb
after omitting devices irrelevant to this analysis
In the enclosing VM, I returned to GNOME Terminal and pressed Ctrl+C
to interrupt capable. Here’s the singular capability check and corresponding stack trace:
TIME UID PID COMM CAP NAME AUDIT
00:00:00 1000 3380 spice-client-gl 3 CAP_FOWNER 1
b'cap_capable+0x1 [kernel]'
b'security_capable+0x40 [kernel]'
b'ns_capable+0x27 [kernel]'
b'set_posix_acl+0x73 [kernel]'
b'vfs_set_acl+0x261 [kernel]'
b'do_set_acl+0x7a [kernel]'
b'setxattr+0x9e [kernel]'
b'path_setxattr+0xd9 [kernel]'
b'__x64_sys_setxattr+0x27 [kernel]'
b'do_syscall_64+0x5b [kernel]'
b'entry_SYSCALL_64_after_hwframe+0x72 [kernel]'
b'__GI_setxattr+0xe [libc.so.6]'
b'set_facl.constprop.0+0x13f [spice-client-glib-usb-acl-helper]'
b'check_authorization_cb+0x241 [spice-client-glib-usb-acl-helper]'
b'g_simple_async_result_complete+0x6b [libgio-2.0.so.0.7600.1]'
b'check_authorization_cb+0x86 [libpolkit-gobject-1.so.0.0.0]'
b'g_task_return_now+0x2c [libgio-2.0.so.0.7600.1]'
b'g_task_return+0x143 [libgio-2.0.so.0.7600.1]'
b'reply_cb+0x9b [libgio-2.0.so.0.7600.1]'
b'g_task_return_now+0x2c [libgio-2.0.so.0.7600.1]'
b'g_task_return+0x143 [libgio-2.0.so.0.7600.1]'
b'g_dbus_connection_call_done+0x172 [libgio-2.0.so.0.7600.1]'
b'g_task_return_now+0x2c [libgio-2.0.so.0.7600.1]'
b'complete_in_idle_cb+0x15 [libgio-2.0.so.0.7600.1]'
b'g_idle_dispatch+0x2d [libglib-2.0.so.0.7600.1]'
b'g_main_context_dispatch+0x1a8 [libglib-2.0.so.0.7600.1]'
b'g_main_context_iterate.isra.0+0x318 [libglib-2.0.so.0.7600.1]'
b'g_main_loop_run+0x7f [libglib-2.0.so.0.7600.1]'
b'main+0xd6 [spice-client-glib-usb-acl-helper]'
b'__libc_start_call_main+0x7a [libc.so.6]'
b'__libc_start_main+0x8b [libc.so.6]'
b'_start+0x25 [spice-client-glib-usb-acl-helper]'
Source:
sudo /usr/share/bcc/tools/capable -KU --unique
after installing glib2-debuginfo-2.76.1-1.fc38.x86_64, polkit-libs-debuginfo-122-3.fc38.x86_64 and spice-glib-debuginfo-0.42-1.fc38.x86_64
This trace demonstrates that the procedure above triggers the execution of spice-client-glib-usb-acl-helper and that this program makes a setxattr(2) call that may require CAP_FOWNER
, which is consistent my interpretation of the source code.
There’s also a check for CAP_FSETID
:
00:00:00 1000 3380 spice-client-gl 4 CAP_FSETID 1
b'cap_capable+0x1 [kernel]'
b'security_capable+0x40 [kernel]'
b'capable_wrt_inode_uidgid+0x38 [kernel]'
b'posix_acl_update_mode+0xd6 [kernel]'
b'simple_set_acl+0x6c [kernel]'
b'vfs_set_acl+0x261 [kernel]'
b'do_set_acl+0x7a [kernel]'
b'setxattr+0x9e [kernel]'
b'path_setxattr+0xd9 [kernel]'
b'__x64_sys_setxattr+0x27 [kernel]'
b'do_syscall_64+0x5b [kernel]'
b'entry_SYSCALL_64_after_hwframe+0x72 [kernel]'
Source:
sudo /usr/share/bcc/tools/capable -KU --unique
after installing glib2-debuginfo-2.76.1-1.fc38.x86_64, polkit-libs-debuginfo-122-3.fc38.x86_64 and spice-glib-debuginfo-0.42-1.fc38.x86_64 with the user-space stack trace omitted
CAP_FSETID
allows processes to modify files without unsetting the SUID and SGID bits concomitantly. It’s checked as part of the setxattr(2) call but isn’t required for the proper function of spice-client-glib-usb-acl-helper.
The targeted SELinux policy doesn’t restrict the permissions of the bin_t
domain on the capability
and capability2
classes, so spice-client-glib-usb-acl-helper runs unconfined.
Since /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper is capability-aware, consider substituting the SUID bit with file capabilities if you plan to run VMs with USB redirection as an unprivileged user. Otherwise, unset the SUID bit without setting any file capabilities.
All line numbers refer to spice-gtk/src/[email protected].
The USE_LIBCAP_NG
macro is assumed to be defined.
1 main
calls the Gio function g_data_input_stream_read_line_async
to read a line of input from stdin asynchronously (lines 340 and 341) containing the USB bus and device numbers. When this operation finishes, Gio executes the stdin_read_complete
callback. stdin_read_complete
finishes the asynchronous read started by g_data_input_stream_read_line_async
(lines 214 and 215). If data was read from stdin (line 216), the process is waiting for data over stdin (line 238), and the process is running with RUID nonzero (line 254), then stdin_read_complete
calls the Polkit function polkit_authority_check_authorization
(line 256–260) to determine whether the process is authorized to perform the org.spice-space.lowlevelusbaccess action, supplying the check_authorization_cb
callback to run after the operation finishes. This callback invokes set_facl
(line 198), which updates the ACLs (lines 71–148) on the appropriate USB device under /dev/bus/usb (line 187). All these operations happen within the context of the Gio main event loop (line 343).
2 When the EUID is zero and the RUID is nonzero, main
drops all capabilities but CAP_FOWNER
and resets the UIDs and GIDs to the RUID and RGID, respectively, retaining supplementary groups (lines 304–320)
- Program to run the VMware user process
- Provided by open-vm-tools-desktop-12.1.5-3.fc38.x86_64
- Has SHA256 checksum 37a0eb7bb4d7f189a3c8f759c44848b06150ebe0b119c5ec43743d0e55ce815e
- Has security context
system_u:object_r:vmtools_helper_exec_t:s0
set by the vmtools 1.0.0 module
open-vm-tools-desktop is inherent to Fedora Workstation 38.
vmware-user-suid-wrapper enables user experience features for Linux VMs hosted on VMware products such as VMware Workstation Pro. These features include resize-guest-window-to-host-console, drag-and-drop and copy-and-paste operations. To this end, vmware-user-suid-wrapper must open the vmblock FUSE device at /var/run/vmblock-fuse/dev, falling back to /proc/fs/vmblock/dev.1 If the environment variable XDG_SESSION_TYPE
is set to wayland
, then vmware-user-suid-wrapper must also open /dev/uinput to emulate a mouse, falling back to /dev/input/uinput.2
I couldn’t perform a complete investigation of this binary because I don’t use VMware products. All I could show was that I could get vmware-user-suid-wrapper to obtain two file descriptors and then try to run the VMware Tools service.
I first emulated the FUSE device using an empty file:
[user@fedora ~]$ sudo mkdir -p /var/run/vmblock-fuse && sudo touch /var/run/vmblock-fuse/dev
I then made the following changes to the binary of interest:
[user@fedora ~]$ sudo chmod u-s /usr/bin/vmware-user-suid-wrapper
[user@fedora ~]$ sudo setcap cap_dac_override=ep /usr/bin/vmware-user-suid-wrapper
… where I set CAP_DAC_OVERRIDE
because both /var/run/vmblock-fuse/dev and /dev/uinput are owned by root.
That did the trick:
2648<vmware-user-sui> openat(AT_FDCWD</home/user>, "/var/run/vmblock-fuse/dev", O_RDWR) = 3</run/vmblock-fuse/dev>
2648<vmware-user-sui> openat(AT_FDCWD</home/user>, "/dev/uinput", O_WRONLY|O_NONBLOCK) = 4</dev/uinput>
2648<vmware-user-sui> getuid() = 1000
2648<vmware-user-sui> getgid() = 1000
2648<vmware-user-sui> setreuid(1000, 1000) = 0
2648<vmware-user-sui> setregid(1000, 1000) = 0
2648<vmware-user-sui> execve("/usr/bin/vmtoolsd", ["/usr/bin/vmtoolsd", "-n", "vmusr", "--blockFd", "3", "--uinputFd", "4"], 0x7ffffa4da3d8 /* 20 vars */) = 0
Source:
sudo strace -u user -fyY -E XDG_SESSION_TYPE=wayland /usr/bin/vmware-user-suid-wrapper
after truncating whitespace for readability
As suggested by the strace(1) output above, vmware-user-suid-wrapper drops privileges via setreuid(2) and setregid(2) after obtaining a file descriptor for /var/run/vmblock-fuse/dev and possibly a file descriptor for /dev/uinput (or their respective fallbacks).3
The type vmtools_helper_exec_t
has an entrypoint to the vmtools_helper_t
application domain:
[user@fedora ~]$ sudo sesearch --allow -s vmtools_helper_t -t vmtools_helper_exec_t -c file -p entrypoint
allow vmtools_helper_t vmtools_helper_exec_t:file entrypoint;
The targeted SELinux policy defines a transition from processes in the unconfined_t
domain to the vmtools_helper_t
domain:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t vmtools_helper_exec_t
type_transition unconfined_t vmtools_helper_exec_t:process vmtools_helper_t;
This transition is allowed:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t vmtools_helper_t -c process -p transition
allow unconfined_t domain:process transition;
Process in the unconfined_t
domain can read and execute files with the vmtools_helper_exec_t
type:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t vmtools_helper_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };
Finally, the unconfined_r
role is associated to the vmtools_helper_t
type:
[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o vmtools_helper_t
vmtools_helper_t
Thus, when an unconfined user runs /usr/bin/vmware-user-suid-wrapper, the process should be subject to the constraints on the vmtools_helper_t
domain. This domain has the following permissions on the capability
and capability2
classes:
[user@fedora ~]$ sudo sesearch --allow -t vmtools_helper_t -c capability,capability2
allow vmtools_helper_t vmtools_helper_t:capability sys_module; [ secure_mode_insmod ]:False
allow vmtools_helper_t vmtools_helper_t:capability { audit_control audit_write chown dac_override dac_read_search fowner fsetid ipc_lock ipc_owner kill lease linux_immutable mknod net_admin net_bind_service net_broadcast net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_pacct sys_ptrace sys_rawio sys_resource sys_time sys_tty_config };
allow vmtools_helper_t vmtools_helper_t:capability2 { audit_read block_suspend bpf checkpoint_restore epolwakeup perfmon syslog wake_alarm };
If you’re not virtualizing Fedora Workstation 38 using a VMware product, then consider unsetting the SUID bit on /usr/bin/vmware-user-suid-wrapper.
vmware-user-suid-wrapper is installed as an XDG Autostart application:
[Desktop Entry]
Type=Application
Exec=/usr/bin/vmware-user-suid-wrapper
Name=VMware User Agent
# KDE bug 190522: KDE does not autostart items with NoDisplay=true...
# NoDisplay=true
X-KDE-autostart-phase=1
Source:
cat /etc/xdg/autostart/vmware-user.desktop
after truncating the trailing newline
systemd-xdg-autostart-generator(8) consumes this desktop file to generate and start a corresponding user service at startup:
[user@fedora ~]$ systemctl --user status 'app-vmware\[email protected]'
○ app-vmware\[email protected] - VMware User Agent
Loaded: loaded (/etc/xdg/autostart/vmware-user.desktop; generated)
Drop-In: /usr/lib/systemd/user/service.d
└─10-timeout-abort.conf
Active: inactive (dead)
Docs: man:systemd-xdg-autostart-generator(8)
vmware-user-suid-wrapper is actually a wrapper for /usr/bin/vmtoolsd, not /usr/bin/vmware-user, which is actually a symbolic link to /usr/bin/vmware-user-suid-wrapper.
All line numbers refer to open-vm-tools/open-vm-tools/vmware-user-suid-wrapper/[email protected] unless stated otherwise.
The __linux__
macro is assumed to be defined.
1 Lines 214–223, where the macros VMBLOCK_FUSE_DEVICE
and VMBLOCK_DEVICE
are defined on lines 122 and 168, respectively, of open-vm-tools/open-vm-tools/lib/include/[email protected]
2 Lines 225–230, where the useWayland
variable is set on lines 208–212
3 Lines 232–236
I identified 25 SUID-root binaries on an installation of Fedora Workstation 38 to a VirtIO disk.
In SUID-root Binaries in Fedora Custom OS 38, I had already analyzed 14 of these binaries. Of these, 2 needed additional capabilities to enable new workflows:
- /usr/bin/mount
- /usr/bin/su
I was able to unset the SUID bit without setting any file capabilities on the following new binaries:
- /usr/libexec/Xorg.wrap
- /usr/libexec/libgtop_server2
I was able to substitute the SUID bit using file capabilities on the following new binaries:
- /usr/bin/fusermount
- /usr/bin/fusermount-glusterfs
- /usr/bin/fusermount3
- /usr/libexec/qemu-bridge-helper
- /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
- /usr/bin/vmware-user-suid-wrapper
Of these, 2 binaries were libcap(3)-aware:
- /usr/libexec/qemu-bridge-helper
- /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
The remaining programs insisted on running with EUID 0 but I was nonetheless able to supplement the SUID bit on the corresponding binaries with file capabilities:
- /usr/sbin/userhelper
- /usr/libexec/dbus-1/dbus-daemon-launch-helper
- /usr/sbin/mount.nfs
The new programs of interest ran either unconfined by SELinux or in a domain that had almost all available permissions on the capability
and capability2
classes (excluding sys_module
, mac_admin
and mac_override
) due to the use of the special unconfined_domain
interface in the corresponding policy modules.
To get a high-level idea of the privileges needed by the SUID-root binaries that come with Fedora Workstation 38 but not Fedora Custom OS 38, I tallied the file capabilities that I ended up setting to enable the reference use cases:
CAP_CHOWN ┤ 0
CAP_DAC_OVERRIDE ┤████ 2
CAP_DAC_READ_SEARCH ┤ 0
CAP_FOWNER ┤██ 1
CAP_SETGID ┤████ 2
CAP_SETUID ┤████ 2
CAP_NET_ADMIN ┤██ 1
CAP_SYS_ADMIN ┤████████ 4
CAP_SYS_RESOURCE ┤ 0
CAP_AUDIT_WRITE ┤██ 1
Adding these counts to the capabilities histogram for Fedora Custom OS 38, plus the counts for /usr/bin/mount and /usr/bin/su, I got the following result:
CAP_CHOWN ┤██████ 4
CAP_DAC_OVERRIDE ┤████████████ 6
CAP_DAC_READ_SEARCH ┤████████ 4
CAP_FOWNER ┤████ 2
CAP_SETGID ┤████████████████ 8
CAP_SETUID ┤████████████████ 8
CAP_NET_ADMIN ┤██ 1
CAP_SYS_ADMIN ┤████████████ 6
CAP_SYS_RESOURCE ┤██ 1
CAP_AUDIT_WRITE ┤████████████████████ 10
The following open questions remain:
- Are there any use cases not enabled by the modifications that I identified in this report?
- How has the state of SUID-root binaries evolved from Fedora Workstation 38 to 39?
ACL Access control list
API Application programming interface
BPF Berkeley Packet Filter
BSD Berkeley Software Distribution
CC Creative Commons
CD-ROM Compact disc read-only memory
CPU Central processing unit
D-Bus Desktop Bus
EGID Effective GID
EUID Effective UID
FSGID File system GID
FSUID File system UID
FUSE Filesystem in Userspace
GDM GNOME Display Manager
GID Group ID
GNU GNU’s Not Unix
GPG GNU Privacy Guard
ID Identifier
IP Internet Protocol
ISO International Organization for Standardization
KMS Kernel mode setting
KVM Kernel-based Virtual Machine
LAN Local area network
NFS Network File System
OS Operating system
PAM Pluggable Authentication Modules
QEMU Quick Emulator
RGID Real GID
RPM RPM Package Manager
RUID Real UID
SATA Serial AT Attachment
SELinux Security-Enhanced Linux
SGID Set-GID
SHA Secure Hash Algorithm
SPICE Simple Protocol for Independent Computing Environments
SUID Set-UID
UID User ID
USB Universal Serial Bus
UUID Universally unique ID
VM Virtual machine
XDG X Desktop Group
I used dnf(8) to look up the reverse dependencies of each package providing a SUID-root binary, stopping at packages installed as part of the workstation-product-environment group. This package group is included on line 9 of the common Fedora Workstation kickstart, which is in turn included on line 6 of the Fedora Workstation Live kickstart. I used this information to build a reverse dependency graph for most packages.
usermode-1.114-7.fc38.x86_64
└─ required by anaconda-live-0:38.23.4-2.fc38.x86_64
anaconda-live is included on line 36 of the base Fedora Live kickstart. In turn, this kickstart is included on line 5 of the Fedora Workstation Live kickstart.
dbus-daemon-1:1.14.6-1.fc38.x86_64
├─ required by anaconda-core-0:38.23.4-2.fc38.x86_64 (A)
│ ├─ required by anaconda-gui-0:38.23.4-2.fc38.x86_64 (B)
│ │ ├─ required by anaconda-0:38.23.4-2.fc38.x86_64 (C)
│ │ └─ required by anaconda-live-0:38.23.4-2.fc38.x86_64
│ └─ required by anaconda-tui-0:38.23.4-2.fc38.x86_64
│ ├─ required by C
│ └─ required by A
├─ required by dbus-x11-1:1.14.6-1.fc38.x86_64
│ └─ required by tigervnc-server-minimal-0:1.13.1-3.fc38.x86_64
│ └─ required by B
└─ required by gdm-1:43.0-7.fc38.x86_64
anaconda is included on line 34 of the base Fedora Live kickstart.
anaconda-live is also included in this kickstart.
gdm is a mandatory package in the gnome-desktop group (line 2611 of the Fedora 38 comps). gnome-desktop is a member of the workstation-product-environment group (line 6147 of the Fedora 38 comps).
xorg-x11-server-Xorg is a package in the base-x group (line 279 of the Fedora 38 comps). base-x is a member of the workstation-product-environment group (line 6142 of the Fedora 38 comps).
libgtop2-2.41.1-1.fc38.x86_64
├─ required by gnome-control-center-0:44.0-1.fc38.x86_64
└─ required by gnome-system-monitor-0:44.0-1.fc38.x86_64
gnome-control-center and gnome-system-monitor are mandatory and default packages, respectively, in the gnome-desktop group (lines 2614 and 2656, respectively, of the Fedora 38 comps).
nfs-utils is a default package in the workstation-product group (line 5654 of the Fedora 38 comps).
fuse-2.9.9-16.fc38.x86_64
├─ required by dracut-live-0:059-2.fc38.x86_64
├─ required by flatpak-0:1.15.4-1.fc38.x86_64
│ ├─ required by fedora-flathub-remote-0:1-5.fc38.noarch
│ └─ required by gnome-software-0:44.0-3.fc38.x86_64
└─ required by zfs-fuse-0:0.7.2.2-24.fc38.x86_64
└─ required by libvirt-daemon-driver-storage-zfs-0:9.0.0-2.fc38.x86_64
└─ required by libvirt-daemon-driver-storage-0:9.0.0-2.fc38.x86_64
└─ required by libvirt-daemon-kvm-0:9.0.0-2.fc38.x86_64
└─ required by gnome-boxes-0:44.1-1.fc38.x86_64
dracut-live is included on line 48 of the base Fedora Live kickstart.
fedora-flathub-remote is a default package in the workstation-product group (line 5621 of the Fedora 38 comps). workstation-product is a member of the workstation-product-environment group (line 6154 of the Fedora 38 comps).
gnome-software and gnome-boxes are mandatory packages in the gnome-desktop group (lines 2620 and 2612, respectively, of the Fedora 38 comps).
glusterfs-fuse-11.0-1.fc38.x86_64
└─ required by libvirt-daemon-driver-storage-gluster-0:9.0.0-2.fc38.x86_64
└─ required by libvirt-daemon-driver-storage-0:9.0.0-2.fc38.x86_64
└─ required by libvirt-daemon-kvm-0:9.0.0-2.fc38.x86_64
└─ required by gnome-boxes-0:44.1-1.fc38.x86_64
gnome-boxes is a mandatory package in the gnome-desktop group.
fuse3-3.13.1-2.fc38.x86_64
├─ required by fuse-overlayfs-0:1.10-3.fc38.x86_64
│ └─ required by containers-common-4:1-86.fc38.noarch
│ ├─ required by containers-common-extra-4:1-86.fc38.noarch
│ │ └─ required by podman-5:4.4.2-2.fc38.x86_64
│ │ └─ required by toolbox-0:0.0.99.4-1.fc38.x86_64 (A)
│ └─ required by A
├─ required by gvfs-fuse-0:1.50.4-1.fc38.x86_64
├─ required by open-vm-tools-0:12.1.5-3.fc38.x86_64
│ └─ required by open-vm-tools-desktop-0:12.1.5-3.fc38.x86_64
└─ required by xdg-desktop-portal-0:1.16.0-3.fc38.x86_64
toolbox is a default package in the workstation-product group (line 5687 of the Fedora 38 comps).
gvfs-fuse and xdg-desktop-portal are default packages in the gnome-desktop group (lines 2665 and 2695, respectively, of the Fedora 38 comps).
open-vm-tools-desktop is a mandatory package in the guest-desktop-agents group (line 2909 of the Fedora 38 comps). guest-desktop-agents is a member of the workstation-product-environment group (line 6148 of the Fedora 38 comps).
[user@fedora ~]$ dnf repoquery --installed --whatrequires qemu-common
qemu-audio-alsa-2:7.2.0-6.fc38.x86_64
qemu-audio-dbus-2:7.2.0-6.fc38.x86_64
qemu-audio-jack-2:7.2.0-6.fc38.x86_64
qemu-audio-oss-2:7.2.0-6.fc38.x86_64
qemu-audio-pa-2:7.2.0-6.fc38.x86_64
qemu-audio-sdl-2:7.2.0-6.fc38.x86_64
qemu-audio-spice-2:7.2.0-6.fc38.x86_64
qemu-block-blkio-2:7.2.0-6.fc38.x86_64
qemu-block-curl-2:7.2.0-6.fc38.x86_64
qemu-block-dmg-2:7.2.0-6.fc38.x86_64
qemu-block-gluster-2:7.2.0-6.fc38.x86_64
qemu-block-iscsi-2:7.2.0-6.fc38.x86_64
qemu-block-nfs-2:7.2.0-6.fc38.x86_64
qemu-block-rbd-2:7.2.0-6.fc38.x86_64
qemu-block-ssh-2:7.2.0-6.fc38.x86_64
qemu-char-baum-2:7.2.0-6.fc38.x86_64
qemu-char-spice-2:7.2.0-6.fc38.x86_64
qemu-device-display-qxl-2:7.2.0-6.fc38.x86_64
qemu-device-display-vhost-user-gpu-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-gpu-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-gpu-ccw-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-gpu-gl-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-gpu-pci-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-gpu-pci-gl-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-vga-2:7.2.0-6.fc38.x86_64
qemu-device-display-virtio-vga-gl-2:7.2.0-6.fc38.x86_64
qemu-device-usb-host-2:7.2.0-6.fc38.x86_64
qemu-device-usb-redirect-2:7.2.0-6.fc38.x86_64
qemu-device-usb-smartcard-2:7.2.0-6.fc38.x86_64
qemu-system-x86-core-2:7.2.0-6.fc38.x86_64
qemu-ui-curses-2:7.2.0-6.fc38.x86_64
qemu-ui-egl-headless-2:7.2.0-6.fc38.x86_64
qemu-ui-gtk-2:7.2.0-6.fc38.x86_64
qemu-ui-opengl-2:7.2.0-6.fc38.x86_64
qemu-ui-sdl-2:7.2.0-6.fc38.x86_64
qemu-ui-spice-app-2:7.2.0-6.fc38.x86_64
qemu-ui-spice-core-2:7.2.0-6.fc38.x86_64
All these packages share qemu-system-x86 as a common requirement, for which I constructed the following reverse dependency graph:
qemu-system-x86-2:7.2.0-6.fc38.x86_64
└─ required by qemu-kvm-2:7.2.0-6.fc38.x86_64
└─ required by libvirt-daemon-kvm-0:9.0.0-2.fc38.x86_64
└─ required by gnome-boxes-0:44.1-1.fc38.x86_64
gnome-boxes is a mandatory package in the gnome-desktop group.
spice-glib-0.42-1.fc38.x86_64
├─ required by gnome-boxes-0:44.1-1.fc38.x86_64 (A)
└─ required by spice-gtk3-0:0.42-1.fc38.x86_64
└─ required by A
gnome-boxes is a mandatory package in the gnome-desktop group.
open-vm-tools-desktop is a mandatory package in the guest-desktop-agents group.
Please connect with me if you have constructive comments about this article, especially if you have a use case I didn’t cover or are unable to reproduce a procedure in an identical environment.
Fedora Linux 38 reached end of life on May 21, 2024. I am therefore no longer maintaining this article. However, I may incorporate relevant constructive feedback into reports pertaining to newer releases of Fedora Linux.
The author acknowledges private funding by one anonymous sponsor.
All original copyrightable content in this document is marked with CC0 1.0 Universal.
- Change Fedora Server 38 references to refer to Fedora Custom OS 38
- Add status header
- Expand OS abbreviation in Appendix A
State discontinuation of maintenance of this article
- Annotate short options to commands in Characterization of the environment
- Link to new article: SUID-root Binaries in Fedora Workstation 39
- Expand CC abbreviation in Appendix A
- Drop unused abbreviation (OS for operating system) from Appendix A
- Edit main text for consistency and clarity
Update capability histograms to reflect rev. 2024-02-09.3 of SUID-root Binaries in Fedora Custom OS 38
- Shorten article name
- Edit main text for style, consistency and clarity
- Drop comment on /usr/libexec/Xorg.wrap
- Use different paths for NFSv4 export and import directories to improve clarity
- Comment on /usr/libexec/qemu-bridge-helper
- Update comment on /usr/libexec/spice-gtk-x86_64/spice-client-glib-usb-acl-helper
- Annotate short options to commands
- Update hyperlink to article on SUID-root binaries in Fedora Custom OS 38
- Add D-Bus hyperlink
- Expand CD-ROM and SATA abbreviations in Appendix A
Initial revision