diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:42:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:42:49 +0000 |
commit | 03815601f93e95e6f1e56dac32de10e546123726 (patch) | |
tree | b1acc790faf13513e5beee2e7ac67a25c70670e4 /debian/tests | |
parent | Adding upstream version 1.0.134. (diff) | |
download | debootstrap-debian/1.0.134.tar.xz debootstrap-debian/1.0.134.zip |
Adding debian version 1.0.134.debian/1.0.134
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'debian/tests')
-rw-r--r-- | debian/tests/arch-all-mitm.py | 120 | ||||
-rwxr-xr-x | debian/tests/arch-all-support | 30 | ||||
-rw-r--r-- | debian/tests/control | 27 | ||||
-rwxr-xr-x | debian/tests/debian-testing | 394 | ||||
-rwxr-xr-x | debian/tests/fake/pbuilder-0.228.4-1 | 37 | ||||
-rwxr-xr-x | debian/tests/fake/pbuilder-0.231 | 64 | ||||
-rwxr-xr-x | debian/tests/fake/schroot-1.6.10-3 | 58 | ||||
-rwxr-xr-x | debian/tests/fake/schroot-proposed | 87 | ||||
-rw-r--r-- | debian/tests/out-of-order-mitm.py | 98 | ||||
-rwxr-xr-x | debian/tests/unsorted-packages-files | 34 |
10 files changed, 949 insertions, 0 deletions
diff --git a/debian/tests/arch-all-mitm.py b/debian/tests/arch-all-mitm.py new file mode 100644 index 0000000..9180cd9 --- /dev/null +++ b/debian/tests/arch-all-mitm.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 Debian Install System Team <debian-boot@lists.debian.org> +# Copyright (C) 2023 Matthias Klumpp <matthias@tenstral.net> +# +# SPDX-License-Identifier: MIT + +""" +Flask app which MITM's a legacy-compatibility archive to make it arch:all-only. +""" +import functools +import gzip +import hashlib +import os + +import requests +import tempfile +from apt_pkg import TagFile, TagSection +from flask import Flask, redirect + +app = Flask(__name__) + +ARCH = os.environ.get("FLASK_ARCH", "amd64") +DIST = os.environ.get("FLASK_DIST", "trixie") +DISTRO = os.environ.get("FLASK_DISTRO", "debian") +MIRROR = os.environ.get("FLASK_MIRROR", "http://deb.debian.org") + + +if DISTRO in ("debian", "pureos"): + hash_funcs = [hashlib.md5, hashlib.sha256] +else: + # Ubuntu includes SHA1 still + hash_funcs = [hashlib.md5, hashlib.sha1, hashlib.sha256] + + +def _munge_release_file(url: str) -> bytes: + """Given a Release file URL, rewrite it for our modified Packages content.""" + original = requests.get(MIRROR + "/" + url).content.decode('utf-8') + packages_content = _packages_arch_content( + f"{DISTRO}/dists/{DIST}/main/binary-{ARCH}/Packages" + ) + size = str(len(packages_content)) + sums = [ + hash_func(packages_content).hexdigest() + for hash_func in hash_funcs + ] + new_lines = [] + filename = f"main/binary-{ARCH}/Packages" + for line in original.splitlines(): + if line.startswith('No-Support-for-Architecture-all:'): + continue + if line.startswith('Architectures:'): + if ' all' not in line: + line += ' all' + new_lines.append(line) + continue + if line.startswith('Acquire-By-Hash:'): + new_lines.append('Acquire-By-Hash: no') + continue + if not line.endswith(filename): + new_lines.append(line) + continue + new_lines.append(" ".join(["", sums.pop(0), size, filename])) + + result = "\n".join(new_lines) + return result.encode('utf-8') + + +@functools.lru_cache +def _packages_arch_content(url: str) -> bytes: + """Given an arch-specific Packages URL, fetch it and filter out arch:all.""" + resp = requests.get(MIRROR + "/" + url + ".gz") + upstream_content = gzip.decompress(resp.content) + + filtered_sections = [] + with tempfile.NamedTemporaryFile() as tmp_f: + tmp_f.write(upstream_content) + tmp_f.flush() + + with TagFile(tmp_f.name) as tf: + for section in tf: + if section.get('Architecture') == 'all': + continue + filtered_sections.append(section) + + result = '\n'.join([str(s) for s in filtered_sections]) + return result.encode('utf-8') + + +@functools.lru_cache +def _packages_indep_content(url: str) -> bytes: + """Given an arch:all Packages URL, just return its uncompressed contents.""" + resp = requests.get(MIRROR + "/" + url + ".gz") + upstream_content = gzip.decompress(resp.content) + + return upstream_content + + +@app.route("/<path:url>", methods=["GET", "POST"]) +def root(url): + """Handler for all requests.""" + if ( + url == f"{DISTRO}/dists/{DIST}/InRelease" + or "by-hash" in url + or "Packages.xz" in url + or "Packages.gz" in url + ): + # 404 these URLs to force clients to fetch by path and without compression, to + # make MITM easier + return "", 404 + if url == f"{DISTRO}/dists/{DIST}/Release": + # If Release is being fetched, return our modified version + return _munge_release_file(url) + if url == f"{DISTRO}/dists/{DIST}/main/binary-all/Packages": + return _packages_indep_content(url) + if url == f"{DISTRO}/dists/{DIST}/main/binary-{ARCH}/Packages": + # If Packages is being fetched, return our modified version + return _packages_arch_content(url) + # For anything we don't need to modify, redirect clients to upstream mirror + return redirect(f"{MIRROR}/{url}") diff --git a/debian/tests/arch-all-support b/debian/tests/arch-all-support new file mode 100755 index 0000000..b6b5608 --- /dev/null +++ b/debian/tests/arch-all-support @@ -0,0 +1,30 @@ +#!/bin/sh + +# This test runs the arch-all-mitm.py Flask app which debootstrap is then +# pointed at. +# It is used to pretend an archive in legacy-compatibility mode is only +# supporting the split-arch-all mode, to test if debootstrap will handle +# that situation correctly. + +export FLASK_ARCH="$(dpkg --print-architecture)" +export FLASK_DIST=testing +export FLASK_DISTRO=debian +export FLASK_MIRROR=http://deb.debian.org +export PATH=$PATH:/usr/sbin + +# Launch our MitM "mirror" server, ensure that request logging is sent to stdout +PYTHONDONTWRITEBYTECODE=true FLASK_APP=debian/tests/arch-all-mitm.py flask run 2>&1 & +flask_pid=$! + +# Give Flask time to come up +sleep 2 + +tempdir=$(mktemp -d) +# Run debootstrap against our MitM "mirror", ignoring the inevitable GPG errors +./debootstrap --download-only --variant minbase --no-check-gpg ${FLASK_DIST} $tempdir http://127.0.0.1:5000/${FLASK_DISTRO}/ +rc=$? + +rm -rf $tempdir +kill $flask_pid + +exit $rc diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..27a3a6c --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,27 @@ +Tests: debian-testing +Depends: + debootstrap, + libdistro-info-perl, + libdpkg-perl, + libipc-run-perl, + perl, + systemd [linux-any], + systemd-container [linux-any], + ca-certificates, +Restrictions: allow-stderr, needs-root + +Tests: unsorted-packages-files +Depends: + debootstrap, + python3-debian, + python3-flask, + python3-requests, +Restrictions: allow-stderr + +Tests: arch-all-support +Depends: + debootstrap, + python3-apt, + python3-flask, + python3-requests, +Restrictions: allow-stderr diff --git a/debian/tests/debian-testing b/debian/tests/debian-testing new file mode 100755 index 0000000..df94254 --- /dev/null +++ b/debian/tests/debian-testing @@ -0,0 +1,394 @@ +#!/usr/bin/perl +# 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) + +use strict; +use warnings; + +use Cwd qw(getcwd); +use Debian::DistroInfo; +use Dpkg::Version; +use IPC::Run qw(run); +use Test::More; + +my $srcdir = getcwd; + +sub verbose_run { + my $argv = shift; + diag("Running: @{$argv}"); + return run($argv, @_); +} + +sub capture { + my $output; + my $argv = shift; + ok(verbose_run($argv, '>', \$output), "@{$argv}"); + chomp $output; + return $output; +} + +my $check_non_docker_env; +if (run([qw(grep docker.*cgroup /proc/1/mountinfo)], '&>', '/dev/null')) { + diag("it seems docker environment"); + $check_non_docker_env = 0; +} +else { + diag("okay, it's not docker environment"); + $check_non_docker_env = 1; +} + +my @maybe_unshare_mount_ns; + +if (verbose_run(['unshare', '-m', 'true'])) { + diag('can unshare mount namespace'); + @maybe_unshare_mount_ns = ('unshare', '-m'); +} +else { + diag('cannot unshare mount namespace, are we in a container?'); +} + +sub check_fake_schroot { + my %params = @_; + my $reference = $params{reference}; + my $version = $params{version} || '1.6.10-3'; + my $extra_argv = $params{extra_argv} || []; + + # Use unshare -m to make sure the /dev mount gets cleaned up on exit, even + # on failures + my $response = capture([@maybe_unshare_mount_ns, + "$srcdir/debian/tests/fake/schroot-$version", @{$extra_argv}, + $params{chroot}, + qw(runuser -u nobody --), + qw(script -q -c), 'cat /etc/debian_version', '/dev/null']); + $response =~ s/\r//g; + is($response, $reference, 'script(1) should work under (fake) schroot'); +} + +sub check_fake_pbuilder { + my %params = @_; + my $reference = $params{reference}; + my $version = $params{version} || '0.228.4-1'; + + my $response = capture([@maybe_unshare_mount_ns, + "$srcdir/debian/tests/fake/pbuilder-$version", $params{chroot}, + qw(runuser -u nobody --), + qw(script -q -c), 'cat /etc/debian_version', '/dev/null']); + $response =~ s/\r//g; + is($response, $reference, + 'script(1) should work under (fake) pbuilder'); +} + +sub check_chroot { + my %params = @_; + my $chroot = $params{chroot}; + my $response; + + ok(-f "$chroot/etc/debian_version", + 'chroot should have /etc/debian_version'); + ok(-x "$chroot/usr/bin/env", + 'chroot should have /usr/bin/env which is Essential'); + + if ($params{has_systemd}) { + for my $p ( + "root/.ssh", "run/lock/subsys", + "var/cache/private", "var/lib/private", + "var/lib/systemd/coredump", "var/lib/systemd/pstore", + "var/log/README", "var/log/private" + ) + { + ok( -e "$chroot/$p", + "chroot should have /$p created by systemd-tmpfiles" ); + } + } + + ok(-x "$chroot/usr/bin/hello", 'chroot should have /usr/bin/hello due to --include'); + ok(-d "$chroot/usr/share/doc", 'chroot should have /usr/share/doc'); + + if (!defined $ENV{container} || $ENV{container} ne "mmdebstrap-unshare") { + diag("not running with container=mmdebstrap-unshare"); + ok(-c "$chroot/dev/full", '/dev/full should be a character device'); + is(capture(['/usr/bin/stat', '--printf=%t %T %a', "$chroot/dev/full"]), + '1 7 666', '/dev/full should be device 1,7 with 0666 permissions'); + ok(-c "$chroot/dev/null"); + is(capture(['/usr/bin/stat', '--printf=%t %T %a', "$chroot/dev/null"]), + '1 3 666', '/dev/null should be device 1,3 with 0666 permissions'); + } + + my $did_mknod_ptmx; + my $output; + + if (verbose_run([qw(ls -l), "$chroot/dev/ptmx"], '>', \$output)) { + diag("$chroot/dev/ptmx: $output"); + } + else { + diag("Unable to list $chroot/dev/ptmx"); + } + if (verbose_run([qw(ls -l), "$chroot/dev/pts/ptmx"], '>', \$output)) { + diag("$chroot/dev/pts/ptmx: $output"); + } + else { + diag("Unable to list $chroot/dev/pts/ptmx"); + } + + if (-l "$chroot/dev/ptmx") { + # 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"); + like(readlink("$chroot/dev/ptmx"), qr{(?:/dev/)?pts/ptmx}, + 'if /dev/ptmx is a symlink it should be to /dev/pts/ptmx'); + $did_mknod_ptmx = 0; + } + else { + diag("/dev/ptmx is not a symbolic link"); + ok(-c "$chroot/dev/ptmx", + 'if /dev/ptmx is not a symlink it should be a character device'); + is(capture(['/usr/bin/stat', '--printf=%t %T %a', + "$chroot/dev/ptmx"]), '5 2 666', + 'if /dev/ptmx is a device node it should be 5,2 with 0666 permissions'); + $did_mknod_ptmx = 1; + } + + if ($params{can_mknod_ptmx}) { + ok($did_mknod_ptmx, 'able to mknod ptmx so should have done so'); + } + + my $reference = capture(['cat', "$chroot/etc/debian_version"]); + + is(capture([qw(chroot chroot.d runuser -u nobody -- + cat /etc/debian_version)]), + $reference); + + # The remaining tests rely on device nodes to either exist or already + # being bind-mounted. Their setups are not prepared to deal with the + # conditions in an environment with an unshared user namespace as + # used in mmdebstrap. + if (defined $ENV{container} && $ENV{container} eq "mmdebstrap-unshare") { + return; + } + + # The schroot behaviour proposed to fix #856877 and #983423 works, + # even inside (privileged) lxc. + check_fake_schroot(%params, reference => $reference, version => 'proposed'); + check_fake_schroot(%params, reference => $reference, version => 'proposed', + extra_argv => ['--sbuild']); + + # As of 1.6.10-3, or equivalently 1.6.10-11, the default profile + # certainly doesn't work in lxc >= 3 or in Docker: + # https://bugs.debian.org/983423 + # It probably won't work in other container managers either, for + # similar reasons. + if (defined $params{container}) { + TODO: { + local $TODO = "schroot default profile doesn't work in lxc >= 3 or Docker"; + check_fake_schroot(%params, reference => $reference, + version => '1.6.10-3'); + } + } + else { + check_fake_schroot(%params, reference => $reference, + version => '1.6.10-3'); + } + + # schroot 1.6.10-3's sbuild profile does work in lxc, but only on newer + # kernels: https://bugs.debian.org/856877 + if (Dpkg::Version->new($params{kernel}) < Dpkg::Version->new('4.7') && + defined $params{container} && $params{container} eq 'lxc') { + TODO: { + local $TODO = "schroot --sbuild doesn't work in lxc on older ". + "kernels"; + check_fake_schroot(%params, reference => $reference, + extra_argv => ['--sbuild']); + } + } + elsif (! $params{can_mknod_ptmx}) { + TODO: { + local $TODO = "schroot --sbuild doesn't work when /dev/ptmx is ". + "a symlink to /dev/pts/ptmx"; + check_fake_schroot(%params, reference => $reference, + extra_argv => ['--sbuild']); + } + } + else { + check_fake_schroot(%params, reference => $reference, + extra_argv => ['--sbuild']); + } + + # pbuilder >= 0.228.6 works fine + check_fake_pbuilder(%params, reference => $reference, + version => '0.231'); + + # Older pbuilder doesn't work if we are in a container where we can't + # create the /dev/ptmx device node: https://bugs.debian.org/841935 + if (! $params{can_mknod_ptmx}) { + TODO: { + local $TODO = "pbuilder 0.228.4-1 doesn't work when /dev/ptmx is ". + "a symlink to /dev/pts/ptmx"; + check_fake_pbuilder(%params, reference => $reference, + version => '0.228.4-1'); + } + } + else { + check_fake_pbuilder(%params, reference => $reference, + version => '0.228.4-1'); + } +} + +# Specify https mirror to check https mirror specific problem +# https://bugs.debian.org/896071 +my $mirror = 'https://deb.debian.org/debian'; +my $tmp = $ENV{AUTOPKGTEST_TMP}; +die "no autopkgtest temporary directory specified" unless $tmp; +chdir $tmp or die "chdir $tmp: $!"; + +$ENV{LC_ALL} = 'C.UTF-8'; + +# Try to inherit a Debian mirror from the host +foreach my $file ('/etc/apt/sources.list', + glob('/etc/apt/sources.list.d/*.list')) { + open(my $fh, '<', $file); + while (<$fh>) { + if (m{^deb\s+(http://[-a-zA-Z0-9.:]+/debian)\s}) { + $mirror = $1; + last; + } + } + close $fh; +} + +if (run(['ischroot'], '>&2')) { + diag("In a chroot according to ischroot(1)"); +} +else { + diag("Not in a chroot according to ischroot(1)"); +} + +my $virtualization; +if ($^O ne 'linux') { + diag("Cannot use systemd-detect-virt on non-Linux"); +} +elsif (run(['systemd-detect-virt', '--vm'], '>', \$virtualization)) { + chomp $virtualization; + diag("Virtualization: $virtualization"); +} +else { + $virtualization = undef; + diag("Virtualization: (not in a virtual machine)"); +} + +my $in_container = 0; +my $container; +if ($^O ne 'linux') { + diag("Cannot use systemd-detect-virt on non-Linux"); +} +elsif (run(['systemd-detect-virt', '--container'], '>', \$container)) { + $in_container = 1; + chomp $container; + diag("Container: $container"); +} +else { + $container = undef; + diag("Container: (not in a container)"); +} + +my $kernel = capture([qw(uname -r)]); +chomp $kernel; + +open(my $fh, '<', '/proc/self/mountinfo'); +while (<$fh>) { + chomp; + diag("mountinfo: $_"); +} +close $fh; + +my $output; +if (verbose_run([qw(ls -l /dev/ptmx)], '>', \$output)) { + diag("/dev/ptmx: $output"); +} +else { + diag("Unable to list /dev/ptmx"); +} +if (verbose_run([qw(ls -l /dev/pts/ptmx)], '>', \$output)) { + diag("/dev/pts/ptmx: $output"); +} +else { + diag("Unable to list /dev/pts/ptmx"); +} + +my $can_mknod_ptmx; +if (run([qw(mknod -m000 ptmx c 5 2)], '&>', '/dev/null')) { + diag("mknod ptmx succeeded"); + $can_mknod_ptmx = 1; +} +else { + diag("mknod ptmx failed, are we in a container?"); + $can_mknod_ptmx = 0; +} + +my $distro_info = DebianDistroInfo->new; +my $testing = $distro_info->testing; + +# Should specify multiple components for checking (see Bug#898738) +if (!verbose_run([length($ENV{DEBOOTSTRAP_SCRIPT}) ? $ENV{DEBOOTSTRAP_SCRIPT} : 'debootstrap', + '--include=debootstrap,debian-archive-keyring,gnupg,hello,systemd', + '--variant=minbase', + '--components=main,contrib,non-free', + $testing, 'chroot.d', $mirror], '>&2')) { + BAIL_OUT("debootstrap failed: $?"); +} + +if (!verbose_run([qw(find chroot.d/dev -ls)], '>&2')) { + BAIL_OUT("Unable to list contents of chroot's /dev: $?"); +} + +if ($check_non_docker_env) { + check_chroot(chroot => 'chroot.d', can_mknod_ptmx => $can_mknod_ptmx, + kernel => $kernel, container => $container, has_systemd => 1); +} + +if ($^O ne 'linux') { + diag("Cannot use systemd-nspawn on non-Linux"); +} +elsif ($in_container) { + diag('in a container according to systemd-detect-virt, not trying to '. + 'use systemd-nspawn'); +} +elsif (defined $ENV{container} && length $ENV{container}) { + diag('in a container according to $container, not trying to '. + 'use systemd-nspawn'); +} +elsif (! -d '/run/systemd/system') { + diag('systemd not booted, not trying to use systemd-nspawn'); +} +else { + if (!verbose_run(['systemd-nspawn', '-D', 'chroot.d', + "--bind=$ENV{AUTOPKGTEST_TMP}:/mnt", + '--bind-ro=/usr/sbin/debootstrap', + '--bind-ro=/usr/share/debootstrap', + '--', + 'debootstrap', '--include=hello', '--variant=minbase', + $testing, '/mnt/from-nspawn.d', $mirror], '>&2')) { + BAIL_OUT("debootstrap wrapped in systemd-nspawn failed: $?"); + } + + check_chroot(chroot => "$ENV{AUTOPKGTEST_TMP}/from-nspawn.d", can_mknod_ptmx => 0, + kernel => $kernel, container => "nspawn"); +} + +if (!defined $ENV{container} || $ENV{container} ne "mmdebstrap-unshare") { + if (!run([qw(rm -fr --one-file-system chroot.d)], '>&2')) { + BAIL_OUT('Unable to remove chroot.d'); + } +} else { + if (!run([qw(env --chdir=chroot.d find . -mount -mindepth 1 -delete)], '>&2')) { + BAIL_OUT('Unable to remove contents of 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..80b0bf3 --- /dev/null +++ b/debian/tests/fake/pbuilder-0.228.4-1 @@ -0,0 +1,37 @@ +#!/bin/sh +# fake/pbuilder-0.228.4-1 -- emulate how pbuilder/0.228.4-1 would chroot. +# +# Please do not modify this script without verifying that its behaviour +# is still equivalent to the stated versions of pbuilder. +# +# This version has #841935 unfixed. 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).) +# +# Reference: pbuilder/pbuilder-modules, search for dev/pts. +# +# 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 + +ls -l "$chroot/dev/ptmx" | sed -e 's/^/# fake-pbuilder: /' >&2 +ls -l "$chroot/dev/pts/ptmx" | sed -e 's/^/# fake-pbuilder: /' >&2 + +e=0 +chroot "$chroot" "$@" || e=$? + +umount "$chroot/dev/pts" +exit "$e" diff --git a/debian/tests/fake/pbuilder-0.231 b/debian/tests/fake/pbuilder-0.231 new file mode 100755 index 0000000..e49e29f --- /dev/null +++ b/debian/tests/fake/pbuilder-0.231 @@ -0,0 +1,64 @@ +#!/bin/sh +# fake/pbuilder-0.231 -- emulate how pbuilder >= 0.228.6 sets up its chroot +# +# Please do not modify this script without verifying that its behaviour +# is still equivalent to the stated versions of pbuilder. If a future +# version of pbuilder changes its behaviour, please copy this script and +# modify the copy instead. +# +# This has #841935 fixed (commit 4a4134dd). It was checked for equivalence +# to pbuilder 0.231, which is the version included in Debian 11 and 12, +# but the versions in Debian 10 and 9 have equivalent code here. +# +# Reference: pbuilder/pbuilder-modules, search for dev/pts. +# +# Copyright © 2017-2021 Simon McVittie +# SPDX-License-Identifier: MIT +# (see debian/copyright) + +set -e + +BUILDPLACE="$1" +shift +if test -z "$BUILDPLACE" || test -z "$1"; then + echo "Usage: $0 CHROOT COMMAND...">&2 + exit 2 +fi + +mkdir -p "$BUILDPLACE/dev/pts" +TTYGRP=5 +TTYMODE=620 +mount -t devpts devpts "$BUILDPLACE/dev/pts" -o "newinstance,noexec,nosuid,gid=$TTYGRP,mode=$TTYMODE,ptmxmode=0666" + +mounted_ptmx=no + +if ! [ -L "$BUILDPLACE/dev/ptmx" ]; then + echo "# fake-pbuilder: redirecting /dev/ptmx to /dev/pts/ptmx" >&2 + mount --bind "$BUILDPLACE/dev/pts/ptmx" "$BUILDPLACE/dev/ptmx" + mounted_ptmx=yes +fi + +mounted_console=no + +if stdin_tty="$(tty)"; then + if [ ! -e "$BUILDPLACE/dev/console" ]; then + echo "# fake-pbuilder: creating /dev/console" >&2 + mknod -m600 "$BUILDPLACE/dev/console" c 5 1 + fi + + echo "# fake-pbuilder: mounting $stdin_tty over /dev/console" >&2 + mount --bind "$stdin_tty" "$BUILDPLACE/dev/console" + mounted_console=yes +fi + +ls -l "$BUILDPLACE/dev/console" | sed -e 's/^/# fake-pbuilder: /' >&2 +ls -l "$BUILDPLACE/dev/ptmx" | sed -e 's/^/# fake-pbuilder: /' >&2 +ls -l "$BUILDPLACE/dev/pts/ptmx" | sed -e 's/^/# fake-pbuilder: /' >&2 + +e=0 +chroot "$BUILDPLACE" "$@" || e=$? + +[ "$mounted_console" = no ] || umount "$BUILDPLACE/dev/console" +[ "$mounted_ptmx" = no ] || umount "$BUILDPLACE/dev/ptmx" +umount "$BUILDPLACE/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..6ebd6dc --- /dev/null +++ b/debian/tests/fake/schroot-1.6.10-3 @@ -0,0 +1,58 @@ +#!/bin/sh +# fake/schroot-1.6.10-3 -- emulate how schroot/1.6.10-3 would chroot. +# +# Please do not modify this script without verifying that its behaviour +# is still equivalent to the stated versions of schroot. If a future +# version of schroot changes its behaviour, please copy this script and +# modify the copy instead. +# +# This version has #856877 unfixed. It bind-mounts /dev/pts and maybe +# /dev from the host system, rather than creating a new instance of /dev/pts. +# (There is of course a lot more that it does, but these are the parts that +# affect pty users like script(1).) +# +# Copyright © 2017-2023 Simon McVittie +# SPDX-License-Identifier: MIT +# (see debian/copyright) + +set -e + +# Reference: /etc/schroot/default/fstab +# (in schroot source tree: etc/profile-templates/default/linux/fstab) +bind_dev=yes + +while true; do + case "$1" in + (--sbuild) + shift + # Reference: /etc/schroot/sbuild/fstab + # (source: etc/profile-templates/sbuild/linux/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" + +ls -l "/dev/ptmx" | sed -e 's/^/# fake-schroot: outside chroot: /' >&2 +ls -l "/dev/pts/ptmx" | sed -e 's/^/# fake-schroot: outside chroot: /' >&2 +ls -l "$chroot/dev/ptmx" | sed -e 's/^/# fake-schroot: /' >&2 +ls -l "$chroot/dev/pts/ptmx" | sed -e 's/^/# fake-schroot: /' >&2 + +e=0 +chroot "$chroot" "$@" || e=$? + +umount "$chroot/dev/pts" +[ "$bind_dev" = no ] || umount "$chroot/dev" + +exit "$e" diff --git a/debian/tests/fake/schroot-proposed b/debian/tests/fake/schroot-proposed new file mode 100755 index 0000000..9157dbc --- /dev/null +++ b/debian/tests/fake/schroot-proposed @@ -0,0 +1,87 @@ +#!/bin/sh +# fake/schroot-proposed -- emulate proposed mount behaviour for schroot +# +# This version emulates the behaviour proposed on #856877. If it needs +# changing, please update the proposed patch on #856877 too. +# +# Copyright © 2017-2023 Simon McVittie +# SPDX-License-Identifier: MIT +# (see debian/copyright) + +set -e + +# Reference: /etc/schroot/default/fstab +# (in schroot source tree: etc/profile-templates/default/linux/fstab) +bind_dev=yes + +while true; do + case "$1" in + (--sbuild) + shift + # Reference: /etc/schroot/sbuild/fstab + # (source: etc/profile-templates/sbuild/linux/fstab) + bind_dev=no + ;; + (*) + break + esac +done + +CHROOT_PATH="$1" +shift +if test -z "$CHROOT_PATH" || test -z "$1"; then + echo "Usage: $0 CHROOT COMMAND...">&2 + exit 2 +fi + +[ "$bind_dev" = no ] || mount --bind /dev "$CHROOT_PATH/dev" +mount -t devpts -o rw,newinstance,ptmxmode=666,mode=620,gid=5 /dev/pts "$CHROOT_PATH/dev/pts" + +ls -l "/dev/ptmx" | sed -e 's/^/# fake-schroot: outside chroot: /' >&2 +ls -l "/dev/pts/ptmx" | sed -e 's/^/# fake-schroot: outside chroot: /' >&2 + +ls -l "$CHROOT_PATH/dev/ptmx" | sed -e 's/^/# fake-schroot: after first step: /' >&2 +ls -l "$CHROOT_PATH/dev/pts/ptmx" | sed -e 's/^/# fake-schroot: after first step: /' >&2 + +mounted_ptmx=no + +# Depending on how /dev was set up, /dev/ptmx might either be +# character device (5,2), or a symbolic link to pts/ptmx. +# Either way we want it to be equivalent to /dev/pts/ptmx, assuming +# both exist. +if [ -e "$CHROOT_PATH/dev/pts/ptmx" ] && \ + [ -e "$CHROOT_PATH/dev/ptmx" ] && \ + ! [ "$CHROOT_PATH/dev/pts/ptmx" -ef "$CHROOT_PATH/dev/ptmx" ]; then + mount --bind "$CHROOT_PATH/dev/pts/ptmx" "$CHROOT_PATH/dev/ptmx" + mounted_ptmx=yes +fi + +mounted_console=no + +# If schroot was invoked from a terminal, we still want to be able to +# access that terminal. lxc and systemd-nspawn achieve this by +# binding it onto /dev/console; so can we. +if stdin_tty="$(tty)"; then + if [ ! -e "$CHROOT_PATH/dev/console" ]; then + # We need something to mount onto, and it might as well be + # the correctly-numbered device node. + mknod -m700 "$CHROOT_PATH/dev/console" c 5 1 + fi + + mount --bind "$stdin_tty" "$CHROOT_PATH/dev/console" + mounted_console=yes +fi + +ls -l "$CHROOT_PATH/dev/console" | sed -e 's/^/# fake-schroot: after fixing mounts: /' >&2 +ls -l "$CHROOT_PATH/dev/ptmx" | sed -e 's/^/# fake-schroot: after fixing mounts: /' >&2 +ls -l "$CHROOT_PATH/dev/pts/ptmx" | sed -e 's/^/# fake-schroot: after fixing mounts: /' >&2 + +e=0 +chroot "$CHROOT_PATH" "$@" || e=$? + +[ "$mounted_console" = no ] || umount "$CHROOT_PATH/dev/console" +[ "$mounted_ptmx" = no ] || umount "$CHROOT_PATH/dev/ptmx" +umount "$CHROOT_PATH/dev/pts" +[ "$bind_dev" = no ] || umount "$CHROOT_PATH/dev" + +exit "$e" diff --git a/debian/tests/out-of-order-mitm.py b/debian/tests/out-of-order-mitm.py new file mode 100644 index 0000000..8a96c1b --- /dev/null +++ b/debian/tests/out-of-order-mitm.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 Debian Install System Team <debian-boot@lists.debian.org> +# +# SPDX-License-Identifier: MIT + +"""Flask app which MITM's an archive to generate out-of-order apt lists. + +Specifically, it prepends an additional Packages file stanza for a non-existent +lower version of apt: a fixed version of debootstrap will find the second +(correct) apt stanza and succeed; a broken version of debootstrap will find +only the first (non-existent) apt stanza and fail. +""" +import functools +import gzip +import hashlib +import os + +import requests +from debian.deb822 import Packages +from flask import Flask, redirect + +app = Flask(__name__) + +ARCH = os.environ.get("FLASK_ARCH", "amd64") +DIST = os.environ.get("FLASK_DIST", "trixie") +DISTRO = os.environ.get("FLASK_DISTRO", "debian") +MIRROR = os.environ.get("FLASK_MIRROR", "http://deb.debian.org") + + +if DISTRO in ("debian", "pureos"): + hash_funcs = [hashlib.md5, hashlib.sha256] +else: + # Ubuntu includes SHA1 still + hash_funcs = [hashlib.md5, hashlib.sha1, hashlib.sha256] + + +def _munge_release_file(url: str) -> bytes: + """Given a Release file URL, rewrite it for our modified Packages content.""" + original = requests.get(MIRROR + "/" + url).content + packages_content = _packages_content( + f"{DISTRO}/dists/{DIST}/main/binary-{ARCH}/Packages" + ) + size = bytes(str(len(packages_content)), "ascii") + sums = [ + bytes(hash_func(packages_content).hexdigest(), "ascii") + for hash_func in hash_funcs + ] + new_lines = [] + filename = f"main/binary-{ARCH}/Packages".encode("ascii") + for line in original.splitlines(): + if not line.endswith(filename): + new_lines.append(line) + continue + new_lines.append(b" ".join([b"", sums.pop(0), size, filename])) + return b"\n".join(new_lines) + + +@functools.lru_cache +def _packages_content(url: str) -> bytes: + """Given a Packages URL, fetch it and prepend a broken apt stanza.""" + resp = requests.get(MIRROR + "/" + url + ".gz") + upstream_content = gzip.decompress(resp.content) + + # Find the first `apt` stanza + for stanza in Packages.iter_paragraphs(upstream_content): + if stanza["Package"] == "apt": + break + + # Generate the broken stanza + new_version = stanza["Version"] + "~test" + stanza["Filename"] = stanza["Filename"].replace(stanza["Version"], new_version) + stanza["Version"] = new_version + + # Prepend the stanza to the upstream content + return bytes(stanza) + b"\n" + upstream_content + + +@app.route("/<path:url>", methods=["GET", "POST"]) +def root(url): + """Handler for all requests.""" + if ( + url == f"{DISTRO}/dists/{DIST}/InRelease" + or "by-hash" in url + or "Packages.xz" in url + or "Packages.gz" in url + ): + # 404 these URLs to force clients to fetch by path and without compression, to + # make MITM easier + return "", 404 + if url == f"{DISTRO}/dists/{DIST}/Release": + # If Release is being fetched, return our modified version + return _munge_release_file(url) + if url == f"{DISTRO}/dists/{DIST}/main/binary-{ARCH}/Packages": + # If Packages is being fetched, return our modified version + return _packages_content(url) + # For anything we don't need to modify, redirect clients to upstream mirror + return redirect(f"{MIRROR}/{url}") diff --git a/debian/tests/unsorted-packages-files b/debian/tests/unsorted-packages-files new file mode 100755 index 0000000..0b9bd5a --- /dev/null +++ b/debian/tests/unsorted-packages-files @@ -0,0 +1,34 @@ +#!/bin/sh + +# This test runs the out-of-order-mitm.py Flask app which debootstrap is then +# pointed at. +# out-of-order-mitm.py will return a Packages file which has an additional +# `apt` stanza prepended, with the Version and Filename adjusted to point at +# a lower, non-existent version. Versions of debootstrap which process +# _all_ Packages files entries will find the original stanza later in the file +# (and succesfully fetch the corresponding package file): versions that don't +# will find the prepended stanza and fail (with a 404 of the nonexistent +# package file). + +export FLASK_ARCH="$(dpkg --print-architecture)" +export FLASK_DIST=testing +export FLASK_DISTRO=debian +export FLASK_MIRROR=http://deb.debian.org +export PATH=$PATH:/usr/sbin + +# Launch our MitM "mirror" server, ensure that request logging is sent to stdout +PYTHONDONTWRITEBYTECODE=true FLASK_APP=debian/tests/out-of-order-mitm.py flask run 2>&1 & +flask_pid=$! + +# Give Flask time to come up +sleep 2 + +tempdir=$(mktemp -d) +# Run debootstrap against our MitM "mirror", ignoring the inevitable GPG errors +debootstrap --download-only --variant minbase --no-check-gpg ${FLASK_DIST} $tempdir http://127.0.0.1:5000/${FLASK_DISTRO}/ +rc=$? + +rm -rf $tempdir +kill $flask_pid + +exit $rc |