Non-root Xorg and modesetting driver on Gentoo (or any non-systemd system)

Gentoo Wiki explains a nice way to run Xorg under user account. While it works fine with intel driver it doesn't work with modesetting driver:

(EE) modeset(0): drmSetMaster failed: Permission denied

This is because drmSetMaster and drmDropMaster require root privileges. So in order to make it work we have to hack Xorg and make it call these functions under root.

Let's first find a place in the modesetting driver's code where drmSetMaster and drmDropMaster are used.

$ grep -r drmSetMaster . --include=*.c
./hw/xfree86/drivers/modesetting/driver.c:    ret = drmSetMaster(ms->fd);
./hw/xfree86/drivers/modesetting/driver.c:        xf86DrvMsg(pScrn->scrnIndex, X_ERROR, "drmSetMaster failed: %s\n",

Okay, we've got something. There're two functions we need to patch. Note that drmSetMaster and drmDropMaster accept file descriptor as an argument.

static Bool
SetMaster(ScrnInfoPtr pScrn)
{
    modesettingPtr ms = modesettingPTR(pScrn);
    int ret;

#ifdef XF86_PDEV_SERVER_FD
    if (ms->pEnt->location.type == BUS_PLATFORM &&
        (ms->pEnt->location.id.plat->flags & XF86_PDEV_SERVER_FD))
        return TRUE;
#endif

    if (ms->fd_passed)
        return TRUE;

    ret = drmSetMaster(ms->fd);
    if (ret)
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR, "drmSetMaster failed: %s\n",
                   strerror(errno));

    return ret == 0;
}
static void
LeaveVT(ScrnInfoPtr pScrn)
{
    modesettingPtr ms = modesettingPTR(pScrn);

    xf86_hide_cursors(pScrn);

    pScrn->vtSema = FALSE;

#ifdef XF86_PDEV_SERVER_FD
    if (ms->pEnt->location.type == BUS_PLATFORM &&
        (ms->pEnt->location.id.plat->flags & XF86_PDEV_SERVER_FD))
        return;
#endif

    if (!ms->fd_passed)
        drmDropMaster(ms->fd);
}

How about we write a suid helper program that will receive a file descriptor, call the desired function and exit? Sounds like a very dirty hack, but hey, it's better than the whole X server running under root.

So here's how it's gonna work. Instead of just calling drmSetMaster(ms->fd), the X server will create a new UNIX domain socket and wait for a connection. At the same time it'll launch our siud helper program from another thread. The helper program will connect to that socket. The X server will accept the connection and write ms->fd to the socket. The helper program will read the fd, call drmSetMaster (or drmDropMaster) and exit. The X server will then close the socket and continue execution as normally.

The first part of our plan is the suid helper program. Here are the most important parts of the code to demonstrate the idea; you will find the complete program and a working patch for Xorg at the end of this post.

// use hardcoded socket name for simplicity
#define DRM_HACK_SOCKET_NAME "xorg_drm_master_util"

// function that reads file descriptor from given socket
int recv_fd(int sock)
{
    struct msghdr msg;
    struct iovec iov[1];
    struct cmsghdr *cmsg = NULL;
    char ctrl_buf[CMSG_SPACE(sizeof(int))];
    char data[1];

    memset(&msg, 0, sizeof(struct msghdr));
    memset(ctrl_buf, 0, CMSG_SPACE(sizeof(int)));

    iov[0].iov_base = data;
    iov[0].iov_len = sizeof(data);

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = CMSG_SPACE(sizeof(int));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    recvmsg(sock, &msg, 0);

    cmsg = CMSG_FIRSTHDR(&msg);

    return *((int *) CMSG_DATA(cmsg));
}

int main(int argc, char *argv[])
{
    bool do_set = false;
    bool do_drop = false;
    // ...some argument parsing logic here...

    // change uid and make sure we've become root
    uid_t euid = geteuid();
    assert(euid == 0);

    setuid(euid);

    uid_t uid = getuid();
    assert(uid == 0);

    // create and connect to unix domain socket
    struct sockaddr_un addr;
    int sock = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&addr, 0, sizeof(addr));
    addr.sun_family  = AF_UNIX;
    strcpy(&addr.sun_path[1], DRM_HACK_SOCKET_NAME);
    connect(sock, (struct sockaddr *)&addr, sizeof(addr));

    // read the file descriptor and close the socket
    int fd = recv_fd(sock);
    close(sock);

    assert(fd != 0);

    // call required function based on received arguments
    if (do_set) {
        drmSetMaster(fd);
    } else if (do_drop) {
        drmDropMaster(fd);
    }

    // ...
}

