Skip to content

ADFA-4676: Shield — verify rootfs download (size + SHA-256) before extraction#192

Merged
luisguzman-adfa merged 4 commits into
mainfrom
feature/ADFA-4676-shield-download-verify
Jul 12, 2026
Merged

ADFA-4676: Shield — verify rootfs download (size + SHA-256) before extraction#192
luisguzman-adfa merged 4 commits into
mainfrom
feature/ADFA-4676-shield-download-verify

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

Adds an app-side integrity gate so an incomplete/corrupt/stale rootfs download can never reach extraction. Previously the pipeline trusted aria2c's exit code alone; an incomplete tarball (1,128,939,201 B vs the .meta4's 1,453,144,579 B) passed and then failed mid-extraction.

Changes

  • MetalinkFile — pure parser for the .meta4 (RFC 5854) into { fileName, size, file-level SHA-256, mirrors }. Takes the <hash type="sha-256"> that is a direct child of <file>, not the per-piece hashes inside <pieces>.
  • DownloadVerifier — size check + streaming SHA-256; fail-closed.
  • Aria2Manager — after exit 0, for Metalink downloads, verify downloads/<file name> against the .meta4 size + SHA-256. On mismatch: delete the artifact (+ .aria2) and report onIntegrityFailure (new DownloadListener default method, so the other callers are unaffected); on success, remove any stale sibling tarball, then report complete.
  • InstallService — surface integrity failures with a dedicated message (install_error_verify) instead of the generic "Download Failed".
  • i18ninstall_error_verify added to values/ (en) + the 33 localized strings.xml.
  • Unit tests — MetalinkFile (incl. "does not pick a piece hash") and DownloadVerifier (OK / WRONG_SIZE / WRONG_HASH / MISSING).

Verification

./gradlew testDebugUnitTest :app:lintDebug assembleDebug (compile + lint + unit tests).

Follow-ups (same ticket)

  • Download to a temp name + atomic rename once verified.
  • One silent piece-level repair pass (aria2 --continue --check-integrity) before surfacing failure.
  • Fold the two .meta4 fetches (split + verify) into a single fetch.

Add an app-side integrity gate so an incomplete/corrupt/stale rootfs download
can never reach extraction. Previously the pipeline trusted aria2c's exit code
alone; an incomplete tarball (1,128,939,201 B vs the .meta4's 1,453,144,579 B)
passed and failed mid-extraction.

- MetalinkFile: pure parser for the .meta4 (RFC 5854) -> { fileName, size,
  file-level SHA-256, mirrors }. Ignores the per-piece <hash> entries in <pieces>.
- DownloadVerifier: size check + streaming SHA-256; fail-closed.
- Aria2Manager: after exit 0, for Metalink downloads, verify downloads/<name>
  against the .meta4 size + SHA-256. On mismatch: delete the artifact (+ .aria2)
  and report onIntegrityFailure (new DownloadListener default method); on success
  remove any stale sibling tarball, then report complete.
- InstallService: surface integrity failures with a dedicated message.
- Unit tests for MetalinkFile (incl. 'does not pick a piece hash') and
  DownloadVerifier (OK / WRONG_SIZE / WRONG_HASH / MISSING).
…a4 fetch

Make the download robust to stop-and-restart and avoid redundant work:
- Fetch the .meta4 once up front; use it for the integrity gate, the --split
  mirror count (via MetalinkSplit.splitForMirrorCount), and to reconcile what is
  already on disk. Removes the second .meta4 fetch (resolveSplitFromMetalink).
- Before profiling/downloading, inspect downloads/<file>: if it is already the
  full, SHA-256-verified artifact, skip the profiler + re-download and go
  straight to onComplete; if it is complete-but-corrupt, oversized, or a partial
  with no .aria2 control file, discard it so the run starts clean; a partial WITH
  a control file is left for aria2 --continue to resume.
- This also fixes the network profiler tripping with errorCode=13 ("file exists
  but a control file does not exist") when a completed download was left in place
  after the user stopped mid-flow.
- Extract pruneStaleSiblings() and reuse the single MetalinkFile in the post-
  download verification (no re-fetch).
Leverage the .meta4 per-piece SHA-256 hashes + aria2 --check-integrity to
recover instead of re-downloading from scratch:
- On (re)start, if downloads/<file> is the full verified artifact, reuse it and
  skip the download; if it is larger than the declared size, discard it (no
  piece map can match); otherwise (partial OR complete-but-corrupt) KEEP it and
  let the main aria2 run (--continue --allow-overwrite --check-integrity) salvage
  it, re-downloading only the damaged/missing 1 MiB pieces.
- Skip the network profiler when resuming/repairing (it lacks --allow-overwrite/
  --check-integrity and would trip errorCode=13 on a file without a control).
- The whole-file SHA-256 gate still runs after the download; if repair still
  cannot produce a byte-perfect file it deletes the artifact and reports an
  integrity failure, so a full re-download is only the last resort.

This stops throwing away a nearly-complete download (e.g. stopped at 95%) and
uses the integrity infrastructure as intended.
- Treat a user-initiated stop as a clean cancellation, not a failure. Closing
  the aria2 stream via stopDownload() made readLine() throw and hit the generic
  catch, which logged 'Native Execution Error' and fired onError -> a spurious
  'Download Failed'. Now, when isCancelled, route to a new no-op
  DownloadListener.onCancelled() (default method) instead: no ERROR log, no
  failure banner, no false analytics event. No new user-facing string.
- Use the .aria2 control file as the 'in progress' signal in the on-disk
  reconciliation. aria2 pre-allocates the full size and keeps its control file
  while downloading, so length alone cannot distinguish 'complete' from
  'in progress'. If the control file exists, resume/piece-repair without wasting
  a full-file SHA-256; only hash when there is no control file (to detect a
  genuinely complete artifact to reuse).
@luisguzman-adfa
luisguzman-adfa marked this pull request as ready for review July 12, 2026 06:00
@luisguzman-adfa

Copy link
Copy Markdown
Collaborator Author

On-device validation (forced stop/restart stress test), both tiers:

  • Standard (1,453,144,579 B) and Basic (1,243,916,344 B): repeatedly stopped mid-download; each restart resumed from the last position (e.g. 30% → 37% → 66% → 77% → 94% → 100%), never from zero, via the .aria2 control file + --check-integrity.
  • Each stop logs Download cancelled by user. — no more Native Execution Error / InterruptedIOException.
  • Each restart logs Interrupted download present (…); resuming via --check-integrity. (no redundant full-file hash while a control file is present).
  • Finished with Integrity verified: … sha-256 OK → extraction started.

Note: the Checksum error detected lines during each resume are aria2's own piece check identifying not-yet-downloaded pieces before fetching them (native log), not an app error.

@luisguzman-adfa
luisguzman-adfa merged commit d5fad02 into main Jul 12, 2026
1 check passed
@luisguzman-adfa
luisguzman-adfa deleted the feature/ADFA-4676-shield-download-verify branch July 12, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant