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
- 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.
- 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.
- 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.
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-32on the first file of the ESP. By then the stub container, the EFI partition and the root partition have already been created, and thep(repair) option cannot recover from it.This is the same traceback as #377.
Environment
j413apv0.9.1via the asahi-alarm.org bootstrap (INSTALLER_BASE=https://asahi-alarm.org/)https://asahi-alarm.org/asahi-desktop-btrfs.zipWhat happened
Timeline (from
installer.log, local time UTC+8)OS package opened(zip central directory read)New OS sizeprompt (laptop lid closed)asahi-desktop-btrfs.ziprebuilt on the server —Last-Modified: Mon, 14 Sep 2026 11:27:59 GMTCopying from esp into disk0s4 partition...→Bad CRC-32Why
osinstall.load_package()wraps the URL inURLCacheand hands it tozipfile.ZipFile, which reads the central directory once, up front. File contents are fetched much later through HTTPRangerequests inurlcache.get_partial():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 firstread()fails its CRC check.Anything that stretches the gap between
OS package openedand 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 currentasahi-desktop-btrfs.zipandasahi-base-btrfs.zipthrough the installer's ownURLCachepasses CRC.Same headers with and without the local proxy; no redirect involved.
The server honours
If-RangewithLast-Modified(Apache):After the failure
The stub container,
EFI - ASAHIand theLinux Filesystempartition exist, but the ESP is half-written and the root partition is empty.action_repair_or_upgrade()only redoes the stub andstep2(); it never callsosins.install()again, sopproduces 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
URLCache.get_size(), rememberETag/Last-Modifiedfrom the HEAD; inget_partial(), send it asIf-Rangeand treat any status other than206as "package changed on the server" — close the connection withoutres.read()(a 200 means the whole file is on its way) and raise an error thatget_block()does not retry. Checking the total inContent-Rangeagainst the size from HEAD would cover servers that ignoreIf-Range.Content-Length) right before the firstaddPartition(). 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.installer_data.jsonpointing 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.