Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

flash: stop OTA from shipping corrupt initramfs (ENOSPC silent failure)

User report: device OTA'd from keen-swallow-hail to polar-swallow-beat,
OS piece reported "update installed!" + "verified 13.3MB written",
prompted reboot — and then panicked at boot with "initramfs unpacking
failed: read error / Kernel panic - not syncing".

Cause: on a typical 600 MB Fedora ESP, the OTA flow renamed the existing
326 MB initramfs.cpio.gz to .prev FIRST (rename keeps disk usage), then
called flash_copy_file to write the new 326 MB initramfs alongside it.
Peak occupancy: 13MB (new kernel) + 13MB (kernel.prev) + 13MB
(bootmgfw.efi) + 326MB (initramfs.prev) + 326MB (writing initramfs) =
691 MB, which doesn't fit. flash_copy_file got ENOSPC mid-stream and
returned the partial byte count (positive!). The post-write check was
`if (initramfs_copied <= 0)` — passed because partial count is > 0. OS
piece reported success, user rebooted, kernel found a truncated
initramfs.cpio.gz and panicked.

Fix:
1. Pre-flight space check: stat() the source, statvfs() the ESP, account
for the old initramfs we're about to delete, abort hard if free space
wouldn't fit src + 4MB margin. Reports the math via flash_tlog so
MongoDB triage can see exactly why.
2. Skip .prev backup for initramfs entirely (no JS rollback path uses it,
and at 326MB it's too big to keep alongside on tight ESPs). Unlink
old, sync, then write new.
3. Also unlink kernel.prev to free a few more MB.
4. Verify byte count matches src_size after copy. flash_copy_file
returning SHORT (e.g. 235MB of 326MB on partial ENOSPC) is now a
hard failure — unlink the truncated dest, mark flash_ok=0, return.
No more silent shipping of corrupt initramfs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+54 -16
+54 -16
fedac/native/src/js-bindings.c
··· 4345 4345 snprintf(initramfs_dst, sizeof(initramfs_dst), "%s/initramfs.cpio.gz", efi_mount); 4346 4346 ac_log("[flash] writing initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); 4347 4347 flash_tlog(rt, "initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); 4348 - // Keep previous as .prev for rollback 4349 - char initramfs_prev[512]; 4350 - snprintf(initramfs_prev, sizeof(initramfs_prev), "%s.prev", initramfs_dst); 4351 - if (access(initramfs_dst, F_OK) == 0) { 4352 - unlink(initramfs_prev); 4353 - rename(initramfs_dst, initramfs_prev); 4348 + 4349 + // CRITICAL: initramfs is 326 MB on a typical 600 MB ESP. The previous 4350 + // approach (rename old → .prev, then write new) double-occupied ~650 4351 + // MB and the new write hit ENOSPC mid-stream. flash_copy_file 4352 + // returned the partial byte count (positive), the `<= 0` failure 4353 + // check passed, OS reported "installed", but the corrupt initramfs 4354 + // panicked on next boot with "initramfs unpacking failed: read error". 4355 + // 4356 + // Fix: skip the .prev backup for initramfs (it's too big to keep 4357 + // alongside the new copy on tight ESPs). Unlink old, write new, 4358 + // verify byte count matches source. If write was short, abort 4359 + // hard so the OS reports failure instead of silently shipping 4360 + // a corrupt boot. 4361 + struct stat src_st; 4362 + long src_size = -1; 4363 + if (stat(rt->flash_initramfs_src, &src_st) == 0) src_size = (long)src_st.st_size; 4364 + // Free space + size of file we're about to delete = effective free. 4365 + struct stat dst_st; 4366 + long dst_existing = (stat(initramfs_dst, &dst_st) == 0) ? (long)dst_st.st_size : 0; 4367 + struct statvfs vfs; 4368 + long free_after_unlink = 0; 4369 + if (statvfs(efi_mount, &vfs) == 0) { 4370 + free_after_unlink = (long)vfs.f_bavail * (long)vfs.f_bsize + dst_existing; 4354 4371 } 4372 + ac_log("[flash] initramfs space: src=%ldMB free_after_unlink=%ldMB existing=%ldMB", 4373 + src_size / 1048576, free_after_unlink / 1048576, dst_existing / 1048576); 4374 + flash_tlog(rt, "initramfs space src=%ldMB free=%ldMB", src_size / 1048576, free_after_unlink / 1048576); 4375 + if (src_size > 0 && free_after_unlink < src_size + (4 * 1048576)) { 4376 + ac_log("[flash] initramfs would NOT fit — need %ldMB, free_after_unlink %ldMB", 4377 + src_size / 1048576, free_after_unlink / 1048576); 4378 + flash_trace_close_and_archive(); 4379 + rt->flash_phase = 4; 4380 + rt->flash_ok = 0; 4381 + rt->flash_pending = 0; 4382 + rt->flash_done = 1; 4383 + return NULL; 4384 + } 4385 + // Also remove old kernel .prev — gives a few more MB of headroom 4386 + // and we don't expose rollback to JS anyway. 4387 + char kernel_prev[512]; 4388 + snprintf(kernel_prev, sizeof(kernel_prev), "%s.prev", dst); 4389 + unlink(kernel_prev); 4390 + // Remove the old initramfs entirely BEFORE writing new (no .prev 4391 + // double-occupation). 4392 + unlink(initramfs_dst); 4393 + sync(); 4355 4394 long initramfs_copied = flash_copy_file(rt->flash_initramfs_src, initramfs_dst); 4356 - flash_tlog(rt, "initramfs wrote %ld bytes", initramfs_copied); 4357 - if (initramfs_copied <= 0) { 4358 - ac_log("[flash] initramfs copy FAILED (copied=%ld) — restoring .prev", 4359 - initramfs_copied); 4360 - if (access(initramfs_prev, F_OK) == 0) { 4361 - unlink(initramfs_dst); 4362 - rename(initramfs_prev, initramfs_dst); 4363 - } 4364 - // Non-fatal to the kernel write — but flag as failure since the 4365 - // new kernel won't boot without a matching initramfs. 4395 + flash_tlog(rt, "initramfs wrote %ld bytes (src=%ld)", initramfs_copied, src_size); 4396 + // Verify byte count matches. flash_copy_file returns SHORT count on 4397 + // ENOSPC mid-stream — without this check we'd ship a broken initramfs. 4398 + if (initramfs_copied <= 0 || (src_size > 0 && initramfs_copied != src_size)) { 4399 + ac_log("[flash] initramfs copy SHORT/FAILED (wrote=%ld src=%ld)", 4400 + initramfs_copied, src_size); 4401 + // Don't try to restore — old initramfs already deleted. Mark 4402 + // failure so OS reports it instead of pretending success. 4403 + unlink(initramfs_dst); 4366 4404 flash_trace_close_and_archive(); 4367 4405 rt->flash_phase = 4; 4368 4406 rt->flash_ok = 0;