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 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-28 13:14:17 +01:00
parent 2d5674108b
commit b0a00649f7

View File

@ -89,13 +89,38 @@ _container_owner() {
} }
# Validate + resolve an app name to its container dir (reject traversal/odd names). # 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() { _app_dir() {
local app="$1" local app="$1"
[[ "$app" =~ ^[A-Za-z0-9._-]+$ && "$app" != "." && "$app" != ".." ]] \ [[ "$app" =~ ^[A-Za-z0-9._-]+$ && "$app" != "." && "$app" != ".." ]] \
|| { echo "libreportal-ownership: invalid app name" >&2; return 1; } || { 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; } if [[ -d "$CONTAINERS_DIR/$app" ]]; then
printf '%s' "$d" 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 # 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; } || { echo "libreportal-ownership: invalid app name" >&2; return 1; }
[[ "$app" == "libreportal" ]] \ [[ "$app" == "libreportal" ]] \
&& { echo "libreportal-ownership: refusing to remove the WebUI app dir" >&2; return 1; } && { echo "libreportal-ownership: refusing to remove the WebUI app dir" >&2; return 1; }
local d="$CONTAINERS_DIR/$app" # Through the resolver, so an app on a registered location is found rather
[[ -d "$d" ]] || return 0 # than quietly skipped.
local d
d=$(_app_dir "$app" 2>/dev/null) || return 0
rm -rf -- "$d" rm -rf -- "$d"
} }