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

Bug#817236: schroot: no access to pseudo-terminals in new chroots



Control: tags 817236 + patch

On Wed, 15 Feb 2017 at 11:13:39 +0000, Simon McVittie wrote:
> If debootstrap inside a container is meant to work, then this would seem
> like a job for an autopkgtest. I'll try to write one if someone can tell me
> which containerization technologies (systemd-nspawn? lxc? both? others?)
> are the ones of interest here.

I've had some trouble making a working autopkgtest that runs under qemu
and runs debootstrap under an additional layer of systemd-nspawn, so the
next best thing for now is manual testing.

The problem cases are actually quite narrow. We basically have two situations
for creating the chroot:

* we may mknod /dev/ptmx (bare metal, VM, lxc or schroot)
* we may not mknod /dev/ptmx and must create a symlink ptmx -> pts/ptmx
  as the next best thing (systemd-nspawn)

but when it comes to chrooting into it, we have several situations:

* systemd-nspawn: mounts its own /dev/pts with ptmxmode=666, always OK
* pbuilder/0.228.4-1: mounts its own /dev/pts but without ptmxmode=666
  - in the real device node case, we're fine
  - in the symlink case, ptys always fail as non-root because ptmx -> pts/ptmx
    has 000 permissions and cannot be accessed
* schroot/1.6.10-3:
  - default profile: bind-mounts host /dev and /dev/pts
    + always OK, assuming that the host is sensibly set up
  - sbuild profile: bind-mounts host /dev/pts only
    + in the real device node case, we're fine
    + in the symlink case:
      - if the "host" for schroot is really systemd-nspawn, we're fine,
        because systemd-nspawn mounted its /dev/pts with ptmxmode=666 so
        ptmx -> pts/ptmx leads to a writeable device node
      - but if the chroot was debootstrapped under systemd-nspawn and then
        *used* under the schroot sbuild profile as a direct "child" of the
        host system (without an intervening systemd-nspawn), *then* we
        have a problem

> If I'm understanding the situation correctly then the next best thing would
> seem to be:
> 
> -       ln -s pts/ptmx $TARGET/dev/ptmx
> +       # Inside a container, we might not be allowed to create /dev/ptmx.
> +       # If not, do the next best thing (but see #817236).
> +       mknod -m 666 $TARGET/dev/ptmx c 5 2 || ln -s pts/ptmx $TARGET/dev/ptmx
> 
> which would result in debootstrap inside a container continuing to create
> a /dev that current schroot etc. cannot successfully use (but that's maybe
> better than it failing completely?), whereas debootstrap outside a container
> would create a /dev that works fine?

I think this is a good answer, and I propose the attached patches.

    S
>From e070e9c9b837815b43eb39140b7005124ed3906f Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@debian.org>
Date: Mon, 20 Feb 2017 09:22:07 +0000
Subject: [PATCH 1/2] Don't make /dev/ptmx a symlink to pts/ptmx if we don't
 have to

In a plain chroot or on real hardware, it is preferable to use mknod
to create /dev/ptmx. This works as intended with older chroot managers
such as sbuild and pbuilder, which were designed for the semantics of
"legacy" /dev/pts (a single non-virtualized pty subsystem per kernel)
and so mount /dev/pts without the newinstance option. It also works
in newer kernels where /dev/pts always behaves as though the
newinstance option was given, because on those kernels, opening a
(c,5,2) device node automatically looks for an adjacent pts directory
and uses its ptmx device node instead.

However, if we are running debootstrap inside a restricted container
such as lxc or systemd-nspawn, mknod ptmx c 5 2 might not be allowed.
If so, fall back to a symlink with a warning. This mode is fine if
the debootstrap will be used with systemd-nspawn or lxc, or if a
devtmpfs will be mounted over its /dev, but will not work for older
chroot managers like sbuild or pbuilder, because those chroot
managers leave the ptmxmode mount option at its default 000, causing
permission to open the pts/ptmx device node to be denied.

Bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=817236
Signed-off-by: Simon McVittie <smcv@debian.org>
---
 functions | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/functions b/functions
