MarshallOfSound

#52283: fix: read asar contents through the archive's retained file handle

Merged
Created: Jul 7, 2026, 7:39:03 PM
Merged: Jul 8, 2026, 11:22:43 PM
4 comments
Target: main

Description of Change

Reads of packed asar contents re-opened the archive by path on every read, while using file offsets and per-file integrity hashes from the cached header. If the asar on disk is replaced while the app is running — enterprise MDM / updater software swapping the app bundle is the common case — the fresh open reads the new file's bytes at the old header's offsets. Depending on timing this presents as:

  • a fatal browser-process crash in asar::ValidateIntegrityOrDie (via asar::ReadFileToString reading a preload script at ReadyToCommitNavigation), or
  • a spurious ENOENT: no such file or directory, open '.../app.asar/...' preload error when the file is briefly missing mid-swap, or
  • the equivalent block-hash LOG(FATAL) in AsarFileValidator for apps that serve their UI over file:// from the asar.

The first two became much more visible after #51602 moved preload reads from main-process JS into C++: the JS read path (asar-fs-wrapper.ts) reads through the archive's retained fd and exits cleanly on mismatch, so the same race previously produced no crash reports.

This PR makes the C++ read paths behave like the JS path — read through the base::File the Archive has held open since its header was validated:

  • Archive::ReadFileAt() — positional reads on the retained handle; asar::ReadFileToString() now uses it (covers the three preload startup-data readers and nativeImage file reads).
  • Archive::DuplicateFile() — hands streaming consumers a duplicate of the retained handle; AsarURLLoader uses it for packed files instead of re-opening by path.
  • The unused Archive::path() accessor is removed so by-path opens of archive contents can't be reintroduced accidentally.

Behavior after this change: reads stay consistent with the header that was integrity-validated at open time, so an app whose asar is swapped underneath it keeps serving the version it launched with until relaunch (the pre-#51602 behavior). Note this also means packed reads from a deleted archive now succeed from the retained handle instead of returning ENOENT — intentional, as the transient-deletion window during non-atomic updates was the source of the spurious preload errors.

Security: integrity enforcement is unchanged in strength — in-place tampering is still caught, because the retained handle sees the modified bytes and the hash check fails exactly as before. A swap can no longer produce a false-positive integrity failure, and an attacker who replaces the file gains nothing since the retained handle never observes the replacement.

Verified on macOS with a fused (embeddedAsarIntegrityValidation) packaged build: swapping the asar mid-run previously crashed with the reported ValidateIntegrityOrDie stack on the next navigation and now serves the original content cleanly; removing the asar mid-run previously produced the handled ENOENT preload error and now also serves the original content; a tampered-in-place asar still fails integrity validation fatally.

Checklist

  • PR description included and stakeholders cc'd
  • npm test passes locally
  • relevant documentation is changed or added (n/a — no API change)
  • PR title follows semantic commit guidelines

Release Notes

Notes: Fixed a browser-process crash (ValidateIntegrityOrDie) and spurious preload ENOENT errors when an app's app.asar is replaced on disk (e.g. by an updater or MDM software) while the app is running.

Backports

42-x-y
In-flight
PR Number
#52294
Waiting to be merged
43-x-y
In-flight
PR Number
#52293
Waiting to be merged
44-x-y
In-flight
PR Number
#52292
Waiting to be merged

Semver Impact

Major
Breaking changes
Minor
New features
Patch
Bug fixes
None
Docs, tests, etc.

Semantic Versioning helps users understand the impact of updates:

  • Major (X.y.z): Breaking changes that may require code modifications
  • Minor (x.Y.z): New features that maintain backward compatibility
  • Patch (x.y.Z): Bug fixes that don't change the API
  • None: Changes that don't affect using facing parts of Electron