[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#829134: debootstrap: Changes needed to support unprivileged userns debootstrap



Package: debootstrap
Version: 1.0.81
Severity: wishlist
Tags: patch

Dear Maintainer,

Now that the kernel supports user_namespaces(7), it should be possible
to debootstrap in them. Some small changes are needed.

Configuration needed:
* Kernel 3.8 or later (3.11 recommended)
* Set the sysctl kernel.unprivileged_userns_clone to 1
    (Debian-specific "temporary" patch from years ago).
* Install the `uidmap` package and add yourself to /etc/sub[ug]id
* Install the `lxc` package (for one helper binary only)
* Make sure the current directory is searchable by other.

I have attached the necessary changes as a wrapper script, but there
really should be some architectural changes:

* The `/usr/sbin/debootstrap` vs `/usr/share/debootstrap/functions`
    split is quite painful. Move everything into one file and then
    replace sbin/debootstrap with basically `source functions; main`.
* Satisfy `shellcheck`s errors and warnings, and suppress the rest.
* Beware that shellcheck currently does not catch `echo $(false)`.
* Make it possible to use more than one `--variant` at once somehow.
* Debootstrap is currently not idempotent - see the `rm dev...` hack.
* If you're in a new mount namespace, no need to `umount` at the end.


-- System Information:
Debian Release: stretch/sid
  APT prefers testing-debug
  APT policy: (600, 'testing-debug'), (600, 'testing'), (500, 'unstable-debug'), (500, 'unstable'), (1, 'experimental')
Architecture: amd64 (x86_64)
Foreign Architectures: i386, x32

Kernel: Linux 4.6.0-1-amd64 (SMP w/4 CPU cores)
Locale: LANG=en_US.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8)
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)

Versions of packages debootstrap depends on:
ii  wget  1.18-1

Versions of packages debootstrap recommends:
ii  debian-archive-keyring  2014.3
ii  gnupg                   1.4.20-6

debootstrap suggests no packages.

-- no debconf information
#!/bin/sh
# userns-debootstrap - debootstrap in a unprivileged new UID namespace
#
# Copyright (c) 2016 Ben Longbons
#
#    Permission is hereby granted, free of charge, to any person obtaining
#    a copy of this software and associated documentation files (the
#    "Software"), to deal in the Software without restriction, including
#    without limitation the rights to use, copy, modify, merge, publish,
#    distribute, sublicense, and/or sell copies of the Software, and to
#    permit persons to whom the Software is furnished to do so, subject to
#    the following conditions:
#
#    The above copyright notice and this permission notice shall be
#    included in all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
#    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
#    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
#    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# (This is the same license as debootstrap itself).

# This currently requires Debootstrap 1.0.76 or later,
# to avoid devices.tar.gz (see debian bug 571136).

# Note that, if everything works *correctly*, we `exec` rather than `exit`.
set -e
trap 'echo Failed during setup' EXIT
bailout()
{
    local status="$?"
    echo "Bailing out with status $status"
    if test -z "$BASH"
    then
        echo 'For more debug info, run under bash (e.g. with --bash)'
        enter_debug_shell
        return
    fi

    echo "Last command: $BASH_COMMAND"
    test -z "$debug_variables" || ( set -o posix; set; )
    backtrace

    enter_debug_shell
}

trap 'bailout' EXIT
if test -n "$BASH"
then
    # bash doesn't set BASH_LINENO correctly for EXIT, so use ERR
    set -E
    # shellcheck disable=SC2039
    trap 'bailout' ERR
    trap 'echo Bug: irregular exit' EXIT
fi
umask 022

enter_debug_shell()
{
    test -n "$debug_shell" || return
    echo 'Entering debug shell!'
    # If run e.g. from vim's :make, it tries to steal our stdio
    exec "$debug_shell" <> /dev/tty 1>&0 2>&0
}

# shellcheck disable=SC2039
backtrace()
{
    local skip_head=0 skip_tail=1
    echo Backtrace:
    for i in $(eval "echo {${skip_head}..$((${#BASH_LINENO[@]}-skip_tail-1))}")
    do
        echo "${BASH_SOURCE[i]}:${BASH_LINENO[i]}: error: ... from ${FUNCNAME[i+1]}"
    done
}

dispatch()
{
    phase=phase1

    suite=
    target=
    mirror=
    script=

    : $suite $target $mirror $script

    local arg

    for arg
    do
        case "$arg" in
            -h | --help)
                usage
                ;;
            --bash)
                if test -z "$BASH"
                then
                    echo 'Restarting under bash ...'
                    exec bash "$0" "$@"
                fi
                ;;
            --debug-shell)
                debug_shell=bash
                ;;
            --debug-variables)
                debug_variables=nz
                ;;
            --phase1)
                phase=phase1
                ;;
            --phase2)
                phase=phase2
                ;;
            -*)
                die "Unrecognized argument $arg"
                ;;
            '')
                die "Empty argument!"
                ;;
            *)
                posarg "$arg" suite target mirror script
                ;;
        esac
    done

    $phase "$@"
    die "Phase returned unexpectedly!"
}

