Skip to content

vm: validate cached disk images before reuse (docker-vm, truenas-vm) - #16243

Open
Ali-Shaikh wants to merge 2 commits into
community-scripts:mainfrom
Ali-Shaikh:fix/vm-validate-cached-images
Open

vm: validate cached disk images before reuse (docker-vm, truenas-vm)#16243
Ali-Shaikh wants to merge 2 commits into
community-scripts:mainfrom
Ali-Shaikh:fix/vm-validate-cached-images

Conversation

@Ali-Shaikh

@Ali-Shaikh Ali-Shaikh commented Aug 3, 2026

Copy link
Copy Markdown

✍️ Description

vm/docker-vm.sh and vm/truenas-vm.sh treat any non-empty file in the image cache as a completed download:

CACHE_FILE="$CACHE_DIR/$(basename "$URL")"
if [[ ! -s "$CACHE_FILE" ]]; then
  curl -f#SL -o "$CACHE_FILE" "$URL"

curl writes as it streams, so an interrupted transfer leaves a partial image at the cache path. It is non-empty, so -s is satisfied and every later run reports "Using cached image" and builds a VM from a truncated disk. Nothing ever re-downloads it.

Interrupting a Debian cloud image download after three seconds:

after interrupt: cache file exists=yes, size=1240002
RESULT: old guard says 'Using cached image' -> TRUNCATED IMAGE REUSED

1.2 MB standing in for a ~350 MB image, cached permanently.

Two related gaps: neither script verifies the image it imports, and because the vendor URLs are latest/current the basename never changes, so a security-updated upstream image is never picked up.

Fix

haos-vm.sh and umbrel-os-vm.sh already solve this with download_and_validate_xz() — validate the cached file, delete and re-download when bad. This applies the same pattern to the two scripts that lack it:

  • download to "$CACHE_FILE.part", rename only after validation, so a partial transfer is never cached
  • verify against the checksum the vendor publishes next to the image
  • validate the cached file on reuse, deleting it on mismatch
Script Source Checksum
docker-vm.sh cloud.debian.org SHA512SUMS in the image directory
docker-vm.sh cloud-images.ubuntu.com SHA256SUMS in the image directory
truenas-vm.sh download.truenas.com .sha256 sidecar next to the ISO

Verification degrades gracefully

This cannot break VM creation if a vendor reorganises their mirror. An unreachable checksum file, a changed layout, or an image absent from the list all proceed exactly as before. Only a definite hash mismatch aborts.

Testing done

Checksum logic exercised against live vendor endpoints:

corrupted image vs real Debian checksum -> REJECTED (rc=1)
corrupted image vs real Ubuntu checksum -> REJECTED (rc=1)
unknown vendor                          -> ACCEPTED (no checksums published)
known vendor, sums 404                  -> ACCEPTED (degrades)
file absent from sums list              -> ACCEPTED (degrades)

Atomic download, A/B against current main:

old: interrupted transfer -> 1.2 MB cached and reused
new: interrupted transfer -> nothing cached -> next run re-downloads cleanly

Checksum URL derivation verified for Debian generic/nocloud and Ubuntu current; live hash extraction returns a 128-char SHA512 for Debian and 64-char SHA256 for Ubuntu, covering both the Debian <hash> <file> and Ubuntu <hash> *<file> formats. bash -n and shellcheck -S warning clean apart from pre-existing findings (SC1090 on the source <(curl ...) lines, the UTF-8 BOM in truenas-vm.sh).

Tested end-to-end on a Proxmox VE node, covering all three paths:

  1. Clean download with an empty cache — image downloaded, verified against SHA512SUMS, VM built normally.

  2. Cached reuse — second run verified the existing cached image and reported Using cached image, no needless re-download.

  3. Corrupted cache — cached image truncated to 50 MB, then re-run. Detected and re-downloaded:

    Cached image debian-13-generic-amd64.qcow2 failed checksum validation. Deleting and retrying download...
    

    On current main that same truncated file is silently used to build the VM.

🔗 Related Issue

N/A — found while reading the VM scripts, no existing issue.

✅ Prerequisites (X in brackets)

  • Self-review completed – Code follows project standards.
  • Tested thoroughly – Changes work as expected.
  • No security risks – No hardcoded secrets, unnecessary privilege escalations, or permission issues.

🤖 AI Assistance (X in brackets)

  • No AI used – Scripts were written without AI assistance.
  • AI was used – I confirm the scripts were built using AGENTS.md and .github/agents/pve-script-creator.agent.md as guidance, and the output has been reviewed and corrected to match those guidelines.

