From 8c1000fd1a070700613fc2648e512290d4a0dd0e Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 20 Apr 2026 11:09:10 -0700 Subject: [PATCH] flash: stop OTA from shipping corrupt initramfs (ENOSPC silent failure) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fedac/native/src/js-bindings.c | 70 ++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/fedac/native/src/js-bindings.c b/fedac/native/src/js-bindings.c index cbf13c7065..043f4d530d 100644 --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -4345,24 +4345,62 @@ static void *flash_thread_fn(void *arg) { snprintf(initramfs_dst, sizeof(initramfs_dst), "%s/initramfs.cpio.gz", efi_mount); ac_log("[flash] writing initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); flash_tlog(rt, "initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); - // Keep previous as .prev for rollback - char initramfs_prev[512]; - snprintf(initramfs_prev, sizeof(initramfs_prev), "%s.prev", initramfs_dst); - if (access(initramfs_dst, F_OK) == 0) { - unlink(initramfs_prev); - rename(initramfs_dst, initramfs_prev); + + // CRITICAL: initramfs is 326 MB on a typical 600 MB ESP. The previous + // approach (rename old → .prev, then write new) double-occupied ~650 + // MB and the new write hit ENOSPC mid-stream. flash_copy_file + // returned the partial byte count (positive), the `<= 0` failure + // check passed, OS reported "installed", but the corrupt initramfs + // panicked on next boot with "initramfs unpacking failed: read error". + // + // Fix: skip the .prev backup for initramfs (it's too big to keep + // alongside the new copy on tight ESPs). Unlink old, write new, + // verify byte count matches source. If write was short, abort + // hard so the OS reports failure instead of silently shipping + // a corrupt boot. + struct stat src_st; + long src_size = -1; + if (stat(rt->flash_initramfs_src, &src_st) == 0) src_size = (long)src_st.st_size; + // Free space + size of file we're about to delete = effective free. + struct stat dst_st; + long dst_existing = (stat(initramfs_dst, &dst_st) == 0) ? (long)dst_st.st_size : 0; + struct statvfs vfs; + long free_after_unlink = 0; + if (statvfs(efi_mount, &vfs) == 0) { + free_after_unlink = (long)vfs.f_bavail * (long)vfs.f_bsize + dst_existing; + } + ac_log("[flash] initramfs space: src=%ldMB free_after_unlink=%ldMB existing=%ldMB", + src_size / 1048576, free_after_unlink / 1048576, dst_existing / 1048576); + flash_tlog(rt, "initramfs space src=%ldMB free=%ldMB", src_size / 1048576, free_after_unlink / 1048576); + if (src_size > 0 && free_after_unlink < src_size + (4 * 1048576)) { + ac_log("[flash] initramfs would NOT fit — need %ldMB, free_after_unlink %ldMB", + src_size / 1048576, free_after_unlink / 1048576); + flash_trace_close_and_archive(); + rt->flash_phase = 4; + rt->flash_ok = 0; + rt->flash_pending = 0; + rt->flash_done = 1; + return NULL; } + // Also remove old kernel .prev — gives a few more MB of headroom + // and we don't expose rollback to JS anyway. + char kernel_prev[512]; + snprintf(kernel_prev, sizeof(kernel_prev), "%s.prev", dst); + unlink(kernel_prev); + // Remove the old initramfs entirely BEFORE writing new (no .prev + // double-occupation). + unlink(initramfs_dst); + sync(); long initramfs_copied = flash_copy_file(rt->flash_initramfs_src, initramfs_dst); - flash_tlog(rt, "initramfs wrote %ld bytes", initramfs_copied); - if (initramfs_copied <= 0) { - ac_log("[flash] initramfs copy FAILED (copied=%ld) — restoring .prev", - initramfs_copied); - if (access(initramfs_prev, F_OK) == 0) { - unlink(initramfs_dst); - rename(initramfs_prev, initramfs_dst); - } - // Non-fatal to the kernel write — but flag as failure since the - // new kernel won't boot without a matching initramfs. + flash_tlog(rt, "initramfs wrote %ld bytes (src=%ld)", initramfs_copied, src_size); + // Verify byte count matches. flash_copy_file returns SHORT count on + // ENOSPC mid-stream — without this check we'd ship a broken initramfs. + if (initramfs_copied <= 0 || (src_size > 0 && initramfs_copied != src_size)) { + ac_log("[flash] initramfs copy SHORT/FAILED (wrote=%ld src=%ld)", + initramfs_copied, src_size); + // Don't try to restore — old initramfs already deleted. Mark + // failure so OS reports it instead of pretending success. + unlink(initramfs_dst); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; -- 2.51.2