From 68613588098d3bf2c7ae6a78ec03992ada5718c7 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 19 Aug 2026 01:54:16 +0100 Subject: [PATCH] fix(init): refuse to copy the install tree onto itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copyFilesFromLocal ran `rm -rf "$script_dir"` before ever reading the source. When source and destination resolve to the same path — which is what happens if you run `sudo ./init.sh init` from inside /libreportal-system/install, an easy mistake when re-running to re-bake the footprint — the rm deletes the source too, the copy then fails, and the script exits. The damage is worse than a failed copy: it exits before initRootHelpers and before the sudoers tightening, so the install tree is gone AND the manager is left holding the install-phase grant (ALL=(ALL) NOPASSWD: ALL) instead of the scoped allowlist. I did exactly this on a live box; the tree was recoverable from git, but the loose sudo rule is the part that matters. Now: resolve both paths and refuse if they match, and validate the source before destroying the destination rather than after. Both checks run ahead of the rm. Co-Authored-By: Claude Opus 5 --- init.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/init.sh b/init.sh index 3a89f5a..8ce9345 100755 --- a/init.sh +++ b/init.sh @@ -453,7 +453,32 @@ copyFilesFromLocal() { fi isHeader "Copying from Local Directory" - + + # Refuse to copy a directory onto itself. Everything below deletes the + # destination BEFORE reading the source, so when the two resolve to the same + # path the rm wipes the source too and the copy fails with the tree already + # gone — taking the install down and stopping short of the root-helper bake + # and the sudoers tightening, leaving install-phase sudo in place. + # Running `sudo ./init.sh init` from inside the install tree is enough to hit + # it, which is an easy mistake when re-running to re-bake the footprint. + local _src_real _dst_real + _src_real="$(readlink -f "$source_dir" 2>/dev/null || echo "$source_dir")" + _dst_real="$(readlink -f "$script_dir" 2>/dev/null || echo "$script_dir")" + if [[ "$_src_real" == "$_dst_real" ]]; then + isError "Source and destination are the same directory ($_dst_real)." + isNotice "Run init.sh from your source checkout, not from inside the install tree." + exit 1 + fi + + # Validate the source BEFORE destroying the destination, for the same reason: + # a bad source used to be discovered only after the install tree was gone. + # detectLocalLibrePortal derives the directory the same way source_dir is + # derived above, so it checks exactly what is about to be copied. + if ! detectLocalLibrePortal >/dev/null 2>&1; then + isError "$source_dir is not a valid LibrePortal source — refusing to replace $script_dir." + exit 1 + fi + # Remove existing install directory sudo rm -rf "$script_dir"