LibrePortal/scripts/dev/lp-configs-guard-test
librelad ac4c11b5e9 fix(storage): move the app->location index out of configs/
Follow-up to 928e244, which stopped configs/ subdirectories being sourced
without a .category marker. That closed the hole; this removes the thing
that fell into it.

storageIndexFile pointed at configs/storage/app_locations. The file's
requirements are only "manager-owned" and "not on a removable disk" —
configs/ satisfies both, which is why I put it there, and it was still
wrong: that tree carries a third property the file violates. sourceScanFiles
SOURCES what it finds under configs/, and sourcing means executing.

The index is a TSV of "<slug><TAB><root>", which bash reads as a command
and its argument. Harmless while no slug matched a real executable. The
row for the app named `libreportal` armed it, because that IS the CLI on
PATH: sourcing ran `libreportal /libreportal-containers`, which re-entered
the scan, which sourced the file again — one process pair per level until
the host OOMed and took the desktop session with it.

It now lives at $system_dir/storage/app_locations, with a one-shot
migration so an install that already has an index keeps knowing where its
apps live rather than silently forgetting. libreportal-ownership
reconciles the new directory, and scan_files.sh gained a note that
configs/storage/ carries no .category on purpose.

scripts/dev/lp-configs-guard-test covers both ends: the index never lands
in configs/, a legacy one migrates, and a file of the exact detonating
shape placed in an unmarked configs/ subdirectory is not executed while a
marked category still loads.

Also wires sourceStorageLocations into the config scan beside
sourceBackupLocations — per-location configs sit at depth 3, below the
generic scan, and need their own walker like the backup ones do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:25:07 +01:00

45 lines
2.5 KiB
Bash
Executable File

#!/bin/bash
# Regression test for the configs/ fork bomb: a data file in a configs/
# subdirectory must never be sourced, and the index must not land there at all.
R="$(cd "$(dirname "$0")/../.." && pwd)"
B=$(mktemp -d); trap 'rm -rf "$B"' EXIT
export LP_SYSTEM_DIR="$B/sys" LP_CONTAINERS_DIR="$B/primary" LP_BACKUPS_DIR="$B/bk"
export LP_STORAGE_REGISTRY="$B/storage.roots"
mkdir -p "$B/sys/configs/general" "$B/primary" "$B/sys/configs/storage"
: > "$B/sys/configs/general/.category"
echo 'CFG_REAL_OPTION=yes' > "$B/sys/configs/general/general_test"
source "$R/scripts/source/paths.sh"
fail=0
echo "--- 1. the index no longer lives under configs/ ---"
idx=$(storageIndexFile)
echo " index path: $idx"
[[ "$idx" == "$B/sys/storage/app_locations" ]] && echo " ok outside configs/" || { echo " FAIL still in configs/"; fail=1; }
echo "--- 2. writing the index does not create configs/storage/app_locations ---"
runInstallOp(){ "$@"; }; runInstallWrite(){ cat > "$1"; }
storageIndexSet libreportal "$B/primary"
[[ -f "$B/sys/configs/storage/app_locations" ]] && { echo " FAIL wrote into configs/"; fail=1; } || echo " ok nothing written into configs/"
[[ -f "$idx" ]] && echo " ok index written to the system tree" || { echo " FAIL no index"; fail=1; }
echo "--- 3. legacy index is migrated, not abandoned ---"
rm -rf "$B/sys/storage"
mkdir -p "$B/sys/configs/storage"
printf 'bookstack\t%s\n' "$B/primary" > "$B/sys/configs/storage/app_locations"
got=$(storageIndexGet bookstack)
[[ "$got" == "$B/primary" ]] && echo " ok legacy entry still resolves ($got)" || { echo " FAIL lost entry"; fail=1; }
[[ -f "$B/sys/configs/storage/app_locations" ]] && { echo " FAIL legacy file left behind"; fail=1; } || echo " ok legacy file removed"
echo "--- 4. the actual bomb: a stray executable-looking file in configs/ ---"
# Recreate the exact shape that detonated: <slug><TAB><path> where slug is a
# real command on PATH. If the scan sources it, the marker file appears.
mkdir -p "$B/sys/configs/storage"
printf 'touch\t%s/DETONATED\n' "$B" > "$B/sys/configs/storage/app_locations"
configs_dir="$B/sys/configs/"
source "$R/scripts/source/loading/scan_files.sh"
sourceScanFiles libreportal_configs >/dev/null 2>&1
[[ -e "$B/DETONATED" ]] && { echo " FAIL the file was executed"; fail=1; } || echo " ok unmarked configs/ subdir not sourced"
[[ "${CFG_REAL_OPTION:-}" == "yes" ]] && echo " ok marked category still loads" || { echo " FAIL real config stopped loading"; fail=1; }
echo; [[ $fail -eq 0 ]] && echo "ALL PASS" || echo "FAILURES"; exit $fail