Skip to content

AsarSharp: GetFileIntegrity assumes full-block reads, producing invalid integrity.blocks on short reads #170

Description

@divya0795

Summary

IntegrityHelper.GetFileIntegrity assumes every Stream.Read returns a full block. A short read produces an undersized entry in integrity.blocks and shifts every boundary after it, yielding metadata that fails Electron's validateAsarIntegrity.

Observed on master @ 643c8f8 (1.0.9.4).

Root cause

AsarSharp/Integrity/IntegrityHelper.cs:63-67

while ((bytesRead = fileStream.Read(reusableBuffer, 0, reusableBuffer.Length)) > 0)
{
    blockHashes.Add(ToLowerHex(blockHash.ComputeHash(reusableBuffer, 0, bytesRead)));
    fileHash.AppendData(reusableBuffer, 0, bytesRead);
}

Stream.Read is contractually allowed to return fewer bytes than requested at any point, not just at end of file. The loop hashes whatever each call returned as though it were a complete block, so:

  • a short read becomes an undersized blocks[] entry, and
  • every subsequent block starts at the wrong offset.

Electron requires every block except the last to be exactly blockSize, so the emitted integrity fails validation. The whole-file hash is unaffected — AppendData is order-correct regardless of chunking — so only per-block validation breaks.

Reachability

GetFileIntegrity is live code, not a dead helper. AsarSharp/AsarFileSystem/FileSystem.cs calls it as the fallback whenever no precomputed integrity is supplied:

// :175 (unpacked branch)  and  :184 (packed branch)
node.Integrity = precomputedIntegrity ?? IntegrityHelper.GetFileIntegrity(path);

Note the StreamingHasher class in the same file (:79-141) already implements the correct accumulate-then-emit rule, and it is what the main WriteFileSystem / CopyAndHash path uses. That is why packing has not visibly broken — the defect is confined to callers that reach GetFileIntegrity directly.

Trigger

Any input whose 4 MiB read returns short mid-file. In practice this is uncommon for ordinary local files, which is why it has gone unnoticed, but it is permitted by the API and becomes likelier with large multi-GB files, network/virtualised paths, or a non-plain-file stream. When it happens the corruption is certain rather than probabilistic.

Verification

I simulated the read/boundary arithmetic against Electron's rule (block size scaled down; identical arithmetic):

scenario                 expected    master    fixed   verdict
short read mid-file             5         6        5   master diverges
every read short (1 byte)       5        40        5   master diverges
ragged reads                    5         6        5   master diverges
all reads full                 32        32       32   both correct
exact multiple of block         4         4        4   both correct
single sub-block file           1         1        1   both correct
empty file                      0         0        0   both correct

master and the fix agree whenever every read is full — which is the normal case, and why this is latent rather than constantly visible. They diverge on every short-read scenario. The whole-file hash is preserved in all cases.

Suggested fix

Accumulate reads into the block buffer and emit a hash only once a full block is present, flushing the remainder after the loop — i.e. the rule StreamingHasher.Append/Finalise already applies. A fix is attached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions