#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; # SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git # SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency # SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; # SPDX-FileType: SOURCE # SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 # SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. # SPDX-PackageName: CISS.debian.live.builder # SPDX-Security-Contact: security@coresecret.eu guard_sourcing || return "${ERR_GUARD_SRCE}" ####################################### # Prints a generic build-directory validation error without disclosing a path. # Arguments: # 1: unsafe input class # 2: quiet flag # Returns: # ERR_BUILD_PATH ####################################### build_directory_validation_error() { declare error_class="$1" quiet="${2:-false}" if [[ "${quiet}" != "true" ]]; then printf "\e[91m❌ Unsafe build-directory input rejected: %s. \e[0m\n" "${error_class}" >&2 fi return "${ERR_BUILD_PATH:-217}" } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f build_directory_validation_error ####################################### # Canonicalises an existing path or a path whose parent exists. # Arguments: # 1: candidate path # 2: output variable name # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### canonicalize_build_directory() { declare candidate="$1" output_variable="$2" basename="" parent="" resolved_path="" [[ -n "${candidate}" && "${candidate}" == /* ]] || return "${ERR_BUILD_PATH:-217}" [[ "${candidate}" == "/" ]] || candidate="${candidate%/}" [[ ! -L "${candidate}" ]] || return "${ERR_BUILD_PATH:-217}" if [[ -e "${candidate}" ]]; then resolved_path="$(realpath "${candidate}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" else basename="${candidate##*/}" parent="${candidate%/*}" [[ -n "${parent}" ]] || parent="/" [[ -n "${basename}" && "${basename}" != "." && "${basename}" != ".." && -d "${parent}" ]] || return "${ERR_BUILD_PATH:-217}" resolved_path="$(realpath "${parent}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" resolved_path="${resolved_path%/}/${basename}" fi [[ "${candidate}" == "${resolved_path}" ]] || return "${ERR_BUILD_PATH:-217}" printf -v "${output_variable}" '%s' "${resolved_path}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f canonicalize_build_directory ####################################### # Validates a build directory against the repository cleanup policy. # Globals: # VAR_TMP_SECRET # VAR_WORKDIR # Arguments: # 1: candidate path # 2: output variable name # 3: quiet flag # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### validate_build_directory_path() { declare candidate="$1" output_variable="${2:-}" quiet="${3:-false}" secret_root="" validated_path="" workdir="" declare -a rejected_paths=( "/" "/bin" "/boot" "/dev" "/etc" "/home" "/lib" "/lib64" "/media" "/mnt" "/opt" "/proc" "/root" "/run" "/sbin" "/srv" "/sys" "/tmp" "/usr" "/usr/local" "/var" "/var/lib" "/var/tmp" ) declare rejected_path="" canonicalize_build_directory "${candidate}" validated_path || { build_directory_validation_error \ "path is empty, non-absolute, non-canonical, missing its parent, or is a symlink" "${quiet}" return "${ERR_BUILD_PATH:-217}" } for rejected_path in "${rejected_paths[@]}"; do if [[ "${validated_path}" == "${rejected_path}" ]]; then build_directory_validation_error "broad or system parent directory" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi done workdir="$(realpath "${VAR_WORKDIR}" 2>/dev/null)" || { build_directory_validation_error "repository work directory cannot be resolved" "${quiet}" return "${ERR_BUILD_PATH:-217}" } secret_root="$(realpath "${VAR_TMP_SECRET}" 2>/dev/null)" || { build_directory_validation_error "secret root cannot be resolved" "${quiet}" return "${ERR_BUILD_PATH:-217}" } if [[ "${workdir}" == "${validated_path}" || "${workdir}" == "${validated_path}/"* || "${validated_path}" == "${workdir}/"* \ || "${validated_path}" == "${secret_root}" || "${validated_path}" == "${secret_root}/"* ]]; then build_directory_validation_error "path is outside the dedicated build-directory policy" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi if [[ -n "${output_variable}" ]]; then printf -v "${output_variable}" '%s' "${validated_path}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_build_directory_path ####################################### # Validates the builder-owned marker for an exact build directory. # Globals: # EUID # Arguments: # 1: candidate path # 2: quiet flag # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### validate_build_directory_marker() { declare candidate="$1" quiet="${2:-false}" directory_mode="" directory_owner="" marker="" marker_build_dir="" declare expected_marker_value="" marker_link_count="" marker_value="" mode="" owner="" validate_build_directory_path "${candidate}" marker_build_dir "${quiet}" || return "${ERR_BUILD_PATH:-217}" [[ -d "${marker_build_dir}" ]] || { build_directory_validation_error "build directory does not exist" "${quiet}" return "${ERR_BUILD_PATH:-217}" } directory_owner="$(secure_stat -c '%u' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" directory_mode="$(secure_stat -c '%a' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then build_directory_validation_error "build directory ownership or permissions are unsafe" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi marker="${marker_build_dir}/.ciss-live-builder-owned" if [[ -L "${marker}" || ! -f "${marker}" ]]; then build_directory_validation_error "builder-owned marker is missing or unsafe" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi owner="$(secure_stat -c '%u' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" mode="$(secure_stat -c '%a' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" marker_link_count="$(secure_stat -c '%h' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" marker_value="$(cat "${marker}" || exit $?; printf '.')" || return "${ERR_BUILD_PATH:-217}" marker_value="${marker_value%.}" expected_marker_value="${marker_build_dir}"$'\n' if [[ "${owner}" != "${EUID}" || "${mode}" != "400" || "${marker_link_count}" != "1" \ || "${marker_value}" != "${expected_marker_value}" ]]; then build_directory_validation_error "builder-owned marker does not match the exact directory" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_build_directory_marker ####################################### # Validates an existing exact subpath below a marker-owned build directory. # Arguments: # 1: build directory # 2: relative subpath # 3: output variable name # 4: quiet flag # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### validate_build_directory_subpath() { declare build_directory="$1" relative_path="$2" output_variable="$3" quiet="${4:-false}" declare candidate_subpath="" resolved_subpath="" validate_build_directory_marker "${build_directory}" "${quiet}" || return "${ERR_BUILD_PATH:-217}" if [[ -z "${relative_path}" || "${relative_path}" == /* || "${relative_path}" == "." || "${relative_path}" == ".." \ || "${relative_path}" == ../* || "${relative_path}" == */../* || "${relative_path}" == */.. \ || "${relative_path}" == ./* || "${relative_path}" == */./* || "${relative_path}" == */. ]]; then build_directory_validation_error "unsafe relative cleanup subpath" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi candidate_subpath="${build_directory}/${relative_path}" if [[ -L "${candidate_subpath}" || ! -e "${candidate_subpath}" ]]; then build_directory_validation_error "cleanup subpath is missing or is a symlink" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi resolved_subpath="$(realpath "${candidate_subpath}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" if [[ "${resolved_subpath}" != "${candidate_subpath}" || "${resolved_subpath}" != "${build_directory}/"* ]]; then build_directory_validation_error "cleanup subpath escapes the exact build directory" "${quiet}" return "${ERR_BUILD_PATH:-217}" fi printf -v "${output_variable}" '%s' "${resolved_subpath}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_build_directory_subpath ####################################### # Initialises a new or empty build directory and its ownership marker. # Arguments: # 1: candidate path # 2: output variable name # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### initialize_build_directory() { declare candidate="$1" output_variable="$2" directory_mode="" directory_owner="" existing_entry="" declare initialized_build_dir="" marker="" validate_build_directory_path "${candidate}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}" if [[ ! -e "${initialized_build_dir}" ]]; then mkdir -m 0700 "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" fi validate_build_directory_path "${initialized_build_dir}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}" [[ -d "${initialized_build_dir}" && ! -L "${initialized_build_dir}" ]] || return "${ERR_BUILD_PATH:-217}" directory_owner="$(secure_stat -c '%u' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" directory_mode="$(secure_stat -c '%a' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then build_directory_validation_error "build directory ownership or permissions are unsafe" return "${ERR_BUILD_PATH:-217}" fi marker="${initialized_build_dir}/.ciss-live-builder-owned" if [[ -e "${marker}" || -L "${marker}" ]]; then validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" else existing_entry="$(find "${initialized_build_dir}" -mindepth 1 -maxdepth 1 -print -quit)" || return "${ERR_BUILD_PATH:-217}" if [[ -n "${existing_entry}" ]]; then build_directory_validation_error "non-empty directory has no builder-owned marker" return "${ERR_BUILD_PATH:-217}" fi (umask 077; printf '%s\n' "${initialized_build_dir}" >| "${marker}") || return "${ERR_BUILD_PATH:-217}" chmod 0400 "${marker}" || return "${ERR_BUILD_PATH:-217}" validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" fi printf -v "${output_variable}" '%s' "${initialized_build_dir}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f initialize_build_directory ####################################### # Removes paths with a one-filesystem boundary where supported. # Arguments: # paths to remove # Returns: # rm exit status ####################################### remove_build_paths() { # shellcheck disable=SC2312 if rm --help 2>&1 | grep -q -- '--one-file-system'; then rm -rf --one-file-system -- "$@" else rm -rf -- "$@" fi } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f remove_build_paths ####################################### # Deletes all content except the ownership marker from an exact build directory. # Arguments: # 1: candidate path # Returns: # 0: on success # ERR_BUILD_PATH: on failure ####################################### clean_build_directory_contents() { declare candidate="$1" build_entry="" declare -a build_entries=() declare -i old_dotglob=0 old_failglob=0 old_nullglob=0 validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}" shopt -q dotglob && old_dotglob=1 shopt -q failglob && old_failglob=1 shopt -q nullglob && old_nullglob=1 shopt -s dotglob nullglob shopt -u failglob build_entries=("${candidate}"/*) for build_entry in "${build_entries[@]}"; do [[ "${build_entry}" == "${candidate}/.ciss-live-builder-owned" ]] && continue remove_build_paths "${build_entry}" || { if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi return "${ERR_BUILD_PATH:-217}" } done if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f clean_build_directory_contents # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh