> ## Content Index
> Fetch the complete content index at: https://www.smallstepsystems.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Running Wayland Clients as Non-Root Users
- URL: https://www.smallstepsystems.com/running-wayland-clients-as-non-root-users/
- Published: 2025-08-11T14:24:12.000Z
- Updated: 2026-05-11T08:13:22.000Z
- Author: Burkhard Stubert
- Tags: Embedded Linux, wayland, systemd, cybersecurity

Many embedded Linux systems use a [Wayland compositor](https://wayland.freedesktop.org/architecture.html?ref=smallstepsystems.com) like Weston for window management. Qt applications act as Wayland clients. [Weston](https://wayland.pages.freedesktop.org/weston/?ref=smallstepsystems.com) 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](https://en.wikipedia.org/wiki/Principle%5Fof%5Fleast%5Fprivilege?ref=smallstepsystems.com). 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](https://wayland.freedesktop.org/architecture.html?ref=smallstepsystems.com) [Weston](https://wayland.pages.freedesktop.org/weston/?ref=smallstepsystems.com) to show one or more Qt applications - the Wayland clients - on a display. From the Qt example applications in the [meta-boot2qt layer](https://code.qt.io/cgit/yocto/meta-boot2qt.git/tree/meta-boot2qt/recipes-qt/boot2qt-addons?ref=smallstepsystems.com) 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](https://www.smallstepsystems.com/tag/eu-cyber-resilience-act/) (EU CRA) turned the tables. Running applications as root violates the cybersecurity [principle of least privilege](https://en.wikipedia.org/wiki/Principle%5Fof%5Fleast%5Fprivilege?ref=smallstepsystems.com) \- 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/<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\*](https://docs.yoctoproject.org/ref-manual/classes.html?ref=smallstepsystems.com#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](https://gitlab.freedesktop.org/wayland/weston/-/blob/12.0/compositor/main.c?ref%5Ftype=heads&ref=smallstepsystems.com)). 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](https://www.smallstepsystems.com/eu-cra-essential-requirements-related-to-product-properties/#availability-of-essential-and-basic-functions) 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](https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.22.0/src/wayland-server.c?ref%5Ftype=tags&ref=smallstepsystems.com)) 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](https://developer.toradex.com/torizon/torizoncore/torizon-os-reference-minimal-image/?ref=smallstepsystems.com) 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`.
- *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`.
- *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**`

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.