Skip to content

lib: zstd: decompress every frame, not just the first - #39

Open
Lachytonner wants to merge 78 commits into
flipperdevices:rk3576from
Lachytonner:btrfs-zstd-multiframe
Open

lib: zstd: decompress every frame, not just the first#39
Lachytonner wants to merge 78 commits into
flipperdevices:rk3576from
Lachytonner:btrfs-zstd-multiframe

Conversation

@Lachytonner

Copy link
Copy Markdown

Summary

Fixes the remaining half of #35: zstd_decompress() only ever decodes the first zstd frame of its input.

lib/zstd/zstd.c probes the input with zstd_find_frame_compressed_size() and passes that single frame to zstd_decompress_dctx(). When the payload is several concatenated frames — which is legal zstd, and what pzstd and appended compressed writes produce — everything after the first frame is silently dropped. The caller gets a short buffer and no error, which is exactly the truncation described in the issue.

Both btrfs read paths funnel through this helper (decompress_zstd() for regular extents, btrfs_read_extent_inline()btrfs_decompress() for inline ones), so a DT overlay on a compressed subvolume reads back short and fdt apply fails.

This is separate from b5b70ee, which fixed the inline-extent destination buffer (dstSize_tooSmall, error 70). That was a real bug, but it does not touch the single-frame limit — @alchark's "there might be more to it" was right.

Change

Loop over the input, decoding frames until the output buffer is full or the input stops beginning with a frame header.

Stopping on a non-frame header is what preserves existing behaviour: the original size probe was added to tolerate junk after the frame, and that still works, since junk does not carry a frame magic. Input that never contained a frame is still rejected with -EINVAL.

Verification

The existing test/lib/compression.c had no multi-frame coverage, so I added compression_test_zstd_multiframe — the same fixture text as two concatenated frames.

I could not complete a sandbox build on my machine (Fedora 44 ships OpenSSL 3.5, which no longer has openssl/engine.h, so the host tools fail to compile before ut lib compression can run — unrelated to this change). Instead I linked U-Boot's bundled zstd objects, built with the sandbox kbuild flags, against a harness calling the real zstd_decompress():

case before after
single frame 350 ✅ 350 ✅
two frames concatenated 166 ❌ 350 ✅
single frame + junk tail 350 ✅ 350 ✅
garbage input -EINVAL -EINVAL

350 bytes is the full fixture; 166 is the first frame alone. Output content is byte-compared, not just the length. The three non-multi-frame cases are unchanged, so trailing-junk tolerance and error handling do not regress.

scripts/checkpatch.pl: 0 errors, 0 warnings. The single CHECK about a blank line after } before LIB_TEST(...) matches how every other test in that file is written.

If someone with the hardware can confirm a zstd-compressed overlay now applies on RK3576 without compression=none, that would close out the issue's original symptom — I don't have a device to test the end-to-end path on.

Relates to #35

Kwiboo and others added 30 commits July 31, 2026 10:39
Split 32-bit size_and_off and size_and_nimage fields of the v2 image
format header into their own 16-bit size, offset and num_images fields.

Set num_images based on number of images passed by the datafile
parameter and size based on the offset to the hash field to fix using a
single init data file and no boot data file for the v2 image format.

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The v2 image format can embed up to 4 data files compared to the two
init and boot data files using the older image format.

Add support for displaying more of the image header information that
exists in the v2 image format, e.g. image load address and flag.

Example for v2 image format:

  > tools/mkimage -l rk3576_idblock_v1.09.107.img
  Rockchip Boot Image (v2)
  Image 1: 4096 @ 0x1000
  - Load address: 0x3ffc0000
  Image 2: 77824 @ 0x2000
  - Load address: 0x3ff81000
  Image 3: 262144 @ 0x15000

Example for older image format:

  > tools/mkimage -l u-boot-rockchip.bin
  Rockchip RK32 (SD/MMC) Boot Image
  Init Data: 20480 @ 0x800
  Boot Data: 112640 @ 0x5800

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The v2 image format embeds boot0 and boot1 parameters, the vendor tool
boot_merger may write these parameters based on the rkboot miniall.ini
files.

E.g. a RK3576 boot image may contain a boot1 parameter that signals
BootROM or vendor blobs to use 1 GHz instead of the regular 24 MHz rate
for the high precision timer.

Add support for printing boot0 and boot1 parameters, e.g.:

  > tools/mkimage -l rk3576_idblock_v1.09.107.img
  Rockchip Boot Image (v2)
  Boot1 2: 0x100
  Image 1: 4096 @ 0x1000
  - Load address: 0x3ffc0000
  Image 2: 77824 @ 0x2000
  - Load address: 0x3ff81000
  Image 3: 262144 @ 0x15000

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The vendor boot_merger tool support a ALIGN parameter that is used to
define offset alignment of the embedded images.

Vendor use this for RK3576 to change offset alignment from the common
2 KiB to 4 KiB, presumably it may have something to do with UFS.
Testing with eMMC has shown that using a 512-byte alignment also work.

Add support for overriding offset alignment in case this is needed for
e.g. RK3576 in the future.

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The v2 image format can support up to 4 embedded images that can be
loaded by the BootROM using the back-to-bootrom method.

Currently two input files can be passed in using the datafile parameter,
separated by a colon (":").

Extend the datafile parameter parsing to support up to 4 input files
separated by a colon (":") for use with the v2 image format.

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The v2 image format supports defining a load address and flag for each
embedded image.

Add initial support for writing the image load address and flag to the
v2 image format header.

This may later be used for RK3576 to embed a minimal initial image that
if required to fix booting from SD-card due to a BootROM issue.

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The BootROM on RK3576 has an issue loading boot images from an SD-card.
This issue can be worked around by injecting an initial boot image
before TPL that:

  writel(0x3ffff800, 0x3ff803b0)

Prepend an image containing binary code that does this and return to
BootROM to load next image, TPL.

Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
[switch from pre-built binary to from-source build via existing Makefiles]
Signed-off-by: Alexey Charkov <alchark@flipper.net>
NanoPi M5 uses the M1 pin configuration for its serial flash controller.
Set the pin mode to M1 explicitly to enable SPL loading from SPI flash.

Signed-off-by: Alexey Charkov <alchark@gmail.com>
RK3576 EVB1 is the evaluation board for the RK3576 SoC, which also serves
as the reference design for board vendors.

Enable building images for it using upstream DTS and a minimal defconfig.

Signed-off-by: Alexey Charkov <alchark@gmail.com>
Introduce CONFIG_USB_FUNCTION_FASTBOOT_EP_BUFFER_SIZE to allow
customization of the endpoint buffer size used for fastboot transfers.

The EP_BUFFER_SIZE must always be an integral multiple of the maxpacket
size (64, 512, or 1024 bytes depending on USB speed), as controllers
like DWC3 expect bulk OUT requests to be divisible by maxpacket size.

On DWC3 controllers operating in SuperSpeed mode, increasing the
endpoint buffer size from the default value significantly improves
download throughput - from approximately 50 MB/s to over 170 MB/s.
This is particularly beneficial for flashing large images during
development or production.

The configurable buffer size allows board maintainers to tune the
trade-off between memory usage and transfer performance based on
their specific requirements and available resources.

Change-Id: 0c3402e0-3855-4d3f-b6c1-293793eebc4a
Signed-off-by: Anton Burticica <mouse@ya.ru>
This change will go via the upstream DTS tree, so just put it in here
temporarily to get Ethernet working on Omni3576 boards.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Remove the supply regulators from the eMMC controller node on Luckfox
Core3576, as they cause the eMMC to endlessly re-tune phase at startup,
and are most likely wrong:

1. The vendor DTS doesn't define those regulators
2. The VCCQ regulator referenced here is actually used for the SD card,
   and it is unlikely that both can share the same regulator with an
   1.8-3.3V range, whereas eMMC only expects 1.8V
3. Rockchip reference schematic, which this board broadly follows,
   drives the VCCQ supply of eMMC flash from VCC_1V8_S0 rather than
   VCCIO_SD_S0, where VCC_1V8_S0 is a fixed 1.8V load switch with VIN tied
   to VCC_1V8_S3 and EN tied to VCCA_1V8_S0 (a.k.a. PMIC PLDO1, not PLDO5)

There is no published schematic for the Core3576 SoM unfortunately.

Cc: stable@vger.kernel.org
Fixes: d7ad90d22abe ("arm64: dts: rockchip: Add Luckfox Omni3576 Board support")
Signed-off-by: Alexey Charkov <alchark@flipper.net>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
gpt_fill_header() computes last_usable_lba and first_usable_lba from
hardcoded block counts that silently assumes 512-byte sectors and the
default number of partition entries.

The partition entry array holds GPT_ENTRY_NUMBERS entries of 128 bytes
each, so the number of blocks it needs depends on both the entry count
and the block size. write_gpt_table() derives that count correctly and
writes the backup array at last_usable_lba + 1, so whenever the two
disagree the backup array no longer immediately precedes the backup GPT
header. Furthermore, on a device with 4096-byte native sectors, such as
UFS flash, and the default 128 entries, the array is 4 blocks rather than
32. Current code reserves 34 blocks at each end of the disk for any block
size and wastes about 114 KiB at each end.

Add a new helper gpt_pte_blocks() and use it in all four places which
currently calculate the number of blocks each in its own way, so the
layout written by gpt_fill_header() and the extent written by
write_gpt_table() cannot drift apart again. With this, first_usable_lba
on 4096-byte sectors is 6, last_usable_lba is lba - 6, and the backup
array occupies lba - 5 .. lba - 2, immediately preceding the backup
header, as the UEFI specification describes.

Note that this changes the 512-byte layout too for boards that do not use
the default entry count. CONFIG_EFI_PARTITION_ENTRIES_NUMBERS is
"default 56 if ARCH_SUNXI" and is set to 64 by a number of Rockchip
defconfigs:

  entries  array blocks  first_usable_lba  last_usable_lba
       56            14        34 -> 16    lba - 34 -> lba - 16
       64            16        34 -> 18    lba - 34 -> lba - 18
      128            32        34 -> 34    lba - 34 -> lba - 34

Existing partition tables stay readable either way, since is_gpt_valid()
locates the entry array from the on-disk partition_entry_lba. Only newly
written tables change. Partitions given without an explicit start= will
now be placed lower on those boards; on sunxi first_usable_lba lands on
the 8 KiB SPL offset, so such boards should keep specifying start=
explicitly.

While here, report both LBAs when the requested layout does not fit, since
the existing "Partitions layout exceeds disk size" debug message gives no
clue as to by how much, making it less helpful in debugging.

Co-developed-by: Anton Burticica <mouse@ya.ru>
Signed-off-by: Anton Burticica <mouse@ya.ru>
Signed-off-by: Alexey Charkov <alchark@flipper.net>
Several GPT header fields are read without converting from
little-endian, which is wrong on big-endian hosts.

gpt_fill_pte() takes my_lba and partition_entry_lba raw when working out
the region a partition must not overlap, so on a big-endian host both
bounds are byte-swapped garbage and the overlap check does not do
anything useful.

gpt_verify_partitions() compares the loop counter against
num_partition_entries raw, so its "More partitions than allowed!" guard
never triggers.

It also swaps gpt_part_size, which is already in host order, having been
computed from two le64_to_cpu() results a few lines above. Drop the
conversion rather than adding one.

All of this is a no-op on little-endian targets.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
part_test_mac(), part_print_mac() and part_get_info_mac() each declare a
single-block buffer sized after the descriptor struct:

	ALLOC_CACHE_ALIGN_BUFFER(mac_driver_desc_t, ddesc, 1);
	ALLOC_CACHE_ALIGN_BUFFER(mac_partition_t, mpart, 1);

Both structs are 512 bytes, but every blk_dread() below them asks for one
block, which transfers desc->blksz bytes. On a device with 4096-byte
logical blocks that writes 4096 bytes into a 512-byte on-stack buffer and
corrupts the stack.

part_test_mac() runs on every block device during partition probing, so on
sandbox with CONFIG_MAC_PARTITION=y this crashes on any access at all to a
device with large blocks, for instance:

	host bind 0 disk.img 4096
	part list host 0

Pad the buffers out to the block size with ALLOC_CACHE_ALIGN_BUFFER_PAD(),
which is what part_efi.c already does for its own block buffers.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Add a test that runs gpt_fill_header() and gpt_fill_pte() over a range of
block sizes and checks that the partition entry array is sized and placed
consistently, in particular that the backup array ends exactly where the
backup GPT header begins.

Neither function performs any block I/O, so the test builds a synthetic
struct blk_desc rather than needing a block device with a configurable
block size. The expected array size is taken from the GPT header fields
rather than from GPT_ENTRY_NUMBERS, so the test still holds for builds
with a non-default CONFIG_EFI_PARTITION_ENTRIES_NUMBERS, such as
ARCH_SUNXI with 56 entries.

Without the preceding fixes this fails on the first block size other than
512:

  test/dm/part.c:313, dm_test_part_gpt_blksz():
      entry_lba + pte_blks == first_lba: Expected 0x12 (18), got 0x22 (34)

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Unlike every other test in this file, test_gpt_write_part_type() never
binds the disk image it operates on. It has worked since it was added
only because some test that ran before it left host 0 bound to the
image this fixture provides.

That breaks as soon as a test which binds something else is added
above it, and it means the test silently depends on collection order
rather than on its own fixture. Bind the image like the other tests do.

