[Concept,11/12] extlinux: Process and cache parsed config during scanning

Message ID 20260324221911.3678307-12-sjg@u-boot.org
State New
Headers
Series bootstd: Support multiple bootflows per partition |

Commit Message

Simon Glass March 24, 2026, 10:19 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

The extlinux scanning code uses pxe_parse() which has no file-loading
callback, so include directives in extlinux.conf are silently ignored.
Labels defined in included files are not discovered during scanning,
but are present at boot time when pxe_boot_entry() calls pxe_prepare()
with the full getfile callback. This mismatch means entry indices from
scanning may not match the label order at boot time.

Replace pxe_parse() with parse_pxefile() + pxe_process_includes()
using a properly initialised PXE context with the extlinux_getfile
callback. Cache the parsed result in the alist context so that entry 0
triggers the parse and subsequent entries reuse the cache.

At boot time, if the config is cached, update the callback fields for
file loading and reuse the parsed menu. For bootmeths that do not
cache (e.g. PXE network boot), fall back to setting up the context
from scratch. This avoids double parsing and ensures the label list
is consistent between scan and boot.

Update the Ubuntu test image to use an include file for the rescue
label, exercising the include-processing code path. The BLS test
detects the filesystem ordering of .conf files at runtime since FAT
directory-order is not deterministic across platforms. Update the
bootctl tests for the extra multi-entry bootflows.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 boot/bootmeth_extlinux.c    | 90 ++++++++++++++++++++++++++++---------
 boot/ext_pxe_common.c       | 28 +++++++++---
 boot/pxe_utils.c            | 30 ++++++++-----
 test/boot/bootctl/bootctl.c |  8 ++++
 test/boot/bootflow.c        | 37 +++++++++++----
 test/py/img/common.py       | 11 ++++-
 test/py/img/ubuntu.py       |  9 ++--
 7 files changed, 165 insertions(+), 48 deletions(-)
  

Patch

diff --git a/boot/bootmeth_extlinux.c b/boot/bootmeth_extlinux.c
index 542daf9ab41..6f22fe19f64 100644
--- a/boot/bootmeth_extlinux.c
+++ b/boot/bootmeth_extlinux.c
@@ -114,30 +114,88 @@  static int extlinux_check_luks(struct bootflow *bflow)
 	return 0;
 }
 
+/**
+ * extlinux_parse_config() - Parse the extlinux config and cache the result
+ *
+ * Parses the configuration file including any include directives, and caches
+ * the result in plat->ctx.cfg. The filesystem must still be accessible for
+ * loading includes.
+ *
+ * @dev: Bootmeth device (needed for file-loading callback)
+ * @bflow: Bootflow containing the config buffer
+ * Return: 0 if OK, -ve on error
+ */
+static int extlinux_parse_config(struct udevice *dev, struct bootflow *bflow,
+				 struct pxe_context *ctx)
+{
+	struct extlinux_plat *plat = dev_get_plat(dev);
+	struct abuf buf;
+	ulong addr;
+	int ret;
+
+	plat->info.dev = dev;
+	plat->info.bflow = bflow;
+	ret = pxe_setup_ctx(ctx, extlinux_getfile, &plat->info, true,
+			    bflow->fname, false, plat->use_fallback, bflow);
+	if (ret)
+		return log_msg_ret("ctx", ret);
+	ctx->quiet = true;
+	ctx->pxe_file_size = bflow->size;
+
+	addr = map_to_sysmem(bflow->buf);
+	abuf_init_addr(&buf, addr, bflow->size);
+	ctx->cfg = parse_pxefile(ctx, &buf);
+	if (!ctx->cfg) {
+		pxe_destroy_ctx(ctx);
+		return log_msg_ret("prs", -EINVAL);
+	}
+
+	ret = pxe_process_includes(ctx, ctx->cfg, addr);
+	if (ret) {
+		pxe_menu_uninit(ctx->cfg);
+		ctx->cfg = NULL;
+		pxe_destroy_ctx(ctx);
+		return log_msg_ret("inc", ret);
+	}
+
+	return 0;
+}
+
 /**
  * extlinux_fill_info() - Decode the extlinux file to find out its info
  *
- * Uses pxe_parse() to parse the configuration file and extract the label
- * selected by @bflow->entry to use as the bootflow OS name.
+ * On the first call (entry 0), calls extlinux_parse_config() to parse
+ * into a context from the alist. For entry > 0, reuses the cached
+ * context.
  *
+ * @dev: Bootmeth device (needed for file-loading callback)
  * @bflow: Bootflow to process (entry selects which label)
  * Return: 0 if OK, -ENOENT if entry index exceeds available labels, other
  * -ve on error
  */
-static int extlinux_fill_info(struct bootflow *bflow)
+static int extlinux_fill_info(struct udevice *dev, struct bootflow *bflow)
 {
+	struct extlinux_priv *priv = dev_get_priv(dev);
 	struct pxe_context *ctx;
 	struct pxe_label *label;
 	const char *name;
-	ulong addr;
 	int i;
 
 	log_debug("parsing bflow file size %x entry %d\n", bflow->size,
 		  bflow->entry);
-	addr = map_to_sysmem(bflow->buf);
-	ctx = pxe_parse(addr, bflow->size, bflow->fname);
+
+	ctx = extlinux_get_ctx(priv, bflow);
 	if (!ctx)
-		return log_msg_ret("prs", -EINVAL);
+		return log_msg_ret("ctx", -ENOMEM);
+
+	/* Parse the config on first entry; reuse the cached result after */
+	if (!ctx->cfg) {
+		int ret;
+
+		ret = extlinux_parse_config(dev, bflow, ctx);
+		if (ret)
+			return log_msg_ret("prs", ret);
+	}
 
 	/* Walk to the requested label */
 	i = 0;
@@ -147,29 +205,21 @@  static int extlinux_fill_info(struct bootflow *bflow)
 		i++;
 	}
 
-	/* No more entries at this index */
-	pxe_cleanup(ctx);
 	return -ENOENT;
 
 found:
 	name = label->menu ? label->menu : label->name;
 	if (name) {
 		bflow->os_name = strdup(name);
-		if (!bflow->os_name) {
-			pxe_cleanup(ctx);
-			return log_msg_ret("os", -ENOMEM);
-		}
+		if (!bflow->os_name)
+			return log_msg_ret("osn", -ENOMEM);
 	}
 	if (label->name) {
 		bflow->entry_name = strdup(label->name);
-		if (!bflow->entry_name) {
-			pxe_cleanup(ctx);
-			return log_msg_ret("xnt", -ENOMEM);
-		}
+		if (!bflow->entry_name)
+			return log_msg_ret("ent", -ENOMEM);
 	}
 
-	pxe_cleanup(ctx);
-
 	return 0;
 }
 
@@ -215,7 +265,7 @@  static int extlinux_read_bootflow(struct udevice *dev, struct bootflow *bflow)
 	if (ret)
 		return log_msg_ret("read", ret);
 
-	ret = extlinux_fill_info(bflow);
+	ret = extlinux_fill_info(dev, bflow);
 	if (ret)
 		return log_msg_ret("inf", ret);
 
diff --git a/boot/ext_pxe_common.c b/boot/ext_pxe_common.c
index 4ea7f8f455d..04b6566ef99 100644
--- a/boot/ext_pxe_common.c
+++ b/boot/ext_pxe_common.c
@@ -129,18 +129,36 @@  int extlinux_boot(struct udevice *dev, struct bootflow *bflow,
 	if (ctx->label) {
 		ctx->fake_go = bflow->flags & BOOTFLOWF_FAKE_GO;
 		ret = pxe_boot(ctx);
+		if (ret)
+			return log_msg_ret("pxb", -EFAULT);
+		return 0;
+	}
+
+	/*
+	 * If the config was cached during scanning, update the callback fields
+	 * for file loading. Otherwise set up the context from scratch.
+	 */
+	if (ctx->cfg) {
+		struct extlinux_plat *plat = dev_get_plat(dev);
+
+		plat->info.dev = dev;
+		plat->info.bflow = bflow;
+		ctx->getfile = getfile;
+		ctx->userdata = &plat->info;
+		ctx->bflow = bflow;
 	} else {
 		ret = extlinux_setup(dev, bflow, getfile, allow_abs_path,
 				     bootfile, ctx);
 		if (ret)
 			return log_msg_ret("elb", ret);
-		ctx->restart = restart;
-		addr = map_to_sysmem(bflow->buf);
-
-		ret = pxe_boot_entry(ctx, addr, bflow->entry);
 	}
+	ctx->restart = restart;
+	ctx->fake_go = bflow->flags & BOOTFLOWF_FAKE_GO;
+	addr = map_to_sysmem(bflow->buf);
+
+	ret = pxe_boot_entry(ctx, addr, bflow->entry);
 	if (ret)
-		return log_msg_ret("elb", -EFAULT);
+		return log_msg_ret("ent", -EFAULT);
 
 	return 0;
 }
diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c
index a7c5e87bef0..727806e91b0 100644
--- a/boot/pxe_utils.c
+++ b/boot/pxe_utils.c
@@ -1513,30 +1513,40 @@  int pxe_boot(struct pxe_context *ctx)
 
 int pxe_boot_entry(struct pxe_context *ctx, ulong addr, int entry)
 {
+	bool free_cfg = false;
 	struct pxe_label *label;
 	struct pxe_menu *cfg;
-	int ret, i = 0;
-	void *ptr;
+	int i = 0;
+	int ret;
 
-	ptr = map_sysmem(addr, 0);
-	ctx->pxe_file_size = strnlen(ptr, SZ_64K);
-	unmap_sysmem(ptr);
+	/* Use cached config if available, otherwise parse */
+	cfg = ctx->cfg;
+	if (!cfg) {
+		void *ptr;
 
-	cfg = pxe_prepare(ctx, addr, false);
-	if (!cfg)
-		return log_msg_ret("prp", -EINVAL);
+		ptr = map_sysmem(addr, 0);
+		ctx->pxe_file_size = strnlen(ptr, SZ_64K);
+		unmap_sysmem(ptr);
+
+		cfg = pxe_prepare(ctx, addr, false);
+		if (!cfg)
+			return log_msg_ret("prp", -EINVAL);
+		free_cfg = true;
+	}
 
 	list_for_each_entry(label, &cfg->labels, list) {
 		if (i == entry)
 			goto found;
 		i++;
 	}
-	pxe_menu_uninit(cfg);
+	if (free_cfg)
+		pxe_menu_uninit(cfg);
 	return log_msg_ret("lab", -ENOENT);
 
 found:
 	ret = label_boot(ctx, label);
-	pxe_menu_uninit(cfg);
+	if (free_cfg)
+		pxe_menu_uninit(cfg);
 
 	return ret;
 }
diff --git a/test/boot/bootctl/bootctl.c b/test/boot/bootctl/bootctl.c
index ababe6f7b21..a3048a25489 100644
--- a/test/boot/bootctl/bootctl.c
+++ b/test/boot/bootctl/bootctl.c
@@ -101,6 +101,10 @@  static int bootctl_oslist_usb(struct unit_test_state *uts)
 	ut_assertok(bc_oslist_next(dev, &iter, &info));
 	ut_asserteq_str("hub1.p4.usb_mass_storage.lun0.bootdev.part_1", bflow->name);
 
+	/* second entry from flash3 (Ubuntu has two extlinux labels) */
+	ut_assertok(bc_oslist_next(dev, &iter, &info));
+	ut_asserteq_str("hub1.p4.usb_mass_storage.lun0.bootdev.part_1", bflow->name);
+
 	ut_asserteq(-ENODEV, bc_oslist_next(dev, &iter, &info));
 
 	return 0;
