Package: autopkgtest
Version: 4.2
Severity: wishlist
Tags: patch

Hi Martin,

in the context of #833407 I told you about my plan of adding a
virtualization backend which would allow completely unprivileged chroot
operation by using linux user namespaces. In contrast to what I thought
was required back then, I now managed to write that backend using just
lxc-usernsexec and lxc-unshare. Thus, I was able to get it to work using
the existing Python modules. You can find the script attached.  As you
can see, it is extremely simple, which I find makes the beauty of it
all. All you need is:

 - the lxc package installed for lxc-usernsexec and lxc-unshare
 - sbuild from git (a tiny fix to its autopkgtest backend is required)
 - autopkgtest
 - a tarball as it is created by sbuild-createchroot for schroot
 - the attached virtualization backend as
   /usr/bin/autopkgtest-virt-uchroot

Then you can do:

$ sbuild --chroot-mode=autopkgtest --autopkgtest-virt-server=uchroot \
    --autopkgtest-virt-server-opts="-- /srv/chroot/%r-%a-sbuild.tar.gz 
/tmp/rootfs"

By putting these arguments into your ~/.sbuildrc the above call can be
reduced to just running "sbuild".

The string /srv/chroot/%r-%a.tar.gz will resolve to, for example,
/srv/chroot/unstable-amd64-sbuild.tar.gz which is a chroot as created by
sbuild-createchroot. Using the script from #829134, this tarball can
also be created without superuser privileges and I might thus add this
script to sbuild-createchroot as well, for unprivileged tarball
generation.

The path /tmp/rootfs is the path that the rootfs will be extracted to
and can be at any location that the user has access to.

I called the backend uchroot because schroot is chroot with _s_uid. So
uchroot is a chroot as a _u_ser.

I don't think there is an existing backend which allows unprivileged
package building with so little overhead in terms of configuration. The
only two inputs are the chroot tarball and the location to extract it
to.

It would be great if this backend could be added to autopkgtest itself.
If you think that it is not a good fit for autopkgtest, then I can
maintain it in a separate package.

What do you think?

Thanks!

cheers, josch
#!/usr/bin/python3
#
# autopkgtest-virt-uchroot is part of autopkgtest
# autopkgtest is a tool for testing Debian binary packages
#
# autopkgtest is Copyright (C) 2006-2007 Canonical Ltd.
# autopkgtest-virt-uchroot is Copyright (C) 2016 Johannes Schauer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# See the file CREDITS for a full list of credits information (often
# installed as /usr/share/doc/autopkgtest/CREDITS).

import sys
import os
import argparse
import shlex
import stat

sys.path.insert(0, '/usr/share/autopkgtest/lib')
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
    os.path.abspath(__file__))), 'lib'))

import VirtSubproc
import adtlog

tarball = None
rootdir = None


def parse_args():
    global tarball, rootdir

    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--debug', action='store_true',
                        help='Enable debugging output')
    parser.add_argument('tarball', help='path to rootfs tarball')
    parser.add_argument('rootdir', help='path to extract the rootfs')

    args = parser.parse_args()

    tarball = args.tarball
    rootdir = args.rootdir

    if args.debug:
        adtlog.verbosity = 2


def hook_open():
    global tarball, rootdir

    # We want to find out our user and group id inside the chroot but we want
    # to avoid having to parse /etc/subuid and /etc/subgid. We solve the
    # situation by creating a temporary file from inside the user namespace
    # and then checking its user and group ids from outside the user namespace.
    probe = VirtSubproc.check_exec(['lxc-usernsexec', 'mktemp',
                                    '/tmp/uchroot.XXXXXX'], outp=True)
    inner_uid = os.stat(probe)[stat.ST_UID]
    inner_gid = os.stat(probe)[stat.ST_GID]
    VirtSubproc.check_exec(['lxc-usernsexec', 'rm', probe])
    outer_uid = os.getuid()
    outer_gid = os.getgid()

    # Make sure that the target directory exists.
    os.makedirs(rootdir)
    # Change its ownership to be root inside the user namespace.
    VirtSubproc.check_exec(['lxc-usernsexec',
                            '-m', 'u:0:%d:1' % outer_uid,
                            '-m', 'g:0:%d:1' % outer_gid,
                            '-m', 'u:1:%d:1' % inner_uid,
                            '-m', 'g:1:%d:1' % inner_gid,
                            '--', 'chown', '1:1', rootdir])
    # Unpack the tarball into the new directory.
    # Make sure not to extract any character special files because we cannot
    # mknod.
    VirtSubproc.check_exec(['lxc-usernsexec', '--', 'tar',
                            '--exclude=./dev/urandom',
                            '--exclude=./dev/random',
                            '--exclude=./dev/full',
                            '--exclude=./dev/null',
                            '--exclude=./dev/zero',
                            '--exclude=./dev/tty',
                            '--directory', rootdir,
                            '--extract', '--file', tarball])

    # A shell script that prepares the environment by bind-mounting all the
    # important things.
    # The chmod is done such that somebody accidentally using the chroot
    # without the right bind-mounts will not fill up their disk.
    shellcommand = """
    mkdir -p {rootdir}/dev
    touch {rootdir}/dev/null
    chmod -rwx {rootdir}/dev/null
    mount -o bind /dev/null {rootdir}/dev/null
    touch {rootdir}/dev/zero
    chmod -rwx {rootdir}/dev/zero
    mount -o bind /dev/zero {rootdir}/dev/zero
    touch {rootdir}/dev/full
    chmod -rwx {rootdir}/dev/full
    mount -o bind /dev/full {rootdir}/dev/full
    touch {rootdir}/dev/random
    chmod -rwx {rootdir}/dev/random
    mount -o bind /dev/random {rootdir}/dev/random
    touch {rootdir}/dev/urandom
    chmod -rwx {rootdir}/dev/urandom
    mount -o bind /dev/urandom {rootdir}/dev/urandom
    touch {rootdir}/dev/tty
    chmod -rwx {rootdir}/dev/tty
    mount -o bind /dev/tty {rootdir}/dev/tty
    mkdir -p {rootdir}/sys
    mount -o rbind /sys {rootdir}/sys
    mkdir -p {rootdir}/proc
    mount -t proc proc {rootdir}/proc
    export PATH=$PATH:/usr/sbin:/sbin
    exec chroot {rootdir} "$@"
    """.format(rootdir=shlex.quote(rootdir))
    VirtSubproc.auxverb = ['lxc-usernsexec', '--', 'lxc-unshare', '-s',
                           'MOUNT|PID|UTSNAME|IPC', '--', 'sh', '-c',
                           shellcommand, '--']

    # Test whether the auxverb is able to successfully run /bin/true
    status = VirtSubproc.execute_timeout(None, 5,
                                         VirtSubproc.auxverb + ['true'])[0]
    if status != 0:
        VirtSubproc.bomb('failed to connect to VM')


def hook_downtmp(path):
    return VirtSubproc.downtmp_mktemp(path)


def hook_revert():
    hook_cleanup()
    hook_open()


def hook_cleanup():
    global rootdir
    VirtSubproc.check_exec(['lxc-usernsexec',
                            '--', 'rm', '-rf', rootdir])


def hook_capabilities():
    return ['revert', 'root-on-testbed']


parse_args()
VirtSubproc.main()

Reply via email to