U-Boot Filesystem Loads: Check Actual Bytes Before Trusting a Successful Return
A boot script reports a successful file load, then the next command
rejects the image or behaves unpredictably. The transfer return code is
useful evidence, but it does not establish that the script obtained the
complete object it intended to boot. A bounded read can succeed after
transferring fewer bytes than the requested maximum.
This article follows upstream U-Boot v2025.01 and its generic
filesystem load implementation. For a Buildroot product, use the exact
U-Boot revision, configuration and memory layout selected by the
release. The checks below are proposed qualification work, not results
from a board test.
Separate four
values that often get conflated
Record the file’s expected complete length, the requested maximum
transfer length, the offset into the file and the actual transferred
length. Each answers a different question. The memory address is a fifth
value: it identifies the destination rather than the size or identity of
the payload.
The load
command reference defines bytes as a maximum and pos as the amount
skipped at the start of the file. It documents hexadecimal
interpretation for partition, address, bytes and position arguments.
After a successful load, filesize contains the transferred byte count
and fileaddr contains the load address. The documented successful return
includes transfers shorter than the requested length.
Consider an illustrative file of 0x6000 bytes and a requested maximum
of 0x8000. A complete read of that file can legitimately report 0x6000.
Conversely, requesting 0x1000 deliberately limits the operation to a
prefix. Neither case makes a zero return code a statement that an entire
expected firmware object is present.

Read the
implementation’s short-read path
In the tagged
filesystem implementation, do_load passes the requested offset and
byte limit to the internal reader. It handles a negative result as
failure and writes the returned len_read into filesize on the successful
path. The internal reader emits a debug message when an explicitly
requested length differs from the actual read length; that check does
not itself convert the result into an error.
That distinction explains why an extra comparison belongs in the
calling policy. A consumer expecting an exact-size record should require
that size. A consumer accepting a variable-size image needs a different
rule, such as an allowed size interval followed by format and
authentication checks. Copying one exact-size assertion into every boot
path would reject valid images as readily as it catches truncated
ones.
Capture the status before another command replaces it. Also consume
filesize and fileaddr as outputs of the specific successful operation
under examination. If a load fails before updating them, an old value is
not evidence about the failed attempt. A diagnostic transcript should
associate every value with its producing command, source device,
partition and filename.
Bound the destination
before loading
A maximum byte argument is useful only when its destination range is
appropriate. Establish the start address and permitted exclusive end
address from the board’s actual RAM map. Check the requested range
without allowing arithmetic wraparound. Account for other loaded
components, the running bootloader and memory reservations before using
a scratch buffer.
The tagged code includes an LMB-based reservation check when the
relevant configuration is enabled. It computes the intended read extent
from file size, offset and limit, then rejects a range that cannot be
allocated at the requested address. That is a configuration-dependent
protection, not permission to assume that every product image has an
unrestricted safe buffer.
There is also a difference between transfer safety and later
execution safety. A compressed image can require additional space when
expanded. Another command may relocate a tree or ramdisk. Passing the
load check does not establish that those later ranges are safe. Keep the
transfer map and the eventual boot layout as separate review
artifacts.
Build a small acceptance
matrix
Use recoverable fixtures and non-executable test data before testing
a real kernel. Start with a known file and record its expected content
and length outside the target. Exercise a normal whole-file load, an
intentionally limited prefix, a maximum larger than the file and a
nonzero offset. For each, record command status, actual bytes and the
bytes present in the intended buffer.
Add a missing file immediately after a successful load. This makes
stale output variables visible without relying on a random failure.
Confirm that the policy stops before consuming the previous buffer or
old filesize. Then try a different file with the same length. A
length-only guard should not be mistaken for an identity check, and this
fixture demonstrates why.
Keep dangerous overlap tests in a suitable sandbox or controlled
qualification environment. A production board with no independent
recovery path is not an appropriate place to discover that a chosen
address overwrites live state. The test plan should name who owns the
safe memory map and how the device is recovered if an experiment
fails.
Make the handoff decision
explicit
A reviewable load policy records the selected source, validates the
return value, checks the transferred extent against the object’s
contract and applies the required content or authenticity validation.
Only then should it hand the buffer to the next boot command. The exact
comparisons depend on the image format and product policy; there is no
universal magic byte count.
The useful diagnostic outcome is not merely another line saying that
loading succeeded. It is evidence that this operation placed the
intended object, with the intended extent, into a permitted region.
Keeping those assertions distinct turns a misleading success message
into a precise starting point for investigation.