debug_var()
{
    local arg val
    for arg
    do
        val="$(indirect_get "$arg")"
        echo "$arg=$val"
    done
}

check_var()
{
    # Verify that something looks like a variable name.
    # $1 - variable name
    case "$1" in
        *[^A-Za-z_0-9]*)
            die "Invalid variable name: $1"
            ;;
    esac
}

indirect_get()
{
    # Get a variable's value, with indirection.
    # $1 - variable name
    check_var "$1"
    eval echo '$'"$1"
}

indirect_set()
{
    # Set a variable's value, with indirection.
    # $1 - variable name
    # $2 - value
    check_var "$1"
    eval "$1"='$''2'
}

posarg()
{
    local value="$1" var zval
    shift
    for var
    do
        zval="$(indirect_get $var)"
        if test -z "$zval"
        then
            indirect_set $var "$value"
            return
        fi
    done
    die 'Too many positional arguments!'
}

echo()
{
    printf '%s\n' "$*"
}
echon()
{
    printf '%s' "$*"
}
echoe()
{
    printf '%b\n' "$*"
}
echoen()
{
    printf '%b' "$*"
}
exit_()
{
    ( exit "$1" )
    echo 'set -e failed???'
    exit "$1"
}
die()
{
    echo "$@" >&2
    exit_ 1
}


phase1()
{
    # Phase 1 is executed directly in the host environment.
    echo 'Beginning phase 1'
    host=$(basename "$target")
    host=$(echo "$host" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g;s/^-*//;s/-*$//')
    test -n "$host" || die "Unable to make hostname from $target"

    # avoid reading /etc/subuid ourselves
    probe=$(lxc-usernsexec mktemp /tmp/userns-debootstrap.XXXXXX)
    inner_uid=$(stat -c %u "$probe")
    inner_gid=$(stat -c %g "$probe")
    lxc-usernsexec rm "$probe"
    outer_uid=$(id -u)
    outer_gid=$(id -g)
    mkdir -p "$target"
    lxc-usernsexec -m "u:0:$outer_uid:1" -m "g:0:$outer_gid:1" -m "u:1:$inner_uid:1" -m "g:1:$inner_gid:1" -- chown 1:1 "$target"
    exec lxc-usernsexec -- lxc-unshare -s 'MOUNT|PID|UTSNAME|IPC' -H "$host" -- env -i TERM="$TERM" "$0" "$@" --phase2
}

make_dir_of()
{
    local dir; dir="$(dirname "$1")"
    mkdir -p "$dir"
}

mount_bind_file()
{
    make_dir_of "$1"
    touch "$1"
    # Just in case someone chroots without binding,
    # make sure they don't e.g. fill up /dev/null
    chmod -rwx "$1"
    mount -o bind /"$1" "$1"
}

mount_bind_dir()
{
    mkdir -p "$1"
    # Recursive bind is needed e.g. for /sys/
    # Otherwise you get nonsensical error messages.
    mount -o rbind /"$1" "$1"
}

just_copy()
{
    make_dir_of "$1"
    cp /"$1" "$1"
}

create_fakebin()
{
    mkdir -p fakebin
    # This is used both inside and outside the chroot
    PATH="$(pwd)/fakebin:/fakebin:$PATH"; export PATH

    ln -sf /bin/true fakebin/mknod

    echo '#!/bin/sh' > fakebin/tar
    echo 'exec /bin/tar --exclude=sys --exclude=proc "$@"' >> fakebin/tar
    chmod +x fakebin/tar

    ln -sf /bin/true fakebin/mount
}

phase2()
{
    # Phase 2 is executed in the new namespace, but without chroot'ing.
    echo 'Beginning phase 2'
    mount -t proc proc /proc
    # Use apt-cacher-ng to make this not take forever.
    # (ideally, read this from apt config if present)
    export http_proxy=http://localhost:3142

    # $target is created during phase1 to get the permissions right.
    cd "$target"
    mkdir -p "etc/apt/apt.conf.d"
    echo 'Acquire::http::proxy "'"$http_proxy"'";' > etc/apt/apt.conf.d/02proxy
    # These are the nodes created by `setup_devices_simple` in debootstrap.
    mount_bind_file dev/null
    mount_bind_file dev/zero
    mount_bind_file dev/full
    mount_bind_file dev/random
    mount_bind_file dev/urandom
    mount_bind_file dev/tty
    mount_bind_dir sys
    # must be *before* `create_fakebin`
    mkdir -p proc
    mount -t proc proc ./proc

    just_copy etc/resolv.conf
    just_copy etc/hostname

    create_fakebin
    # A failed debootstrap cannot be rerun unless these are deleted.
    rm -rf dev/pts dev/shm dev/ptmx dev/fd dev/stdin dev/stdout dev/stderr
    echo 'Beginning actual debootstrap!'
    exec debootstrap "$suite" . "$mirror" "$script" --variant=buildd
}

dispatch "$@"

Reply to: