Enable script_raw for sandbox and add a GPT disk with valid legacy and
FIT scripts, as well as malformed and unsupported images. Bind the
bootmeth inside the new C test so existing bootmeth lists and bootflow
tests, including bootflow scan -G coverage, remain unchanged.

Introduce test/py/img/ as a package for disk-image generators. Existing
C-test image setup lives in test/py/tests/test_ut.py; keep the raw-script
case definitions and image generation in the new module and call it
from the existing test_ut_dm_init_bootstd() initialization.

Allow fs_helper.setup_image() to accept an sfdisk partition specification
and reuse it for the named GPT partitions. This avoids a dependency on
sgdisk or a separate initialization test. Annotate each expected C error
with its Python case name to keep the two lists in sync.

Signed-off-by: He Yong <[email protected]>
---
Changes in v3:
- Split sandbox coverage into its own patch.
- Bind script_raw only inside its test and preserve existing scan -G coverage.
- Annotate expected errors with Python case names.
- Reuse fs_helper and sfdisk from the existing bootstd initialization test.

 arch/sandbox/dts/test.dts  |  8 ++++
 configs/sandbox_defconfig  |  1 +
 test/boot/bootflow.c       | 81 +++++++++++++++++++++++++++++++
 test/py/img/__init__.py    |  2 +
 test/py/img/script_raw.py  | 98 ++++++++++++++++++++++++++++++++++++++
 test/py/tests/fs_helper.py | 19 +++++---
 test/py/tests/test_ut.py   |  3 ++
 7 files changed, 205 insertions(+), 7 deletions(-)
 create mode 100644 test/py/img/__init__.py
 create mode 100644 test/py/img/script_raw.py

diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index 25c99ef8147..3470e4368d3 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -50,6 +50,7 @@
                mmc9 = "/mmc9";
                mmc10 = "/mmc10";
                mmc11 = "/mmc11";
+               mmc12 = "/mmc12";
                pci0 = &pci0;
                pci1 = &pci1;
                pci2 = &pci2;
@@ -1398,6 +1399,13 @@
                filename = "mmc11.img";
        };
 
+       /* This is used for raw script bootflow tests */
+       mmc12 {
+               status = "disabled";
+               compatible = "sandbox,mmc";
+               filename = "mmc12.img";
+       };
+
        pch {
                compatible = "sandbox,pch";
        };
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index a2708e6fb33..09b2980f79b 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -28,6 +28,7 @@ CONFIG_FIT_VERBOSE=y
 CONFIG_BOOTMETH_ANDROID=y
 CONFIG_BOOTMETH_BLS=y
 CONFIG_BOOTMETH_RAUC=y
+CONFIG_BOOTMETH_SCRIPT_RAW=y
 CONFIG_UPL=y
 CONFIG_LEGACY_IMAGE_FORMAT=y
 CONFIG_MEASURED_BOOT=y
diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c
index c4b41668a62..d7e5a51f7b5 100644
--- a/test/boot/bootflow.c
+++ b/test/boot/bootflow.c
@@ -35,6 +35,7 @@ extern U_BOOT_DRIVER(bootmeth_android);
 extern U_BOOT_DRIVER(bootmeth_cros);
 extern U_BOOT_DRIVER(bootmeth_rauc);
 extern U_BOOT_DRIVER(bootmeth_2script);
+extern U_BOOT_DRIVER(bootmeth_2script_raw);
 
 /* Use this as the vendor for EFI to tell the app to exit boot services */
 static u16 __efi_runtime_data test_vendor[] = u"U-Boot testing";
@@ -1908,4 +1909,84 @@ static int bootdev_hunt_fallthrough(struct 
unit_test_state *uts)
 }
 BOOTSTD_TEST(bootdev_hunt_fallthrough,
              UTF_DM | UTF_SCAN_FDT | UTF_SF_BOOTDEV | UTF_CONSOLE);
+
+/* Exercise valid and rejected images without restarting U-Boot per case. */
+static int check_script_raw(struct unit_test_state *uts,
+                           struct bootflow_iter *iter)
+{
+       static const int errors[] = {
+               /* Keep in sync with CASES in test/py/img/script_raw.py. */
+               0,              /* legacy */
+               0,              /* fit */
+               -EINVAL,        /* external */
+               -EINVAL,        /* bad_crc */
+               -E2BIG,         /* too_large */
+               -ENOENT,        /* wrong_name */
+               -ENOEXEC,       /* empty */
+               -EINVAL,        /* bad_header */
+               -E2BIG,         /* partition_size */
+               -EINVAL,        /* bad_fit */
+       };
+       struct bootflow bflow;
+       int ret, part;
+
+       ret = bootflow_scan_first(NULL, NULL, iter, BOOTFLOWIF_ALL, &bflow);
+       /* A raw script belongs to a partition, never to the whole disk. */
+       ut_asserteq(-ENOENT, ret);
+       ut_asserteq(0, bflow.part);
+       bootflow_free(&bflow);
+       for (part = 1; part <= ARRAY_SIZE(errors); part++) {
+               ret = bootflow_scan_next(iter, &bflow);
+               ut_asserteq(part, bflow.part);
+               ut_asserteq(errors[part - 1], ret);
+               if (!ret) {
+                       ut_asserteq(BOOTFLOWST_READY, bflow.state);
+                       ut_asserteq_str("uboot_scr", bflow.fname);
+                       /* A script returning successfully did not boot an OS. 
*/
+                       ut_asserteq(-EFAULT, bootflow_run_boot(iter, &bflow));
+                       ut_assert_skip_to_line("%sraw-script-ok mmc c %d",
+                                              part == 2 ? "sha256+ " : "", 
part);
+                       ut_asserteq_str("mmc", env_get("devtype"));
+               } else {
+                       ut_assert(bflow.state < BOOTFLOWST_READY);
+                       ut_assertnull(bflow.buf);
+               }
+               bootflow_free(&bflow);
+       }
+
+       return 0;
+}
+
+static int bootflow_script_raw(struct unit_test_state *uts)
+{
+       static const char *order[] = {"mmc12", NULL};
+       struct bootflow_iter iter;
+       struct bootstd_priv *std;
+       struct udevice *bootstd;
+       const char **old_order;
+       int ret;
+
+       if (!IS_ENABLED(CONFIG_BOOTMETH_SCRIPT_RAW))
+               return -EAGAIN;
+
+       /* Bind only for this test, leaving the default bootmeth list intact. */
+       ut_assertok(uclass_first_device_err(UCLASS_BOOTSTD, &bootstd));
+       ut_assertok(device_bind(bootstd, DM_DRIVER_REF(bootmeth_2script_raw),
+                               "script_raw", 0, ofnode_null(), NULL));
+
+       ut_assertok(prep_mmc_bootdev(uts, "mmc12", false, &old_order));
+       ut_assertok(bootstd_get_priv(&std));
+       std->bootdev_order = order;
+       ut_assertok(bootmeth_set_order("script_raw"));
+       ut_assertok(env_set("verify", "yes"));
+       ret = check_script_raw(uts, &iter);
+       bootflow_iter_uninit(&iter);
+       std->bootdev_order = old_order;
+       /* Restore verify even if a scan or boot assertion failed. */
+       ut_assertok(env_set("verify", NULL));
+       ut_assertok(ret);
+
+       return 0;
+}
+BOOTSTD_TEST(bootflow_script_raw, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
 #endif /* CONFIG_SANDBOX */
diff --git a/test/py/img/__init__.py b/test/py/img/__init__.py
new file mode 100644
index 00000000000..526c5dda958
--- /dev/null
+++ b/test/py/img/__init__.py
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0+
+"""Disk-image generators for U-Boot C tests."""
diff --git a/test/py/img/script_raw.py b/test/py/img/script_raw.py
new file mode 100644
index 00000000000..671c5917d6d
--- /dev/null
+++ b/test/py/img/script_raw.py
@@ -0,0 +1,98 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright 2026 He Yong <[email protected]>
+
+"""Create a GPT disk containing valid and malformed raw script images."""
+
+import os
+import struct
+import zlib
+
+import utils
+from tests import fs_helper
+
+
+CASES = ('legacy', 'fit', 'external', 'bad_crc',
+         'too_large', 'wrong_name', 'empty', 'bad_header', 'partition_size',
+         'bad_fit')
+
+
+def script_image(ubman, case):
+    """Build one script image, optionally corrupting its header or data."""
+    workdir = ubman.config.build_dir
+    script = os.path.join(workdir, 'raw-script.cmd')
+    image = os.path.join(workdir, 'raw-script.scr')
+    mkimage = os.path.join(ubman.config.build_dir, 'tools', 'mkimage')
+    with open(script, 'w', encoding='utf-8') as outf:
+        outf.write('echo raw-script-ok ${devtype} ${devnum} '
+                   '${distro_bootpart}\n')
+    if case in ('fit', 'external', 'bad_fit'):
+        its = os.path.join(workdir, 'raw-script.its')
+        with open(its, 'w', encoding='utf-8') as outf:
+            outf.write('''/dts-v1/;
+/ {
+    description = "Raw script test";
+    #address-cells = <1>;
+    images {
+        script {
+            description = "Boot script";
+            data = /incbin/("raw-script.cmd");
+            type = "script";
+            arch = "sandbox";
+            compression = "none";
+            hash { algo = "sha256"; };
+        };
+    };
+    configurations {
+        default = "conf";
+        conf {
+            script = "script";
+        };
+    };
+};
+''')
+        args = [mkimage, '-f', its]
+        if case == 'external':
+            args.append('-E')
+        utils.run_and_log(ubman, args + [image])
+    else:
+        utils.run_and_log(ubman, [mkimage, '-A', 'sandbox', '-T', 'script',
+                                  '-C', 'none', '-d', script, image])
+    with open(image, 'rb') as inf:
+        data = bytearray(inf.read())
+    if case == 'bad_crc':
+        data[-1] ^= 1
+    elif case == 'too_large':
+        struct.pack_into('>I', data, 12, 2 * 1024 * 1024)
+    elif case == 'empty':
+        data = bytearray(512)
+    if case == 'too_large':
+        struct.pack_into('>I', data, 4, 0)
+        struct.pack_into('>I', data, 4, zlib.crc32(data[:64]))
+    return data
+
+
+def setup_script_raw_image(ubman):
+    """Create one disk for all cases, with 512 KiB non-bootable partitions."""
+    spec = 'label: gpt\n'
+    for part, case in enumerate(CASES, 1):
+        start = part * 2048
+        label = 'other' if case == 'wrong_name' else 'uboot_scr'
+        spec += f'start={start}, size=1024, name="{label}"\n'
+    disk, _ = fs_helper.setup_image(ubman, 12, None, img_size=16,
+                                    part_spec=spec)
+    with open(disk, 'r+b') as outf:
+        for part, case in enumerate(CASES, 1):
+            data = script_image(ubman, case)
+            if case == 'bad_header':
+                data[4] ^= 1
+            elif case == 'partition_size':
+                struct.pack_into('>I', data, 12, 512 * 1024)
+            elif case == 'bad_fit':
+                # Keep the header valid but corrupt the structure block.
+                offset = struct.unpack_from('>I', data, 8)[0]
+                struct.pack_into('>I', data, offset, 0xffffffff)
+            if case == 'partition_size':
+                struct.pack_into('>I', data, 4, 0)
+                struct.pack_into('>I', data, 4, zlib.crc32(data[:64]))
+            outf.seek(part * 1024 * 1024)
+            outf.write(data)
diff --git a/test/py/tests/fs_helper.py b/test/py/tests/fs_helper.py
index e3824b2c1fd..7c9577e28b1 100644
--- a/test/py/tests/fs_helper.py
+++ b/test/py/tests/fs_helper.py
@@ -8,7 +8,7 @@
 import re
 import os
 import shutil
-from subprocess import call, check_call, check_output, CalledProcessError
+from subprocess import call, check_call, check_output, CalledProcessError, run
 from subprocess import DEVNULL
 import tempfile
 
@@ -262,8 +262,8 @@ def mk_fs(config, fs_type, size, prefix, src_dir=None, 
fs_img=None, quiet=False)
         raise
 
 def setup_image(ubman, devnum, part_type, img_size=20, second_part=False,
-                basename='mmc'):
-    """Create a disk image with one or two partitions
+                basename='mmc', part_spec=None):
+    """Create a disk image with default or caller-specified partitions
 
     Args:
         ubman (ConsoleBase): Console to use
@@ -272,6 +272,8 @@ def setup_image(ubman, devnum, part_type, img_size=20, 
second_part=False,
         img_size (int): Image size in MiB
         second_part (bool): True to contain a small second partition
         basename (str): Base name to use in the filename, e.g. 'mmc'
+        part_spec (str): Optional sfdisk partition specification, overriding
+            part_type and second_part
 
     Returns:
         tuple:
@@ -281,14 +283,17 @@ def setup_image(ubman, devnum, part_type, img_size=20, 
second_part=False,
     fname = os.path.join(ubman.config.source_dir, f'{basename}{devnum}.img')
     mnt = os.path.join(ubman.config.persistent_data_dir, 'scratch')
 
-    spec = f'type={part_type:x}, size={img_size - 2}M, start=1M, bootable'
-    if second_part:
-        spec += '\ntype=c'
+    spec = part_spec
+    if spec is None:
+        spec = f'type={part_type:x}, size={img_size - 2}M, start=1M, bootable'
+        if second_part:
+            spec += '\ntype=c'
 
     try:
         check_call(f'mkdir -p {mnt}', shell=True)
         check_call(f'qemu-img create {fname} {img_size}M', shell=True)
-        check_call(f'printf "{spec}" | sfdisk {fname}', shell=True)
+        # Pass the specification directly, including quoted GPT names.
+        run(['sfdisk', fname], input=spec.encode(), check=True)
     except CalledProcessError:
         call(f'rm -f {fname}', shell=True)
         raise
diff --git a/test/py/tests/test_ut.py b/test/py/tests/test_ut.py
index 2a5acc18c61..c52d7f46ac2 100644
--- a/test/py/tests/test_ut.py
+++ b/test/py/tests/test_ut.py
@@ -16,6 +16,7 @@ import re
 import pytest
 
 import utils
+from img.script_raw import setup_script_raw_image
 # pylint: disable=E0611
 from tests import fs_helper
 from fs_helper import DiskHelper, FsHelper
@@ -755,6 +756,8 @@ def test_ut_dm_init_bootstd(ubman):
     setup_efi_image(ubman)
     setup_rauc_image(ubman)
     setup_bls_prefix_image(ubman)
+    if ubman.config.buildconfig.get('config_bootmeth_script_raw') == 'y':
+        setup_script_raw_image(ubman)
 
     # Restart so that the new mmc1.img is picked up
     ubman.restart_uboot()
-- 
2.43.0

Reply via email to