diff --git a/AppImage/scripts/managed_installs.py b/AppImage/scripts/managed_installs.py index 1e5f7567..75886fa7 100644 --- a/AppImage/scripts/managed_installs.py +++ b/AppImage/scripts/managed_installs.py @@ -615,7 +615,20 @@ _helpers_cache_lock = threading.RLock() _helpers_cache: Optional[dict] = None _helpers_cache_ts: float = 0.0 -_UPDATE_SLUG_RE = re.compile(r"ct/([a-z0-9._-]+)\.sh") +_HELPER_SLUG_VALUE = r"[a-z0-9][a-z0-9._-]*" +_UPDATE_SLUG_RE = re.compile(rf"ct/({_HELPER_SLUG_VALUE})\.sh") +_SCRIPT_SLUG_RE = re.compile( + rf"^\s*(?:export\s+)?SCRIPT_SLUG\s*=\s*" + rf'(?:"({_HELPER_SLUG_VALUE})"|\'({_HELPER_SLUG_VALUE})\'|({_HELPER_SLUG_VALUE}))' + rf"\s*(?:#.*)?$", + re.MULTILINE, +) +_UPDATE_SCRIPT_NAME_RE = re.compile( + rf"^\s*(?:export\s+)?UPDATE_SCRIPT_NAME\s*=\s*" + rf'(?:"({_HELPER_SLUG_VALUE})"|\'({_HELPER_SLUG_VALUE})\'|({_HELPER_SLUG_VALUE}))' + rf"\s*(?:#.*)?$", + re.MULTILINE, +) _BASE_OS_HELPER_SLUGS = frozenset({ "alpine", "archlinux", "archlinux-vm", "debian", "fedora", "gentoo", "opensuse", "ubuntu", @@ -690,15 +703,29 @@ def _fetch_helpers_cache() -> dict: return _helpers_cache -def _probe_helper_scripts_slug(vmid: str) -> Optional[str]: - """Return the community-scripts app slug for a CT by extracting the - ``ct/.sh`` reference embedded in ``/usr/bin/update``. +def _extract_helper_slug_from_update_wrapper(content: str) -> Optional[str]: + """Extract a static app slug from a Helper-Scripts update wrapper. - The community-scripts installers write ``/usr/bin/update`` as a - single line: ``bash -c "$(curl -fsSL …/ct/.sh)"``. Parsing - that URL gives us both the app identity AND a stable key into - :func:`_fetch_helpers_cache`. Returns None when the file is - missing, unreadable, or doesn't match the expected pattern. + Historical wrappers contain a literal ``ct/.sh`` URL. Current + wrappers are regenerated after successful updates and declare + ``SCRIPT_SLUG`` / ``UPDATE_SCRIPT_NAME`` before constructing that URL + with shell variables. Only a plain, tightly constrained assignment is + accepted; the wrapper is never evaluated or sourced. + """ + for pattern in (_SCRIPT_SLUG_RE, _UPDATE_SCRIPT_NAME_RE): + match = pattern.search(content or "") + if match: + return next((value for value in match.groups() if value), None) + match = _UPDATE_SLUG_RE.search(content or "") + return match.group(1) if match else None + + +def _probe_helper_scripts_slug(vmid: str) -> Optional[str]: + """Return the Helper-Scripts app slug declared by ``/usr/bin/update``. + + Supports both the historical literal URL and the current generated + entrypoint format. Returns None when the file is missing, unreadable, + or contains no safe static application slug. """ try: r = subprocess.run( @@ -708,8 +735,7 @@ def _probe_helper_scripts_slug(vmid: str) -> Optional[str]: ) if r.returncode != 0: return None - m = _UPDATE_SLUG_RE.search(r.stdout) - return m.group(1) if m else None + return _extract_helper_slug_from_update_wrapper(r.stdout) except (subprocess.TimeoutExpired, FileNotFoundError, OSError): return None diff --git a/scripts/lxc/apply_updates.sh b/scripts/lxc/apply_updates.sh index a952953c..1a0ad991 100755 --- a/scripts/lxc/apply_updates.sh +++ b/scripts/lxc/apply_updates.sh @@ -11,7 +11,7 @@ # BACKUP_STORAGE — PVE storage name for vzdump (required when BACKUP=1) # RESTART — "1" to `pct reboot` after update, "0" to skip # RUN_HELPER — "1" to run the verified community-scripts -# updater referenced by /usr/bin/update, "0" +# updater declared by /usr/bin/update, "0" # to leave it alone. Never inferred from names. # UPDATE_COMMAND — optional user-defined bash string. When set # and TARGET is "app" or "both", the script @@ -82,6 +82,36 @@ if [[ "$UPDATE_DOCKER_ENGINE" != "0" && "$UPDATE_DOCKER_ENGINE" != "1" ]]; then exit 5 fi +# Resolve only static, constrained metadata from a Proxmox VE +# Helper-Scripts update entrypoint. Historical wrappers contain a literal +# ct/.sh URL; current wrappers are regenerated after updates and +# declare SCRIPT_SLUG / UPDATE_SCRIPT_NAME before composing the URL. +# Never source or evaluate the wrapper: its contents belong to the CT. +extract_helper_slug_from_wrapper() { + local wrapper="$1" + local key slug + + for key in SCRIPT_SLUG UPDATE_SCRIPT_NAME; do + slug=$(printf '%s\n' "$wrapper" | sed -nE \ + "s/^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=[[:space:]]*[\"']?([a-z0-9][a-z0-9._-]*)[\"']?[[:space:]]*(#.*)?$/\\2/p" \ + | head -n 1) + if [[ "$slug" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then + printf '%s\n' "$slug" + return 0 + fi + done + + slug=$(printf '%s\n' "$wrapper" \ + | grep -oE 'https?://[^"'"'"' ]+ct/[a-z0-9][a-z0-9._-]*\.sh' \ + | head -n 1 \ + | sed -nE 's|.*/ct/([a-z0-9][a-z0-9._-]*)\.sh$|\1|p') + if [[ "$slug" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then + printf '%s\n' "$slug" + return 0 + fi + return 1 +} + # One update per CT at a time, regardless of whether it came from the # UI or the scheduler. The descriptor remains open for this process. LOCK_DIR="${PROXMENUX_LOCK_DIR:-/run/lock}" @@ -197,8 +227,9 @@ fi # 6) Application update. Explicit methods only: # a) RUN_HELPER=1 + a valid /usr/bin/update wrapper → -# parses the ct/.sh URL from the wrapper, canonicalises it -# to the official repository, then runs the current helper +# parses its static slug (legacy URL or current generated +# SCRIPT_SLUG format), canonicalises it to the official repository, +# then runs the current helper # inside the CT with PHS_SILENT=1. # b) UPDATE_COMMAND set → run it verbatim via `sh -c` # inside the CT. The one intentional shell-exec-with-variable @@ -222,11 +253,12 @@ if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then echo fi if [[ "$RUN_HELPER" == "1" ]]; then + UPDATE_WRAPPER="" UPDATE_URL="" RESOLVED_SLUG="" if pct exec "$VMID" -- test -f /usr/bin/update 2>/dev/null; then - UPDATE_URL=$(pct exec "$VMID" -- cat /usr/bin/update 2>/dev/null | grep -oE 'https?://[^"'"'"' ]+ct/[a-zA-Z0-9._-]+\.sh' | head -1) - RESOLVED_SLUG=$(echo "$UPDATE_URL" | sed -nE 's|.*/ct/([a-zA-Z0-9._-]+)\.sh$|\1|p') + UPDATE_WRAPPER=$(pct exec "$VMID" -- cat /usr/bin/update 2>/dev/null) + RESOLVED_SLUG=$(extract_helper_slug_from_wrapper "$UPDATE_WRAPPER") fi case "$RESOLVED_SLUG" in alpine|archlinux|archlinux-vm|debian|fedora|gentoo|opensuse|ubuntu) @@ -237,7 +269,7 @@ if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then esac if [[ -z "$RESOLVED_SLUG" ]]; then if [[ "$APP_FAILED" -eq 0 ]]; then - echo "ERROR: RUN_HELPER=1 but /usr/bin/update contains no valid community-scripts app reference." >&2 + echo "ERROR: RUN_HELPER=1 but /usr/bin/update contains no valid Proxmox VE Helper-Scripts app slug." >&2 APP_FAILED=1 fi else