Sway and hidepid

TL;DR: Mounting /proc with hidepid can prevent you from executing Sway as an unprivileged user from a tty.

Update (August 31): As expected, newly released wlroots 0.7.0 fixes this issue!

Following some reading about differences between X11 and Wayland, I decided to try the later out. I naturally took a look at Sway. Sway is a tiling Wayland compositor and is supposed to be a drop-in replacement for i3, which I’ve been using for many years now.

I was in such a good mood that I even chose to integrate Sway with systemd. A few moments later, I was already getting angry at systemd for failing to start my sway.service, and I decided not to cut any corners and rather go step by step. I thus tried launching Sway directly from a tty to see if it was at least able to execute correctly. It did not:

thithib@hyperion:~$ sway
2019-08-09 14:04:44 - [wlroots-0.6.0/backend/session/logind.c:575] Session '4' isn't a graphical session (type: 'tty')
2019-08-09 14:04:44 - [wlroots-0.6.0/backend/session/direct-ipc.c:35] Do not have CAP_SYS_ADMIN; cannot become DRM master
2019-08-09 14:04:44 - [wlroots-0.6.0/backend/session/session.c:96] Failed to load session backend
2019-08-09 14:04:44 - [wlroots-0.6.0/backend/backend.c:321] Failed to start a DRM session
2019-08-09 14:04:44 - [sway-1.1.1/sway/server.c:47] Unable to create backend
FAIL: 1

Just as Xorg, Sway needs to open various device nodes in order to handle devices attached to the active session’s seat. Those are privileged operations. When you’re not using a display manager (such as GDM or LXDM) and not insane enough to run your display server as root, you can rely on the fact that Xorg works in concert with systemd-logind, which opens the required device nodes and then passes file descriptors to Xorg. I’ve been relying on that for a long time, launching Xorg with startx from a tty after logging in, and it works pretty well.

However, it seemed that in my case Sway was not able to obtain such handles from systemd-logind. I read many interesting threads about people having issues with Sway, such as this or this. It kind of pointed towards some issue with interactions between systemd user sessions and Sway.

This answer to the GitHub issue linked above made me take a look at wlroots’ source code (a modular Wayland compositor library written by Sway guys). Apparently, some code was merged around two months ago to allow compositors to directly get the active session ID by reading the XDG_SESSION_ID environment variable set by pam_systemd in conjunction with systemd-logind.

Unfortunately, this code is not yet released in a stable version of wlroots (I’m using the latest, which is 0.6.0). Therefore, get_display_session() tries to use the systemd API to retrieve the active session ID. It appears this doesn’t work as, according to the error messages I get, I reach the call to sd_session_get_type() further down logind_session_create(), followed by code that checks the type of the returned session, which is tty in my case as I’m executing Sway from a tty, and then prints an error before exiting.

It’d be great if sd_pid_get_session() could return a session ID, since get_display_session() would then return true and Sway’s startup sequence would hopefully fail a bit further^W^W^W^W reach in particular the calls to find_session_path(), session_activate() and take_control(), then the end of wlr_session_create() and at long last the end of wlr_backend_autocreate() which is part of wlroot’s API, subsequently allowing Sway to obtain the precious device handles from systemd-logind (the interested reader can start by looking at logind_take_device() to see how device file descriptors are obtained thereafter).

I tried to run the following to see what was happening:

thithib@hyperion:~$ cat test.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <systemd/sd-login.h>
#include <errno.h>

int main(void)
{
    char *session;
    pid_t my_pid;
    int ret;

    my_pid = getpid();
    printf("My PID is %ld\n", (long)my_pid);

    ret = sd_pid_get_session(my_pid, &session);
    if (ret) {
        printf("sd_pid_get_session() returned %d...\n", ret);
    } else {
        printf("Session: %s\n", session);
        free(session);
    }

    return 0;
}
thithib@hyperion:~$ gcc -L/usr/lib/systemd -lsystemd-shared-242 test.c
thithib@hyperion:~$ LD_LIBRARY_PATH=/usr/lib/systemd ./a.out
My PID is 2590
sd_pid_get_session() returned -3...
thithib@hyperion:~$ errno 3
ESRCH 3 No such process

According to man 3 sd_pid_get_session, ESRCH means that “the specified PID does not refer to a running process”. Well it sure does! Quite fortunately, I had stumbled upon this post earlier and kept the tab open. It pointed to some systemd source code:

fs = procfs_file_alloca(pid, "cgroup");
f = fopen(fs, "re");
if (!f)
    return errno == ENOENT ? -ESRCH : -errno;

Was systemd-logind failing to open /proc/<pid>/cgroup? cat /proc/self/cgroup seemed to run fine. It’s high time I rebooted with Yama’s ptrace_scope lowered to 1 and run strace!

