#!/bin/bash # ----------------------------------------------------------------------------- # # Copyright (c) 2014..2018 bwLehrpool-Projektteam # # This program/file is free software distributed under the GPL version 2. # See https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html # # If you have any feedback please consult https://bwlehrpool.de and # send your feedback to bwlehrpool@hs-offenburg.de. # # General information about bwLehrpool can be found at https://bwlehrpool.de # # ----------------------------------------------------------------------------- # # Common functions for chrooting # # ----------------------------------------------------------------------------- declare -rg CHROOT_TEMPDIR="${ROOT_DIR}/tmp/chroot" declare -rg CHROOT_MOUNTDIR="${CHROOT_TEMPDIR}/rootmount" declare -rg CHROOT_BINDDIR="${CHROOT_TEMPDIR}/rootbind" declare -rg CHROOT_LOWERDIR="/" declare -rg CHROOT_BINDMOUNTS="/dev /proc /sys /run" # Helper for the helper to detect overlay filesystem name chroot_detect_overlayfs() { declare -g OVERLAYFS_FSTYPES="$(grep -oE '\boverlay(fs)?$' /proc/filesystems)" if [ "$(<<< $OVERLAYFS_FSTYPES wc -w)" -ge 1 ]; then # more than one overlayfs support in /proc/filesystems # either one should work, will try them all later pdebug "Found overlayfs types: $OVERLAYFS_FSTYPES" readonly OVERLAYFS_FSTYPES return 0 fi unset OVERLAYFS_FSTYPES return 1 } # Helper to make sure we can use overlayfs chroot_init_overlayfs() { # check if we already support it chroot_detect_overlayfs && return 0 # nothing found, try to load kernel module pwarning "No overlayfs found in /proc/filesystems, trying to load module..." for NAME in overlay overlayfs; do if modprobe "${NAME}"; then chroot_detect_overlayfs && break fi done if [ -z "$OVERLAYFS_FSTYPES" ]; then perror "Could not initialize overlayfs!" fi } # Helper function to setup the directory structure chroot_prepare_dirs() { # first check if CHROOT_TEMPDIR exists if [ -d "${CHROOT_TEMPDIR}" ]; then # try to umount and rmdir CHROOT_MOUNTDIR umount "${CHROOT_MOUNTDIR}" 2>/dev/null if [ -d "${CHROOT_MOUNTDIR}" ]; then rmdir "${CHROOT_MOUNTDIR}" || perror "Could not remove CHROOT_MOUNTDIR '${CHROOT_MOUNTDIR}', meaning it has stuff in it. Aborting..." fi # try to umount and rmdir CHROOT_BINDDIR umount "${CHROOT_BINDDIR}" 2>/dev/null if [ -d "${CHROOT_BINDDIR}" ]; then rmdir "${CHROOT_BINDDIR}" || perror "Could not remove CHROOT_BINDDIR '${CHROOT_BINDDIR}', meaning it has stuff in it. Aborting..." fi # try to rmdir CHROOT_TEMPDIR if [ -d "${CHROOT_TEMPDIR}" ]; then rmdir "${CHROOT_TEMPDIR}" || perror "Could not remove CHROOT_TEMPDIR '${CHROOT_TEMPDIR}', meaning it has stuff in it. Aborting..." fi fi mkdir -p "${CHROOT_TEMPDIR}" || perror "Could not create base directory for mount directories $CHROOT_TEMPDIR." for DIR in "${CHROOT_BINDDIR}" "${CHROOT_MOUNTDIR}"; do mkdir -p "${DIR}" || perror "Could not create directory for mount directory $DIR." done } # Helper to mount the overlay structure: # - bind mount system / to CHROOT_BINDDIR and make it read-only # - make an overlay from CHROOT_LOWERDIR CHROOT_UPPERDIR # - bind mount additional pseudo-fs (as given in CHROOT_BINDMOUNTS) chroot_prepare_mounts() { # first mount / on CHROOT_BINDDIR and remount read-only mount -o bind "${CHROOT_LOWERDIR}" "${CHROOT_BINDDIR}" \ || perror "Could not bind-mount CHROOT_LOWERDIR '$CHROOT_LOWERDIR' to CHROOT_BINDDIR '$CHROOT_BINDDIR'." mount -o remount,ro,bind "${CHROOT_BINDDIR}" || perror "Could not remount CHROOT_BINDDIR '$CHROOT_BINDDIR' read-only." # check that it really is read-only [ "x$(mount | grep -E "^/ on ${CHROOT_BINDDIR}" | grep -v '\(.*ro.*\)')" != "x" ] \ && perror "CHROOT_BINDDIR '${CHROOT_BINDDIR}' is not read-only! Aborting..." # Note: The overlay fs mount syntax seems to be changed between Ubuntu 14.04.2 and 14.04.3 (Kernel 3.13 and 3.19). Instead of # checking overlay-modinfo (which may fail if overlayfs is not incorporated as module) or kernel versions, we simply try to # mount 'old school' first and then, if that fails, the new way to mount with workdir. See differences in mount syntax below. pinfo "Now mounting overlayfs. Trying old mount syntax (up to Kernel 3.13) ..." for OVERLAYFS_NAME in ${OVERLAYFS_FSTYPES}; do mount -t "${OVERLAYFS_NAME}" "${OVERLAYFS_NAME}" \ -o lowerdir="${CHROOT_LOWERDIR}",upperdir="${CHROOT_UPPERDIR}" \ "${CHROOT_MOUNTDIR}" 2>/dev/null if [ $? -ne 0 ]; then pinfo "Old mount syntax failed. Trying new mount syntax (Kernel 3.19+) ..." # We have to use a overlayfs workdir which _must_ be in the same filesystem as CHROOT_UPPERDIR. So # we traverse to the directory below CHROOT_UPPERDIR and mkdir/mktemp a workdir there. In the possible # case that CHROOT_UPPERDIR is the root dir of a filesystem there's nothing we can do but exit. CHROOT_WORKDIR="$(dirname ${CHROOT_UPPERDIR})/workdir-$MODULE" mkdir -p "$CHROOT_WORKDIR" if [ -z "$CHROOT_WORKDIR" ] || ! [ -d "$CHROOT_WORKDIR" ]; then perror "Could not mkdir overlayfs workdir $CHROOT_WORKDIR for new overlayfs mount syntax." fi # Now we try to mount the overlayfs in the new fashion: lsof -n > /tmp/bbboboboooo mount -v -t "${OVERLAYFS_NAME}" "${OVERLAYFS_NAME}" \ -o lowerdir="${CHROOT_BINDDIR}",upperdir="${CHROOT_UPPERDIR}",workdir="${CHROOT_WORKDIR}" \ "${CHROOT_MOUNTDIR}" \ || perror "Could not mount (overlayfs) $CHROOT_BINDDIR, $CHROOT_UPPERDIR, ${CHROOT_WORKDIR} to $CHROOT_MOUNTDIR." pinfo "New overlayfs mount syntax has worked, commencing." else pinfo "Old overlayfs mount syntax has worked, commencing." fi done # mount pseudo-filesystems for DIR in $CHROOT_BINDMOUNTS; do mount -o bind "${DIR}" "${CHROOT_MOUNTDIR}/${DIR}" \ || perror "Could not bind mount '$DIR' into CHROOT_MOUNTDIR/DIR '$CHROOT_MOUNTDIR/$DIR'." done } # Helper to generate the mighty autoexec.bat chroot_gen_autoexec() { # create the script to be automatically executed. cat >"${CHROOT_MOUNTDIR}/autoexec.bat"<<-EOF #!/bin/bash ####################################################### # # # Warning! # # # # This file is only meant to be executed within # # the specially chrooted mltk building environment. # # # # Do NOT execute it if you are not sure what you do, # # it may be very harmful if being run in a normal # # system environment! # # # ####################################################### echo "chroot started successfully." EOF # dump the piped input to it cat >> "${CHROOT_MOUNTDIR}/autoexec.bat" # make it executable chmod +x "${CHROOT_MOUNTDIR}/autoexec.bat" || perror "Failed to make '${CHROOT_MOUNTDIR}/autoexec.bat' exeutable." } chroot_handle_whiteouts() { local WHITEOUT_LIST="${CHROOT_UPPERDIR}/overlay.whiteout.list" rm -f -- "$WHITEOUT_LIST" mkdir -p "$(dirname "$WHITEOUT_LIST")" || perror "Could not create $(dirname "$WHITEOUT_LIST")" pdebug "Searching for overlayfs-whiteouts ..." for WHITEOUT in $(find "$CHROOT_UPPERDIR" \( -type c -perm 0000 \) -o -lname "(overlay-whiteout)"); do pdebug "Whiteout found: $WHITEOUT" echo "/./${WHITEOUT#$CHROOT_UPPERDIR}" >> "$WHITEOUT_LIST" rm -f -- "$WHITEOUT" || perror "Could not delete whiteout $WHITEOUT!" done [ -s "$WHITEOUT_LIST" ] && pinfo "Whiteout list dumped to '${CHROOT_UPPERDIR}/overlay.whiteout.list'" } ############################################################################### # # MAIN FUNCTION # # Main function to be called from the outside # Usage: # chroot_run < # # Example: # chroot_run /tmp/chroot_build <<-EOF # echo "This will be executed inside the chroot" # EOF # # It will run: # - chroot_prepare # - chroot $CHROOT_TEMPDIR/rootmount # - executes $CHROOT_TEMPDIR/rootmount/autoexec.bat # - chroot_cleanup chroot_run() { # check args [ $# -eq 1 ] || perror "'chroot_run' requires exactly 1 parameter. Given $#. Use 'chroot_run '" local CHROOT_UPPERDIR="$1" mkdir -p "$1" # init overlayfs chroot_init_overlayfs || perror "Failed to initialize overlayfs with $?." # first prepare the dir structure chroot_prepare_dirs || perror "'chroot_prepare_dirs' failed with $?." chroot_prepare_mounts || perror "'chroot_prepare_mounts' failed with $?." # generate the code to be executed when chroot'ing chroot_gen_autoexec || perror "'chroot_gen_autoexec' failed with $?." # do the chroot exec 0>&8 # This redirection is used for debugging within a chroot chroot --userspec root:root "${CHROOT_MOUNTDIR}" /autoexec.bat local RET=$? if [ "$RET" -eq 0 ]; then pinfo "chroot executed '${CHROOT_MOUNTDIR}/autoexec.bat' succeeded." else perror "Failed to run '$CHROOT_MOUNTDIR/autoexec.bat' inside the chroot to '$CHROOT_MOUNTDIR' with error code: $RET" fi # handle whiteouts chroot_handle_whiteouts || perror "'chroot_handle_whiteouts' failed with error code: $?" # finally cleanup all the mounting stuff we did previously chroot_cleanup_mounts || perror "'chroot_cleanup' failed with $?." } ############################################################################### # # CLEANUP FUNCTIONS # # Helper to check if the given path is mounted chroot_check_mount_point() { [ "$#" -eq 1 ] || perror "'chroot_check_mount_point' called with $# arguements, only 1 accepted." local MOUNT="$1" if [ "x$(mount | grep "$(readlink -f $MOUNT)")" != "x" ]; then # still mounted pdebug "'$MOUNT' is mounted!" return 1 else pdebug "'$MOUNT' is not mounted." return 0 fi } # Helper to umount the given path chroot_umount() { [ "$#" -eq 1 ] || perror "'chroot_umount' called with $# arguments, only 1 accepted." local MOUNT="$1" # check if MOUNT is mounted if ! chroot_check_mount_point "${MOUNT}"; then # still mounted if umount "${MOUNT}"; then pdebug "Successfully umounted '${MOUNT}'." else pwarning "Could not umount '${MOUNT}'! Trying again..." # now it gets ugly for i in `seq 1 5`; do umount -l "${MOUNT}" && return 0 done perror "Could not umount '${MOUNT}' after 5 tries! This shouldn't happen. Check your scripts." fi else pdebug "'${MOUNT}' is not mounted." fi # better be safe than sorry } # Helper to cleanup the temporary mounts chroot_cleanup_mounts() { local exe FILE pid tries local DOKILL= for tries in 1 2 0; do for exe in /proc/*/exe; do pid=${exe#/proc/} pid=${pid%/exe} FILE=$(readlink -f "$exe") if [ "${FILE#$CHROOT_TEMPDIR}" != "$FILE" ]; then pwarning "Killing $FILE ($pid)" kill $DOKILL "$pid" DOKILL="-9" kill "$pid" fi done [ -z "$DOSLEEP" ] && break sleep "$tries" done if [[ "$(mount | grep -c $CHROOT_TEMPDIR)" -gt 0 ]]; then # No point in unmounting then... for tries in 1 2 3 4 5; do for DIR in $CHROOT_BINDMOUNTS; do umount "${CHROOT_MOUNTDIR}/${DIR}" done umount "${CHROOT_MOUNTDIR}" umount "${CHROOT_BINDDIR}" done for DIR in $CHROOT_BINDMOUNTS; do chroot_check_mount_point "$CHROOT_MOUNTDIR/$DIR" || perror "'$CHROOT_MOUNTDIR/$DIR' is still mounted, exiting before something bad happens..." done else pinfo "Nothing chroot-related is mounted - exiting." fi }