From 88c0c9db0b612286fae72582234c1e6cf4294920 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sun, 17 Oct 2021 16:13:06 -0400 Subject: [PATCH 1/3] Don't warn if ownership changes when copying /etc/pacman.d/gnupg In an unshare environment, /etc/pacman.d/gnupg is owned by the original root, who is now "nobody". cp will warn about this, since we can't create files owned by the original root, and it instead creates them as the unshare'd root (the original user). This is benign, so ignore it. --- pacstrap.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pacstrap.in b/pacstrap.in index 703df11..4937c9f 100644 --- a/pacstrap.in +++ b/pacstrap.in @@ -107,7 +107,7 @@ chroot_setup "$newroot" || die "failed to setup chroot %s" "$newroot" if (( copykeyring )); then # if there's a keyring on the host, copy it into the new root, unless it exists already if [[ -d /etc/pacman.d/gnupg && ! -d $newroot/etc/pacman.d/gnupg ]]; then - cp -a /etc/pacman.d/gnupg "$newroot/etc/pacman.d/" + cp -a --no-preserve=ownership /etc/pacman.d/gnupg "$newroot/etc/pacman.d/" fi fi From ee9db7d580f59d868b4fdb1ef739e8a3d66a5541 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sun, 17 Oct 2021 17:13:31 -0400 Subject: [PATCH 2/3] Add unshare mode to pacstrap This adds an "unshare" mode to pacstrap. This mode lets a regular user create a new arch root filesystem. We use -N because both -U and -u are taken in pacstrap and arch-chroot, respectively. There are two major changes to pacstrap: we need to run many commands in under unshare, and the setup process for mounts is different. Because unshare starts a new shell, it is difficult to run many commands in sequence. To get around this, we create a function for the rest of the commands we wish to run, and then declare all functions and variables in the unshare'd shell. This is pretty convenient. An alternative method would be to generate the shell script as a HERE document, and pipe it to bash. Because unshare starts a new shell, we can only communicate using stdin/out and any command line arguments. And we need to defer some setup until after we are root. To get around this, we create a function for the rest of the commands we wish to run, and then declare all functions and variables in the unshare'd shell. I also considered having a separate helper script which would contain the contents of pacstrap(). But I think this would be confusing, because the logic would then live in a separate file (instead of just a separate function). That method is also tricky because every variable has to be passed in through the command-line arguments. One last method would be to generate a script on the fly (e.g. using a HERE doc). I think that method could work as well. The primary difference to the setup process is that we need to mount filesystems in a different manner: - We bind-mount the root directory. This is so commands which want to determine how much free space there is (or otherwise work with mounts) expect a mount on /. We unmount it with --lazy, since otherwise sys will cause an error (see below). - proc can be mounted multiple times and is mounted in the same way - sys cannot be mounted again, but we can recursively bind-mount it. When mounted this way, we can't unmount it until the mount namespace is deleted (likely because sys has a number of sub-mounts), so we have to use --lazy when unmounting it. - dev can be bind-mounted, but this results in errors because some packages try and modify files in /dev if they exist. Since we don't have permission to do that on the host system, this fails. Instead, we just bind-mount a minimal set of files. - run is not bind-mounted, but is instead created as a new tmpfs. According to aea51ba ("Bind mount /run from host into new root"), the reason this was done was to avoid lengthy timeouts when scanning for lvm devices. Because unshare does not (and cannot) use lvm devices, we don't need to bind-mount. - tmp is created as usual. Closes: #8 --- common | 71 ++++++++++++++++++++++++++++++++++++++++ completion/pacstrap.bash | 2 +- doc/pacstrap.8.asciidoc | 5 +++ pacstrap.in | 57 +++++++++++++++++++------------- 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/common b/common index 0fe3cb0..7ff0652 100644 --- a/common +++ b/common @@ -100,6 +100,77 @@ chroot_teardown() { unset CHROOT_ACTIVE_MOUNTS } +chroot_add_mount_lazy() { + mount "$@" && CHROOT_ACTIVE_LAZY=("$2" "${CHROOT_ACTIVE_LAZY[@]}") +} + +chroot_bind_device() { + touch "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}") + chroot_add_mount $1 "$2" --bind +} + +chroot_add_link() { + ln -sf "$1" "$2" && CHROOT_ACTIVE_FILES=("$2" "${CHROOT_ACTIVE_FILES[@]}") +} + +unshare_setup() { + CHROOT_ACTIVE_MOUNTS=() + CHROOT_ACTIVE_LAZY=() + CHROOT_ACTIVE_FILES=() + [[ $(trap -p EXIT) ]] && die '(BUG): attempting to overwrite existing EXIT trap' + trap 'unshare_teardown' EXIT + + chroot_add_mount_lazy "$1" "$1" --bind && + chroot_add_mount proc "$1/proc" -t proc -o nosuid,noexec,nodev && + chroot_add_mount_lazy /sys "$1/sys" --rbind && + chroot_add_link "$1/proc/self/fd" "$1/dev/fd" && + chroot_add_link "$1/proc/self/fd/0" "$1/dev/stdin" && + chroot_add_link "$1/proc/self/fd/1" "$1/dev/stdout" && + chroot_add_link "$1/proc/self/fd/2" "$1/dev/stderr" && + chroot_bind_device /dev/full "$1/dev/full" && + chroot_bind_device /dev/null "$1/dev/null" && + chroot_bind_device /dev/random "$1/dev/random" && + chroot_bind_device /dev/tty "$1/dev/tty" && + chroot_bind_device /dev/urandom "$1/dev/urandom" && + chroot_bind_device /dev/zero "$1/dev/zero" && + chroot_add_mount run "$1/run" -t tmpfs -o nosuid,nodev,mode=0755 && + chroot_add_mount tmp "$1/tmp" -t tmpfs -o mode=1777,strictatime,nodev,nosuid +} + +unshare_teardown() { + chroot_teardown + + if (( ${#CHROOT_ACTIVE_LAZY[@]} )); then + umount --lazy "${CHROOT_ACTIVE_LAZY[@]}" + fi + unset CHROOT_ACTIVE_LAZY + + if (( ${#CHROOT_ACTIVE_FILES[@]} )); then + rm "${CHROOT_ACTIVE_FILES[@]}" + fi + unset CHROOT_ACTIVE_FILES +} + +root_unshare="unshare --fork --pid" +user_unshare="$root_unshare --mount --map-auto --map-root-user --setuid 0 --setgid 0" + +# This outputs code for declaring all variables to stdout. For example, if +# FOO=BAR, then running +# declare -p FOO +# will result in the output +# declare -- FOO="bar" +# This function may be used to re-declare all currently used variables and +# functions in a new shell. +declare_all() { + # Remove read-only variables to avoid warnings. Unfortunately, declare +r -p + # doesn't work like it looks like it should (declaring only read-write + # variables). However, declare -rp will print out read-only variables, which + # we can then use to remove those definitions. + declare -p | grep -Fvf <(declare -rp) + # Then declare functions + declare -pf +} + try_cast() ( _=$(( $1#$2 )) ) 2>/dev/null diff --git a/completion/pacstrap.bash b/completion/pacstrap.bash index fb948f0..a77cb04 100644 --- a/completion/pacstrap.bash +++ b/completion/pacstrap.bash @@ -8,7 +8,7 @@ _pacstrap() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="-C -c -G -i -M -h" + opts="-C -c -G -i -M -N -h" for i in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do if [[ -d ${i} ]]; then diff --git a/doc/pacstrap.8.asciidoc b/doc/pacstrap.8.asciidoc index d3d517c..6eed23e 100644 --- a/doc/pacstrap.8.asciidoc +++ b/doc/pacstrap.8.asciidoc @@ -37,6 +37,11 @@ Options *-M*:: Avoid copying the host's mirrorlist to the target. +*-N*:: + Run in unshare mode. This will use linkman:unshare[1] to create a new + mount and user namespace, allowing regular users to create new system + installations. + *-U*:: Use pacman -U to install packages. Useful for obtaining fine-grained control over the installed packages. diff --git a/pacstrap.in b/pacstrap.in index 4937c9f..9ffe17c 100644 --- a/pacstrap.in +++ b/pacstrap.in @@ -16,6 +16,8 @@ hostcache=0 copykeyring=1 copymirrorlist=1 pacmode=-Sy +setup=chroot_setup +unshare="$root_unshare" usage() { cat < Date: Wed, 17 Nov 2021 14:08:10 -0500 Subject: [PATCH 3/3] Add unshare mode to arch-chroot This is effectively the same transformation as in the previous patch. We move the mountpoint warning later to avoid warning when we are about to bind-mount the chroot dir ourselves. --- arch-chroot.in | 36 +++++++++++++++++++++++++----------- completion/arch-chroot.bash | 2 +- doc/arch-chroot.8.asciidoc | 5 +++++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/arch-chroot.in b/arch-chroot.in index fd6140e..055fb85 100644 --- a/arch-chroot.in +++ b/arch-chroot.in @@ -4,11 +4,15 @@ shopt -s extglob m4_include(common) +setup=chroot_setup +unshare="$root_unshare" + usage() { cat <[:group] Specify non-root user and optional group to use If 'command' is unspecified, ${0##*/} will launch /bin/bash. @@ -51,12 +55,16 @@ chroot_add_resolv_conf() { chroot_add_mount /etc/resolv.conf "$resolv_conf" --bind } -while getopts ':hu:' flag; do +while getopts ':hNu:' flag; do case $flag in h) usage exit 0 ;; + N) + setup=unshare_setup + unshare="$user_unshare" + ;; u) userspec=$OPTARG ;; @@ -70,21 +78,27 @@ while getopts ':hu:' flag; do done shift $(( OPTIND - 1 )) -(( EUID == 0 )) || die 'This script must be run with root privileges' (( $# )) || die 'No chroot directory specified' chrootdir=$1 shift -[[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir" +arch-chroot() { + (( EUID == 0 )) || die 'This script must be run with root privileges' + + [[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir" -if ! mountpoint -q "$chrootdir"; then - warning "$chrootdir is not a mountpoint. This may have undesirable side effects." -fi + $setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir" + chroot_add_resolv_conf "$chrootdir" || die "failed to setup resolv.conf" -chroot_setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir" -chroot_add_resolv_conf "$chrootdir" || die "failed to setup resolv.conf" + if ! mountpoint -q "$chrootdir"; then + warning "$chrootdir is not a mountpoint. This may have undesirable side effects." + fi + + chroot_args=() + [[ $userspec ]] && chroot_args+=(--userspec "$userspec") -chroot_args=() -[[ $userspec ]] && chroot_args+=(--userspec "$userspec") + SHELL=/bin/bash chroot "${chroot_args[@]}" -- "$chrootdir" "${args[@]}" +} -SHELL=/bin/bash unshare --fork --pid chroot "${chroot_args[@]}" -- "$chrootdir" "$@" +args=("$@") +$unshare bash -c "$(declare_all); arch-chroot" diff --git a/completion/arch-chroot.bash b/completion/arch-chroot.bash index 707208a..583bd8f 100644 --- a/completion/arch-chroot.bash +++ b/completion/arch-chroot.bash @@ -2,7 +2,7 @@ _arch_chroot() { compopt +o dirnames local cur prev opts i _init_completion -n : || return - opts="-u -h" + opts="-N -u -h" for i in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do if [[ -d ${i} ]]; then diff --git a/doc/arch-chroot.8.asciidoc b/doc/arch-chroot.8.asciidoc index 5586a46..11fa36d 100644 --- a/doc/arch-chroot.8.asciidoc +++ b/doc/arch-chroot.8.asciidoc @@ -32,6 +32,11 @@ i.e.: Options ------- +*-N*:: + Run in unshare mode. This will use linkman:unshare[1] to create a new + mount and user namespace, allowing regular users to create new system + installations. + *-u [:group]*:: Specify non-root user and optional group to use.