15 Jul 2026
Legacy uImage's CRC32 Was Never a Security Feature — And a Decade of Bug Reports Proves It's Barely a Reliability Feature Either
#u-boot#embedded-linux#boot-loaders#firmware-security#secure-boot
The claim
If you've flashed a Linux kernel to an embedded board with U-Boot, you've probably seen this:
## Booting kernel from Legacy Image at 10800000 ...
Image Name: Linux-3.0.35
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 3686476 Bytes = 3.5 MB
Load Address: 10008000
Entry Point: 10008000
Verifying Checksum ... Bad Data CRC
ERROR: can't get kernel image!
I want to make an unfashionable argument: this error message isn't a bug report about your SD card. It's a bug report about the image format. Legacy uImage's CRC32 checksum was designed to catch accidental bit-flips, nothing more — and fifteen-plus years of it being U-Boot's single most common kernel-load failure, across completely unrelated vendors, is exactly the evidence you'd expect if that checksum were an inadequate safety net for what people actually use it for today. U-Boot's own successor format, fitImage, was built specifically to fix this. Most people still aren't using it.
What "Verifying Checksum" is actually doing
U-Boot supports three kernel image shapes, and the difference matters here:
A raw zImage gets no protection at all — U-Boot just sets up registers and jumps. uImage wraps the kernel in a small header (name, load address, entry point, and a CRC32 over both the header and the payload) built by mkimage. When you bootm it, U-Boot recomputes both CRC32s and refuses to boot on a mismatch — that's the "Verifying Checksum" line. fitImage replaces the fixed header with a device-tree-structured wrapper where every component — kernel, DTB, ramdisk — carries its own configurable hash or signature (crc32, sha256, RSA, take your pick).
CRC32 is a 32-bit polynomial checksum. It's excellent at catching random bit-flips — the kind caused by a flaky NAND cell or a truncated transfer. It is not a cryptographic hash. Anyone who wants to tamper with your kernel can trivially forge a payload with the same CRC32 value, which is precisely why serious secure-boot flows use SHA-256 or RSA instead. The uImage format has no mechanism for either.
The data: same error, same words, thirteen-plus years, unrelated silicon
Here's what makes this worth writing about instead of just another troubleshooting FAQ: the exact string "Verifying Checksum ... Bad Data CRC" shows up, character for character, across boards and vendors that share no code, no BSP, and often no CPU architecture.
A user hits it on a Samsung S3C2440-based board flashing NAND in 2010: Verifying Checksum ... Bad Data CRC ERROR: can't get kernel image! A Freescale i.MX6 SABRELITE user hits the identical error in 2014 loading from eMMC after adding initramfs based on busy box in linux kernel compiling, making the uImage bigger than default. A TI DaVinci DM368 team spends a multi-year forum thread chasing it back to bad data CRC error issue frequently after writing the kernel uImage into NAND due to management of bad blocks and ECC handling. An Orange Pi Zero user hits it in 2019 over SPI flash with a fresh sunxi build. Armbian users on Allwinner H3 boards are still filing near-identical threads years later. Netgear's own community forum has a customer asking, almost word for word, "Verifying Checksum ... Bad Data CRC. Is some way to upload image via TFTP? Where can i download uImage.netgear?"
Different CPUs. Different vendors. Different decades. Same three words: Bad Data CRC.
That diagram is the whole story. By the time U-Boot prints "Verifying Checksum," the image has already survived a network transfer, a flash write, and a flash read — any of which can silently flip bits. The CRC32 check is downstream of all of it, which means the same three-word error is standing in for at least four structurally different failures.
The insight: CRC32 is doing its job, and that's the problem
Go read enough of these threads and a pattern emerges: the checksum is rarely lying. It's almost always catching something real — it's just catching the wrong thing for what the user thinks it's telling them.
| Reported root cause | What actually happened | Vendor |
|---|---|---|
| Truncated read | Declared block/byte count in the load command was smaller than the actual uImage size after adding an initramfs | NXP i.MX6 |
| NAND ECC bug | Old U-Boot 1.3.x NAND driver used a different/weaker ECC scheme than the kernel's own NAND driver, silently corrupting 1–2 bits per read | TI DaVinci |
| Bad block creep | New bad blocks appear in the kernel partition after months on a shelf, undetected until the next boot | TI DaVinci |
| mkimage/CRC bugs | Old or mismatched mkimage builds producing headers with endianness or CRC computation errors | denx mailing list |
None of these are "your kernel is compromised." All of them are "something in the pipeline between build and boot silently changed bytes." CRC32 is architecturally incapable of telling you which — it just says "the numbers don't match," and the user is left doing the same manual detective work engineers were doing on 2.6.24 kernels in 2012: was it the read size? The NAND? The transfer mode? The header itself gives you name, size, load address, entry point — and then a single pass/fail bit.
The contradiction nobody's fixing
Here's the part that should bother you more than it seems to bother the ecosystem: U-Boot's own developers describe legacy uImage bluntly. As one longtime U-Boot contributor puts it, the monolithic uImage has "no flexible indexing, no hash integrity and no support for security at all." That's not a criticism from an outsider — that's the format's own community stating, in writing, that its integrity model is a bare accident-detector.
And yet uImage is still what most vendor quick-start guides, BSP examples, and board tutorials tell you to build. Practitioners keep hitting "Bad Data CRC" and treating it like a meaningful diagnostic signal — reflashing, re-downloading, re-checking hashes of the file on disk — when the message is really just "a CRC32 didn't match," full stop, with zero information about why.
FIT was built specifically to close this gap over a decade ago. Each image or config node in a FIT can declare its own hash algorithm — including stronger options than CRC32 — and U-Boot reports success or failure per component instead of one opaque line. The difference in the boot log is stark:
Using 'config@1' configuration
Trying 'kernel@1' kernel subimage
Hash algo: crc32
Hash value: 1837f127
Verifying Hash Integrity ... crc32+ OK
versus:
Verifying Checksum ... Bad Data CRC
ERROR: can't get kernel image!
The first tells you which component, which algorithm, and whether it passed. The second tells you nothing.
The strongest counter-argument — and why it doesn't save uImage
If FIT is unambiguously more capable, why hasn't it replaced uImage everywhere? The honest answer, from someone who's spent years in this exact corner of embedded Linux: FIT "has been around for a while but is for unknown reason rarely used in real systems". That's a real, fair objection to my thesis — the superior format existing on paper doesn't mean it's the pragmatic choice, and there's a legitimate case that legacy uImage persists because it's simpler to generate, smaller, and "good enough" for boards where corruption is rare.
I don't think that objection survives contact with the data above. "Good enough" doesn't produce the same failure string across fifteen years of unrelated hardware. A checksum mechanism that's actually matched to its job doesn't need a decade of forum archaeology to diagnose. The gap here isn't technical — FIT's tooling has existed for years — it's inertia: vendor quick-start guides keep copy-pasting mkimage -A arm -O linux -T kernel -C none because that's what the last generation of quick-start guides did, not because it's the right tool for a device that will sit in the field being flashed over flaky transports for a decade.
The zero-cost migration
The part that undercuts the inertia argument entirely: switching to FIT does not require adopting signatures, keys, or a device-tree-authoring exercise. You can keep the exact same CRC32 checksum you have today and still gain FIT's per-component structure and diagnostics, with one command.
Legacy invocation, the one every BSP guide still teaches:
mkimage -A arm -O linux -T kernel -C none \
-a 0x80008000 -e 0x80008000 \
-d zImage uImage
Minimal FIT migration — same checksum, same trust model, better diagnostics:
mkimage -f auto -A arm -O linux -T kernel -C none \
-a 0x80008000 -e 0 \
-d zImage kernel.itb
mkimage -f auto builds a FIT without hand-writing an .its source file, and by default it hashes each component with CRC32 — the exact same integrity guarantee you had before. Nothing about your trust model changes. What changes is that a corrupted component now fails with a named node and a named algorithm instead of one undifferentiated string, and the door is already open: adding -k <keydir> -g <keyname> -o sha256,rsa2048 -r upgrades the same pipeline to signed SHA-256/RSA verification whenever you're ready, with no format migration required later.
Where I land
CRC32-in-uImage was never meant to answer "is this kernel authentic," and it was only ever a partial answer to "did this kernel arrive intact." Fifteen years of "Bad Data CRC" tickets — from Freescale i.MX6 boards to TI DaVinci NAND drivers to Allwinner SBCs to Netgear cable modems — is what an inadequate safety net looks like when nobody replaces it. FIT was built to fix exactly this, the migration cost for the CRC32-only case is one flag, and as long as vendor quick-start guides keep defaulting to legacy uImage, I'd bet on seeing this exact error string in forums a decade from now.
— Source: janiele.dev