thithib@hyperion:~$ LD_LIBRARY_PATH=/usr/lib/systemd strace -e trace=openat ./a.out 2>&1 | grep cgroup
openat(AT_FDCWD, "/proc/1756/cgroup", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/1/cgroup", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

And that’s when I remembered I was mounting my proc pseudo-filesystem with the hidepid=2 option, which by the way is known not to cope well with systemd. At the time I set this up, I followed Archlinux’s wiki instructions and in particular used a drop-in to enable systemd-logind to bypass the restriction and make user sessions work correctly. But it now evidently appears not to be enough. Anyway, I tried remounting my /proc without hidepid, and:

thithib@hyperion:~$ _ mount -o remount,hidepid=0 proc
thithib@hyperion:~$ LD_LIBRARY_PATH=/usr/lib/systemd ./a.out
My PID is 3057
Session: 3

And guess what? Sway was finally working!

This leaves us with the following question: why is adding the systemd-logind service to the “hidepid bypass” group enough to make unprivileged Xorg work but not Sway? Below are some elements of response.

First of all, I think that the aforementioned “get active session ID by reading XDG_SESSION_ID env var” patch should fix the issue for Sway once it’s released (hopefully in next stable wlroots):

thithib@hyperion:~$ cat test2.c
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <systemd/sd-login.h>

int main(void)
{
    char *xdg_session_id;

    xdg_session_id = secure_getenv("XDG_SESSION_ID");
    if (xdg_session_id) {
        if (sd_session_is_active(xdg_session_id))
            printf("XDG_SESSION_ID = %s\n", xdg_session_id);
        else
            puts("Invalid XDG_SESSION_ID!");
    } else {
        puts("Could not get XDG_SESSION_ID env var!");
    }

    return 0;
}
thithib@hyperion:~$ gcc -L/usr/lib/systemd -lsystemd-shared-242 test2.c
thithib@hyperion:~$ LD_LIBRARY_PATH=/usr/lib/systemd ./a.out
XDG_SESSION_ID = 3
thithib@hyperion:~$ mount | grep '/proc '
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime,gid=26,hidepid=2)

Indeed, it means that get_display_session() would return true and, as already mentioned, we would hopefully reach the end of wlr_session‘s creation, supposing there are no more libsystemd calls resulting in /proc accesses that would be thwarted by hidepid. After some very quick testing and source code reading, it seems OK, but don’t trust me on that one.

Second of all, is Xorg already using this same technique? Unsetting environment variable XDG_SESSION_ID before executing startx doesn’t prevent unprivileged Xorg from running. We can take a look at Xorg’s source code and in particular at one interesting function, connect_hook() (no need to read in detail):

static void
connect_hook(DBusConnection *connection, void *data)
{
    struct systemd_logind_info *info = data;
    DBusError error;
    DBusMessage *msg = NULL;
    DBusMessage *reply = NULL;
    dbus_int32_t arg;
    char *session = NULL;

    dbus_error_init(&error);

    msg = dbus_message_new_method_call("org.freedesktop.login1",
            "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
            "GetSessionByPID");
    if (!msg) {
        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
        goto cleanup;
    }

    arg = getpid();
    if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &arg,
                                  DBUS_TYPE_INVALID)) {
        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
        goto cleanup;
    }

    reply = dbus_connection_send_with_reply_and_block(connection, msg,
                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
    if (!reply) {
        LogMessage(X_ERROR, "systemd-logind: failed to get session: %s\n",
                   error.message);
        goto cleanup;
    }
    dbus_message_unref(msg);

    if (!dbus_message_get_args(reply, &error, DBUS_TYPE_OBJECT_PATH, &session,
                               DBUS_TYPE_INVALID)) {
        LogMessage(X_ERROR, "systemd-logind: GetSessionByPID: %s\n",
                   error.message);
        goto cleanup;
    }
    session = XNFstrdup(session);

    dbus_message_unref(reply);
    reply = NULL;


    msg = dbus_message_new_method_call("org.freedesktop.login1",
            session, "org.freedesktop.login1.Session", "TakeControl");
    if (!msg) {
        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
        goto cleanup;
    }

    arg = FALSE; /* Don't forcibly take over over the session */
    if (!dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &arg,
                                  DBUS_TYPE_INVALID)) {
        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
        goto cleanup;
    }

    reply = dbus_connection_send_with_reply_and_block(connection, msg,
                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
    if (!reply) {
        LogMessage(X_ERROR, "systemd-logind: TakeControl failed: %s\n",
                   error.message);
        goto cleanup;
    }

    [...]

    LogMessage(X_INFO, "systemd-logind: took control of session %s\n",
               session);
    info->conn = connection;
    info->session = session;
    info->vt_active = info->active = TRUE; /* The server owns the vt during init */
    session = NULL;

cleanup:
    [...]
}

There’s no need to read the code in detail to grasp that Xorg is using D-Bus to call the GetSessionByPID() method provided by systemd-logind. Therefore Xorg does delegate the work of retrieving the session ID from the PID to systemd-logind, which is whitelisted with relation to hidepid.

This is thus different from what wlroots does. wlroots calls sd_pid_get_session() which itself calls cg_pid_get_session() which itself calls both cg_pid_get_path_shifted() and cg_path_get_session(). Using gdb, I then observed that, through the former, we reach the cg_shift_path(raw, root, &c) call with root being NULL, which thus calls cg_get_root_path() which finally does cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, 1, &p) and thus reaches the systemd source code snippet showed earlier, trying to open, this time, the root-owned /proc/1/cgroup! Given that all this code runs in the context of the calling user, i.e. me, it rightfully fails due to hidepid. In order to verify that, let’s temporarily whitelist my user’s group instead of the proc one:

thithib@hyperion:~$ _ mount -o remount,hidepid=2,gid=1000 proc
thithib@hyperion:~$ gcc -L/usr/lib/systemd -lsystemd-shared-242 test.c
thithib@hyperion:~$ LD_LIBRARY_PATH=/usr/lib/systemd ./a.out
My PID is 6496
Session: 1

Great. Now let’s just hope that the remaining of wlroot’s startup sequence is not doing similar calls to the sd-login part of the systemd API that may be impacted by hidepid, and that it uses D-Bus instead (again, after some very quick testing and source code reading, it seems OK).

To conclude, kudos to the Xorg guys for having handled this with D-Bus so as to avoid systemd’s weird way of querying /proc/1/ files from other processes (which seems unavoidable).

And now I can finally go back to configuring Sway.