@@ -456,6 +460,10 @@  static int check_multiboot_ui(struct unit_test_state *uts,
 	ut_assertok(bc_oslist_next(oslist_dev, &iter, &info[0]));
 	ut_asserteq_str("mmc11.bootdev.part_1", info[0].bflow.name);
 
+	/* skip the second mmc11 entry (Ubuntu has two extlinux labels) */
+	ut_assertok(bc_oslist_next(oslist_dev, &iter, &info[1]));
+	ut_asserteq_str("mmc11.bootdev.part_1", info[1].bflow.name);
+
 	ut_assertok(bc_oslist_next(oslist_dev, &iter, &info[1]));
 	ut_asserteq_str("hub1.p4.usb_mass_storage.lun0.bootdev.part_1",
 			info[1].bflow.name);
diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c
index 7a0e4f9acb7..59037f04ea3 100644
--- a/test/boot/bootflow.c
+++ b/test/boot/bootflow.c
@@ -292,7 +292,7 @@  static int bootflow_scan_boot(struct unit_test_state *uts)
 	ut_assertok(run_command("bootflow scan -b", 0));
 	ut_assert_nextline(
 		"** Booting bootflow 'mmc1.bootdev.part_1' with extlinux");
-	ut_assert_nextline("Ignoring unknown command: ui");
+	/* cached parse from scanning suppresses parser warnings */
 
 	/*
 	 * We expect it to get through to boot although sandbox always returns
@@ -636,7 +636,7 @@  static int bootflow_cmd_boot(struct unit_test_state *uts)
 	ut_asserteq(1, run_command("bootflow boot", 0));
 	ut_assert_nextline(
 		"** Booting bootflow 'mmc1.bootdev.part_1' with extlinux");
-	ut_assert_nextline("Ignoring unknown command: ui");
+	/* cached parse from scanning suppresses parser warnings */
 
 	/*
 	 * We expect it to get through to boot although sandbox always returns
@@ -1861,6 +1861,8 @@  static int bootflow_cmd_bls(struct unit_test_state *uts)
 {
 	struct bootstd_priv *std;
 	const char **old_order;
+	struct bootflow *bflow;
+	bool test_first;
 
 	ut_assertok(prep_mmc_bootdev(uts, "mmc15", true, &old_order));
 	ut_assertok(run_command("bootflow scan", 0));
@@ -1871,13 +1873,25 @@  static int bootflow_cmd_bls(struct unit_test_state *uts)
 	free(std->bootdev_order);
 	std->bootdev_order = old_order;
 
+	/*
+	 * BLS entry order depends on the filesystem, so detect which
+	 * .conf file came first and check accordingly
+	 */
+	bflow = alist_getw(&std->bootflows, 1, struct bootflow);
+	test_first = !strcmp(bflow->os_name, "Test Boot");
+
 	ut_assertok(run_command("bootflow list", 0));
 	ut_assert_nextline("Showing all bootflows");
 	ut_assert_nextline(HEADER);
 	ut_assert_nextlinen("---");
 	ut_assert_nextlinen("  0  extlinux");
-	ut_assert_nextline("  1  bls          ready   mmc          1    0     mmc15.bootdev.part_1      /loader/entries/6.8.0.conf");
-	ut_assert_nextline("  2  bls          ready   mmc          1    1     mmc15.bootdev.part_1      /loader/entries/6.8.0-rescue.conf");
+	if (test_first) {
+		ut_assert_nextline("  1  bls          ready   mmc          1    0     mmc15.bootdev.part_1      /loader/entries/6.8.0.conf");
+		ut_assert_nextline("  2  bls          ready   mmc          1    1     mmc15.bootdev.part_1      /loader/entries/6.8.0-rescue.conf");
+	} else {
+		ut_assert_nextline("  1  bls          ready   mmc          1    0     mmc15.bootdev.part_1      /loader/entries/6.8.0-rescue.conf");
+		ut_assert_nextline("  2  bls          ready   mmc          1    1     mmc15.bootdev.part_1      /loader/entries/6.8.0.conf");
+	}
 	ut_assert_nextlinen("---");
 	ut_assert_nextline("(3 bootflows, 3 valid)");
 	ut_assert_console_end();
@@ -1892,12 +1906,15 @@  static int bootflow_cmd_bls(struct unit_test_state *uts)
 	ut_assert_nextline("Method:    bls");
 	ut_assert_nextline("State:     ready");
 	ut_assert_nextline("Partition: 1");
-	ut_assert_nextline("Entry:     0: Test Boot");
+	ut_assert_nextline("Entry:     0: %s",
+			   test_first ? "Test Boot" : "Rescue Boot");
 	if (IS_ENABLED(CONFIG_BLK_LUKS))
 		ut_assert_nextline("Encrypted: no");
 	ut_assert_nextline("Subdir:    (none)");
-	ut_assert_nextline("Filename:  /loader/entries/6.8.0.conf");
-	ut_assert_skip_to_line("OS:        Test Boot");
+	ut_assert_nextline("Filename:  /loader/entries/%s",
+			   test_first ? "6.8.0.conf" : "6.8.0-rescue.conf");
+	ut_assert_skip_to_line("OS:        %s",
+			       test_first ? "Test Boot" : "Rescue Boot");
 	ut_assert_skip_to_line("Error:     0");
 	ut_assert_console_end();
 
@@ -1906,8 +1923,10 @@  static int bootflow_cmd_bls(struct unit_test_state *uts)
 	ut_assert_console_end();
 	ut_assertok(run_command("bootflow info", 0));
 	ut_assert_nextline("Name:      mmc15.bootdev.part_1");
-	ut_assert_skip_to_line("Filename:  /loader/entries/6.8.0-rescue.conf");
-	ut_assert_skip_to_line("OS:        Rescue Boot");
+	ut_assert_skip_to_line("Filename:  /loader/entries/%s",
+			       test_first ? "6.8.0-rescue.conf" : "6.8.0.conf");
+	ut_assert_skip_to_line("OS:        %s",
+			       test_first ? "Rescue Boot" : "Test Boot");
 	ut_assert_skip_to_line("Error:     0");
 	ut_assert_console_end();
 
diff --git a/test/py/img/common.py b/test/py/img/common.py
index 25edc84944e..6a1176e3dc5 100644
--- a/test/py/img/common.py
+++ b/test/py/img/common.py
@@ -34,7 +34,8 @@  def copy_partition(ubman, fsfile, outname):
 
 def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir,
                          script, part2_size=1, use_fde=0, luks_kdf='pbkdf2',
-                         encrypt_keyfile=None, master_keyfile=None):
+                         encrypt_keyfile=None, master_keyfile=None,
+                         extra_conf=None):
     """Create a 20MB disk image with a single FAT partition
 
     Args:
@@ -54,6 +55,8 @@  def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir,
             If provided, takes precedence over passphrase.
         master_keyfile (str, optional): Path to file containing the raw master
             key. If provided, this exact key is used as the LUKS master key.
+        extra_conf (dict, optional): Extra files to create in the extlinux
+            directory, as {filename: content} pairs.
     """
     fsh = FsHelper(config, 'vfat', 18, prefix=basename)
     fsh.setup()
@@ -65,6 +68,12 @@  def setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir,
     with open(conf, 'w', encoding='ascii') as fd:
         print(script, file=fd)
 
+    if extra_conf:
+        for fname, content in extra_conf.items():
+            with open(os.path.join(ext, fname), 'w',
+                      encoding='ascii') as fd:
+                print(content, file=fd)
+
     inf = os.path.join(config.persistent_data_dir, 'inf')
     with open(inf, 'wb') as fd:
         fd.write(gzip.compress(b'vmlinux'))
diff --git a/test/py/img/ubuntu.py b/test/py/img/ubuntu.py
index 1f3016b79a6..3ef089da6b6 100644
--- a/test/py/img/ubuntu.py
+++ b/test/py/img/ubuntu.py
@@ -51,13 +51,16 @@  label l0
 
 	append root=/dev/disk/by-uuid/bcfdda4a-8249-4f40-9f0f-7c1a76b6cbe8 ro earlycon
 
-label l0r
+include rescue.conf
+''' % (version, vmlinux, initrd)
+    rescue = '''label l0r
 	menu label Ubuntu %s 6.8.0-53-generic (rescue target)
 	linux /boot/%s
 	initrd /boot/%s
-''' % ((version, vmlinux, initrd) * 2)
+''' % (version, vmlinux, initrd)
     setup_extlinux_image(config, log, devnum, basename, vmlinux, initrd, dtbdir,
                          script, part2_size=60 if use_fde else 1,
                          use_fde=use_fde, luks_kdf=luks_kdf,
                          encrypt_keyfile=encrypt_keyfile,
-                         master_keyfile=master_keyfile)
+                         master_keyfile=master_keyfile,
+                         extra_conf={'rescue.conf': rescue})