From a93136232f55fff9b03f2f4a303a0448493148b6 Mon Sep 17 00:00:00 2001 From: Valentin David Date: Thu, 16 Jun 2022 13:21:47 +0200 Subject: [PATCH] Use mount namespace instead of chroot This allow to not worry about mounts done within the namespace. We can now bind mount files into the sysroot instead of copying them. --- Makefile | 14 +--- hooks/001-extra-packages.chroot | 4 - hooks/999-clean-resolv-conf.chroot | 5 -- mount-ns.sh | 125 +++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 20 deletions(-) delete mode 100755 hooks/999-clean-resolv-conf.chroot create mode 100755 mount-ns.sh diff --git a/Makefile b/Makefile index 1df0b694..41db689f 100644 --- a/Makefile +++ b/Makefile @@ -16,21 +16,12 @@ install: fi rm -rf $(DESTDIR) cp -aT $(CRAFT_STAGE)/base $(DESTDIR) - # ensure resolving works inside the chroot - cat /etc/resolv.conf > $(DESTDIR)/etc/resolv.conf # copy-in launchpad's build archive if grep -q ftpmaster.internal /etc/apt/sources.list; then \ cp /etc/apt/sources.list $(DESTDIR)/etc/apt/sources.list; \ cp /etc/apt/trusted.gpg $(DESTDIR)/etc/apt/ || true; \ cp -r /etc/apt/trusted.gpg.d $(DESTDIR)/etc/apt/ || true; \ fi - # since recently we're also missing some /dev files that might be - # useful during build - make sure they're there - [ -e $(DESTDIR)/dev/null ] || mknod -m 666 $(DESTDIR)/dev/null c 1 3 - [ -e $(DESTDIR)/dev/zero ] || mknod -m 666 $(DESTDIR)/dev/zero c 1 5 - [ -e $(DESTDIR)/dev/random ] || mknod -m 666 $(DESTDIR)/dev/random c 1 8 - [ -e $(DESTDIR)/dev/urandom ] || \ - mknod -m 666 $(DESTDIR)/dev/urandom c 1 9 # copy static files verbatim /bin/cp -a static/* $(DESTDIR) mkdir -p $(DESTDIR)/install-data @@ -38,8 +29,9 @@ install: # customize set -eux; for f in ./hooks/[0-9]*.chroot; do \ base="$$(basename "$${f}")"; \ - cp -a "$${f}" $(DESTDIR)/install-data/; \ - chroot $(DESTDIR) "/install-data/$${base}"; \ + ./mount-ns.sh spawn $(DESTDIR) \ + --ro-bind $$f "/install-data/$${base}" \ + -- "/install-data/$${base}"; \ rm "$(DESTDIR)/install-data/$${base}"; \ done rm -rf $(DESTDIR)/install-data diff --git a/hooks/001-extra-packages.chroot b/hooks/001-extra-packages.chroot index 2a9539e7..cb840087 100755 --- a/hooks/001-extra-packages.chroot +++ b/hooks/001-extra-packages.chroot @@ -12,10 +12,6 @@ export DEBIAN_FRONTEND=noninteractive rm -f /etc/apt/sources.list.d/proposed.list -# ensure we have /proc or systemd will fail -mount -t proc proc /proc -trap 'umount /proc' EXIT - # systemd postinst needs this mkdir -p /var/log/journal diff --git a/hooks/999-clean-resolv-conf.chroot b/hooks/999-clean-resolv-conf.chroot deleted file mode 100755 index b0e0cdc1..00000000 --- a/hooks/999-clean-resolv-conf.chroot +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -e - -echo "" > /etc/resolv.conf diff --git a/mount-ns.sh b/mount-ns.sh new file mode 100755 index 00000000..2b237955 --- /dev/null +++ b/mount-ns.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +set -eu + +# FIXME: This should be replace by a mount of devtmpfs when +# it is supported in user namespaces +bind_dev() { + dev="${1}/dev" + + for device in null zero full random urandom tty; do + touch "${dev}/${device}" + mount --bind "/dev/${device}" "${dev}/${device}" + done +} + +# Because systemd debian package post-install script is not very +# robust, we have to create a symlink and bind mount resolv.conf +# there. +bind_resolv() { + sysroot="${1}" + + resolv_real_path="${sysroot}/run/fake-resolv.conf" + create_symlink=yes + if [ -L "${sysroot}/etc/resolv.conf" ]; then + resolv="$(readlink "${sysroot}/etc/resolv.conf")" + if [ "${resolv}" = "../run/systemd/resolve/stub-resolv.conf" ]; then + resolv_real_path="${sysroot}/run/systemd/resolve/stub-resolv.conf" + create_symlink=no + fi + fi + mkdir -p "$(dirname "${resolv_real_path}")" + touch "${resolv_real_path}" + mount --bind -o ro /etc/resolv.conf "${resolv_real_path}" + if [ "${create_symlink}" = yes ]; then + ln -srf "${resolv_real_path}" "${sysroot}/etc/resolv.conf" + fi +} + +if [ $# -lt 3 ]; then + echo "Expected at least 3 arguments" 1>&2 + exit 1 +fi + +command="${1}" +sysroot="${2}" +shift 2 + +case "${command}" in + spawn) + # This is the first phase. This will re-spawn the script with + # `init` subcommand. There are limitations of what can be + # done within a LXD container. So in the first phase we mount + # a tmpfs filesytem which cannot always be done in a mount + # namespace. Because it is outside of the mount namespace, + # it has to be removed from manually. + # Then we spawn the second phase into a namespace, + # but without changing the root. + + tmpdir="$(mktemp -d --tmpdir mount-ns.XXXXXXXXXX)" + cleanup() { + umount "${tmpdir}" || true + rm -rf "${tmpdir}" + } + mount -t tmpfs tmpfs "${tmpdir}" + mkdir -m 0755 -p "${tmpdir}/dev" + mkdir -m 1777 -p "${tmpdir}/tmp" + mkdir -m 0755 -p "${tmpdir}/run" + options=( + --bind "${tmpdir}/dev" /dev + --bind "${tmpdir}/tmp" /tmp + --bind "${tmpdir}/run" /run + ) + trap cleanup EXIT + unshare --pid --fork --mount -- "${0}" init "${sysroot}" "${options[@]}" "${@}" + ;; + init) + # This is the second phase. Here we are in a mount namespace, + # spawned from the `spawn` subcommand. But we still have the + # same root directory. So we can bind mount all we need in the + # sysroot. Then we can change the root to that sysroot. + + mount -t proc proc "${sysroot}/proc" + while [ $# -gt 1 ]; do + case "${1}" in + --) + shift + break + ;; + --bind|--ro-bind) + if [ -d "$2" ]; then + if ! [ -d "${sysroot}/$3" ]; then + mkdir -p "${sysroot}/$3" + fi + else + if ! [ -e "${sysroot}/$3" ]; then + dir="$(dirname "${sysroot}/$3")" + if ! [ -d "${dir}" ]; then + mkdir -p "${dir}" + fi + touch "${sysroot}/$3" + fi + fi + extra_args=() + case "$1" in + --ro-bind) + extra_args=("-o" "ro") + ;; + esac + mount --bind "${extra_args[@]}" "$2" "${sysroot}/$3" + shift 3 + ;; + *) + break + ;; + esac + done + bind_dev "${sysroot}" + bind_resolv "${sysroot}" + exec unshare --mount --root="${sysroot}" -- "${@}" + ;; + *) + echo "Unknown command" 1>&2 + exit 1 + ;; +esac