Fixes: 7a598e6 ("test/py: tests: gpt: add test_gpt_write_part_type")
Signed-off-by: Alexey Charkov <alchark@flipper.net>
Bind a blank image with a 4096-byte logical block size, let U-Boot write a
GPT to it, and then check the result both through 'part list' and by
unpacking the primary and backup headers straight out of the image. The
interesting assertion is that the backup partition entry array ends
immediately before the backup header in the last block, which is what
regresses when the array size is computed with a hardcoded block count.

No sgdisk is needed since U-Boot creates the table itself, so unlike the
other tests here this one has no requiredtool marker. The number of entries
comes from the build config rather than being hardcoded to 128, so the
expected LBAs are recomputed for builds that set
CONFIG_EFI_PARTITION_ENTRIES_NUMBERS differently.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Update FASTBOOT_COMMAND_LEN and FASTBOOT_RESPONSE_LEN to match the
buffer sizes defined in the Android fastboot client:

  https://android.googlesource.com/platform/system/core/+/refs/heads/main/fastboot/constants.h

  #define FB_COMMAND_SZ  4096
  #define FB_RESPONSE_SZ  256

The original 64-byte command limit dates back to the early fastboot
protocol specification. Modern Android fastboot clients support
commands up to 4096 bytes, enabling:
- Longer partition names in flash/erase commands
- Extended getvar queries with complex arguments
- OEM commands with substantial payloads

Change-Id: 1aaaa615-0811-4f81-949c-1ae4b9935fb0
Signed-off-by: Anton Burticica <mouse@ya.ru>
… consoles

The bitmap console driver does not implement ops->measure, so
vidconsole_measure() fell back to returning a single-line bounding box
(y1 = y_charsize) with x1 = x_charsize * strlen(text), regardless of
any pixel-width limit passed by the caller.

This caused Expo scene objects using CENTRE or RIGHT alignment to
compute incorrect x offsets for long strings.  For example, the ~68-
character help prompt on a 220 px wide scene produced:
  x1 = 6 * 68 = 408 px, xofs = (220 - 408) / 2 = -94 px
pushing the text ~94 pixels off the left edge of the display.

Add a word-wrapping fallback that is taken when a pixel-width limit is
provided and the caller requests line records (lines != NULL).  The
fallback iterates character-by-character, breaks at the last space
before an overflow, and populates the vidconsole_mline array and the
overall bbox.y1 in the same way a truetype driver would, so that callers
that lay out text using the returned metrics (e.g. scene_render_txt)
produce correctly wrapped, on-screen output.
Add a simple video uclass driver for the Flipper One's 256x144 8-bit
grayscale SPI display. The display accepts a full frame in the BPP8 format
(8-bit grayscale) in a single SPI write transaction, padded to a line
stride of 258 bytes (256 pixels + 2 footer bytes per row).

No other configuration is required as long as SPI mode 3 (CPOL=1, CPHA=1)
is used and the SPI clock is up to 24 MHz.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Flipper One has an onboard MCU which exposes its front panel buttons over
the I2C interconnect bus.

Add a small keyboard driver to use these buttons for menu navigation.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
This will ultimately need to come from upstream, but for now we need to
get it in here so that we can build a working image for the device.

Drop DP nodes for now, as they are not yet included in rk3576.dtsi
in its U-boot version.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Flipper One is a portable multi-tool device with a built-in display and
various interfaces, built around the Rockchip RK3576 SoC.

Add a new defconfig file for building U-Boot for this device.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
… PHY

dwc3_glue_probe() currently fetches the "usb3-phy" handle and calls
generic_phy_init() on it as the very first thing, before the glue's own
clocks and resets are touched. Only afterwards does it call
dwc3_glue_clk_init() and dwc3_glue_reset_init(), and later still
generic_phy_power_on().

For most platforms this happens to work because the controller has been
held in reset since power-on, so whatever state it presents on its PIPE
interface is benign while the PHY runs its bring-up sequence.

It breaks, however, when an earlier stage has already taken the
controller out of reset. A typical case is the Rockchip BootROM in USB
download (Maskrom) mode on RK3576: it leaves USB3OTG0 active so that it
can talk to the host over USB 2.0, which means the DWC3 wrapper is
clocked and out of reset by the time U-Boot probes it. The wrapper then
keeps driving phy_reset / phy_powerdown / phy_rate / etc. into the
USBDP combo PHY's PMA while rk3588_udphy_init() tries to bring up the
LCPLL, which never reaches AFC/LOCK_DONE and times out with:

  rockchip_udphy phy@2b010000: cmn ana lcpll lock timeout
  rockchip_udphy phy@2b010000: failed to init usbdp combophy

Linux's dwc3-of-simple deasserts the glue resets and enables the glue
clocks in its own probe, and only then calls of_platform_populate(), so
the dwc3 child (and therefore phy_init()) cannot run until the wrapper
is in a known state. Mirror that ordering here by moving the clk/reset
init ahead of generic_phy_init().