🛠️ Type of Change (X in brackets)

  • 🐞 Bug fix – Resolves an issue without breaking functionality.
  • New feature – Adds new, non-breaking functionality.
  • 💥 Breaking change – Alters existing functionality in a way that may require updates.
  • 🆕 New script – A fully functional and tested script or script set.
  • 🌍 Website update – Changes to script metadata (PocketBase/website data).
  • 🔧 Refactoring / Code Cleanup – Improves readability or maintainability without changing functionality.
  • 📝 Documentation update – Changes to README, AppName.md, CONTRIBUTING.md, or other docs.

Both scripts treat any non-empty file in the image cache as a good
download:

    if [[ ! -s "$CACHE_FILE" ]]; then
      curl -f#SL -o "$CACHE_FILE" "$URL"

curl writes as it streams, so an interrupted transfer leaves a partial
image at the cache path. It is non-empty, so every later run reports
"Using cached image" and builds a VM from a truncated disk. Interrupting
a Debian cloud image download after three seconds leaves 1.2 MB standing
in for a ~350 MB image, and nothing ever re-downloads it.

Neither script verifies the image either, and because the vendor URLs
are "latest"/"current" the basename never changes, so a security-updated
upstream image is never picked up.

Fixed by following the pattern download_and_validate_xz() already uses in
haos-vm.sh and umbrel-os-vm.sh - validate the cached file, delete and
retry when it is bad:

  - download to "$CACHE_FILE.part" and rename only after validation, so a
    partial transfer is never cached
  - verify against the checksum the vendor publishes next to the image:
    SHA512SUMS (Debian), SHA256SUMS (Ubuntu), .sha256 sidecar (TrueNAS)
  - validate the cached file on reuse, deleting it on mismatch

Verification degrades gracefully by design: an unreachable checksum file,
a changed vendor layout, or an image absent from the list all proceed as
before. Only a definite mismatch aborts, so this cannot break VM creation
if a vendor reorganises their mirror.
@Ali-Shaikh
Ali-Shaikh requested a review from a team as a code owner August 3, 2026 19:46
@github-actions github-actions Bot added bugfix update script A change that updates a script vm labels Aug 3, 2026

@asylumexp asylumexp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be a func inside vm-core and not defined in each vm script

Per review, the checksum helpers belong in the shared library rather than
being defined in each VM script.

misc/vm-core.func gains three functions:

  get_checksum_source       - maps an image URL to the checksum the vendor
                              publishes, and its format
  verify_image_checksum     - best-effort verification, mismatch only fails
  download_and_validate_image - cached-copy validation plus an atomic
                              download through a .part file

docker-vm.sh now calls download_and_validate_image and carries no
validation logic of its own.

truenas-vm.sh is reverted to its current state in this PR. It sources only
api.func, and vm-core.func applies "set -euo pipefail" at source time,
which truenas-vm.sh is not written for - it runs under plain "set -e" and
has pipelines and optional variables that "set -u" and "pipefail" would
change the behaviour of. Wiring it up is a larger change than this fix
warrants, so it is left for a follow-up.

The TrueNAS sidecar format is kept in get_checksum_source so that
follow-up is a one-line call.
@github-actions github-actions Bot added the core label Aug 5, 2026
@Ali-Shaikh

Copy link
Copy Markdown
Author

Thanks — moved into vm-core.func as get_checksum_source, verify_image_checksum and download_and_validate_image. docker-vm.sh now just calls download_and_validate_image "$URL" "$CACHE_FILE" and carries no validation logic of its own.

One judgement call I'd like your view on: I've dropped truenas-vm.sh from this PR rather than wiring it to the shared function.

It sources only api.func, and vm-core.func applies set -euo pipefail at source time (line 4, ahead of the _CORE_FUNC_LOADED guard). truenas-vm.sh runs under plain set -e and has things set -u and pipefail would change — optional $VLAN/$MTU, and the curl | grep | sed chain in truenas_iso_lookup. Sourcing the library into it is a bigger and riskier change than this bug fix warrants, and not one I can test well.

So this PR now fixes docker-vm.sh only. The truncated-cache bug is still live in truenas-vm.sh. Happy to do either as a follow-up, whichever you prefer:

  • migrate truenas-vm.sh (and haos-vm.sh / umbrel-os-vm.sh, which duplicate download_and_validate_xz between them) onto vm-core.func properly, or
  • leave them until they're modernised anyway

I've kept the TrueNAS sidecar format in get_checksum_source so that follow-up is a one-line call.

Re-tested after the move — mismatch detection against live Debian, Ubuntu and TrueNAS checksum endpoints, and the four degradation paths (unknown vendor, checksum 404, image absent from listing, sidecar 404) all still pass through untouched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix core update script A change that updates a script vm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants