Skip to content

Bad CRC-32 during install when the OS image is rebuilt on the server between opening the package and copying it #443

Description

@1A7432

Disclosure, per the Generative AI policy: the root cause below was found with the help of an AI assistant (Claude Code) while I was debugging my own failed install. I re-ran every check on my machine and the text here is written by me. If that still puts this report on the wrong side of the policy, feel free to close it — no hard feelings.

Summary

If the OS image on the server is rebuilt (same URL, new content) between the moment the installer opens the package and the moment it starts copying files out of it, the install dies with zipfile.BadZipFile: Bad CRC-32 on the first file of the ESP. By then the stub container, the EFI partition and the root partition have already been created, and the p (repair) option cannot recover from it.

This is the same traceback as #377.

Environment

  • MacBook Air (M2, 2022), j413ap
  • macOS 26.6.2 (25G83)
  • installer v0.9.1 via the asahi-alarm.org bootstrap (INSTALLER_BASE=https://asahi-alarm.org/)
  • package: https://asahi-alarm.org/asahi-desktop-btrfs.zip

What happened

Stub OS installation complete.

Adding partition EFI (524.29 MB)...
  Formatting as FAT...
Adding partition Root (146.64 GB)...
Collecting firmware...
  100.00% Installing OS...
  Copying from esp into disk0s4 partition...
\   0.00% (0 B/s)root        : ERROR    Exception caught
Traceback (most recent call last):
  ...
  File "/private/tmp/asahi-install/osinstall.py", line 161, in install
    self.extract_tree(source, mountpoint)
  File "/private/tmp/asahi-install/util.py", line 212, in fdcopy
    d = sfd.read(BLOCK)
  File ".../zipfile/__init__.py", line 1033, in _update_crc
    raise BadZipFile("Bad CRC-32 for file %r" % self.name)
zipfile.BadZipFile: Bad CRC-32 for file 'esp/m1n1/boot.bin'

Timeline (from installer.log, local time UTC+8)

Time Event
08:56 chose the OS → OS package opened (zip central directory read)
08:56 – 21:30 sat at the New OS size prompt (laptop lid closed)
19:27 asahi-desktop-btrfs.zip rebuilt on the server — Last-Modified: Mon, 14 Sep 2026 11:27:59 GMT
21:30 entered the size, stub install ran
21:32 Copying from esp into disk0s4 partition... → Bad CRC-32

Why

osinstall.load_package() wraps the URL in URLCache and hands it to zipfile.ZipFile, which reads the central directory once, up front. File contents are fetched much later through HTTP Range requests in urlcache.get_partial():

con.request("GET", path, headers={
    "Connection": "keep-alive",
    "Range": f"bytes={off}-{off+size-1}",
})
res = con.getresponse()
d = res.read()

There is no If-Range (or any validator) on the request and the status code is never checked, so once the file behind the URL changes, the ranges come back from the new file while the offsets and CRCs still belong to the old one. The first read() fails its CRC check.

Anything that stretches the gap between OS package opened and the copy makes this more likely: a lid closed at a prompt (my case), or a slow stub install (the log in #377 shows ~70 minutes between the two).

Checks

  • The rebuilt image is fine: reading every esp/* entry of the current asahi-desktop-btrfs.zip and asahi-base-btrfs.zip through the installer's own URLCache passes CRC.

  • Same headers with and without the local proxy; no redirect involved.

  • The server honours If-Range with Last-Modified (Apache):

    curl -r 0-99 -H "If-Range: Mon, 14 Sep 2026 00:00:00 GMT" …/asahi-base-btrfs.zip   → HTTP 200, Content-Length: 1379934047 (whole file)
    curl -r 0-99 -H "If-Range: <current Last-Modified>"       …/asahi-base-btrfs.zip   → HTTP 206, Content-Range: bytes 0-99/1379934047
    

After the failure

The stub container, EFI - ASAHI and the Linux Filesystem partition exist, but the ESP is half-written and the root partition is empty. action_repair_or_upgrade() only redoes the stub and step2(); it never calls osins.install() again, so p produces an entry in the boot picker that cannot boot. The only way out is to delete the three partitions by hand and start over. (Might be worth its own issue.)

Suggestions

  1. In URLCache.get_size(), remember ETag / Last-Modified from the HEAD; in get_partial(), send it as If-Range and treat any status other than 206 as "package changed on the server" — close the connection without res.read() (a 200 means the whole file is on its way) and raise an error that get_block() does not retry. Checking the total in Content-Range against the size from HEAD would cover servers that ignore If-Range.
  2. Re-validate the package (HEAD again, compare validator and Content-Length) right before the first addPartition(). That turns the common case — a long pause at a prompt — into a clean "the image changed, please restart the installer" with nothing written to disk yet.
  3. On the asahi-alarm side, versioned file names for the images (with installer_data.json pointing at the current one) would remove the race entirely. I can raise that with them separately.

Happy to write a patch for 1 and 2 by hand if that is welcome.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions