Running TMUX in a LXD container with a pty device from another namespace

by brauner
GNU/Linux ◆ xterm-256color ◆ bash 3687 views

I’ve been working on getting TMUX to work in LXD containers. This is required because LXD makes sure to allocate a fresh pty device from the host and pass it to the container when we attach to it via lxc exec. We allocate the pty device from the host for security reasons! The main issue is that as soon as init has started it is possible that someone might tamper with the devpts mount inside the container (e.g. via fuse) and hand us a malicious pty file descriptor that can be used to escape to the host!

This requires a version of glibc that includes this patch (This commit will not change tmux behavior wrt to an unpatched glib version.):

Author: Christian Brauner <christian.brauner@canonical.com>
Date:   Fri Jan 27 15:59:59 2017 +0100

    linux ttyname and ttyname_r: do not return wrong results

    If a link (say /proc/self/fd/0) pointing to a device, say /dev/pts/2, in a
    parent mount namespace is passed to ttyname, and a /dev/pts/2 exists (in a
    different devpts) in the current namespace, then it returns /dev/pts/2.
    But /dev/pts/2 is NOT the current tty, it is a different file and device.

    Detect this case and return ENODEV.  Userspace can choose to take this as a hint
    that the fd points to a tty device but to act on the fd rather than the link.

    Signed-off-by: Serge Hallyn <serge@hallyn.com>
    Signed-off-by: Christian Brauner <christian.brauner@ubuntu.com>

To test whether you are running a patched glibc version you can do:

    sudo  unshare --fork --pid --mount-proc

and then to mount a new devpts instance:

    mount -t devpts devpts /dev/pts

If you call

    tty

you should see “not a tty”. Now you can compile this test program:

/*
 *
 * Copyright © 2017 Christian Brauner <christian.brauner@ubuntu.com>.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2, as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/* Test whether this is a correctly patched version of ttyname(). */
int main(int argc, char *argv[])
{
        int fd;
        char buf[4096];

        fd = open("/proc/self/fd/0", O_RDONLY);
        if (fd < 0) {
                fprintf(stderr, "Could not open \"/proc/self/fd/0\": %s.\n",
                        strerror(errno));
                exit(EXIT_FAILURE);
        }

        if (!ttyname(fd)) {
                /* COMMENT(brauner): ENODEV will only be set by a patched
                 * glibc.
                 */
                if (errno == ENODEV) {
                        printf("ttyname(): The pty device might exist in a "
                               "different "
                               "namespace: %s\n",
                               strerror(errno));
                } else {
                        exit(EXIT_FAILURE);
                }
        }

        if (ttyname_r(fd, buf, sizeof(buf))) {
                /* COMMENT(brauner): ENODEV will only be set by a patched
                 * glibc.
                 */
                if (errno == ENODEV) {
                        printf("ttyname_r(): The pty device might exist in a "
                               "different "
                               "namespace: %s\n",
                               strerror(errno));
                } else {
                        exit(EXIT_FAILURE);
                }
        }

        exit(EXIT_SUCCESS);
}

Now, in this setting you can also test a patched tmux version. A non-patched tmux version should refuse to run, a patched tmux version should run just fine.