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()