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>
108 lines
5.0 KiB
Bash
Executable File
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
|
|
}
|