Many embedded Linux systems use a Wayland compositor like Weston for window management. Qt applications act as Wayland clients. Weston composes the windows of the Qt applications into a single window and displays it on a screen. I still have to find a Yocto layer that does not start Qt applications as root. This violates the cybersecurity principle that every application should only run with the least privileges possible. Let us figure out how to run Qt applications as non-root users and make our system more secure.
Context
We build our embedded Linux system with Yocto (Yocto 5.0 “scarthgap” at the time of writing). The system uses the default Wayland compositor Weston to show one or more Qt applications – the Wayland clients – on a display. From the Qt example applications in the meta-boot2qt layer or in other vendor BSPs, we might have cobbled together a systemd service unit b4-simple-app.service
like this:
[Service]
Type=simple
User=root
Environment=XDG_RUNTIME_DIR=/run/user/0
Environment=WAYLAND_DISPLAY=/run/wayland-0
Environment=QT_QPA_PLATFORM=wayland-egl
ExecStart=/usr/bin/B4SimpleApp
This service starts the Qt application B4SimpleApp
as root
. I always felt a bit uneasy about this solution, but it worked and was suggested by experts. Anyway, customers didn’t want to pay for anything better. However, the arrival of the EU Cyber Resilience Act (EU CRA) turned the tables. Running applications as root violates the cybersecurity principle of least privilege – and the EU CRA.
Why are Wayland clients run as root? The reason are the permissions of the socket file /run/wayland-0
, which the Wayland server and client use to communicate with each other.
torizon@verdin-imx8mp-06965633:~$ sudo ls -l /run/wayland-0
srwxr-xr-x 1 weston weston 0 Aug 10 12:28 /run/wayland-0
If the Qt application is started as a non-root user other than weston
– say, torizon
with user and group ID 1000, starting the Qt application B4SimpleApp will fail with the error
B4SimpleApp[817]: Failed to create wl_display (Permission denied)
The same error occurs, if we start Weston as root. Some BSPs do this, some don’t.
Two options suggest themselves:
- We start B4SimpleApp as user
weston
. - We add the default user
torizon
to the groupweston
and change the permissions of/run/wayland-0
. The socket file is created at runtime – most likely by the the serviceweston.socket
from the layeropenembedded-core
. According to definition inweston.socket
, the socket file should have permissions0775
, but it has0755
. I don’t know (yet) where this change happens. This rules out this option for the time being.
This leaves us with the first option: starting the application as user weston
by setting User=weston
in the service unit of the application.
The environment variable XDG_RUNTIME_DIR
follows the pattern /run/user/<uid>
. <uid>
is the ID of the user running the application. Now, <uid>
is not the root ID 0 any more but the ID of the user weston
.
By default, the Yocto build creates the IDs for the users dynamically. If we add or remove users, the ID for weston
may differ from build to build. We better use static user and group IDs as described for the class useradd*
. For example, I assigned the static ID 2000 to the user weston
. Hence, Weston writes runtime information for its Wayland clients into the directory /run/user/2000
. Clients use the environment variable XDG_RUNTIME_DIR
to read the information from Weston or to pass information to Weston.
As Wayland clients like B4SimpleApp run as user weston
with the ID 2000, we could skip setting XDG_RUNTIME_DIR
in the clients’ service units. However, this may break, if we figure out how to run Qt applications as other users than weston
and root
or if we use a relative path for the socket file given in WAYLAND_DISPLAY
. So, we should set XDG_RUNTIME_DIR
to /run/user/2000
to avoid future debugging sessions.
The environment variable WAYLAND_DISPLAY
is the filename of the socket that the Wayland compositor and client use for inter-process communication. If WAYLAND_DISPLAY
is an absolute path, it is used as is. If it is a relative path, $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY
is used as the socket file name.
Weston ignores the value of WAYLAND_DISPLAY
. The Weston main()
function calls the function weston_create_listening_socket(display, NULL)
, where the socket file name is NULL
(both functions in main.c
). The latter function generates a socket base name wayland-x
– with x
between 1 and 32 – so that the socket $XDG_RUNTIME_DIR/wayland-x
can be set up successfully. As the socket file name may change, the Wayland client will eventually fail to start with the error message
B4SimpleApp[822]: Failed to create wl_display (No such file or directory)
Waiting for an application to fail in the field is a pretty bad idea. Moreover, it violates the availability requirement of the EU CRA. We must ensure that the socket file name is the same for the Wayland server, Weston, and its clients.
We can pass the option -S <socket-file-name>
to Weston in the service unit weston.service
.
ExecStart=/usr/bin/weston --modules=systemd-notify.so -S "/run/wayland-0"
Weston main()
calls weston_create_listening_socket(display, "/run/wayland-0")
. The latter function creates the socket with the name /run/wayland-0
. If the socket name is relative, say, wayland-1
, it will create a socket with the name $XDG_RUNTIME_DIR/wayland-1
. See the function wl_socket_init_for_display_name
called by wl_display_add_socket
(both in wayland-server.c
) called by weston_create_listening_socket
to fully understand the generation of the socket filename.
On the client side, we set the environment variable WAYLAND_DISPLAY
to the same value /run/wayland-0
passed to Weston with option -S
.
We could also pass -S "wayland-1"
to Weston and set WAYLAND_DISPLAY
to wayland-1
for B4SimpleApp. Weston and B4SimpleApp would then communicate through the socket /run/user/2000/wayland-1
.
We could add the following to lines to the service unit of every Wayland client:
Environment=XDG_RUNTIME_DIR=/run/user/2000
Environment=WAYLAND_DISPLAY=/run/wayland-0
Obviously, we would have to duplicate the same two lines in all service files. We can fix this by moving the two lines to an environment file /etc/default/weston-client
and include this file in the clients’ service units:
EnvironmentFile=/etc/default/weston-client
The environment file has the following content:
XDG_RUNTIME_DIR=/run/user/2000
WAYLAND_DISPLAY=/run/wayland-0
Furthermore, it would be a bad idea to let the client recipe b4-simple-app.bb
create the environment file weston-client
and the recipe weston-init.bbappend
create the option -S
for the Weston call. When we change the socket filename in one place, we might forget to change it in the other place. We avoid this by creating both the environment file and the option in the recipe weston-init.bbappend
.
Solution
We can change the user and group in weston.service
and weston.socket
, pass the socket file name to the weston
command and generate the environment file weston-client
in a single place: in the do_install
task of the recipe extension weston-init.bbappend
. We create the file recipes-graphics/wayland/weston-init.bbappend
in our own Yocto layer (e.g., meta-b4-apps
for me).
What we add to the do_install
task depends on how many weston-init.bbappend
files extend the original recipe openembedded-core/meta/recipes-graphics/wayland/weston-init.bb
. As I’m extending the Torizon Minimal image by Weston and Qt applications, I’m facing three extensions:
meta-freescale/recipes-graphics/wayland/weston-init.bbappend
meta-toradex-ti/recipes-graphics/wayland/weston-init.bbappend
meta-toradex-bsp-common/recipes-graphics/wayland/weston-init.bbappend
A good approach is to check the generated files in the work directory of the image recipe (e.g., b4-hmi-product-image
) for the required changes. The relevant files are:
# In build-verdin-imx8mp/tmp/work/verdin_imx8mp-tdx-linux/b4-hmi-product-image/1.0
rootfs/usr/lib/systemd/system/weston.service
rootfs/usr/lib/systemd/system/weston.socket
rootfs/etc/default/weston-client
In my case, the do_install
task looks as follows:
do_install:append() {
if ${@bb.utils.contains('DISTRO_FEATURES','systemd','true','false',d)}; then
# (1)
sed -i "s/User=root/User=weston/" ${D}${systemd_system_unitdir}/weston.service
sed -i "s/Group=root/Group=weston/" ${D}${systemd_system_unitdir}/weston.service
# (2)
sed -i "s/SocketUser=root/SocketUser=weston/" ${D}${systemd_system_unitdir}/weston.socket
sed -i "s/SocketGroup=root/SocketGroup=wayland/" ${D}${systemd_system_unitdir}/weston.socket
# (3)
# (3a)
socket_name="/run/wayland-0"
sed -i "s|--modules=systemd-notify.so|--modules=systemd-notify.so -S \"$socket_name\" |" ${D}${systemd_system_unitdir}/weston.service
# (3b)
echo "XDG_RUNTIME_DIR=/run/user/2000" >> ${D}${sysconfdir}/default/weston-client
echo "WAYLAND_DISPLAY=${socket_name}" >> ${D}${sysconfdir}/default/weston-client
fi
}
The changes do the following:
- Change (1). We replace, in the service unit
weston.service
, the userroot
byweston
and the grouproot
byweston
. Then, Weston is started with the non-root privileges of the userweston
. We undo the changes fromweston-init.bbappend
in the layermeta-toradex-bsp-common
. - Change (2). We replace, in the service unit
weston.socket
, the userroot
byweston
and the grouproot
bywayland
. Then, the global socket/run/wayland-0
is owned by the non-root userweston
. Again, we undo the changes from the layermeta-toradex-bsp-common
. - Change (3). The changes (3a) and (3b) ensure that the same
socket_name
– here/run/wayland-0
– is used in the Wayland server (Weston) and in the Wayland clients (e.g., Qt applications).- Change (3a). For the Wayland server, we pass the option
-S "/run/wayland-0"
to the command starting Weston. In the finalweston.service
, we should have a line similar to this:
ExecStart=/usr/bin/weston --modules=systemd-notify.so -S "/run/wayland-0"
. - Change (3b). For the Wayland clients, we generate the file
weston-client
with the contents:XDG_RUNTIME_DIR=/run/user/2000
WAYLAND_DISPLAY=/run/wayland-0
- Change (3a). For the Wayland server, we pass the option
We are almost done. The service unit of the Wayland clients – e.g., b4-simple-app.service
– needs two little changes.
[Service]
Type=simple
User=weston
EnvironmentFile=/etc/default/weston-client
Environment=QT_QPA_PLATFORM=wayland-egl
ExecStart=/usr/bin/B4SimpleApp
Systemd starts the Qt application B4SimpleApp
as the non-root user weston
. It also passes the correct socket name WAYLAND_DISPLAY
and runtime directory XDG_RUNTIME_DIR
to the application – through the environment file /etc/default/weston-client
. If all Wayland clients use the same environment file and run as user weston
, Weston will be able to display the clients without problems. Neither Weston nor the Qt applications must run as root.