Files
CISS.debian.live.builder/lib/lib_build_directory.sh

341 lines
13 KiB
Bash

#!/bin/bash
# SPDX-Version: 3.0
# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; <msw@coresecret.dev>
# 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.; <msw@coresecret.dev>
# 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