index 6cbbd3b..4a4f2f9 100644
--- a/functions
+++ b/functions
@@ -1173,7 +1173,12 @@ setup_devices_simple () {
 	mknod -m 666 $TARGET/dev/urandom	c 1 9
 	mknod -m 666 $TARGET/dev/tty	c 5 0
 	mkdir $TARGET/dev/pts/ $TARGET/dev/shm/
-	ln -s pts/ptmx $TARGET/dev/ptmx
+	# Inside a container, we might not be allowed to create /dev/ptmx.
+	# If not, do the next best thing.
+	if ! mknod -m 666 $TARGET/dev/ptmx c 5 2; then
+		warning MKNOD "Could not create /dev/ptmx, falling back to symlink. This chroot will require /dev/pts mounted with ptmxmode=666"
+		ln -s pts/ptmx $TARGET/dev/ptmx
+	fi
 	ln -s /proc/self/fd   $TARGET/dev/fd
 	ln -s /proc/self/fd/0 $TARGET/dev/stdin
 	ln -s /proc/self/fd/1 $TARGET/dev/stdout
-- 
2.11.0

>From 00e10b4ba28aacbb5fc210c47aeaa7f4e7a8bc26 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@debian.org>
Date: Fri, 24 Feb 2017 09:45:33 +0000
Subject: [PATCH 2/2] Add an autopkgtest covering #817236 and various other
 sanity checks

Because debootstrap is relatively slow, I've named the test according
to what is being bootstrapped (Debian testing) rather than the checks
that are performed, with the intention that additional checks can
be added to it.

Signed-off-by: Simon McVittie <smcv@debian.org>
---
 debian/tests/control                 |   3 +
 debian/tests/debian-testing          | 135 +++++++++++++++++++++++++++++++++++
 debian/tests/fake/pbuilder-0.228.4-1 |  28 ++++++++
 debian/tests/fake/schroot-1.6.10-3   |  44 ++++++++++++
 debian/tests/tap.sh                  |  66 +++++++++++++++++
 5 files changed, 276 insertions(+)
 create mode 100644 debian/tests/control
 create mode 100755 debian/tests/debian-testing
 create mode 100755 debian/tests/fake/pbuilder-0.228.4-1
 create mode 100755 debian/tests/fake/schroot-1.6.10-3
 create mode 100644 debian/tests/tap.sh

diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..6979413
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,3 @@
+Tests: debian-testing
+Depends: debootstrap, distro-info
+Restrictions: allow-stderr, needs-root
diff --git a/debian/tests/debian-testing b/debian/tests/debian-testing
new file mode 100755
index 0000000..7ce71d6
--- /dev/null
+++ b/debian/tests/debian-testing
@@ -0,0 +1,135 @@
+#!/bin/sh
+# Verify that debootstrap'ing Debian testing produces a usable chroot,
+# and in particular that using it with early 2017 versions of schroot and
+# pbuilder results in working pseudo-terminals (#817236)
+#
+# Copyright © 2017 Simon McVittie
+# SPDX-License-Identifier: MIT
+# (see debian/copyright)
+
+set -e
+set -u
+
+srcdir="$(pwd)"
+. "$srcdir/debian/tests/tap.sh"
+
+export LC_ALL="C.UTF-8"
+
+# Try to inherit a Debian mirror from the host
+mirror="$(cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null |
+perl -ne \
+    'if (m{^deb\s+(http://[-a-zA-Z0-9.:]+/debian)\s}) { print $1; last; }' )"
+
+if [ -z "$mirror" ]; then
+    mirror=http://deb.debian.org/debian
+fi
+
+cd "${AUTOPKGTEST_TMP:-"${ADTTMP}"}"
+
+if ischroot; then
+    diag "In a chroot according to ischroot(1)"
+else
+    diag "Not in a chroot according to ischroot(1)"
+fi
+
+diag "Virtualization: $(systemd-detect-virt --vm || :)"
+diag "Container: $(systemd-detect-virt --container || :)"
+
+if mknod -m000 ptmx c 5 2 2>/dev/null; then
+    diag "mknod ptmx succeeded"
+    can_mknod_ptmx=yes
+else
+    diag "mknod ptmx failed, are we in a container?"
+    can_mknod_ptmx=no
+fi
+
+rm -f ptmx
+
+testing="${DEBIAN_TESTING_SUITE:-}"
+if [ -z "${testing}" ]; then
+    testing="$(debian-distro-info --testing)"
+fi
+
+bail_unless debootstrap \
+    --include=hello \
+    --variant=minbase \
+    "${testing}" \
+    "chroot.d" \
+    "${mirror}"
+
+ok test -f chroot.d/etc/debian_version
+ok test -x chroot.d/usr/bin/env
+ok test -x chroot.d/usr/bin/hello
+ok test -d chroot.d/usr/share/doc
+ok test -d chroot.d/dev/pts
+
+ok test -c chroot.d/dev/full
+ok /usr/bin/stat --printf='%t %T %a' chroot.d/dev/full > output
+is "$(cat output)" "1 7 666"
+ok test -c chroot.d/dev/null
+ok /usr/bin/stat --printf='%t %T %a' chroot.d/dev/null > output
+is "$(cat output)" "1 3 666"
+
+if [ -L chroot.d/dev/ptmx ]; then
+    # Necessary if debootstrap is run inside some containers, see
+    # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=817236#77
+    diag "/dev/ptmx is a symbolic link"
+    ok readlink chroot.d/dev/ptmx > output
+    is "$(cat output)" "pts/ptmx"
+    did_mknod_ptmx=no
+else
+    diag "/dev/ptmx is not a symbolic link"
+    ok test -c chroot.d/dev/ptmx
+    ok /usr/bin/stat --printf='%t %T %a' chroot.d/dev/ptmx > output
+    is "$(cat output)" "5 2 666"
+    did_mknod_ptmx=yes
+fi
+
+ok eval 'test "$can_mknod_ptmx,$did_mknod_ptmx" != yes,no'
+
+bail_unless cat chroot.d/etc/debian_version > reference
+
+ok chroot chroot.d runuser -u nobody -- cat /etc/debian_version > response
+is "$(cat response)" "$(cat reference)"
+
+# Use unshare -m to make sure the /dev mount gets cleaned up on exit, even
+# on failures
+ok unshare -m \
+    "$srcdir/debian/tests/fake/schroot-1.6.10-3" chroot.d \
+    runuser -u nobody -- \
+    script -q -c "cat /etc/debian_version" /dev/null \
+    > response
+is "$(sed -e 's/\r//' response)" "$(sed -e 's/\r//' reference)"
+
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=817236
+if [ "$can_mknod_ptmx" = yes ]; then
+    ok unshare -m \
+        "$srcdir/debian/tests/fake/schroot-1.6.10-3" --sbuild chroot.d \
+        runuser -u nobody -- \
+        script -q -c "cat /etc/debian_version" /dev/null \
+        > response
+    is "$(sed -e 's/\r//' response)" "$(sed -e 's/\r//' reference)"
+
+    ok unshare -m "$srcdir/debian/tests/fake/pbuilder-0.228.4-1" chroot.d \
+        runuser -u nobody -- \
+        script -q -c "cat /etc/debian_version" /dev/null \
+        > response
+    is "$(sed -e 's/\r//' response)" "$(sed -e 's/\r//' reference)"
+else
+    diag "Unable to mknod ptmx"
+    unshare -m "$srcdir/debian/tests/fake/schroot-1.6.10-3" --sbuild chroot.d \
+        runuser -u nobody -- \
+        script -q -c "cat /etc/debian_version" /dev/null \
+        2>&1 | sed -e 's/^/# fake-schroot-sbuild: /' >&7
+    skip "ignored result of fake schroot: it only works under some circumstances"
+    unshare -m "$srcdir/debian/tests/fake/pbuilder-0.228.4-1" chroot.d \
+        runuser -u nobody -- \
+        script -q -c "cat /etc/debian_version" /dev/null \
+        2>&1 | sed -e 's/^/# fake-pbuilder: /' >&7
+    skip "ignored result of fake pbuilder: it will probably not work"
+fi
+
+bail_unless rm -fr --one-file-system chroot.d
+done_testing
+
+# vim:set sw=4 sts=4 et:
diff --git a/debian/tests/fake/pbuilder-0.228.4-1 b/debian/tests/fake/pbuilder-0.228.4-1
new file mode 100755
index 0000000..c9044c8
--- /dev/null
+++ b/debian/tests/fake/pbuilder-0.228.4-1
@@ -0,0 +1,28 @@
+#!/bin/sh
+# fake/pbuilder-0.228.4-1 -- emulate how pbuilder/0.228.4-1 would chroot.
+# It mounts /dev/pts, without explicitly requesting a new instance or a
+# usable /dev/pts/ptmx.
+# (There is of course a lot more that it does, but these are the parts that
+# affect pty users like script(1).)
+#
+# Copyright © 2017 Simon McVittie
+# SPDX-License-Identifier: MIT
+# (see debian/copyright)
+
+set -e
+
+chroot="$1"
+shift
+if test -z "$chroot" || test -z "$1"; then
+	echo "Usage: $0 CHROOT COMMAND...">&2
+	exit 2
+fi
+
+mkdir -p "$chroot/dev/pts"
+mount -t devpts none "$chroot/dev/pts" -onoexec,nosuid,gid=5,mode=620
+
+e=0
+chroot "$chroot" "$@" || e=$?
+
+umount "$chroot/dev/pts"
+exit "$e"
diff --git a/debian/tests/fake/schroot-1.6.10-3 b/debian/tests/fake/schroot-1.6.10-3
new file mode 100755
index 0000000..648261b
--- /dev/null
+++ b/debian/tests/fake/schroot-1.6.10-3
@@ -0,0 +1,44 @@
+#!/bin/sh
+# fake/schroot-1.6.10-3 -- emulate how schroot/1.6.10-3 would chroot.
+# It bind-mounts /dev/pts and maybe /dev from the host system.
+# (There is of course a lot more that it does, but these are the parts that
+# affect pty users like script(1).)
+#
+# Copyright © 2017 Simon McVittie
+# SPDX-License-Identifier: MIT
+# (see debian/copyright)
+
+set -e
+
+# /etc/schroot/default/fstab
+bind_dev=yes
+
+while true; do
+	case "$1" in
+		(--sbuild)
+			shift
+			# /etc/schroot/sbuild/fstab
+			bind_dev=no
+			;;
+		(*)
+			break
+	esac
+done
+
+chroot="$1"
+shift
+if test -z "$chroot" || test -z "$1"; then
+	echo "Usage: $0 CHROOT COMMAND...">&2
+	exit 2
+fi
+
+[ "$bind_dev" = no ] || mount --bind /dev "$chroot/dev"
+mount --bind /dev/pts "$chroot/dev/pts"
+
+e=0
+chroot "$chroot" "$@" || e=$?
+
+umount "$chroot/dev/pts"
+[ "$bind_dev" = no ] || umount "$chroot/dev"
+
+exit "$e"
diff --git a/debian/tests/tap.sh b/debian/tests/tap.sh
new file mode 100644
index 0000000..ab19b7f
--- /dev/null
+++ b/debian/tests/tap.sh
@@ -0,0 +1,66 @@
+# vim:set sw=4 sts=4 et ft=sh:
+# TAP helper functions for shell scripts. To be sourced, not executed.
+#
+# Copyright © 2017 Simon McVittie
+# SPDX-License-Identifier: MIT
+# (see debian/copyright)
+
+tap_test_num=0
+tap_exit_status=0
+# fd 7 is machine-readable TAP
+exec 7>&1
+# fd 1 is treated as the same as 2 - diagnostics
+exec >&2
+
+ok () {
+    tap_test_num=$(( $tap_test_num + 1 ))
+    echo "# Running: $*" >&7
+    if "$@"; then
+        echo "ok $tap_test_num - $*" >&7
+    else
+        echo "not ok $tap_test_num - exit status $? from $*" >&7
+        tap_exit_status=1
+    fi
+}
+
+diag () {
+    echo "# $*" >&7
+}
+
+skip () {
+    tap_test_num=$(( $tap_test_num + 1 ))
+    echo "ok $tap_test_num # SKIP: $*" >&7
+}
+
+is () {
+    local got="$1"
+    local expected="$2"
+    local label="${3:-"$2"}"
+
+    tap_test_num=$(( $tap_test_num + 1 ))
+    if test "$got" = "$expected"; then
+        echo "ok $tap_test_num - $label" >&7
+    else
+        echo "# Expected: $expected" >&2
+        echo "# Got:      $got" >&2
+        echo "not ok $tap_test_num - $label" >&7
+        tap_exit_status=1
+    fi
+}
+
+bail_unless () {
+    tap_test_num=$(( $tap_test_num + 1 ))
+    echo "# Running: $*" >&7
+    if "$@"; then
+        echo "ok $tap_test_num - $*" >&7
+    else
+        echo "Bail out! Exit status $? from $*" >&7
+        exit 1
+    fi
+}
+
+done_testing () {
+    echo "1..$tap_test_num" >&7
+    exit "$tap_exit_status"
+}
+
-- 
2.11.0


Reply to: