fix(artifacts): split U-Boot script uImages on their size table (#7805)

U-Boot's mkimage writes the multi-file size table for script images
(`mkimage -T script`, the boot.scr format) exactly as it does for multi
images, `mkimage -l` lists the script as "Image 0", and the `source`
command skips the table before running the script ("scripts are just
multi-image files with one component", boot/image-board.c).

The uImage reader parsed the table only for IH_TYPE_MULTI, so a script
image reported sub_image_count 0 and exposed only payload.bin, whose
first bytes are the binary size table rather than the script text.

Treat IH_TYPE_SCRIPT like IH_TYPE_MULTI: parse the same bounded table,
expose image-<n>.bin components and report the size-table limitation.
payload.bin is unchanged. A script payload without a valid table now
fails closed, as a multi image already does.
This commit is contained in:
Dragolone authored and GitHub committed 2026-10-29 01:57:09 +08:00
1 parent 44331d04d2
commit f19436c156
5 files changed
+59 -7

No files matched your search

+2 -2
View File
@@ -317,8 +317,8 @@ portable and requires no external tool. See
The root facts record the 64-byte header (architecture, OS, type, and
compression as raw values and names, load and entry addresses, image name,
and timestamp) with the header and data CRC32 verified; the stored payload
is extractable as `payload.bin`, and multi-file images additionally expose
each sub-image as `image-<n>.bin` from the stored size table. Declared
is extractable as `payload.bin`, and multi-file and script images additionally
expose each sub-image as `image-<n>.bin` from the stored size table. Declared
addresses are facts from the header, not proven entry points, and compressed
payloads stay as stored. Intel HEX/S-record and FIT images are separate
readers and are not covered here. See [U-Boot legacy uImage](uimage.md) for
+3 -2
View File
@@ -15,8 +15,9 @@ The header's CRC32 and the declared payload's CRC32 must verify before a
successful inventory. `payload.bin` contains the complete stored payload.
Multi-file images also expose `image-0.bin`, `image-1.bin`, and so on. These
component names are generated from table order. Components follow the zero-terminated big-endian size table, with four-byte
padding after every nonfinal component. Their physical source ranges and
content hashes retain precise provenance. Extraction writes the complete
padding after every nonfinal component. Script images (`boot.scr`) carry the
same table, so their script text is `image-0.bin`. Component physical source
ranges and content hashes retain precise provenance. Extraction writes the complete
payload and every component, so multi-file output can approach twice the input
size. Compressed payloads remain compressed.
+43 -1
View File
@@ -266,7 +266,44 @@ it("splits multi images and keeps compressed payloads as stored", async () => {
}
});
it("fails closed on corrupt CRCs, truncation, and bad multi tables", async () => {
it("splits a script image's size table like a multi image", async () => {
// mkimage writes the multi-file size table for scripts too, and U-Boot's
// `source` skips it: "scripts are just multi-image files with one component".
const script = Buffer.from("setenv bootargs console=ttyS0\nbootz\n");
const payload = buildMultiPayload([script]);
const path = await writeCarrier(
"uimage-script",
buildUImage({ type: 6, name: "boot script", payload }),
);
const snapshot = await scanCanonicalArtifactInventory(path);
const root = snapshot.occurrences.find((row) => row.logical_path === ".");
expect(root?.format_metadata).toMatchObject({
type_name: "script",
sub_image_count: 1,
});
expect(snapshot.limitations.join(" ")).toContain("size table");
const reader = new UImageArtifactReader(path);
try {
const entries = [];
for await (const item of reader.entries()) entries.push(item);
const byPath = new Map(entries.map((item) => [item.path, item]));
expect([...byPath.keys()].sort()).toEqual(["image-0.bin", "payload.bin"]);
expect(
(await collect(await reader.open(byPath.get("image-0.bin")!))).equals(
script,
),
).toBe(true);
expect(
(await collect(await reader.open(byPath.get("payload.bin")!))).equals(
payload,
),
).toBe(true);
} finally {
await reader.close();
}
});
it("fails closed on corrupt CRCs, truncation, and bad size tables", async () => {
const cases: [string, Buffer, RegExp][] = [
["data CRC", buildUImage({ corruptDataCrc: true }), /data CRC32 mismatch/u],
[
@@ -287,6 +324,11 @@ it("fails closed on corrupt CRCs, truncation, and bad multi tables", async () =>
}),
/no zero terminator/u,
],
[
"script without size table",
buildUImage({ type: 6, payload: Buffer.from("echo hi\n") }),
/no zero terminator/u,
],
];
for (const [label, bytes, pattern] of cases) {
const path = await writeCarrier(
+3 -2
View File
@@ -21,6 +21,7 @@ import { ArtifactResourceScope } from "./ArtifactResourceScope.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js";
import {
hasUImageSizeTable,
parseMultiImageTable,
parseUImageHeaderFields,
UIMAGE_HEADER_SIZE,
@@ -82,7 +83,7 @@ export class UImageArtifactReader implements ArtifactReader {
options?.onCoverage?.({
status: "complete",
limitations:
this.#header?.type_name === "multi"
this.#header !== undefined && hasUImageSizeTable(this.#header.type)
? [
"Multi-image sub-images are split from the stored size table; the payload entry retains the full stored bytes.",
]
@@ -216,7 +217,7 @@ export class UImageArtifactReader implements ArtifactReader {
let dataCrc32 = 0;
let read = 0;
let subImages: readonly UImageSubImage[] = [];
if (header.type === 4) {
if (hasUImageSizeTable(header.type)) {
const tablePrefix = await readArtifactFileBytes(
handle,
UIMAGE_HEADER_SIZE,
+8
View File
@@ -17,6 +17,14 @@ export const UIMAGE_MAX_SUB_IMAGES = 4096;
/** Bytes of payload prefix that can hold a maximal multi-image size table. */
export const UIMAGE_MULTI_TABLE_WINDOW = (UIMAGE_MAX_SUB_IMAGES + 1) * 4;
/**
* Multi (4) and script (6) payloads begin with the size table: mkimage writes
* it for both, and U-Boot's `source` skips it because "scripts are just
* multi-image files with one component".
*/
export const hasUImageSizeTable = (type: number): boolean =>
type === 4 || type === 6;
export interface UImageSubImage {
readonly index: number;
/** Byte offset of the sub-image payload relative to the carrier start. */