This is a prerequisite for any per-compatible "pulse the reset before
deasserting it" fix in dwc3_glue_reset_init() to actually take effect
on platforms where the controller may be left running by an earlier
boot stage. With the old ordering, such a pulse would happen after the
PHY had already failed its LCPLL poll and would have no effect.

No functional change is expected on platforms where the controller is
already in reset at probe time.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
dwc3_glue_reset_init() currently only calls reset_deassert_bulk() on
the resets it has just acquired (with a pre-existing assert+udelay
exception for "qcom,dwc3"). If the controller has already been taken
out of reset by an earlier boot stage, deasserting an already-deasserted
reset is a no-op, and the glue probe leaves whatever state that stage
left behind in place.

This becomes a problem when the controller has been actively used
before U-Boot runs. For example, on Rockchip RK3576 the BootROM keeps
USB3OTG0 powered up and clocked while it talks to the host in USB
download (Maskrom) mode. With the controller still alive on its PIPE
interface, the subsequent USBDP combo PHY bring-up cannot reliably get
its LCPLL to lock and the PHY .init callback fails with:

  rockchip_udphy phy@2b010000: cmn ana lcpll lock timeout

Drop the qcom-only special case and always assert the reset, wait
briefly, then deassert it. A bulk-assert followed by a small delay and
a bulk-deassert is harmless on platforms where the controller was
already in reset (the reset just gets re-pulsed before the rest of the
glue runs), and forces the controller into a known state on platforms
where it was not.

This relies on the preceding reordering of dwc3_glue_probe() so that
the glue resets are toggled before generic_phy_init() runs; otherwise
the PHY would already have observed the stale controller state and
failed before this code is reached.

Closes: https://lore.kernel.org/u-boot/CAKTNdwGo434ShEsP=e=uUAbJzVxfQPPfNPeTxOOqwbuoCyzRjw@mail.gmail.com/
Signed-off-by: Alexey Charkov <alchark@flipper.net>
Add the missing parameter and enum value descriptions reported by
scripts/kernel-doc when building with W=1:

  - get_relfile():           document @type
  - get_pxelinux_path():     document @pxefile_addr_r
  - enum lex_state:          document L_NORMAL, L_KEYWORD, L_SLITERAL
  - get_token():             document @t and @State

No functional change.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
The Boot Loader Specification [1] type flipperdevices#2 entry files use the keyword
"options" for the kernel command line, which corresponds to extlinux's
"append". Recognising it here lets the existing pxelinux parser ingest
BLS entries unchanged, paving the way for a BLS bootmeth that reuses
the parser instead of duplicating it.

No effect on existing extlinux/pxelinux files: "options" is not a valid
keyword in those formats, so it cannot collide with prior usage.

[1] https://uapi-group.org/specifications/specs/boot_loader_specification/

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Split the body of parse_label() into a standalone parse_label_keys()
helper that walks key/value lines and populates a pre-existing
struct pxe_label. parse_label() becomes a thin wrapper that creates
the label, reads its name, attaches it to the menu, and delegates.

This is a pure refactor: the new helper contains the original loop
verbatim, with the local variable declarations moved to its scope.
No call sites or behaviour change.

