Skip to content

fix(updater): harden DMG downloads#787

Closed
ryan-mt wants to merge 2 commits into
ilysenko:mainfrom
ryan-mt:codex/fix-updater-dmg-io
Closed

fix(updater): harden DMG downloads#787
ryan-mt wants to merge 2 commits into
ilysenko:mainfrom
ryan-mt:codex/fix-updater-dmg-io

Conversation

@ryan-mt

@ryan-mt ryan-mt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • download upstream DMGs into a unique same-directory temporary file and atomically replace the cache only after flush and sync
  • preserve an existing valid DMG when HTTP status, streaming, or filesystem operations fail
  • bound upstream connect and stalled-read operations without imposing a total large-download deadline
  • hash cached DMGs incrementally instead of loading the full image into memory

Root cause

The updater created Codex.dmg before the GET request succeeded. That truncated a valid cache and allowed failed transfers to leave partial data at the recorded path. The wrapper-update path also read the entire DMG into memory to derive its package version.

Validation

  • cargo fmt --check
  • cargo check -p codex-update-manager
  • cargo test -p codex-update-manager upstream — 7 passed
  • cargo test -p codex-update-manager wrapper_apply::tests::derives_package_version_with_streamed_dmg_hash — 1 passed
  • updater suite excluding three independently reproduced upstream/main feature-picker race failures — 217 passed
  • clean upstream/main baseline reproduced the same three feature-picker failures (214 passed, 3 failed)
  • git diff --check

Commit synced downloads atomically so failed transfers preserve a valid cache. Bound stalled upstream I/O and stream cached-image hashing to avoid whole-file allocations.
@ryan-mt
ryan-mt marked this pull request as ready for review July 10, 2026 20:30
Copilot AI review requested due to automatic review settings July 10, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This pull request hardens the updater’s upstream DMG download and wrapper-update flows to avoid corrupting an existing cached Codex.dmg on failed transfers, while also reducing memory usage when hashing DMGs to derive candidate package versions.

Changes:

  • Switches DMG downloading to write into a unique temporary file in the destination directory and atomically rename into place only after flush + sync_all, preserving any existing cache on failures.
  • Centralizes reqwest client construction in upstream::http_client() and applies connect/stalled-read timeouts consistently across upstream requests.
  • Streams DMG hashing for wrapper package version derivation (instead of reading the full DMG into memory) and adds test coverage for the streamed hashing behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
updater/src/wrapper_apply.rs Uses streamed hashing to derive wrapper package versions from DMGs and adds a unit test for the new hashing approach.
updater/src/upstream.rs Implements temp-file + atomic replace DMG downloads, adds shared HTTP client factory with timeouts, and expands download failure-mode tests.
updater/src/app.rs Switches upstream-check cycle to use the shared upstream HTTP client configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +508 to +510
let file = fs::File::open(dmg_path)
.with_context(|| format!("Failed to read {}", dmg_path.display()))?;
let mut reader = BufReader::new(file);
@Leay15
Leay15 self-requested a review July 10, 2026 22:05

@Yo-DDV Yo-DDV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for hardening the updater path. I checked current head c1af3bf7.

I ran:

  • cargo fmt --check
  • cargo check -p codex-update-manager
  • cargo test -p codex-update-manager upstream
  • cargo test -p codex-update-manager wrapper_apply::tests::derives_package_version_with_streamed_dmg_hash
  • git diff --check origin/main...HEAD

CI is green. The atomic download/cache behavior and streamed hash path look aligned from my side. Final review and merge stay with @ilysenko.

@Leay15

Leay15 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

@ryan-mt Thank you for these hardening changes!
I reviewed the runtime behavior, and everything looks good.
Could you please bump the codex-update-manager crate to the next patch version, 0.9.1?

@ilysenko

Copy link
Copy Markdown
Owner

Thanks for the update. I found two additional blockers:

  • download_dmg() publishes every download to the shared downloads/Codex.dmg, but the wrapper-apply path does not use the daemon check lock. Concurrent downloads can overwrite each other and leave a caller using SHA/version metadata that no longer matches the file at the returned path. Please serialize publication/use across both paths or publish immutable content-addressed files.
  • The temporary file is synced before rename, but the parent directory is not synced afterward. Please make the completed publication crash-durable before reporting success and persisting updater state.

@avifenesh avifenesh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(withdrawn — duplicate of feedback already given by other reviewers)

@avifenesh
avifenesh dismissed their stale review July 11, 2026 09:20

Withdrawing — duplicates the owner's existing review.

@ilysenko

Copy link
Copy Markdown
Owner

Thank you for identifying and implementing the updater download-hardening direction. We incorporated that work into #834 with the outstanding review concerns addressed: downloads are published to immutable SHA-256-addressed paths so daemon and wrapper flows cannot replace each other's input, the completed file and parent directory are synced, and hashing is streamed in both the updater and acceptance engine. I am closing this PR as superseded by #834.

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.

6 participants