Series Secure boot on an i.MX8M gateway Part 6 of 10 · all parts

Building the first unsigned bootloader

Last time I wrote about pinning the exact versions of everything a bootloader build for this board actually needs.

Last time I wrote about pinning the exact versions of everything a bootloader build for this board actually needs. This post is the part where I actually try building it — completely unsigned, on purpose, since I want to know for certain that any problem I hit is a build problem, not a signing problem. This one is real, not a plan — I did all of this today.

A nice surprise: I didn't need to build everything myself

One of the pieces I spent real effort pinning last time was ARM Trusted Firmware (I'll call it ATF from here, same as everyone else does) — a small, low-level firmware component that runs on this chip right after the very first boot stage, before the main bootloader even starts. Normally you'd compile this from its own separate source code and feed the result into the bootloader build.

Turns out, while I was digging into exactly how the RAM-training firmware gets wired into the build, I found something better: the board vendor's own bootloader source code already ships a ready-made, compressed copy of that exact ATF binary, sitting right there in the source tree. I decompressed it and checked the human-readable text embedded inside — version number and build timestamp, down to the second — and it matched, exactly, what I'd already confirmed the actual device is running, from a serial console session weeks ago. Three completely independent things (the live device's own boot log, the upstream source history, and this vendored file) all agreeing is about as sure as I can be without literally asking the vendor for their build server logs.

So: no need to compile ATF myself after all. One less moving part.

Two real errors, and how I fixed them

The build itself needs a cross-compiler (since I'm building code for a different kind of processor than the one doing the building) and a handful of host tools. Configuring the board's own preset build configuration went fine. Actually running the build did not, twice, in a row — both times for genuine, understandable reasons.

First error: missing a tool called swig, which generates bindings between C code and other languages — here, needed so the build can create Python bindings for a device-tree library. I already had a Python virtual environment set up on this machine for exactly this kind of project-local tooling, and it turned out I'd actually already put a working copy of swig in there — I just hadn't added that environment's tools to my shell's search path yet for this particular build. Fixed in one line.

Second error, right after the first: a missing Python module called pkg_resources. This one's a rabbit hole worth mentioning, because installing the newest available version of the package that's supposed to provide it did NOT fix the problem — very recent releases of that underlying package actually removed that specific module on purpose, as the end of a long-flagged deprecation. The board vendor's build tooling still expects the old-style module though, so the real fix was pinning an older (but still perfectly modern) version into that same virtual environment instead of grabbing the newest one blindly.

And then it worked

LD spl/u-boot-spl
BINMAN .binman_stamp
OFCHK .config

Exit code zero. A real file called flash.bin, a bit over 2 megabytes, sitting there. I ran the build a second time right after, just to be sure — it finished instantly and reported nothing left to do, which is exactly what you'd expect from a genuinely complete build rather than something that half-finished and got lucky. I recorded its cryptographic fingerprint too, mostly out of habit at this point in the project — a good practice to keep, since it means I can always check later whether I'm looking at the exact same file.

Building cleanly is one claim. Actually booting the real board is a completely different claim, and building doesn't prove the second one. That's the next thing I need to go do, physically, at the bench.

Actually testing it — cable by cable

I did this part right after writing the plan above, so I can tell you how it really went instead of just how I expected it to go.

Quick vocabulary, since mixing these two up has cost me real time before on this same project: the board has two separate micro-USB ports. One, on the front, is just a serial console — a window into what the board is printing, nothing more, safe to have connected any time. The other, on the bottom, is a programming port that turns into either a storage device or a "fastboot" listener when the board's own software asks it to. The dangerous part: if that second cable is already plugged into a powered-on computer the moment the board gets turned on, the chip's very first boot code sees a host waiting and jumps straight into a recovery mode instead of booting normally. So the rule is simple: never have that second cable connected at power-on.

  1. Connect only the front (console) cable. Open a serial terminal at 115200 baud.
  2. Power on (or reset) the board with only that cable connected. When it offers a moment to interrupt the normal startup, take it, landing on the bootloader's own command prompt.
  3. Only now, with the bootloader already running, plug in the second (bottom) cable too. The dangerous moment was only at power-on, and that's already past.
  4. At the bootloader prompt, tell it to start listening for a "fastboot" connection. The console goes quiet at this point — that's expected, not a hang.
  5. On my laptop, in a second terminal window, confirm it actually sees the board, then send the new file over: "flash this as the bootloader."

Step 5 is exactly where I made my first real mistake of this project: I typed the partition name wrong — a one-letter typo, easy to make, embarrassingly easy to miss when reading it back. The board correctly told me that partition didn't exist and refused to write anything. That's actually the reassuring part: the tool checks the target's name before touching any storage, so a typo here just means "nothing happened," not "something got overwritten." I fixed the spelling and sent it again. This time: a real write, confirmed independently on both ends — the board's own console showed it switching to the right internal partition and writing several thousand storage blocks successfully, and my laptop reported the same success back.

  1. Unplug that second cable before resetting — leaving it connected through a power cycle would trigger the same dangerous recovery mode again.
  2. Reset with only the console cable connected, and watch closely. Since I built this today, the version banner's build timestamp should show today's date, not the original factory build date — that's my concrete proof the new image is really running, not the old one just looking similar.

One thing that tripped me up for a second, worth mentioning: right after the successful write, I scrolled back up in my still-open console window expecting to see something different — and of course I didn't, it still showed the old banner from before I'd even started. That makes sense once you think about it: writing a new image to storage doesn't retroactively change the copy that's already running in the board's memory right now. Nothing actually confirms anything until step 7 — a genuinely fresh boot, after unplugging that second cable and resetting.

I've confirmed the write itself succeeded, cleanly, on the second try. I haven't yet watched it actually boot with my own eyes (step 7 above) — that's the very next thing I'm doing, and I'll report back honestly on how it goes rather than assume "it wrote fine" already means "it works."