And the second part is the patched Xorg:

// use hardcoded socket name for simplicity
#define DRM_HACK_SOCKET_NAME "xorg_drm_master_util"

// function that writes file descriptor to a unix socket
static int
send_fd(int sock, int fd)
{
    struct msghdr msg;
    struct iovec iov[1];
    struct cmsghdr *cmsg = NULL;
    char ctrl_buf[CMSG_SPACE(sizeof(int))];
    char data[1];

    memset(&msg, 0, sizeof(struct msghdr));
    memset(ctrl_buf, 0, CMSG_SPACE(sizeof(int)));

    data[0] = ' ';
    iov[0].iov_base = data;
    iov[0].iov_len = sizeof(data);

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_controllen =  CMSG_SPACE(sizeof(int));
    msg.msg_control = ctrl_buf;

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));

    *((int *) CMSG_DATA(cmsg)) = fd;

    return sendmsg(sock, &msg, 0);
}

// function to run in a separate thread
static void*
thread_func(void* argument)
{
    int ret;
    int option = *(int *)argument;
    char cmd[32];
    sprintf(cmd, "/path/to/our/suid_helper_program %s -r", (!option ? "-s" : "-d"));

    ret = system(cmd);
    if (ret == -1 || WEXITSTATUS(ret) != 0) {
        fprintf(stderr, "%s\n", strerror(errno));
    }

    pthread_exit(NULL);
}

static Bool
SetMaster(ScrnInfoPtr pScrn)
{
    modesettingPtr ms = modesettingPTR(pScrn);
    int ret = 0;
    pthread_t my_thread;
    struct sockaddr_un addr;
    int sock, conn, option = 0;

#ifdef XF86_PDEV_SERVER_FD
    if (ms->pEnt->location.type == BUS_PLATFORM &&
        (ms->pEnt->location.id.plat->flags & XF86_PDEV_SERVER_FD))
        return TRUE;
#endif

    if (ms->fd_passed)
        return TRUE;

    // create socket
    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(&addr.sun_path[1], DRM_HACK_SOCKET_NAME);
    bind(sock, (struct sockaddr *)&addr, sizeof(addr));

    // start listening
    listen(sock, 1);

    // spawn the helper program from a new thread
    pthread_create(&my_thread, NULL, thread_func, &option);

    // accept a connection from the helper program
    conn = accept(sock, NULL, 0);

    // send the fd and close the socket
    send_fd(conn, ms->fd);
    close(conn);
    close(sock);

    // wait for the thread to complete
    pthread_join(my_thread, NULL);

    return ret == 0;
}

static void
LeaveVT(ScrnInfoPtr pScrn)
{
    modesettingPtr ms = modesettingPTR(pScrn);
    pthread_t my_thread;
    struct sockaddr_un addr;
    int sock, conn, option = 1;

    xf86_hide_cursors(pScrn);

    pScrn->vtSema = FALSE;

#ifdef XF86_PDEV_SERVER_FD
    if (ms->pEnt->location.type == BUS_PLATFORM &&
        (ms->pEnt->location.id.plat->flags & XF86_PDEV_SERVER_FD))
        return;
#endif

    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(&addr.sun_path[1], DRM_HACK_SOCKET_NAME);
    bind(sock, (struct sockaddr *)&addr, sizeof(addr));

    listen(sock, 1);
    pthread_create(&my_thread, NULL, thread_func, &option);

    conn = accept(sock, NULL, 0);
    send_fd(conn, ms->fd);
    close(conn);
    close(sock);

    pthread_join(my_thread, NULL);
}

Basically, that's it. Now modesetting can work without the need to start Xorg as root.

Below the ready-to-use patches are attached for xorg-server-1.19.5-r2 and xorg-server-1.20.3. If you use Gentoo, copy the patches to /etc/portage/patches/x11-base/xorg-server-1.19.5-r2/ and /etc/portage/patches/x11-base/xorg-server-1.20.3/ respectively (read this article if you don't know how to apply user patches).

You can find complete source code of the helper program here. make install will install it to /usr/bin/drm_master_util, which is the path hardcoded in the patches.

Rebuild the Xorg, build and install the helper program and voila!

rootless_modesetting_1.20.3.patch (works for 1.20.4 as well) 3.88 KiB
If you have any comments, contact me by email.