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 group weston and change the permissions of /run/wayland-0 . The socket file is created at runtime – most likely by the the service weston.socket from the layer openembedded-core . According to definition in weston.socket , the socket file should have permissions 0775 , but it has 0755 . 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/ . is the ID of the user running the application. Now, 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 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 user root by weston and the group root by weston . Then, Weston is started with the non-root privileges of the user weston . We undo the changes from weston-init.bbappend in the layer meta-toradex-bsp-common .
, the user by and the group by . Then, Weston is started with the non-root privileges of the user . We undo the changes from in the layer . Change (2). We replace, in the service unit weston.socket , the user root by weston and the group root by wayland . Then, the global socket /run/wayland-0 is owned by the non-root user weston . Again, we undo the changes from the layer meta-toradex-bsp-common .
, the user by and the group by . Then, the global socket is owned by the non-root user . Again, we undo the changes from the layer . 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 final weston.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
– here – is used in the Wayland server (Weston) and in the Wayland clients (e.g., Qt applications).
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