From b0a00649f79bf6335192dcaad7ad2225faa371af Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 28 Aug 2026 13:14:17 +0100 Subject: [PATCH] ownership: find an app wherever it lives, not only under the primary root _app_dir resolved an app name to "$CONTAINERS_DIR/$app" and gave up if that did not exist. An app on a registered storage location is not there, so every root action keyed on an app name quietly did nothing for those: app-data-remove linkding -> rc=0, nothing removed which is what restoreAppStart calls at step 4, "Wiping existing app folder". So restoring an app that lives on a second disk laid the snapshot over whatever was already there instead of replacing it, and files deleted since the backup would survive a restore meant to undo their deletion. Search the primary root first, then each REGISTERED location. Candidate paths come from the root-owned registry and never from the caller, so this cannot be pointed anywhere root does not already own. app_data_remove goes through the resolver now instead of building the path itself. Found by the flow test: destroying linkding on disk1 reported success and left all nineteen files in place. Co-Authored-By: Claude Opus 5 --- scripts/system/libreportal-ownership | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/scripts/system/libreportal-ownership b/scripts/system/libreportal-ownership index 90534f9..2938bca 100644 --- a/scripts/system/libreportal-ownership +++ b/scripts/system/libreportal-ownership @@ -89,13 +89,38 @@ _container_owner() { } # Validate + resolve an app name to its container dir (reject traversal/odd names). +# +# An app does not have to be under CONTAINERS_DIR: it can live on any REGISTERED +# storage location. Looking only in the primary root meant every action keyed on +# an app name silently did nothing for those — app-data-remove returned 0 having +# removed nothing, so restore's "Wiping existing app folder" wiped nothing and a +# restore laid new data over old. +# +# Candidate roots come from the root-owned registry, never from the caller, so +# this still cannot be pointed anywhere root does not already own. _app_dir() { local app="$1" [[ "$app" =~ ^[A-Za-z0-9._-]+$ && "$app" != "." && "$app" != ".." ]] \ || { echo "libreportal-ownership: invalid app name" >&2; return 1; } - local d="$CONTAINERS_DIR/$app" - [[ -d "$d" ]] || { echo "libreportal-ownership: no such app dir: $d" >&2; return 1; } - printf '%s' "$d" + + if [[ -d "$CONTAINERS_DIR/$app" ]]; then + printf '%s' "$CONTAINERS_DIR/$app" + return 0 + fi + + if [[ -r "$STORAGE_REGISTRY" ]]; then + local _id _path _rest + while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do + [[ -z "$_path" || "$_id" == \#* ]] && continue + if [[ -d "${_path%/}/$app" ]]; then + printf '%s' "${_path%/}/$app" + return 0 + fi + done < "$STORAGE_REGISTRY" + fi + + echo "libreportal-ownership: no such app dir for '$app'" >&2 + return 1 } # Let the rootless container user reach the few system-tree files it must read as @@ -273,8 +298,10 @@ app_data_remove() { || { echo "libreportal-ownership: invalid app name" >&2; return 1; } [[ "$app" == "libreportal" ]] \ && { echo "libreportal-ownership: refusing to remove the WebUI app dir" >&2; return 1; } - local d="$CONTAINERS_DIR/$app" - [[ -d "$d" ]] || return 0 + # Through the resolver, so an app on a registered location is found rather + # than quietly skipped. + local d + d=$(_app_dir "$app" 2>/dev/null) || return 0 rm -rf -- "$d" }