librelad 928e244696 fix(config): stop sourcing files in unmarked configs/ subdirectories
sourceScanFiles sourced every file two levels deep under configs/, and
sourcing means executing. A directory used there as ordinary storage
therefore turned its contents into a script.

storageIndexSet caches an app -> root TSV at configs/storage/app_locations,
with no .category marker alongside it. Every line is `<slug><TAB><path>`,
which bash reads as a command and its argument. That stayed invisible while
no slug matched a real executable — and became a fork bomb the moment the
index recorded the app named `libreportal`, because that IS the CLI on PATH:
sourcing ran `libreportal /libreportal-containers`, which re-entered the same
scan, which sourced the file again, one process pair per level until the host
died of OOM. Every CLI invocation on the box detonated it, the task
processor's own poll included, so the machine black-screened out of memory
minutes after each boot.

Files in a SUBDIRECTORY are now sourced only when that directory carries
.category — the contract commandReloadConfigs already enforces in the CLI
wrapper, and one every real config category (webui, general, security,
backup, network) already satisfies. Files directly in configs/ are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:03:02 +01:00

108 lines
5.0 KiB
Bash
Executable File

#!/bin/bash
sourceScanFiles()
{
local load_type="$1"
local file_pattern
# Specific LibrePortal config files
if [ "$load_type" = "libreportal_configs" ]; then
# Load new subdirectory config files only (no more old structure fallback)
local folder_dir="$configs_dir"
# Load new subdirectory config files with exclusions
while IFS= read -r -d '' file; do
if [ -f "$file" ]; then
local filename=$(basename "$file")
local should_load=true
# Skip .category files and excluded files
if [[ "$file" =~ \.category$ ]] || [[ "$filename" == "app_categories" ]]; then
should_load=false
fi
# A file in a SUBDIRECTORY is only sourced when that directory is
# a declared config category (carries .category) — the same
# contract commandReloadConfigs enforces in the CLI wrapper.
#
# Sourcing means EXECUTING, so an unmarked directory used as
# ordinary storage turned its contents into a script. That is not
# hypothetical: storageIndexSet caches an app -> root TSV at
# configs/storage/app_locations, no .category alongside it. Every
# line there is `<slug><TAB><path>`, which bash reads as a command
# and its argument. Harmless while no slug matched a real
# executable — and a fork bomb the moment the row was for the app
# named `libreportal`, because that IS the CLI: sourcing ran
# `libreportal /libreportal-containers`, which re-entered this
# scan, which sourced the file again, one process pair per level
# until the host died of OOM. Every CLI invocation on the box,
# including the task processor's own, detonated it.
#
# Depth-1 files (directly in configs/) keep loading as before —
# only the category dirs gained a marker requirement.
if [ "$should_load" = true ]; then
local parent_dir="${file%/*}"
if [[ "$parent_dir" != "${folder_dir%/}" && ! -f "$parent_dir/.category" ]]; then
should_load=false
fi
fi
if [ "$should_load" = true ]; then
source "$file"
# echo "$load_type NEW FILE $file"
fi
fi
done < <(find "$folder_dir" -maxdepth 2 -type f ! -name "*.category" ! -name "config_*" ! -name ".*" ! -name "*.bak" -print0)
# Per-location backup configs live nested at depth 3
# (configs/backup/locations/<idx>/location.config) — source them via
# the dedicated loader so CFG_BACKUP_LOC_<idx>_* vars are populated.
if declare -f sourceBackupLocations >/dev/null 2>&1; then
sourceBackupLocations
fi
# Specific for LibrePortal app container configs
elif [ "$load_type" = "app_configs" ]; then
local file_pattern="*.config"
local folder_dir="$containers_dir"
# Specific for LibrePortal app install scripts
elif [ "$load_type" = "containers" ]; then
local file_pattern="*.sh"
local folder_dir="$install_containers_dir"
else
echo "Invalid load type: $load_type"
fi
# Scanning function for other types (not libreportal_configs).
# app_configs live under /docker/containers (owned by the docker install user
# and not list-readable by the manager), so enumerate them AS that user via
# runFileOp; the manager still sources each (the .config files are o+r). The
# 'containers' install templates are manager-owned, so a plain find suffices.
#
# Unreadable dirs are PRUNED, not descended into: an app's container-created
# data dirs are owned by the container's (sub)uid and mode 0700 — e.g.
# containers/<app>/postgres, uid 231141 under rootless — so even the docker
# install user can't list them, and find would print a "Permission denied"
# line per dir straight into the middle of every install's output. They never
# hold a LibrePortal .config, so skipping them loses nothing; pruning (rather
# than discarding stderr) keeps genuine find errors visible.
if [ "$load_type" != "libreportal_configs" ]; then
local scan_op=""
[[ "$load_type" == "app_configs" ]] && scan_op="runFileOp"
while IFS= read -r -d '' file; do
if [ -f "$file" ]; then
source "$file"
# echo "$load_type FILE $file"
fi
done < <($scan_op find "$folder_dir" -maxdepth 3 \( -type d \( -name 'resources' -o ! -readable -o ! -executable \) -prune \) -o -type f -name "$file_pattern" -print0)
fi
# Load the categories from the file into an array
if [ "$load_type" = "libreportal_configs" ]; then
if [ -f "$configs_dir/app_categories" ]; then
mapfile -t app_categories < $configs_dir/app_categories
fi
fi
}