A subsequent change will export this helper so callers parsing
formats that lack a 'label' header (notably Boot Loader Specification
type flipperdevices#2 entries) can populate a label directly from a file body
without duplicating the parser.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
alchark and others added 22 commits July 31, 2026 20:08
Selecting the next integer multiplier m is coupled to setting a negative
fractional coefficient k. The current code checks for negative k in two
separate places, which is error-prone.

Let rockchip_rk3588_pll_k_get update m directly, to make it the single
source of truth for the final value of the integer multiplier m, which
also reduces the number of scattered conditional branches in the code.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
The TRM defines the fractional PLL adjustment coefficient as a signed
two's complement number, 16 bits wide, so store it as such to avoid
confusion.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
…rk3588_pll_get_rate

Current code calculates the fractional component in 32 bits before
assigning it to a 64-bit holding variable, causing overflow for real-world
values of k, given that OSC_HZ is 24000000U. It also does bitwise manual
massaging of an unsigned representation of what is actually a two's
complement signed value, which is confusing and makes the code harder to
read.

Read k into a properly signed type and promote operands to avoid overflow,
which also enables the use of div_s64() to express the math more clearly.

Fixes: b851c00 ("clk: rockchip: pll: Add pll_rk3588 type for rk3588")
Signed-off-by: Alexey Charkov <alchark@flipper.net>
The USB boot path in the RK3576 boot ROM is very slow to jump to the 2nd
stage uploaded via the Maskrom 0x472 command. This is due to a very slow
unoptimized CRC16 routine checking the 0x472 payload byte-by-byte and
fetching ~100 instructions per byte from uncached ROM.

Enable the I-cache in EL3 before jumping to the 2nd stage, which provides
a ~16x speedup (~2 MB/s) for the 0x472 path.

This is most relevant for booting meaningful (megabytes-sized) payloads
via USB, as the slow CRC routine is never called from other boot paths.
The change is harmless for normal boot from persistent storage though,
so can be enabled unconditionally.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
spl_load_legacy_lzma() lives in common/spl/spl_legacy.c, which is only
built when CONFIG_$(PHASE_)LEGACY_IMAGE_FORMAT is set, but the call site
in _spl_load() is guarded by CONFIG_SPL_LZMA alone. Enabling LZMA
decompression without the legacy image format therefore fails to link:

  ld: common/spl/spl.o: in function `_spl_load':
  include/spl_load.h:73: undefined reference to `spl_load_legacy_lzma'

CONFIG_SPL_LZMA is also needed to support LZMA-compressed payloads with a
FIT-only SPL, so add the missing condition to avoid including the legacy
image related function call when legacy image support is not built.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
load_simple_fit() is expected to fill in the image_info structure it
receives upon successful return, but the path which skips a zero-sized
image returns success without touching it. The result is that
spl_fit_record_loadable() then publishes whatever else the descriptor
happened to hold in /fit-images under the skipped image's name: the size
and entry point of the previous loadable, or - for the first one, since
image_info is declared without an initialiser - uninitialised stack.

This is reachable whenever a FIT carries an image node with no content,
which binman produces for an optional blob that was not supplied, such as
an OP-TEE which the build did not provide.

Ensure that the image_info structure is filled in with a size and entry
point before returning, same way as other successful paths do (but
skipping the actual load).

Fixes: 6d99f86 ("spl: fit: Skip attempting to load 0 length image")
Signed-off-by: Alexey Charkov <alchark@flipper.net>
WriteDocs() and write_bintool_docs() strip four characters from the start
of every docstring line but the first, to undo the indentation the source
file gives them. Since Python 3.13 the compiler already removes the common
indentation from docstrings [1], so this removes four characters of actual
text from every line of every entry and bintool description:

  $ binman entry-docs | head
  ...
   that an image node whose only content is an optional entry which was
   is an example showing ATF, TEE and a device tree all combined::

Use inspect.cleandoc() instead, which produces the same result on both
older and newer interpreters.

The existing tests only checked that some output was produced, so they
missed this entirely; make them also confirm that a known line of a known
description survives intact.

Link: python/cpython#81283 [1]
Signed-off-by: Alexey Charkov <alchark@flipper.net>
Board implementations of spl_start_uboot() are not required to be
idempotent - the documented examples sample a GPIO or read a character
from the SPL console - so calling it more than once can yield different
answers.

Add spl_falcon_boot(), which calls spl_start_uboot() at most once and
caches the result.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Modern SoCs such as Rockchip RK3576 need TFA to be running to provide
firmware services to the OS.

Enable the TFA boot flow to allow using Linux as BL33 (including its
calling convention) to facilitate Falcon mode boot on such SoCs.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
In Falcon mode, the SPL loads a FIT image containing a Linux kernel
instead of U-boot proper, and it may need to fall back to loading U-boot
if Linux is unavailable.

Add support for images containing a Linux kernel, optionally on a
different UFS LUN and/or offset vs. U-boot proper to enable fallback.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Add an simple named Linux kernel blob type to binman, activated by the
"LINUX_KERNEL" make variable. No processing is done on the passed blob.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Add a simple named Linux ramdisk blob type to binman, activated by the
"LINUX_INITRD" make variable. No processing is done on the blob.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
When a generated FIT is used to boot Linux directly, bypassing U-Boot
proper (Falcon mode), there is no runtime code to discover the kernel
command line and (optional) initrd location and include them in a FDT.

To facilitate easier preparation of a ready-to-boot FIT, add support for
pre-patching the FDTs in a FIT with a preconfigured /chosen node including
the bootargs and initrd location.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
… and /chosen

Add a test for a FIT image with a fake Linux kernel and initrd, and a
valid device tree into which a /chosen node is added (containing a
bootargs property, as is relevant for Falcon mode boot).

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Common code calls into a platform specific function to determine whether
to attempt OS boot in Falcon mode.

There is currently no platform logic to drive that decision on Rockchip,
so attempt Falcon mode boot whenever it is enabled in the configuration.

This can be overridden by board code if needed with appropriate logic, such
as checking for a button state to skip Falcon mode boot when pressed.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Modern Rockchip SoCs such as RK3576 require TF-A to be running to provide
firmware services to the OS. To enable booting Linux in Falcon mode on
such SoCs, add a binman template for generation of FIT images containing
appropriately split TF-A and TEE binaries (as is currently done by binman
for U-boot proper images), externally provided Linux kernel and optionally
an initrd.

Any FDTs provided to the generator will be pre-patched with a /chosen
node containing an initrd load address and size (if an initrd is provided)
and a bootargs property containing the kernel command line (if provided),
so that the kernel can be booted directly without external preprocessing.

A config fragment rockchip-falcon.config can be used to enable Falcon
mode boot and the building of TF-A+Falcon with binman, e.g.:

  make nanopi-m5-rk3576_defconfig \
        rockchip-falcon.config

This will produce a FIT image u-boot-rockchip-falcon.itb containing TF-A,
TEE, Linux kernel and optionally an initrd, which can be flashed to UFS
and booted in Falcon mode.

This can also be combined with the existing rockchip-ramboot.config
fragment to obtain images suitable for booting from RAM, e.g.:

  make nanopi-m5-rk3576_defconfig \
        rockchip-falcon.config \
        rockchip-ramboot.config

The resulting images can be uploaded directly to RAM via Maskrom with no
storage or USB loader required, e.g.:

  rockusb download-sram u-boot-rockchip-usb471.bin
  rockusb download-ddr u-boot-rockchip-usb472-falcon.bin

Signed-off-by: Alexey Charkov <alchark@flipper.net>
Falcon Mode documentation only covers SPL entering the kernel itself, which
is not what happens on ARM64 SoCs needing ARM Trusted Firmware resident to
provide firmware services: SPL loads a FIT holding BL31 as its firmware
image and the kernel as a loadable, and BL31 enters the kernel as BL33.

Describe that flow, what the kernel image may be given that neither
booti_setup() nor bootz_setup() runs, how it differs from the classic flow,
and how to configure and build it on Rockchip.

Signed-off-by: Alexey Charkov <alchark@flipper.net>
The kernel compresses an inline extent as a whole block:
run_delalloc_inline() calls btrfs_compress_bio(inode, 0, blocksize, ...),
so the data is zero-filled past EOF and the resulting zstd frame declares
a content size of one block. The extent item records the unaligned file
size though - __cow_file_range_inline() passes i_size down to
insert_inline_extent(), which stores it as ram_bytes.

btrfs_read_extent_inline() sizes its decompression buffer from ram_bytes,
so for a 1900-byte file the destination is 1900 bytes while the frame
decodes to 4096. Since commit 918adf8 ("btrfs: Use U-Boot API for
decompression") btrfs decompresses through the common U-Boot helper,
which uses the one-shot zstd_decompress_dctx(). That API requires the
destination to cover the whole frame and fails with dstSize_tooSmall,
error 70, otherwise. The streaming ZSTD_decompressStream() path it
replaced stopped once the output buffer was full, so it never hit this.

The kernel side does not notice because fs/btrfs/zstd.c streams into its
own buffer and copies out at most destlen.

Allocate a full block for the decompression buffer and copy only
ram_bytes back to the caller. An inline extent never spans more than one
block, which bounds the allocation.

This shows up on RK3399 and ODROID-N2 as "zstd_decompress: failed to
decompress: 70" (armbian/build#9651, #10208), where it breaks fdt apply
on zstd-compressed overlays. Images built with mkfs.btrfs --rootdir
--compress zstd do not reproduce it, since btrfs-progs writes a frame
whose content size already equals ram_bytes. Only files written at
runtime through the kernel trip it.

Fixes: 918adf8 ("btrfs: Use U-Boot API for decompression")
Signed-off-by: Cole Munz <Munzzyy1@proton.me>
Reviewed-by: Qu Wenruo <wqu@suse.com>
btrfs_readdir() zeroes the dirent and fills in only the name and the
type, so dent->size stays 0 and every file is listed as zero bytes:

  => ls host 0 /
          0   f_192k.bin
          0   small_3k.bin

Reads themselves are fine, since btrfs_read() takes the size from
btrfs_size(), which does its own inode item lookup. It affects EFI
too: dir_read() in lib/efi_loader/efi_file.c copies dent->size into
both file_size and physical_size, so an EFI application enumerating a
directory on btrfs sees every file as empty, which is the generic-code
path Alexey's readdir series moves btrfs onto.

The custom listing that fs_ls_generic() replaced looked the inode item
up and printed the real size, and every other filesystem in the tree
fills dent->size in its own readdir: ext4fs.c:327, exfat io.c:805,
erofs fs.c:186, squashfs sqfs.c:1095 and fat.c:1555.

btrfs_next_dir_entry() already has the dir item mapped, so read the
key it points at while we are there and hand it back to the caller,
and use that to reach the inode item. A subvolume entry points at a
root item instead and has no size of its own, so leave that one at 0.

  => ls host 0 /
     196608   f_192k.bin
       3000   small_3k.bin

Fixes: 31cf3f1 ("fs: btrfs: use fs_ls_generic() and drop custom implementation")
Signed-off-by: Cole Munz <Munzzyy1@proton.me>
The U-Boot copy of btrfs_search_slot() returns on error with the nodes
it has descended through still attached to the path. The kernel one
releases the path on any error unless p->skip_release_on_error is set,
and callers written against that convention treat a failed search as
owning nothing. btrfs_size() is one: it returns straight away on a
search error and never reaches its btrfs_release_path() call, so the
attached extent buffer references leak.

Route both error exits through a release of the path. The error
returns of read_node_slot() carry no extra reference, so the path is
the only thing to clean up.

Suggested-by: Qu Wenruo <quwenruo.btrfs@gmx.com>
Signed-off-by: Cole Munz <Munzzyy1@proton.me>
btrfs_readdir() and btrfs_size() both open code the same search for an
inode item to read its size field. Move it into one helper.

Signed-off-by: Cole Munz <Munzzyy1@proton.me>
zstd_decompress() probes the input with zstd_find_frame_compressed_size()
and hands that single frame to zstd_decompress_dctx(). A zstd payload is
allowed to be several frames concatenated, and when it is, everything
after the first frame is silently dropped: the caller gets a short
buffer and no error.

On btrfs this shows up as truncated reads of zstd-compressed files. Both
read paths funnel into this helper - decompress_zstd() for regular
extents and btrfs_read_extent_inline() via btrfs_decompress() for inline
ones - so a device-tree overlay stored on a compressed subvolume comes
back short and "fdt apply" fails. Storing the file uncompressed is
currently the only reliable workaround.

Loop over the input instead, decoding frames until the output buffer is
full or the input stops beginning with a frame header. Stopping on a
non-frame header preserves the trailing-junk tolerance the size probe
was added for, so a single frame followed by padding still decodes
exactly as before, and input that never had a frame is still rejected.

Add a regression test covering a two-frame payload. Without this change
it decodes 166 of 350 bytes.

Signed-off-by: lachytonner <lachytonner32@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes zstd_decompress() in U-Boot’s bundled zstd helper so it can decode inputs containing multiple concatenated zstd frames (a valid zstd encoding pattern), and adds a unit test to cover the multi-frame case.

Changes:

  • Update lib/zstd/zstd.c:zstd_decompress() to iterate over consecutive frames, tolerating a non-frame “junk” tail after the last frame.
  • Add a new test/lib/compression.c fixture containing two concatenated zstd frames.
  • Add compression_test_zstd_multiframe to ensure concatenated-frame payloads decompress fully.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
test/lib/compression.c Adds a multi-frame zstd fixture and a new unit test validating full decompression across concatenated frames.
lib/zstd/zstd.c Extends zstd_decompress() to process multiple concatenated frames instead of only the first.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/zstd/zstd.c
Comment on lines +65 to +69
* ever decodes one. Keep decoding until the output fills up or the
* input stops looking like a frame; a non-frame tail is the junk the
* size probe below has always been here to tolerate.
*/
len = zstd_find_frame_compressed_size(abuf_data(in), abuf_size(in));
if (zstd_is_error(len)) {
log_err("%s: failed to detect compressed size: %d\n", __func__,
zstd_get_error_code(len));
ret = -EINVAL;
goto do_free;
while (out_left && zstd_frame_starts_at(in_pos, in_left)) {
@alchark

alchark commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Hi @Lachytonner, thanks a lot for your contribution!

Can you please clarify if Btrfs ever produces concatenated multi-frame Zstd compressed blobs? It doesn't follow from your commit description, but seemingly implied given that you link it to the Btrfs related issue. If it does, it would be great to highlight the specific code path / circumstances where it gets produced.

Please also have a look at https://git.u-boot-project.org/u-boot/u-boot/-/blob/main/doc/develop/process.rst?ref_type=heads and related docs (e.g. full name is required in the Signed-off-by trailer by upstream standards, by which you legally provide the Developer Certificate of Origin)

@alchark
alchark force-pushed the rk3576 branch 2 times, most recently from 1f5af61 to 761c654 Compare August 26, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants