Skip to content

mini-boot: shrink toward the bootrom SRAM window, and gate the size - #6

Merged
openipc-ai merged 2 commits into
masterfrom
fix/mini-boot-fits-bootrom-sram-window
Aug 25, 2026
Merged

mini-boot: shrink toward the bootrom SRAM window, and gate the size#6
openipc-ai merged 2 commits into
masterfrom
fix/mini-boot-fits-bootrom-sram-window

Conversation

@openipc-ai

@openipc-ai openipc-ai commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Partial fix for #5. Cuts the overrun from 5040 bytes to 3072, makes the size a build failure if it ever grows again, and prints the remaining shortfall on every build. It does not yet close the gap — see "What's left" below.

The constraint

The bootrom loads this image into SRAM at 0x04010500; SRAM ends at 0x04014000. So the prelude — vector table + 4 KB reg_info + all mini-boot code, everything ahead of the compressed payload — must fit in 0x3B00 = 15104 bytes.

That window is not a whole number of 1 KB blocks, and the payload has to start on a 1 KB boundary (see ALIGN below), so the number actually to reach is the window rounded down: 0x3800 = 14336.

Both images this repo builds were at 19376. Past the boundary the upload lands in the bootrom's own stack: the chip stops answering mid-transfer and cannot be recovered over UART at all. In OpenIPC/firmware#2299 a user's blanked hi3518ev200 died on the 15th 1 KB chunk — the first one to cross 0x04014000:

DEBUG: === SPL === address=0x04010500 size=18432 chunks=18
DEBUG: TX DATA ACKed (attempt 1/32)          <- seq 1..14, first try
DEBUG: TX DATA silent timeout (attempt 1/32) <- seq=15 -> 0x04013D00..0x04014100
DEBUG: TX DATA FAILED after 32 retries

Nothing checked this, so the build shipped an unrecoverable image with a green tick.

Changes

-Os, -ffunction-sections/-fdata-sections, --gc-sections 19376 → 16432. Pure code-size; no behavioural change.
ALIGN(1024) before .image Recovery tools locate the end of the SRAM-resident program by scanning for the compressed header and rounding down to 1 KB, so an unaligned payload silently clips the tail of the last function. Padding makes that rounding exact. Costs up to 1 KB — which is why the result is 17408, not 16432.
ASSERT in mini-boot.lds + a check on the shipped .bin in CI The linker only sees the ELF; mini-boot.bin is assembled from it by dd afterwards, so both are worth checking.

The CI check finds the boundary by locating the exact image_data.lzma this build produced and requiring it to appear once — not by scanning for a compression header. A header scan is what a recovery tool has to do, having only the image, but here the build knows which bytes it embedded, and a coincidental match in the prelude would report a boundary that is not the payload's and pass an oversized image.

Because 17408 is still over, the assert cannot be set to 0x3800 without failing the build. It ratchets against the current size instead — that number may only ever go down — and CI reports the shortfall every build, as a warning annotation so it shows on the checks UI:

- u-boot-hi3518ev200-universal.bin: prelude [17408/14336 bytes]
::warning title=mini-boot exceeds bootrom SRAM window::u-boot-hi3518ev200-universal.bin
  prelude is 3072 bytes over the 14336-byte target; this SoC cannot be recovered
  over UART with its own u-boot. Tracking: OpenIPC/u-boot-hi3516cv200#5

That stays non-fatal on purpose: the image boots from flash, and failing the build would stop both boards shipping a working bootloader over a defect confined to the UART recovery path. On the currently-shipped binary the same check does fail outright:

- u-boot-hi3518ev200-universal.bin: prelude [19376/14336 bytes]
-- payload not 1 KB aligned (recovery tools round the boundary down to 1 KB and would clip code)

What's left, and one dead end documented

The obvious next lever is -mthumb for the C. It does fit — 14336, exactly at the target — but it does not boot. This tree's assembly is not interworking-safe:

  • start.S enters C with ldr pc, _start_armboot, a raw PC load that never sets the Thumb bit
  • mmu.S and the _divsi3.S/_udivsi3.S helpers return with mov pc, lr, which keeps the CPU in ARM state when returning to a Thumb caller

Fixing all of those (→ bx) gets much further but still ends in a prefetch abort that branches into the compressed payload. I stopped there rather than ship it: the failure mode is a camera nobody can recover, and that needs real silicon to sign off, not a QEMU pass. Recording it so the next person doesn't rediscover it.

Cutting DDR training would also fit and is deliberately not done — that is the code that makes RAM work across silicon and temperature spread.

Verification

Vendor arm-hisiv510 toolchain (the one build.yml downloads), both boards:

board prelude before after boots in QEMU
hi3518ev200 19376 17408 (1 KB aligned) yes, to a U-Boot prompt
hi3516cv200 19376 17408 (1 KB aligned) yes, to a U-Boot prompt

QEMU is widgetii/qemu-hisilicon's hi3516cv200 machine — the same model build.yml's existing qemu_smoke job uses. The unmodified binary was smoke-tested first to confirm the harness.

The CI check was exercised against fixtures covering each path: honest image, payload absent, payload duplicated, unaligned payload, growth past the ratchet, a prelude at the target, and one with a coincidental gzip header planted in the prelude — the last of which the previous header-scanning version passed.

Not validated on real hardware — I have neither SoC on a bench.

The bootrom copies this image into SRAM at 0x04010500 and SRAM ends at
0x04014000, so everything ahead of the compressed payload must fit in
0x3B00 (15104) bytes. Both images this repo builds were at 19376 —
4272 over. Past that boundary the upload writes into the bootrom's own
stack, the chip stops answering mid-transfer, and the board cannot be
recovered over UART at all. A user hit exactly that on a blanked
hi3518ev200 in OpenIPC/firmware#2299; recovery died on the 15th 1 KB
chunk, the first one to cross 0x04014000.

Nothing checked the size, so the build shipped an unrecoverable image
with a green tick. This does three things about that:

  -Os plus -ffunction-sections/-fdata-sections and --gc-sections in the
  link. Purely a code-size change, no behavioural difference: 19376 ->
  16432 bytes of prelude.

  ALIGN(1024) before .image. Recovery tools find the end of the
  SRAM-resident program by scanning for the compressed header and
  rounding DOWN to 1 KB, so an unaligned payload means the tail of the
  last function is silently clipped. Padding to the boundary makes that
  rounding exact. It costs up to 1 KB, which is why the prelude lands at
  17408 rather than 16432.

  An ASSERT in mini-boot.lds and a matching check on the shipped .bin in
  CI — the linker sees the ELF, but mini-boot.bin is assembled from it by
  dd afterwards, so both are worth checking.

17408 is still 2304 over the window, so the assert cannot yet be set to
0x3B00 without failing the build. It ratchets against the current size
instead: that number may only go down. CI prints the remaining shortfall
on every build so it stays visible. Closing it is #5.

The obvious next lever, -mthumb for the C, does fit (14336) but does not
boot: this tree's assembly is not interworking-safe (`ldr pc,
_start_armboot` in start.S, `mov pc, lr` returns throughout mmu.S and the
division helpers), and fixing those still leaves a fault that branches
into the compressed payload. Left alone deliberately — the failure mode
here is a camera that cannot be recovered, so it needs real silicon, not
just a QEMU pass.

Verified with the vendor arm-hisiv510 toolchain: both hi3518ev200 and
hi3516cv200 build at 17408 and boot to a U-Boot prompt in the
qemu-hisilicon hi3516cv200 model that build.yml already uses.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Shrink mini-boot and gate SRAM prelude size regressions

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Shrinks mini-boot preludes from 19,376 to 17,408 bytes without behavioral changes.
• Aligns compressed payloads to recovery tools’ required 1 KB boundary.
• Ratchets linker and shipped artifacts while reporting the remaining 2,304-byte SRAM overrun.
Diagram

graph TD
  A["Mini-boot sources"] -->|compile| B["Size flags"] -->|link| C["Linker layout"] -->|emits| D["Mini-boot ELF"] -->|assemble| E["Shipped binary"] -->|scan payload| F["CI SRAM gate"] -->|publish| G["Build artifact"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Complete ARM/Thumb interworking conversion
  • ➕ Reduces the prelude enough to fit the actual 15,104-byte SRAM window.
  • ➕ Preserves DDR training and recovery functionality.
  • ➖ Requires auditing all assembly call and return boundaries.
  • ➖ Observed prefetch aborts make real-silicon validation mandatory before release.
2. Remove DDR training
  • ➕ Likely reaches the SRAM target with a smaller implementation change.
  • ➖ Weakens memory initialization across silicon and temperature variation.
  • ➖ Trades a recovery defect for potentially unreliable runtime behavior.
3. Gate directly at the SRAM limit
  • ➕ Guarantees no unrecoverable oversized artifact can be published.
  • ➖ Immediately blocks both supported board builds until another 2,304 bytes are removed.

Recommendation: Use the current optimization, alignment, and ratchet as the safest interim approach: it materially reduces risk without introducing unverified execution-mode changes or weakening DDR initialization. Follow with a hardware-validated interworking effort, then lower both gates to the true 0x3B00 limit.

Files changed (3) +79 / -11

Enhancement (1) +3 / -2
MakefileCompile and link mini-boot for minimum size +3/-2

Compile and link mini-boot for minimum size

• Switches mini-boot C compilation from -O2 to -Os, emits functions and data into independent sections, and garbage-collects unused sections during linking. These changes reduce the SRAM-resident prelude without changing intended behavior.

arch/arm/cpu/hi3518ev200/compressed/Makefile

Bug fix (1) +32 / -9
mini-boot.ldsAlign payload and enforce the mini-boot size ratchet +32/-9

Align payload and enforce the mini-boot size ratchet

• Collects split text and data sections, aligns the compressed payload to a 1 KB recovery-tool boundary, and records its start. A linker assertion prevents the prelude from exceeding the current 0x4400 ceiling while documenting the eventual 0x3B00 target.

arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds

Other (1) +44 / -0
build.ymlValidate shipped prelude alignment and SRAM size in CI +44/-0

Validate shipped prelude alignment and SRAM size in CI

• Adds artifact-level compressed-payload discovery for both supported boards. CI rejects missing or unaligned payloads and growth beyond 0x4400, while continuously reporting the remaining gap to the 0x3B00 bootrom window.

.github/workflows/build.yml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Prelude remains 2304 bytes oversized 📎 Requirement gap ≡ Correctness
Description
The live mini-boot prelude is allowed to reach 0x4400 (17408 bytes), 2304 bytes beyond the
0x3B00 UART SRAM window, because both the linker ratchet and artifact check accept this oversized
result for both SoCs. Consequently, unrecoverable images still link and pass CI instead of
triggering the required hard build-time rejection.
Code

arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[R14-18]

+/* What this build actually achieves today.  It is NOT yet inside the window
+ * above -- see OpenIPC/u-boot-hi3516cv200#5 -- so the assert below ratchets
+ * against the current size instead: it may only ever be lowered, never
+ * raised, so the gap cannot silently grow again while it is being closed. */
+MINI_BOOT_SIZE_CEILING = 0x4400;
Evidence
Rules 1 and 2 require the complete prelude and effective linker assertion to be capped at 0x3B00
(15104 bytes), but the changed linker script acknowledges the current build is outside that limit
and compares __image_start - ADDR(.text) against MINI_BOOT_SIZE_CEILING, defined as 0x4400;
both supported board configurations use this script. Rule 3 requires transferable, executable UART
recovery images for both SoCs, yet the CI artifact check explicitly recognizes off > WINDOW as
unrecoverable without failing unless the larger 0x4400 ratchet is exceeded, and the workflow
matrix applies this permissive check to both boards.

Fit Mini-Boot Prelude Within the UART Download Window
arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[6-18]
.github/workflows/build.yml[94-95]
arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[12-18]
arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[60-61]
Makefile[3307-3308]
.github/workflows/build.yml[14-15]
.github/workflows/build.yml[110-116]
arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[6-11]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The mini-boot prelude remains 2304 bytes larger than the boot ROM UART SRAM window, while the linker assertion and CI artifact check permit images up to `0x4400` instead of rejecting any prelude beyond `0x3B00`.
## Issue Context
Both supported board configurations use the compressed mini-boot linker script and are checked by the workflow matrix. Keep the required DDR initialization and decompression code live, but reduce or restructure the prelude until `__image_start` is at or below `0x3B00` for both SoCs; then enforce 15104 bytes as the linker limit and make any artifact offset beyond `0x3B00` a hard CI failure so an SRAM-overrunning UART recovery image cannot ship.
## Fix Focus Areas
- arch/arm/cpu/hi3518ev200/compressed/Makefile[20-20]
- arch/arm/cpu/hi3518ev200/compressed/Makefile[93-94]
- arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[27-49]
- arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[60-61]
- .github/workflows/build.yml[94-116]
- .github/workflows/build.yml[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Magic scan misidentifies payload ✓ Resolved 🐞 Bug ≡ Correctness
Description
The CI scanner treats the first LZMA-like or gzip-like byte sequence anywhere in the executable
prelude as the payload boundary, even though this build embeds only the generated image_data.lzma;
a coincidental aligned signature can make the ratchet validate an earlier offset and allow the real
prelude to exceed its ceiling. The gzip branch is especially invalid because mini-boot's runtime
recognizes only the LZMA 0x5D format and otherwise copies the bytes as uncompressed data.
Code

.github/workflows/build.yml[R100-103]

+              if d[i] == 0x5D and int.from_bytes(d[i+1:i+5], 'little') in valid:
+                  off = i; break
+              if d[i] == 0x1F and d[i+1] == 0x8B and d[i+2] == 0x08:
+                  off = i; break
Evidence
The Makefile always creates image_data.lzma, and image_data.S embeds that exact file between
input_data symbols. Runtime code recognizes only a leading 0x5D as compressed, while the new
workflow scans arbitrary bytes and additionally accepts gzip magic, proving that its first match
need not identify the actual payload.

arch/arm/cpu/hi3518ev200/compressed/Makefile[112-124]
arch/arm/cpu/hi3518ev200/compressed/image_data.S[1-6]
arch/arm/cpu/hi3518ev200/compressed/startup.c[82-103]
.github/workflows/build.yml[97-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The artifact check locates the payload using ambiguous compression magic and can validate an unrelated byte sequence in the prelude instead of the actual `.image` data.
## Issue Context
`image_data.lzma` is the payload included by `image_data.S`; gzip is not a supported mini-boot payload format. Preserve the final-artifact check, but identify the embedded payload deterministically, such as by finding the exact generated `image_data.lzma` byte sequence and requiring a unique match.
## Fix Focus Areas
- .github/workflows/build.yml[92-112]
- arch/arm/cpu/hi3518ev200/compressed/Makefile[112-124]
- arch/arm/cpu/hi3518ev200/compressed/image_data.S[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +14 to +18
/* What this build actually achieves today. It is NOT yet inside the window
* above -- see OpenIPC/u-boot-hi3516cv200#5 -- so the assert below ratchets
* against the current size instead: it may only ever be lowered, never
* raised, so the gap cannot silently grow again while it is being closed. */
MINI_BOOT_SIZE_CEILING = 0x4400;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Prelude remains 2304 bytes oversized 📎 Requirement gap ≡ Correctness

The live mini-boot prelude is allowed to reach 0x4400 (17408 bytes), 2304 bytes beyond the
0x3B00 UART SRAM window, because both the linker ratchet and artifact check accept this oversized
result for both SoCs. Consequently, unrecoverable images still link and pass CI instead of
triggering the required hard build-time rejection.
Agent Prompt
## Issue description
The mini-boot prelude remains 2304 bytes larger than the boot ROM UART SRAM window, while the linker assertion and CI artifact check permit images up to `0x4400` instead of rejecting any prelude beyond `0x3B00`.

## Issue Context
Both supported board configurations use the compressed mini-boot linker script and are checked by the workflow matrix. Keep the required DDR initialization and decompression code live, but reduce or restructure the prelude until `__image_start` is at or below `0x3B00` for both SoCs; then enforce 15104 bytes as the linker limit and make any artifact offset beyond `0x3B00` a hard CI failure so an SRAM-overrunning UART recovery image cannot ship.

## Fix Focus Areas
- arch/arm/cpu/hi3518ev200/compressed/Makefile[20-20]
- arch/arm/cpu/hi3518ev200/compressed/Makefile[93-94]
- arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[27-49]
- arch/arm/cpu/hi3518ev200/compressed/mini-boot.lds[60-61]
- .github/workflows/build.yml[94-116]
- .github/workflows/build.yml[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread .github/workflows/build.yml Outdated
Review on #6 raised two things.

The CI size gate found the prelude/payload boundary by scanning for an
LZMA or gzip header, which is what a recovery tool has to do when all it
has is the image -- but here the build knows exactly which bytes it
embedded. A coincidental match in the prelude made the gate report a
boundary that was not the payload's and pass, i.e. call an oversized,
unrecoverable image fine. Planting a gzip magic at offset 8192 in a
17408-byte prelude reproduced it: the old check reported 8192 and
passed. Locate image_data.lzma by exact bytes instead, and require it to
appear exactly once.

The gzip branch was wrong regardless: the Makefile only ever produces
image_data.lzma and startup.c only recognises the LZMA 0x5D form.

Second, the target was off by an alignment. 0x3B00 is not a multiple of
1 KB and the payload must start on a 1 KB boundary, so the largest
prelude that both loads and survives the tools' round-down is 0x3800
(14336). The shortfall is 3072 bytes, not 2304.

Report the remaining gap as a workflow warning annotation so it shows in
the checks UI rather than only in the log. It stays non-fatal on
purpose: the image boots from flash, and failing the build would stop
both boards shipping a working bootloader over a defect that only
affects the UART recovery path.
@openipc-ai

Copy link
Copy Markdown
Contributor Author

Both findings taken; one is a real bug, the other I'm answering rather than fixing.

The payload scan could pass an oversized image (fixed)

This is right, and it defeats the point of the gate. The check was scanning for a compression header — which is what a recovery tool has to do, having only the image — when the build knows exactly which bytes it embedded.

Reproduced by planting a gzip magic at offset 8192 of a 17408-byte prelude:

--- old: - OLD scanner says prelude = 8192 bytes
         -- OLD verdict: PASS          <-- oversized image, green tick
--- new: - a_gzip.bin: prelude [17408/14336 bytes]
         ::warning ... 3072 bytes over the 14336-byte target

Now located by searching for the exact image_data.lzma the build produced and requiring exactly one match, with a hard failure if it is absent (the artifact is not the one that was linked) or appears twice (ambiguous boundary). The gzip branch is gone — as you note, the Makefile only ever produces image_data.lzma and startup.c only recognises the LZMA 0x5D form, so that branch could only ever produce false positives.

The target was also wrong, by one alignment

Chasing the above turned up an error of mine. 0x3B00 is 15104, which is not a multiple of 1024, and the payload must start on a 1 KB boundary. So the largest prelude that both loads and survives the tools' round-down is 0x3800 = 14336, and the shortfall is 3072, not 2304. Corrected in the linker script, the CI check and the PR description.

Still 3072 over (by design, for now)

Not fixed, and I don't think it should be here. Of the three alternatives in the assessment, the third — gate at the true limit — fails both boards immediately, and the images it would reject boot fine from flash. Refusing to ship a working bootloader over a defect confined to the UART recovery path is a worse trade than shipping it with the defect recorded, especially as that recovery path is already broken today.

The first alternative is the right one and I got some way into it; -mthumb fits at exactly 14336, but this tree's assembly is not interworking-safe and it still prefetch-aborts after the bx conversions. That needs silicon to sign off, not a QEMU pass — the failure mode is a camera nobody can recover. The second, cutting DDR training, I'm not willing to do: it is what makes RAM work across silicon and temperature spread.

What has changed is that the gap can no longer widen unnoticed, and it is now a warning annotation on the checks UI rather than a line buried in a log.

@openipc-ai
openipc-ai merged commit d1b84d7 into master Aug 25, 2026
5 checks passed
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.

1 participant