[PATCH v2 0/8] bootm: size the noload buffer from the compressor header

Aristo Chen <[email protected]>
Newsgroups org.u-boot-project.lists.u-boot
Message-ID <[email protected]>
This is v2 of "bootm: size the noload decompression buffer from the
compressor header". Tom pushed back on v1
(https://patchwork.ozlabs.org/project/uboot/patch/[email protected]/)
on two grounds:

  1. No concrete problem report driving the change.
  2. ~1297 platforms grew by ~170-400 bytes; the change is not
     opt-in, so the size cost falls on everyone.

On the first point, Nora Schiffer replied with a concrete use case
(EFI-in-FIT plus padded loaders such as shim, systemd-boot, and
OpenWrt's lzma-loader can produce compression ratios that outrun the
8x heuristic), and mentioned this is on the road map for TQ-Systems
standard BSPs.

On the second point, v2 reworks the implementation to cut the size
cost, measures it across the format and architecture buckets, and
splits the work per format so each decompressor's support can be
taken or dropped on its own.

Background: for a compressed kernel_noload image, bootm_load_os()
sizes the decompression buffer as ALIGN(image_len * 8, SZ_1M). The
8x heuristic works for typical kernels, but any well-compressed
payload can exceed it, and no fixed multiplier is safe against
arbitrarily compressible input.

Each implementation patch adds a small static header-parse helper in
bootm.c (no new public API) and wires it into a size-hint switch;
helper and switch case are only compiled when the matching
decompressor is enabled, so boards that do not build a format pay no
code for it. gzip's ISIZE is a fixed trailer read, lzma's size a
fixed header read, lz4 mirrors ulz4fn()'s frame-header validation,
and zstd asks zstd_get_frame_header(), whose frame-parsing code
already ships with the zstd decompressor. The header-recorded value
is attacker-controlled, so it is capped at CONFIG_SYS_BOOTM_LEN, and
it is only an allocation hint: the decoder stays authoritative
during the actual decompression.

Text size deltas of the u-boot ELF (size(1), distro gcc 13.3 cross
toolchains); data/bss are unchanged everywhere. To make the columns
directly comparable, the v1 column is v1's implementation commit
cherry-picked onto this series' base, so both columns share one
baseline:

  board          arch     decompressors        v1        v2
  qemu_arm       arm      gzip                +160      +104
  qemu-ppce500   powerpc  gzip                +176      +112
  mt7623n_bpir2  arm      gzip+lzma           +184      +128
  qemu_arm64     arm64    gzip+lzma+lz4       +384      +368
  qemu-riscv64   riscv64  gzip+lzma+lz4       +332      +352
  th1520_lpi4a   riscv64  all four            +412      +404
  am62x_evm_a53  arm64    all four, LTO       +0 *    +8192 *
  qemu-x86       x86      none                +108        -2

  * am62x_evm_a53's number is dominated by the Cortex-A53 erratum
    843419 linker workaround (default-enabled in distro binutils for
    aarch64): symbol-level code growth (nm -S) is +392 for v1 and
    +452 for v2, but those bytes shift which ADRP instructions land
    at the erratum's page offsets, and ld pads each inserted veneer
    to a full 4 KiB page. v2 happens to trigger two such pages here;
    v1 triggered the same two on its own original base and none on
    this one. See the world-build note below.

To see how much each bucket weighs, I configured all 1550 defconfigs
and sorted them by which decompressors they enable next to bootm:

  858  gzip only (722 of them arm, essentially the 32-bit boards)
  530  gzip+lzma+lz4 (494 arm, mostly arm64, plus 36 riscv)
   38  gzip+lzma
   31  bootm with no decompressor at all
   27  gzip+lzma+lz4+zstd
   19  gzip+lz4
   15  gzip+zstd
    6  other combinations
   26  do not link bootm at all

To measure at the same scale as the original objection, I also ran a
full world build (buildman, all 1550 defconfigs, distro plus
kernel.org toolchains, gcc 13.3/14.2) over one branch holding the
base, the v1 implementation, its revert, and this series. 1496
boards built on all four commits with the revert reproducing the
base sizes exactly (44 boards did not build on every commit, and 10
built nondeterministically; both sets were excluded). Of those 1496,
the same 1375 change under either version and the rest are
untouched, including every board without bootm or without a
decompressor:

                                        v1            v2
  mean delta over all boards          +194 B        +166 B
  median delta (changed boards)       +160 B         +96 B
  median, 856 gzip-only boards        +112 B         +80 B
  median, 517 gzip+lzma+lz4 boards    +392 B        +376 B
  boards cheaper with v2                   -          1222
  boards costlier with v2                  -            74

The world build also puts the am62x footnote in proportion: 53
boards under v1 and 49 under v2 (28 in both sets), all arm64, show
size(1) jumps of one or two 4 KiB pages in either direction (min
-8192, max +8192). The mechanism is the Cortex-A53 erratum 843419
linker workaround: when a code change shifts which ADRP instructions
land at page offsets 0xff8/0xffc, ld materialises a 16-byte veneer
and pads it to a full 4 KiB page so the page offsets of all
downstream code stay unchanged. Any few-hundred-byte change re-rolls
which boards are affected, in both directions; symbol-level growth
on every such board I checked matches the byte ranges above.

Since Tom noted the higher growth in his run was on multi-algorithm
platforms: building each patch in sequence on a gzip+lzma+lz4 board
(qemu_arm64) and an all-four board (th1520_lpi4a) gives the
per-format cost directly, in bytes:

               qemu_arm64    th1520_lpi4a
  gzip             +112             +86
  zstd               +0             +62
  lz4              +176            +176
  lzma              +80             +80

(zstd is +0 on qemu_arm64 because that board does not enable it, so
the guard really does compile the helper out.) lz4 is the most
expensive parser because it mirrors ulz4fn()'s frame validation;
zstd is the cheapest because zstd_get_frame_header() already ships
with the decompressor. Since the series is split per format, if the
multi-algorithm cost still looks too high, dropping the lz4 patch
alone would cut the 517-board gzip+lzma+lz4 bucket from a median of
+376 to roughly +200; lz4 images then simply keep the 8x fallback.

In the v1 thread Simon suggested recording the uncompressed size as
a FIT property instead. As discussed there, the two compose: a FIT
property could be layered on top later, with bootm preferring the
property, then the stream header, then the 8x fallback. This series
provides the part that works for every existing image and for the
legacy uImage form of kernel_noload.

Series layout, one decompressor at a time:

  1. gzip helper + wiring
  2. gzip pytests (lying-header overflow, header-sized, boundary)
  3. zstd helper
  4. zstd pytest (guarded by requiredtool zstd)
  5. lz4 helper
  6. lz4 pytest (guarded by requiredtool lz4)
  7. lzma helper
  8. lzma pytests (real size patched into the header field, plus the
     "unknown" size marker fallback; needs no external tool since
     Python's lzma module is in the standard library)

Every patch builds in isolation on sandbox_defconfig and
qemu_arm_defconfig; the seven kernel_noload_decomp pytests and the
full test_fit class pass on sandbox.

Changes in v2:
- split the single implementation patch into per-format patches
  (gzip, zstd, lz4, lzma), each acceptable or droppable on its own
- make the helpers static in bootm.c, compiled only when the
  matching decompressor is enabled, instead of one always-built
  public image_decomp_get_uncompressed_size() in boot/image.c; this
  removes the cost from boards without the formats entirely
- regroup the pytests per format, next to the patch they exercise
- add runtime lzma coverage (header-recorded size and the "unknown"
  marker fallback) in place of v1's parser-only C unit test, which
  cannot reach a static helper
- measure the size cost on a common base across all 1550 boards and
  document the per-bucket numbers and the arm64 erratum-843419 page
  effect in this cover letter

Aristo Chen (8):
  bootm: size the noload gzip decompression buffer from ISIZE
  test: fit: cover the kernel_noload gzip header-size and lying-header
    paths
  bootm: size the noload zstd decompression buffer from
    Frame_Content_Size
  test: fit: cover the kernel_noload zstd header-size path
  bootm: size the noload lz4 decompression buffer from Content_Size
  test: fit: cover the kernel_noload lz4 header-size path
  bootm: size the noload lzma decompression buffer from the header
  test: fit: cover the kernel_noload lzma header-size and unknown-size
    paths

 boot/bootm.c              | 144 ++++++++++++++++++++++-
 test/py/tests/test_fit.py | 282 ++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 400 insertions(+), 26 deletions(-)

--
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.