Merge patch series "Universal Payload initial series"

Simon Glass <sjg@chromium.org> says:

Universal Payload (UPL) is an Industry Standard for firmware
components[1]. UPL is designed to improve interoperability within the
firmware industry, allowing mixing and matching of projects with less
friction and fewer project-specific implementations. UPL is
cross-platform, supporting ARM, x86 and RISC-V initially.

This series provides some initial support for this, targeting 0.9.1 and
sandbox only.

Features still to come include:
- Support for architectures
- FIT validation
- Handoff validation
- Interoperability tests
diff --git a/MAINTAINERS b/MAINTAINERS
index ddcb712..44e9c2f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1709,6 +1709,19 @@
 S:	Maintained
 F:	drivers/ufs/
 
+UPL
+M:	Simon Glass <sjg@chromium.org>
+S:	Maintained
+T:	git https://source.denx.de/u-boot/custodians/u-boot-dm.git
+F:	boot/upl*
+F:	cmd/upl.c
+F:	common/spl/spl_upl.c
+F:	doc/usage/upl.rst
+F:	doc/usage/cmd/upl.rst
+F:	include/upl.h
+F:	test/boot/upl.c
+F:	test/py/tests/test_upl.py
+
 USB
 M:	Marek Vasut <marex@denx.de>
 S:	Maintained
diff --git a/Makefile b/Makefile
index 2861b4d..b35a472 100644
--- a/Makefile
+++ b/Makefile
@@ -1473,8 +1473,10 @@
 u-boot-lzma.img: u-boot.bin.lzma FORCE
 	$(call if_changed,mkimage)
 
+fit_image := $(if $(CONFIG_SANDBOX_VPL),u-boot,u-boot-nodtb.bin)
+
 u-boot-dtb.img u-boot.img u-boot.kwb u-boot.pbl u-boot-ivt.img: \
-		$(if $(CONFIG_SPL_LOAD_FIT),u-boot-nodtb.bin \
+		$(if $(CONFIG_SPL_LOAD_FIT),$(fit_image) \
 			$(if $(CONFIG_OF_SEPARATE)$(CONFIG_OF_EMBED)$(CONFIG_SANDBOX),dts/dt.dtb) \
 		,$(UBOOT_BIN)) FORCE
 	$(call if_changed,mkimage)
diff --git a/arch/sandbox/cpu/cpu.c b/arch/sandbox/cpu/cpu.c
index 0ed85b3..4f15a56 100644
--- a/arch/sandbox/cpu/cpu.c
+++ b/arch/sandbox/cpu/cpu.c
@@ -340,6 +340,8 @@
 	int err;
 	int fd;
 
+	if (gd->fdt_blob)
+		return (void *)gd->fdt_blob;
 	blob = map_sysmem(CONFIG_SYS_FDT_LOAD_ADDR, 0);
 	*ret = 0;
 	if (!state->fdt_fname) {
diff --git a/arch/sandbox/cpu/os.c b/arch/sandbox/cpu/os.c
index 46ff305..f5c9a8a 100644
--- a/arch/sandbox/cpu/os.c
+++ b/arch/sandbox/cpu/os.c
@@ -47,12 +47,24 @@
 
 ssize_t os_read(int fd, void *buf, size_t count)
 {
-	return read(fd, buf, count);
+	ssize_t ret;
+
+	ret = read(fd, buf, count);
+	if (ret == -1)
+		return -errno;
+
+	return ret;
 }
 
 ssize_t os_write(int fd, const void *buf, size_t count)
 {
-	return write(fd, buf, count);
+	ssize_t ret;
+
+	ret = write(fd, buf, count);
+	if (ret == -1)
+		return -errno;
+
+	return ret;
 }
 
 int os_printf(const char *fmt, ...)
@@ -69,6 +81,8 @@
 
 off_t os_lseek(int fd, off_t offset, int whence)
 {
+	off_t ret;
+
 	if (whence == OS_SEEK_SET)
 		whence = SEEK_SET;
 	else if (whence == OS_SEEK_CUR)
@@ -77,7 +91,11 @@
 		whence = SEEK_END;
 	else
 		os_exit(1);
-	return lseek(fd, offset, whence);
+	ret = lseek(fd, offset, whence);
+	if (ret == -1)
+		return -errno;
+
+	return ret;
 }
 
 int os_open(const char *pathname, int os_flags)
@@ -808,7 +826,7 @@
  * @count: Number of arguments in @add_args
  * Return: 0 if OK, -ENOMEM if out of memory
  */
-static int add_args(char ***argvp, char *add_args[], int count)
+static int add_args(char ***argvp, const char *add_args[], int count)
 {
 	char **argv, **ap;
 	int argc;
@@ -859,7 +877,7 @@
 	struct sandbox_state *state = state_get_current();
 	char mem_fname[30];
 	int fd, err;
-	char *extra_args[5];
+	const char *extra_args[5];
 	char **argv = state->argv;
 	int argc;
 #ifdef DEBUG
@@ -964,7 +982,7 @@
 	p = strstr(fname, subdir);
 	if (p) {
 		if (*next_prefix)
-			/* e.g. ".../tpl/u-boot-spl"  to "../spl/u-boot-spl" */
+			/* e.g. ".../tpl/u-boot-spl"  to ".../spl/u-boot-spl" */
 			memcpy(p + 1, next_prefix, strlen(next_prefix));
 		else
 			/* e.g. ".../spl/u-boot" to ".../u-boot" */
diff --git a/arch/sandbox/cpu/spl.c b/arch/sandbox/cpu/spl.c
index 9ad9da6..bcb1ca1 100644
--- a/arch/sandbox/cpu/spl.c
+++ b/arch/sandbox/cpu/spl.c
@@ -3,13 +3,18 @@
  * Copyright (c) 2016 Google, Inc
  */
 
+#define LOG_CATEGORY	LOGC_BOOT
+
 #include <dm.h>
 #include <hang.h>
 #include <handoff.h>
+#include <image.h>
 #include <init.h>
 #include <log.h>
+#include <mapmem.h>
 #include <os.h>
 #include <spl.h>
+#include <upl.h>
 #include <asm/global_data.h>
 #include <asm/spl.h>
 #include <asm/state.h>
@@ -51,7 +56,8 @@
 void board_boot_order(u32 *spl_boot_list)
 {
 	spl_boot_list[0] = BOOT_DEVICE_VBE;
-	spl_boot_list[1] = BOOT_DEVICE_BOARD;
+	spl_boot_list[1] = BOOT_DEVICE_UPL;
+	spl_boot_list[2] = BOOT_DEVICE_BOARD;
 }
 
 static int spl_board_load_file(struct spl_image_info *spl_image,
@@ -179,3 +185,115 @@
 
 	return 0;
 }
+
+/* Context used to hold file descriptor */
+struct load_ctx {
+	int fd;
+};
+
+static ulong read_fit_image(struct spl_load_info *load, ulong offset,
+			    ulong size, void *buf)
+{
+	struct load_ctx *load_ctx = load->priv;
+	off_t ret;
+	ssize_t res;
+
+	ret = os_lseek(load_ctx->fd, offset, OS_SEEK_SET);
+	if (ret < 0) {
+		printf("Failed to seek to %zx, got %zx\n", offset, ret);
+		return log_msg_ret("lse", ret);
+	}
+
+	res = os_read(load_ctx->fd, buf, size);
+	if (res < 0) {
+		printf("Failed to read %lx bytes, got %ld\n", size, res);
+		return log_msg_ret("osr", res);
+	}
+
+	return size;
+}
+
+int sandbox_spl_load_fit(char *fname, int maxlen, struct spl_image_info *image)
+{
+	struct legacy_img_hdr *header;
+	struct load_ctx load_ctx;
+	struct spl_load_info load;
+	int ret;
+	int fd;
+
+	memset(&load, '\0', sizeof(load));
+	spl_set_bl_len(&load, IS_ENABLED(CONFIG_SPL_LOAD_BLOCK) ? 512 : 1);
+	load.read = read_fit_image;
+
+	ret = sandbox_find_next_phase(fname, maxlen, true);
+	if (ret) {
+		printf("%s not found, error %d\n", fname, ret);
+		return log_msg_ret("nph", ret);
+	}
+
+	header = spl_get_load_buffer(-sizeof(*header), sizeof(*header));
+
+	log_debug("reading from %s\n", fname);
+	fd = os_open(fname, OS_O_RDONLY);
+	if (fd < 0) {
+		printf("Failed to open '%s'\n", fname);
+		return log_msg_ret("ope", -errno);
+	}
+	ret = os_read(fd, header, sizeof(*header));
+	if (ret != sizeof(*header)) {
+		printf("Failed to read %lx bytes, got %d\n", sizeof(*header),
+		       ret);
+		return log_msg_ret("rea", ret);
+	}
+	load_ctx.fd = fd;
+
+	load.priv = &load_ctx;
+
+	ret = spl_load_simple_fit(image, &load, 0, header);
+	if (ret)
+		return log_msg_ret("slf", ret);
+
+	return 0;
+}
+
+static int upl_load_from_image(struct spl_image_info *spl_image,
+			       struct spl_boot_device *bootdev)
+{
+	long long size;
+	char *fname;
+	int ret, fd;
+	ulong addr;
+
+	if (!CONFIG_IS_ENABLED(UPL_OUT))
+		return -ENOTSUPP;
+
+	spl_upl_init();
+	fname = os_malloc(256);
+
+	ret = sandbox_spl_load_fit(fname, 256, spl_image);
+	if (ret)
+		return log_msg_ret("fit", ret);
+	spl_image->flags = SPL_SANDBOXF_ARG_IS_BUF;
+	spl_image->arg = map_sysmem(spl_image->load_addr, 0);
+	/* size is set by load_simple_fit(), offset is left as 0 */
+
+	/* now read the whole FIT into memory */
+	fd = os_open(fname, OS_O_RDONLY);
+	if (fd < 0)
+		return log_msg_ret("op2", -ENOENT);
+	if (os_get_filesize(fname,  &size))
+		return log_msg_ret("fis", -ENOENT);
+
+	/* place it after the loaded image, allowing plenty of space */
+	addr = ALIGN(spl_image->load_addr + size, 0x1000);
+	log_debug("Loading whole FIT to %lx\n", addr);
+	if (os_read(fd, map_sysmem(addr, 0), size) != size)
+		return log_msg_ret("rea", -EIO);
+	os_close(fd);
+
+	/* tell UPL where it is */
+	upl_set_fit_addr(addr);
+
+	return 0;
+}
+SPL_LOAD_IMAGE_METHOD("upl", 4, BOOT_DEVICE_UPL, upl_load_from_image);
diff --git a/arch/sandbox/cpu/start.c b/arch/sandbox/cpu/start.c
index dce8041..9ad5d46 100644
--- a/arch/sandbox/cpu/start.c
+++ b/arch/sandbox/cpu/start.c
@@ -431,6 +431,14 @@
 }
 SANDBOX_CMDLINE_OPT(autoboot_keyed, 0, "Allow keyed autoboot");
 
+static int sandbox_cmdline_cb_upl(struct sandbox_state *state, const char *arg)
+{
+	state->upl = true;
+
+	return 0;
+}
+SANDBOX_CMDLINE_OPT(upl, 0, "Enable Universal Payload (UPL)");
+
 static void setup_ram_buf(struct sandbox_state *state)
 {
 	/* Zero the RAM buffer if we didn't read it, to keep valgrind happy */
@@ -483,6 +491,9 @@
 
 	text_base = os_find_text_base();
 
+	memset(&data, '\0', sizeof(data));
+	gd = &data;
+
 	/*
 	 * This must be the first invocation of os_malloc() to have
 	 * state->ram_buf in the low 4 GiB.
@@ -501,8 +512,6 @@
 		os_exit(1);
 	memcpy(os_argv, argv, size);
 
-	memset(&data, '\0', sizeof(data));
-	gd = &data;
 	gd->arch.text_base = text_base;
 
 	state = state_get_current();
@@ -539,6 +548,9 @@
 			goto err;
 	}
 
+	if (state->upl)
+		gd->flags |= GD_FLG_UPL;
+
 #if CONFIG_IS_ENABLED(SYS_MALLOC_F)
 	gd->malloc_base = CFG_MALLOC_F_ADDR;
 #endif
@@ -557,7 +569,7 @@
 	log_debug("debug: %s\n", __func__);
 
 	/* Do pre- and post-relocation init */
-	board_init_f(0);
+	board_init_f(gd->flags);
 
 	board_init_r(gd->new_gd, 0);
 
diff --git a/arch/sandbox/include/asm/spl.h b/arch/sandbox/include/asm/spl.h
index 4fab24c..d824b21 100644
--- a/arch/sandbox/include/asm/spl.h
+++ b/arch/sandbox/include/asm/spl.h
@@ -6,6 +6,8 @@
 #ifndef __asm_spl_h
 #define __asm_spl_h
 
+struct spl_image_info;
+
 enum {
 	BOOT_DEVICE_MMC1,
 	BOOT_DEVICE_MMC2,
@@ -16,6 +18,7 @@
 	BOOT_DEVICE_NOR,
 	BOOT_DEVICE_SPI,
 	BOOT_DEVICE_NAND,
+	BOOT_DEVICE_UPL,
 };
 
 /**
@@ -31,4 +34,16 @@
  */
 int sandbox_find_next_phase(char *fname, int maxlen, bool use_img);
 
+/**
+ * sandbox_spl_load_fit() - Load the next phase from a FIT
+ *
+ * Loads a FIT containing the next phase and sets it up for booting
+ *
+ * @fname: Returns filename loaded
+ * @maxlen: Maximum length for @fname including \0
+ * @image: Place to put SPL-image information
+ * Return: 0 if OK, -ve on error
+ */
+int sandbox_spl_load_fit(char *fname, int maxlen, struct spl_image_info *image);
+
 #endif
diff --git a/arch/sandbox/include/asm/state.h b/arch/sandbox/include/asm/state.h
index c84a1f7..6b50473 100644
--- a/arch/sandbox/include/asm/state.h
+++ b/arch/sandbox/include/asm/state.h
@@ -97,6 +97,7 @@
 	bool autoboot_keyed;		/* Use keyed-autoboot feature */
 	bool disable_eth;		/* Disable Ethernet devices */
 	bool disable_sf_bootdevs;	/* Don't bind SPI flash bootdevs */
+	bool upl;			/* Enable Universal Payload (UPL) */
 
 	/* Pointer to information for each SPI bus/cs */
 	struct sandbox_spi_info spi[CONFIG_SANDBOX_SPI_MAX_BUS]
diff --git a/boot/Kconfig b/boot/Kconfig
index 940389d..7ac3457 100644
--- a/boot/Kconfig
+++ b/boot/Kconfig
@@ -745,6 +745,76 @@
 	  This provides a way to try out standard boot on an existing boot flow.
 	  It is not enabled by default to save space.
 
+config UPL
+	bool "upl - Universal Payload Specification"
+	imply CMD_UPL
+	imply UPL_READ
+	imply UPL_WRITE
+	imply SPL_UPL if SPL
+	help
+	  Provides support for UPL payloads and handoff information. U-Boot
+	  supports generating and accepting handoff information. The mkimage
+	  tool will eventually support creating payloads.
+
+if UPL
+
+config UPL_READ
+	bool "upl - Support reading a Universal Payload handoff"
+	help
+	  Provides support for decoding a UPL-format payload into a C structure
+	  which can be used elsewhere in U-Boot. This is just the reading
+	  implementation, useful for trying it out. See UPL_IN for how
+	  to tell U-Boot to actually read it on startup and use it for memory
+	  and device information, etc.
+
+config UPL_WRITE
+	bool "upl - Support writing a Universal Payload handoff"
+	help
+	  Provides support for encoding a UPL-format payload from a C structure
+	  so it can be passed to another program. This is just the writing
+	  implementation, useful for trying it out. See SPL_UPL_OUT
+	  for how to tell U-Boot SPL to actually write it before jumping to
+	  the next phase.
+
+config UPL_IN
+	bool "upl - Read the UPL handoff on startup"
+	select UPL_READ
+	help
+	  Read an SPL handoff when U-Boot starts and use it to provide
+	  devices, memory layout, etc. required by U-Boot. This allows U-Boot
+	  to function as a payload in the meaning of the specification.
+
+if SPL
+
+config SPL_UPL
+	bool "Write a UPL handoff in SPL"
+	imply SPL_UPL_OUT
+	help
+	  This tells SPL to write a UPL handoff and pass it to the next phase
+	  (e.g. to U-Boot or another program which SPL loads and runs). THis
+	  provides information to help that program run correctly and
+	  efficiently on the machine.
+
+config SPL_UPL_WRITE
+	bool  # upl - Support writing a Universal Payload handoff in SPL
+	select SPL_BLOBLIST
+	help
+	  Provides support for encoding a UPL-format payload from a C structure
+	  so it can be passed to another program. This is just the writing
+	  implementation, useful for trying it out.
+
+config SPL_UPL_OUT
+	bool "upl - Support writing a Universal Payload handoff in SPL"
+	select SPL_UPL_WRITE
+	help
+	  Provides support for encoding a UPL-format payload and passing it to
+	  the next firmware phase. This allows U-Boot SPL to function as
+	  Platform Init in the meaning of the specification.
+
+endif  # SPL
+
+endif  # UPL
+
 endif # BOOTSTD
 
 config LEGACY_IMAGE_FORMAT
diff --git a/boot/Makefile b/boot/Makefile
index dff6f99..f4675d6 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -43,6 +43,10 @@
 obj-$(CONFIG_$(SPL_TPL_)OF_LIBFDT) += fdt_support.o
 obj-$(CONFIG_$(SPL_TPL_)FDT_SIMPLEFB) += fdt_simplefb.o
 
+obj-$(CONFIG_$(SPL_TPL_)UPL) += upl_common.o
+obj-$(CONFIG_$(SPL_TPL_)UPL_READ) += upl_read.o
+obj-$(CONFIG_$(SPL_TPL_)UPL_WRITE) += upl_write.o
+
 obj-$(CONFIG_$(SPL_TPL_)OF_LIBFDT) += image-fdt.o
 obj-$(CONFIG_$(SPL_TPL_)FIT_SIGNATURE) += fdt_region.o
 obj-$(CONFIG_$(SPL_TPL_)FIT) += image-fit.o
diff --git a/boot/image-fit.c b/boot/image-fit.c
index 9253f81..7d56f0b 100644
--- a/boot/image-fit.c
+++ b/boot/image-fit.c
@@ -36,6 +36,7 @@
 #include <bootm.h>
 #include <image.h>
 #include <bootstage.h>
+#include <upl.h>
 #include <u-boot/crc.h>
 
 /*****************************************************************************/
@@ -2294,6 +2295,8 @@
 
 	bootstage_mark(bootstage_id + BOOTSTAGE_SUB_LOAD);
 
+	upl_add_image(fit, noffset, load, len);
+
 	*datap = load;
 	*lenp = len;
 	if (fit_unamep)
diff --git a/boot/upl_common.c b/boot/upl_common.c
new file mode 100644
index 0000000..3924423
--- /dev/null
+++ b/boot/upl_common.c
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * UPL handoff command functions
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <string.h>
+#include <upl.h>
+
+/* Names of bootmodes */
+const char *const bootmode_names[UPLBM_COUNT] = {
+	[UPLBM_FULL]	= "full",
+	[UPLBM_MINIMAL]	= "minimal",
+	[UPLBM_FAST]	= "fast",
+	[UPLBM_DIAG]	= "diag",
+	[UPLBM_DEFAULT]	= "default",
+	[UPLBM_S2]	= "s2",
+	[UPLBM_S3]	= "s3",
+	[UPLBM_S4]	= "s4",
+	[UPLBM_S5]	= "s5",
+	[UPLBM_FACTORY]	= "factory",
+	[UPLBM_FLASH]	= "flash",
+	[UPLBM_RECOVERY] = "recovery",
+};
+
+/* Names of memory usages */
+const char *const usage_names[UPLUS_COUNT] = {
+	[UPLUS_ACPI_RECLAIM]	= "acpi-reclaim",
+	[UPLUS_ACPI_NVS]	= "acpi-nvs",
+	[UPLUS_BOOT_CODE]	= "boot-code",
+	[UPLUS_BOOT_DATA]	= "boot-data",
+	[UPLUS_RUNTIME_CODE]	= "runtime-code",
+	[UPLUS_RUNTIME_DATA]	= "runtime-data",
+};
+
+/* Names of access types */
+const char *const access_types[UPLUS_COUNT] = {
+	[UPLAT_MMIO]	= "mmio",
+	[UPLAT_IO]	= "io",
+};
+
+/* Names of graphics formats */
+const char *const graphics_formats[UPLUS_COUNT] = {
+	[UPLGF_ARGB32]	= "a8r8g8b8",
+	[UPLGF_ABGR32]	= "a8b8g8r8",
+	[UPLGF_ABGR64]	= "a16b16g16r16",
+};
+
+void upl_init(struct upl *upl)
+{
+	memset(upl, '\0', sizeof(struct upl));
+	alist_init_struct(&upl->image, struct upl_image);
+	alist_init_struct(&upl->mem, struct upl_mem);
+	alist_init_struct(&upl->memmap, struct upl_memmap);
+	alist_init_struct(&upl->memres, struct upl_memres);
+}
diff --git a/boot/upl_common.h b/boot/upl_common.h
new file mode 100644
index 0000000..cc517dc
--- /dev/null
+++ b/boot/upl_common.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * UPL handoff command functions
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#ifndef __UPL_COMMON_H
+#define __UPL_COMMON_H
+
+/* Names of bootmodes */
+extern const char *const bootmode_names[UPLBM_COUNT];
+
+/* Names of memory usages */
+extern const char *const usage_names[UPLUS_COUNT];
+
+/* Names of access types */
+extern const char *const access_types[UPLUS_COUNT];
+
+/* Names of graphics formats */
+extern const char *const graphics_formats[UPLUS_COUNT];
+
+#endif /* __UPL_COMMON_H */
diff --git a/boot/upl_read.c b/boot/upl_read.c
new file mode 100644
index 0000000..5063897
--- /dev/null
+++ b/boot/upl_read.c
@@ -0,0 +1,588 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * UPL handoff parsing
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <log.h>
+#include <upl.h>
+#include <dm/ofnode.h>
+#include "upl_common.h"
+
+/**
+ * read_addr() - Read an address
+ *
+ * Reads an address in the correct format, either 32- or 64-bit
+ *
+ * @upl: UPL state
+ * @node: Node to read from
+ * @prop: Property name to read
+ * @addr: Place to put the address
+ * Return: 0 if OK, -ve on error
+ */
+static int read_addr(const struct upl *upl, ofnode node, const char *prop,
+		     ulong *addrp)
+{
+	int ret;
+
+	if (upl->addr_cells == 1) {
+		u32 val;
+
+		ret = ofnode_read_u32(node, prop, &val);
+		if (!ret)
+			*addrp = val;
+	} else {
+		u64 val;
+
+		ret = ofnode_read_u64(node, prop, &val);
+		if (!ret)
+			*addrp = val;
+	}
+
+	return ret;
+}
+
+/**
+ * read_size() - Read a size
+ *
+ * Reads a size in the correct format, either 32- or 64-bit
+ *
+ * @upl: UPL state
+ * @node: Node to read from
+ * @prop: Property name to read
+ * @addr: Place to put the size
+ * Return: 0 if OK, -ve on error
+ */
+static int read_size(const struct upl *upl, ofnode node, const char *prop,
+		     ulong *sizep)
+{
+	int ret;
+
+	if (upl->size_cells == 1) {
+		u32 val;
+
+		ret = ofnode_read_u32(node, prop, &val);
+		if (!ret)
+			*sizep = val;
+	} else {
+		u64 val;
+
+		ret = ofnode_read_u64(node, prop, &val);
+		if (!ret)
+			*sizep = val;
+	}
+
+	return ret;
+}
+
+/**
+ * ofnode_read_bitmask() - Read a bit mask from a string list
+ *
+ * @node: Node to read from
+ * @prop: Property name to read
+ * @names: Array of names for each bit
+ * @count: Number of array entries
+ * @value: Returns resulting bit-mask value on success
+ * Return: 0 if OK, -EINVAL if a bit number is not defined, -ENOSPC if the
+ * string is too long for the (internal) buffer, -EINVAL if no such property
+ */
+static int ofnode_read_bitmask(ofnode node, const char *prop,
+			       const char *const names[], uint count,
+			       uint *valuep)
+{
+	const char **list;
+	const char **strp;
+	uint val;
+	uint bit;
+	int ret;
+
+	ret = ofnode_read_string_list(node, prop, &list);
+	if (ret < 0)
+		return log_msg_ret("rea", ret);
+
+	val = 0;
+	for (strp = list; *strp; strp++) {
+		const char *str = *strp;
+		bool found = false;
+
+		for (bit = 0; bit < count; bit++) {
+			if (!strcmp(str, names[bit])) {
+				found = true;
+				break;
+			}
+		}
+		if (found)
+			val |= BIT(bit);
+		else
+			log_warning("%s/%s: Invalid value '%s'\n",
+				    ofnode_get_name(node), prop, str);
+	}
+	*valuep = val;
+
+	return 0;
+}
+
+/**
+ * ofnode_read_value() - Read a string value as an int using a lookup
+ *
+ * @node: Node to read from
+ * @prop: Property name to read
+ * @names: Array of names for each int value
+ * @count: Number of array entries
+ * @valuep: Returns int value read
+ * Return: 0 if OK, -EINVAL if a bit number is not defined, -ENOENT if the
+ * property does not exist
+ */
+static int ofnode_read_value(ofnode node, const char *prop,
+			     const char *const names[], uint count,
+			     uint *valuep)
+{
+	const char *str;
+	int i;
+
+	str = ofnode_read_string(node, prop);
+	if (!str)
+		return log_msg_ret("rd", -ENOENT);
+
+	for (i = 0; i < count; i++) {
+		if (!strcmp(names[i], str)) {
+			*valuep = i;
+			return 0;
+		}
+	}
+
+	log_debug("Unnamed value '%s'\n", str);
+	return log_msg_ret("val", -EINVAL);
+}
+
+static int read_uint(ofnode node, const char *prop, uint *valp)
+{
+	u32 val;
+	int ret;
+
+	ret = ofnode_read_u32(node, prop, &val);
+	if (ret)
+		return ret;
+	*valp = val;
+
+	return 0;
+}
+
+/**
+ * decode_root_props() - Decode root properties from the tree
+ *
+ * @upl: UPL state
+ * @node: Node to decode
+ * Return 0 if OK, -ve on error
+ */
+static int decode_root_props(struct upl *upl, ofnode node)
+{
+	int ret;
+
+	ret = read_uint(node, UPLP_ADDRESS_CELLS, &upl->addr_cells);
+	if (!ret)
+		ret = read_uint(node, UPLP_SIZE_CELLS, &upl->size_cells);
+	if (ret)
+		return log_msg_ret("cel", ret);
+
+	return 0;
+}
+
+/**
+ * decode_root_props() - Decode UPL parameters from the tree
+ *
+ * @upl: UPL state
+ * @node: Node to decode
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_params(struct upl *upl, ofnode options)
+{
+	ofnode node;
+	int ret;
+
+	node = ofnode_find_subnode(options, UPLN_UPL_PARAMS);
+	if (!ofnode_valid(node))
+		return log_msg_ret("par", -EINVAL);
+	log_debug("decoding '%s'\n", ofnode_get_name(node));
+
+	ret = read_addr(upl, node, UPLP_SMBIOS, &upl->smbios);
+	if (ret)
+		return log_msg_ret("smb", ret);
+	ret = read_addr(upl, node, UPLP_ACPI, &upl->acpi);
+	if (ret)
+		return log_msg_ret("acp", ret);
+	ret = ofnode_read_bitmask(node, UPLP_BOOTMODE, bootmode_names,
+				  UPLBM_COUNT, &upl->bootmode);
+	if (ret)
+		return log_msg_ret("boo", ret);
+	ret = read_uint(node, UPLP_ADDR_WIDTH, &upl->addr_width);
+	if (ret)
+		return log_msg_ret("add", ret);
+	ret = read_uint(node, UPLP_ACPI_NVS_SIZE, &upl->acpi_nvs_size);
+	if (ret)
+		return log_msg_ret("nvs", ret);
+
+	return 0;
+}
+
+/**
+ * decode_upl_images() - Decode /options/upl-image nodes
+ *
+ * @node: /options node in which to look for the node
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_images(struct upl *upl, ofnode options)
+{
+	ofnode node, images;
+	int ret;
+
+	images = ofnode_find_subnode(options, UPLN_UPL_IMAGE);
+	if (!ofnode_valid(images))
+		return log_msg_ret("img", -EINVAL);
+	log_debug("decoding '%s'\n", ofnode_get_name(images));
+
+	ret = read_addr(upl, images, UPLP_FIT, &upl->fit);
+	if (!ret)
+		ret = read_uint(images, UPLP_CONF_OFFSET, &upl->conf_offset);
+	if (ret)
+		return log_msg_ret("cnf", ret);
+
+	ofnode_for_each_subnode(node, images) {
+		struct upl_image img;
+
+		ret = read_addr(upl, node, UPLP_LOAD, &img.load);
+		if (!ret)
+			ret = read_size(upl, node, UPLP_SIZE, &img.size);
+		if (!ret)
+			ret = read_uint(node, UPLP_OFFSET, &img.offset);
+		img.description = ofnode_read_string(node, UPLP_DESCRIPTION);
+		if (!img.description)
+			return log_msg_ret("sim", ret);
+		if (!alist_add(&upl->image, img))
+			return log_msg_ret("img", -ENOMEM);
+	}
+
+	return 0;
+}
+
+/**
+ * decode_addr_size() - Decide a set of addr/size pairs
+ *
+ * Each base/size value from the devicetree is written to the region list
+ *
+ * @upl: UPL state
+ * @buf: Bytes to decode
+ * @size: Number of bytes to decode
+ * @regions: List of regions to process (struct memregion)
+ * Returns: number of regions found, if OK, else -ve on error
+ */
+static int decode_addr_size(const struct upl *upl, const char *buf, int size,
+			    struct alist *regions)
+{
+	const char *ptr, *end = buf + size;
+	int i;
+
+	alist_init_struct(regions, struct memregion);
+	ptr = buf;
+	for (i = 0; ptr < end; i++) {
+		struct memregion reg;
+
+		if (upl->addr_cells == 1)
+			reg.base = fdt32_to_cpu(*(u32 *)ptr);
+		else
+			reg.base = fdt64_to_cpu(*(u64 *)ptr);
+		ptr += upl->addr_cells * sizeof(u32);
+
+		if (upl->size_cells == 1)
+			reg.size = fdt32_to_cpu(*(u32 *)ptr);
+		else
+			reg.size = fdt64_to_cpu(*(u64 *)ptr);
+		ptr += upl->size_cells * sizeof(u32);
+		if (ptr > end)
+			return -ENOSPC;
+
+		if (!alist_add(regions, reg))
+			return log_msg_ret("reg", -ENOMEM);
+	}
+
+	return i;
+}
+
+/**
+ * node_matches_at() - Check if a node name matches "base@..."
+ *
+ * Return: true if the node name matches the base string followed by an @ sign;
+ * false otherwise
+ */
+static bool node_matches_at(ofnode node, const char *base)
+{
+	const char *name = ofnode_get_name(node);
+	int len = strlen(base);
+
+	return !strncmp(base, name, len) && name[len] == '@';
+}
+
+/**
+ * decode_upl_memory_node() - Decode a /memory node from the tree
+ *
+ * @upl: UPL state
+ * @node: Node to decode
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_memory_node(struct upl *upl, ofnode node)
+{
+	struct upl_mem mem;
+	const char *buf;
+	int size, len;
+
+	buf = ofnode_read_prop(node, UPLP_REG, &size);
+	if (!buf) {
+		log_warning("Node '%s': Missing '%s' property\n",
+			    ofnode_get_name(node), UPLP_REG);
+		return log_msg_ret("reg", -EINVAL);
+	}
+	len = decode_addr_size(upl, buf, size, &mem.region);
+	if (len < 0)
+		return log_msg_ret("buf", len);
+	mem.hotpluggable = ofnode_read_bool(node, UPLP_HOTPLUGGABLE);
+	if (!alist_add(&upl->mem, mem))
+		return log_msg_ret("mem", -ENOMEM);
+
+	return 0;
+}
+
+/**
+ * decode_upl_memmap() - Decode memory-map nodes from the tree
+ *
+ * @upl: UPL state
+ * @root: Parent node containing the /memory-map nodes
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_memmap(struct upl *upl, ofnode root)
+{
+	ofnode node;
+
+	ofnode_for_each_subnode(node, root) {
+		struct upl_memmap memmap;
+		int size, len, ret;
+		const char *buf;
+
+		memmap.name = ofnode_get_name(node);
+		memmap.usage = 0;
+
+		buf = ofnode_read_prop(node, UPLP_REG, &size);
+		if (!buf) {
+			log_warning("Node '%s': Missing '%s' property\n",
+				    ofnode_get_name(node), UPLP_REG);
+			continue;
+		}
+
+		len = decode_addr_size(upl, buf, size, &memmap.region);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+		ret = ofnode_read_bitmask(node, UPLP_USAGE, usage_names,
+					  UPLUS_COUNT, &memmap.usage);
+		if (ret && ret != -EINVAL)	/* optional property */
+			return log_msg_ret("bit", ret);
+
+		if (!alist_add(&upl->memmap, memmap))
+			return log_msg_ret("mmp", -ENOMEM);
+	}
+
+	return 0;
+}
+
+/**
+ * decode_upl_memres() - Decode reserved-memory nodes from the tree
+ *
+ * @upl: UPL state
+ * @root: Parent node containing the reserved-memory nodes
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_memres(struct upl *upl, ofnode root)
+{
+	ofnode node;
+
+	ofnode_for_each_subnode(node, root) {
+		struct upl_memres memres;
+		const char *buf;
+		int size, len;
+
+		log_debug("decoding '%s'\n", ofnode_get_name(node));
+		memres.name = ofnode_get_name(node);
+
+		buf = ofnode_read_prop(node, UPLP_REG, &size);
+		if (!buf) {
+			log_warning("Node '%s': Missing 'reg' property\n",
+				    ofnode_get_name(node));
+			continue;
+		}
+
+		len = decode_addr_size(upl, buf, size, &memres.region);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+		memres.no_map = ofnode_read_bool(node, UPLP_NO_MAP);
+
+		if (!alist_add(&upl->memres, memres))
+			return log_msg_ret("mre", -ENOMEM);
+	}
+
+	return 0;
+}
+
+/**
+ * decode_upl_serial() - Decode the serial node
+ *
+ * @upl: UPL state
+ * @root: Parent node contain node
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_serial(struct upl *upl, ofnode node)
+{
+	struct upl_serial *ser = &upl->serial;
+	const char *buf;
+	int len, size;
+	int ret;
+
+	ser->compatible = ofnode_read_string(node, UPLP_COMPATIBLE);
+	if (!ser->compatible) {
+		log_warning("Node '%s': Missing compatible string\n",
+			    ofnode_get_name(node));
+		return log_msg_ret("com", -EINVAL);
+	}
+	ret = read_uint(node, UPLP_CLOCK_FREQUENCY, &ser->clock_frequency);
+	if (!ret)
+		ret = read_uint(node, UPLP_CURRENT_SPEED, &ser->current_speed);
+	if (ret)
+		return log_msg_ret("spe", ret);
+
+	buf = ofnode_read_prop(node, UPLP_REG, &size);
+	if (!buf) {
+		log_warning("Node '%s': Missing 'reg' property\n",
+			    ofnode_get_name(node));
+		return log_msg_ret("reg", -EINVAL);
+	}
+
+	len = decode_addr_size(upl, buf, sizeof(buf), &ser->reg);
+	if (len < 0)
+		return log_msg_ret("buf", len);
+
+	/* set defaults */
+	ser->reg_io_shift = UPLD_REG_IO_SHIFT;
+	ser->reg_offset = UPLD_REG_OFFSET;
+	ser->reg_io_width = UPLD_REG_IO_WIDTH;
+	read_uint(node, UPLP_REG_IO_SHIFT, &ser->reg_io_shift);
+	read_uint(node, UPLP_REG_OFFSET, &ser->reg_offset);
+	read_uint(node, UPLP_REG_IO_WIDTH, &ser->reg_io_width);
+	read_addr(upl, node, UPLP_VIRTUAL_REG, &ser->virtual_reg);
+	ret = ofnode_read_value(node, UPLP_ACCESS_TYPE, access_types,
+				ARRAY_SIZE(access_types), &ser->access_type);
+	if (ret && ret != -ENOENT)
+		return log_msg_ret("ser", ret);
+
+	return 0;
+}
+
+/**
+ * decode_upl_graphics() - Decode graphics node
+ *
+ * @upl: UPL state
+ * @root: Node to decode
+ * Return 0 if OK, -ve on error
+ */
+static int decode_upl_graphics(struct upl *upl, ofnode node)
+{
+	struct upl_graphics *gra = &upl->graphics;
+	const char *buf, *compat;
+	int len, size;
+	int ret;
+
+	compat = ofnode_read_string(node, UPLP_COMPATIBLE);
+	if (!compat) {
+		log_warning("Node '%s': Missing compatible string\n",
+			    ofnode_get_name(node));
+		return log_msg_ret("com", -EINVAL);
+	}
+	if (strcmp(UPLC_GRAPHICS, compat)) {
+		log_warning("Node '%s': Ignoring compatible '%s'\n",
+			    ofnode_get_name(node), compat);
+		return 0;
+	}
+
+	buf = ofnode_read_prop(node, UPLP_REG, &size);
+	if (!buf) {
+		log_warning("Node '%s': Missing 'reg' property\n",
+			    ofnode_get_name(node));
+		return log_msg_ret("reg", -EINVAL);
+	}
+
+	len = decode_addr_size(upl, buf, sizeof(buf), &gra->reg);
+	if (len < 0)
+		return log_msg_ret("buf", len);
+
+	ret = read_uint(node, UPLP_WIDTH, &gra->width);
+	if (!ret)
+		ret = read_uint(node, UPLP_HEIGHT, &gra->height);
+	if (!ret)
+		ret = read_uint(node, UPLP_STRIDE, &gra->stride);
+	if (!ret) {
+		ret = ofnode_read_value(node, UPLP_GRAPHICS_FORMAT,
+					graphics_formats,
+					ARRAY_SIZE(graphics_formats),
+					&gra->format);
+	}
+	if (ret)
+		return log_msg_ret("pro", ret);
+
+	return 0;
+}
+
+int upl_read_handoff(struct upl *upl, oftree tree)
+{
+	ofnode root, node;
+	int ret;
+
+	if (!oftree_valid(tree))
+		return log_msg_ret("tre", -EINVAL);
+
+	root = oftree_root(tree);
+
+	upl_init(upl);
+	ret = decode_root_props(upl, root);
+	if (ret)
+		return log_msg_ret("roo", ret);
+
+	ofnode_for_each_subnode(node, root) {
+		const char *name = ofnode_get_name(node);
+
+		log_debug("decoding '%s'\n", name);
+		if (!strcmp(UPLN_OPTIONS, name)) {
+			ret = decode_upl_params(upl, node);
+			if (ret)
+				return log_msg_ret("opt", ret);
+
+			ret = decode_upl_images(upl, node);
+		} else if (node_matches_at(node, UPLN_MEMORY)) {
+			ret = decode_upl_memory_node(upl, node);
+		} else if (!strcmp(UPLN_MEMORY_MAP, name)) {
+			ret = decode_upl_memmap(upl, node);
+		} else if (!strcmp(UPLN_MEMORY_RESERVED, name)) {
+			ret = decode_upl_memres(upl, node);
+		} else if (node_matches_at(node, UPLN_SERIAL)) {
+			ret = decode_upl_serial(upl, node);
+		} else if (node_matches_at(node, UPLN_GRAPHICS)) {
+			ret = decode_upl_graphics(upl, node);
+		} else {
+			log_debug("Unknown node '%s'\n", name);
+			ret = 0;
+		}
+		if (ret)
+			return log_msg_ret("err", ret);
+	}
+
+	return 0;
+}
diff --git a/boot/upl_write.c b/boot/upl_write.c
new file mode 100644
index 0000000..7d637c15
--- /dev/null
+++ b/boot/upl_write.c
@@ -0,0 +1,622 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * UPL handoff generation
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <log.h>
+#include <upl.h>
+#include <dm/ofnode.h>
+#include "upl_common.h"
+
+/**
+ * write_addr() - Write an address
+ *
+ * Writes an address in the correct format, either 32- or 64-bit
+ *
+ * @upl: UPL state
+ * @node: Node to write to
+ * @prop: Property name to write
+ * @addr: Address to write
+ * Return: 0 if OK, -ve on error
+ */
+static int write_addr(const struct upl *upl, ofnode node, const char *prop,
+		      ulong addr)
+{
+	int ret;
+
+	if (upl->addr_cells == 1)
+		ret = ofnode_write_u32(node, prop, addr);
+	else
+		ret = ofnode_write_u64(node, prop, addr);
+
+	return ret;
+}
+
+/**
+ * write_size() - Write a size
+ *
+ * Writes a size in the correct format, either 32- or 64-bit
+ *
+ * @upl: UPL state
+ * @node: Node to write to
+ * @prop: Property name to write
+ * @size: Size to write
+ * Return: 0 if OK, -ve on error
+ */
+static int write_size(const struct upl *upl, ofnode node, const char *prop,
+		      ulong size)
+{
+	int ret;
+
+	if (upl->size_cells == 1)
+		ret = ofnode_write_u32(node, prop, size);
+	else
+		ret = ofnode_write_u64(node, prop, size);
+
+	return ret;
+}
+
+/**
+ * ofnode_write_bitmask() - Write a bit mask as a string list
+ *
+ * @node: Node to write to
+ * @prop: Property name to write
+ * @names: Array of names for each bit
+ * @count: Number of array entries
+ * @value: Bit-mask value to write
+ * Return: 0 if OK, -EINVAL if a bit number is not defined, -ENOSPC if the
+ * string is too long for the (internal) buffer
+ */
+static int ofnode_write_bitmask(ofnode node, const char *prop,
+				const char *const names[], uint count,
+				uint value)
+{
+	char buf[128];
+	char *ptr, *end = buf + sizeof(buf);
+	uint bit;
+	int ret;
+
+	ptr = buf;
+	for (bit = 0; bit < count; bit++) {
+		if (value & BIT(bit)) {
+			const char *str = names[bit];
+			uint len;
+
+			if (!str) {
+				log_debug("Unnamed bit number %d\n", bit);
+				return log_msg_ret("bit", -EINVAL);
+			}
+			len = strlen(str) + 1;
+			if (ptr + len > end) {
+				log_debug("String array too long\n");
+				return log_msg_ret("bit", -ENOSPC);
+			}
+
+			memcpy(ptr, str, len);
+			ptr += len;
+		}
+	}
+
+	ret = ofnode_write_prop(node, prop, buf, ptr - buf, true);
+	if (ret)
+		return log_msg_ret("wri", ret);
+
+	return 0;
+}
+
+/**
+ * ofnode_write_value() - Write an int as a string value using a lookup
+ *
+ * @node: Node to write to
+ * @prop: Property name to write
+ * @names: Array of names for each int value
+ * @count: Number of array entries
+ * @value: Int value to write
+ * Return: 0 if OK, -EINVAL if a bit number is not defined, -ENOSPC if the
+ * string is too long for the (internal) buffer
+ */
+static int ofnode_write_value(ofnode node, const char *prop,
+			      const char *const names[], uint count,
+			      uint value)
+{
+	const char *str;
+	int ret;
+
+	if (value >= count) {
+		log_debug("Value of range %d\n", value);
+		return log_msg_ret("val", -ERANGE);
+	}
+	str = names[value];
+	if (!str) {
+		log_debug("Unnamed value %d\n", value);
+		return log_msg_ret("val", -EINVAL);
+	}
+	ret = ofnode_write_string(node, prop, str);
+	if (ret)
+		return log_msg_ret("wri", ret);
+
+	return 0;
+}
+
+/**
+ * add_root_props() - Add root properties to the tree
+ *
+ * @node: Node to add to
+ * Return 0 if OK, -ve on error
+ */
+static int add_root_props(const struct upl *upl, ofnode node)
+{
+	int ret;
+
+	ret = ofnode_write_u32(node, UPLP_ADDRESS_CELLS, upl->addr_cells);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_SIZE_CELLS, upl->size_cells);
+	if (ret)
+		return log_msg_ret("cel", ret);
+
+	return 0;
+}
+
+/**
+ * add_upl_params() - Add UPL parameters node
+ *
+ * @upl: UPL state
+ * @options: /options node to add to
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_params(const struct upl *upl, ofnode options)
+{
+	ofnode node;
+	int ret;
+
+	ret = ofnode_add_subnode(options, UPLN_UPL_PARAMS, &node);
+	if (ret)
+		return log_msg_ret("img", ret);
+
+	ret = write_addr(upl, node, UPLP_SMBIOS, upl->smbios);
+	if (!ret)
+		ret = write_addr(upl, node, UPLP_ACPI, upl->acpi);
+	if (!ret && upl->bootmode)
+		ret = ofnode_write_bitmask(node, UPLP_BOOTMODE, bootmode_names,
+					   UPLBM_COUNT, upl->bootmode);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_ADDR_WIDTH, upl->addr_width);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_ACPI_NVS_SIZE,
+				       upl->acpi_nvs_size);
+	if (ret)
+		return log_msg_ret("cnf", ret);
+
+	return 0;
+}
+
+/**
+ * add_upl_image() - Add /options/upl-image nodes and properties to the tree
+ *
+ * @upl: UPL state
+ * @node: /options node to add to
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_image(const struct upl *upl, ofnode options)
+{
+	ofnode node;
+	int ret, i;
+
+	ret = ofnode_add_subnode(options, UPLN_UPL_IMAGE, &node);
+	if (ret)
+		return log_msg_ret("img", ret);
+
+	if (upl->fit)
+		ret = ofnode_write_u32(node, UPLP_FIT, upl->fit);
+	if (!ret && upl->conf_offset)
+		ret = ofnode_write_u32(node, UPLP_CONF_OFFSET,
+				       upl->conf_offset);
+	if (ret)
+		return log_msg_ret("cnf", ret);
+
+	for (i = 0; i < upl->image.count; i++) {
+		const struct upl_image *img = alist_get(&upl->image, i,
+							struct upl_image);
+		ofnode subnode;
+		char name[10];
+
+		snprintf(name, sizeof(name), UPLN_IMAGE "-%d", i + 1);
+		ret = ofnode_add_subnode(node, name, &subnode);
+		if (ret)
+			return log_msg_ret("sub", ret);
+
+		ret = write_addr(upl, subnode, UPLP_LOAD, img->load);
+		if (!ret)
+			ret = write_size(upl, subnode, UPLP_SIZE, img->size);
+		if (!ret && img->offset)
+			ret = ofnode_write_u32(subnode, UPLP_OFFSET,
+					       img->offset);
+		ret = ofnode_write_string(subnode, UPLP_DESCRIPTION,
+					  img->description);
+		if (ret)
+			return log_msg_ret("sim", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * buffer_addr_size() - Generate a set of addr/size pairs
+ *
+ * Each base/size value from each region is written to the buffer in a suitable
+ * format to be written to the devicetree
+ *
+ * @upl: UPL state
+ * @buf: Buffer to write to
+ * @size: Buffer size
+ * @num_regions: Number of regions to process
+ * @region: List of regions to process (struct memregion)
+ * Returns: Number of bytes written, or -ENOSPC if the buffer is too small
+ */
+static int buffer_addr_size(const struct upl *upl, char *buf, int size,
+			    uint num_regions, const struct alist *region)
+{
+	char *ptr, *end = buf + size;
+	int i;
+
+	ptr = buf;
+	for (i = 0; i < num_regions; i++) {
+		const struct memregion *reg = alist_get(region, i,
+							struct memregion);
+
+		if (upl->addr_cells == 1)
+			*(u32 *)ptr = cpu_to_fdt32(reg->base);
+		else
+			*(u64 *)ptr = cpu_to_fdt64(reg->base);
+		ptr += upl->addr_cells * sizeof(u32);
+
+		if (upl->size_cells == 1)
+			*(u32 *)ptr = cpu_to_fdt32(reg->size);
+		else
+			*(u64 *)ptr = cpu_to_fdt64(reg->size);
+		ptr += upl->size_cells * sizeof(u32);
+		if (ptr > end)
+			return -ENOSPC;
+	}
+
+	return ptr - buf;
+}
+
+/**
+ * add_upl_memory() - Add /memory nodes to the tree
+ *
+ * @upl: UPL state
+ * @root: Parent node to contain the new /memory nodes
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_memory(const struct upl *upl, ofnode root)
+{
+	int i;
+
+	for (i = 0; i < upl->mem.count; i++) {
+		const struct upl_mem *mem = alist_get(&upl->mem, i,
+						      struct upl_mem);
+		char buf[mem->region.count * sizeof(64) * 2];
+		const struct memregion *first;
+		char name[26];
+		int ret, len;
+		ofnode node;
+
+		if (!mem->region.count) {
+			log_debug("Memory %d has no regions\n", i);
+			return log_msg_ret("reg", -EINVAL);
+		}
+		first = alist_get(&mem->region, 0, struct memregion);
+		sprintf(name, UPLN_MEMORY "@0x%lx", first->base);
+		ret = ofnode_add_subnode(root, name, &node);
+		if (ret)
+			return log_msg_ret("mem", ret);
+
+		len = buffer_addr_size(upl, buf, sizeof(buf), mem->region.count,
+				       &mem->region);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+
+		ret = ofnode_write_prop(node, UPLP_REG, buf, len, true);
+		if (!ret && mem->hotpluggable)
+			ret = ofnode_write_bool(node, UPLP_HOTPLUGGABLE,
+						mem->hotpluggable);
+		if (ret)
+			return log_msg_ret("lst", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * add_upl_memmap() - Add memory-map nodes to the tree
+ *
+ * @upl: UPL state
+ * @root: Parent node to contain the new /memory-map node and its subnodes
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_memmap(const struct upl *upl, ofnode root)
+{
+	ofnode mem_node;
+	int i, ret;
+
+	if (!upl->memmap.count)
+		return 0;
+	ret = ofnode_add_subnode(root, UPLN_MEMORY_MAP, &mem_node);
+	if (ret)
+		return log_msg_ret("img", ret);
+
+	for (i = 0; i < upl->memmap.count; i++) {
+		const struct upl_memmap *memmap = alist_get(&upl->memmap, i,
+							struct upl_memmap);
+		char buf[memmap->region.count * sizeof(64) * 2];
+		const struct memregion *first;
+		char name[26];
+		int ret, len;
+		ofnode node;
+
+		if (!memmap->region.count) {
+			log_debug("Memory %d has no regions\n", i);
+			return log_msg_ret("reg", -EINVAL);
+		}
+		first = alist_get(&memmap->region, 0, struct memregion);
+		sprintf(name, "%s@0x%lx", memmap->name, first->base);
+		ret = ofnode_add_subnode(mem_node, name, &node);
+		if (ret)
+			return log_msg_ret("memmap", ret);
+
+		len = buffer_addr_size(upl, buf, sizeof(buf),
+				       memmap->region.count, &memmap->region);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+		ret = ofnode_write_prop(node, UPLP_REG, buf, len, true);
+		if (!ret && memmap->usage)
+			ret = ofnode_write_bitmask(node, UPLP_USAGE,
+						   usage_names,
+						   UPLUS_COUNT, memmap->usage);
+		if (ret)
+			return log_msg_ret("lst", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * add_upl_memres() - Add /memory-reserved nodes to the tree
+ *
+ * @upl: UPL state
+ * @root: Parent node to contain the new node
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_memres(const struct upl *upl, ofnode root,
+			  bool skip_existing)
+{
+	ofnode mem_node;
+	int i, ret;
+
+	if (!upl->memmap.count)
+		return 0;
+	ret = ofnode_add_subnode(root, UPLN_MEMORY_RESERVED, &mem_node);
+	if (ret) {
+		if (skip_existing && ret == -EEXIST)
+			return 0;
+		return log_msg_ret("img", ret);
+	}
+
+	for (i = 0; i < upl->memres.count; i++) {
+		const struct upl_memres *memres = alist_get(&upl->memres, i,
+							struct upl_memres);
+		char buf[memres->region.count * sizeof(64) * 2];
+		const struct memregion *first;
+		char name[26];
+		int ret, len;
+		ofnode node;
+
+		if (!memres->region.count) {
+			log_debug("Memory %d has no regions\n", i);
+			return log_msg_ret("reg", -EINVAL);
+		}
+		first = alist_get(&memres->region, 0, struct memregion);
+		sprintf(name, "%s@0x%lx", memres->name, first->base);
+		ret = ofnode_add_subnode(mem_node, name, &node);
+		if (ret)
+			return log_msg_ret("memres", ret);
+
+		len = buffer_addr_size(upl, buf, sizeof(buf),
+				       memres->region.count, &memres->region);
+		ret = ofnode_write_prop(node, UPLP_REG, buf, len, true);
+		if (!ret && memres->no_map)
+			ret = ofnode_write_bool(node, UPLP_NO_MAP,
+						memres->no_map);
+		if (ret)
+			return log_msg_ret("lst", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * add_upl_serial() - Add serial node
+ *
+ * @upl: UPL state
+ * @root: Parent node to contain the new node
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_serial(const struct upl *upl, ofnode root,
+			  bool skip_existing)
+{
+	const struct upl_serial *ser = &upl->serial;
+	const struct memregion *first;
+	char name[26];
+	ofnode node;
+	int ret;
+
+	if (!ser->compatible || skip_existing)
+		return 0;
+	if (!ser->reg.count)
+		return log_msg_ret("ser", -EINVAL);
+	first = alist_get(&ser->reg, 0, struct memregion);
+	sprintf(name, UPLN_SERIAL "@0x%lx", first->base);
+	ret = ofnode_add_subnode(root, name, &node);
+	if (ret)
+		return log_msg_ret("img", ret);
+	ret = ofnode_write_string(node, UPLP_COMPATIBLE, ser->compatible);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_CLOCK_FREQUENCY,
+				       ser->clock_frequency);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_CURRENT_SPEED,
+				       ser->current_speed);
+	if (!ret) {
+		char buf[16];
+		int len;
+
+		len = buffer_addr_size(upl, buf, sizeof(buf), 1, &ser->reg);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+
+		ret = ofnode_write_prop(node, UPLP_REG, buf, len, true);
+	}
+	if (!ret && ser->reg_io_shift != UPLD_REG_IO_SHIFT)
+		ret = ofnode_write_u32(node, UPLP_REG_IO_SHIFT,
+				       ser->reg_io_shift);
+	if (!ret && ser->reg_offset != UPLD_REG_OFFSET)
+		ret = ofnode_write_u32(node, UPLP_REG_OFFSET, ser->reg_offset);
+	if (!ret && ser->reg_io_width != UPLD_REG_IO_WIDTH)
+		ret = ofnode_write_u32(node, UPLP_REG_IO_WIDTH,
+				       ser->reg_io_width);
+	if (!ret && ser->virtual_reg)
+		ret = write_addr(upl, node, UPLP_VIRTUAL_REG, ser->virtual_reg);
+	if (!ret) {
+		ret = ofnode_write_value(node, UPLP_ACCESS_TYPE, access_types,
+					 ARRAY_SIZE(access_types),
+					 ser->access_type);
+	}
+	if (ret)
+		return log_msg_ret("ser", ret);
+
+	return 0;
+}
+
+/**
+ * add_upl_graphics() - Add graphics node
+ *
+ * @upl: UPL state
+ * @root: Parent node to contain the new node
+ * Return 0 if OK, -ve on error
+ */
+static int add_upl_graphics(const struct upl *upl, ofnode root)
+{
+	const struct upl_graphics *gra = &upl->graphics;
+	const struct memregion *first;
+	char name[36];
+	ofnode node;
+	int ret;
+
+	if (!gra->reg.count)
+		return log_msg_ret("gra", -ENOENT);
+	first = alist_get(&gra->reg, 0, struct memregion);
+	sprintf(name, UPLN_GRAPHICS "@0x%lx", first->base);
+	ret = ofnode_add_subnode(root, name, &node);
+	if (ret)
+		return log_msg_ret("gra", ret);
+
+	ret = ofnode_write_string(node, UPLP_COMPATIBLE, UPLC_GRAPHICS);
+	if (!ret) {
+		char buf[16];
+		int len;
+
+		len = buffer_addr_size(upl, buf, sizeof(buf), 1, &gra->reg);
+		if (len < 0)
+			return log_msg_ret("buf", len);
+
+		ret = ofnode_write_prop(node, UPLP_REG, buf, len, true);
+	}
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_WIDTH, gra->width);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_HEIGHT, gra->height);
+	if (!ret)
+		ret = ofnode_write_u32(node, UPLP_STRIDE, gra->stride);
+	if (!ret) {
+		ret = ofnode_write_value(node, UPLP_GRAPHICS_FORMAT,
+					 graphics_formats,
+					 ARRAY_SIZE(graphics_formats),
+					 gra->format);
+	}
+	if (ret)
+		return log_msg_ret("pro", ret);
+
+	return 0;
+}
+
+int upl_write_handoff(const struct upl *upl, ofnode root, bool skip_existing)
+{
+	ofnode options;
+	int ret;
+
+	ret = add_root_props(upl, root);
+	if (ret)
+		return log_msg_ret("ad1", ret);
+	ret = ofnode_add_subnode(root, UPLN_OPTIONS, &options);
+	if (ret && ret != -EEXIST)
+		return log_msg_ret("opt", -EINVAL);
+
+	ret = add_upl_params(upl, options);
+	if (ret)
+		return log_msg_ret("ad1", ret);
+
+	ret = add_upl_image(upl, options);
+	if (ret)
+		return log_msg_ret("ad2", ret);
+
+	ret = add_upl_memory(upl, root);
+	if (ret)
+		return log_msg_ret("ad3", ret);
+
+	ret = add_upl_memmap(upl, root);
+	if (ret)
+		return log_msg_ret("ad4", ret);
+
+	ret = add_upl_memres(upl, root, skip_existing);
+	if (ret)
+		return log_msg_ret("ad5", ret);
+
+	ret = add_upl_serial(upl, root, skip_existing);
+	if (ret)
+		return log_msg_ret("ad6", ret);
+
+	ret = add_upl_graphics(upl, root);
+	if (ret && ret != -ENOENT)
+		return log_msg_ret("ad6", ret);
+
+	return 0;
+}
+
+int upl_create_handoff_tree(const struct upl *upl, oftree *treep)
+{
+	ofnode root;
+	oftree tree;
+	int ret;
+
+	ret = oftree_new(&tree);
+	if (ret)
+		return log_msg_ret("new", ret);
+
+	root = oftree_root(tree);
+	if (!ofnode_valid(root))
+		return log_msg_ret("roo", -EINVAL);
+
+	ret = upl_write_handoff(upl, root, false);
+	if (ret)
+		return log_msg_ret("wr", ret);
+
+	*treep = tree;
+
+	return 0;
+}
diff --git a/cmd/Kconfig b/cmd/Kconfig
index 978f44e..8af136d 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -388,6 +388,13 @@
 	help
 	  Support reading NAND Seattle Image (SEAMA) images.
 
+config CMD_UPL
+	bool "upl - Universal Payload Specification"
+	help
+	  Provides commands to deal with UPL payloads and handoff information.
+	  U-Boot supports generating and accepting handoff information. The
+	  mkimage tool will eventually support creating payloads.
+
 config CMD_VBE
 	bool "vbe - Verified Boot for Embedded"
 	depends on BOOTMETH_VBE
diff --git a/cmd/Makefile b/cmd/Makefile
index 87133cc..91227f1 100644
--- a/cmd/Makefile
+++ b/cmd/Makefile
@@ -189,6 +189,7 @@
 obj-$(CONFIG_CMD_UNIVERSE) += universe.o
 obj-$(CONFIG_CMD_UNLZ4) += unlz4.o
 obj-$(CONFIG_CMD_UNZIP) += unzip.o
+obj-$(CONFIG_CMD_UPL) += upl.o
 obj-$(CONFIG_CMD_VIRTIO) += virtio.o
 obj-$(CONFIG_CMD_WDT) += wdt.o
 obj-$(CONFIG_CMD_LZMADEC) += lzmadec.o
diff --git a/cmd/upl.c b/cmd/upl.c
new file mode 100644
index 0000000..c974588
--- /dev/null
+++ b/cmd/upl.c
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Commands for UPL handoff generation
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <abuf.h>
+#include <alist.h>
+#include <command.h>
+#include <display_options.h>
+#include <mapmem.h>
+#include <string.h>
+#include <upl.h>
+#include <dm/ofnode.h>
+#include <test/ut.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+static int do_upl_info(struct cmd_tbl *cmdtp, int flag, int argc,
+		       char *const argv[])
+{
+	const struct upl *upl = gd_upl();
+
+	printf("UPL state: %sactive\n", upl ? "" : "in");
+	if (!upl)
+		return 0;
+	if (argc > 1 && !strcmp("-v", argv[1])) {
+		int i;
+
+		printf("fit %lx\n", upl->fit);
+		printf("conf_offset %x\n", upl->conf_offset);
+		for (i = 0; i < upl->image.count; i++) {
+			const struct upl_image *img =
+				alist_get(&upl->image, i, struct upl_image);
+
+			printf("image %d: load %lx size %lx offset %x: %s\n", i,
+			       img->load, img->size, img->offset,
+			       img->description);
+		}
+	}
+
+	return 0;
+}
+
+static int do_upl_write(struct cmd_tbl *cmdtp, int flag, int argc,
+			char *const argv[])
+{
+	struct upl s_upl, *upl = &s_upl;
+	struct unit_test_state uts;
+	struct abuf buf;
+	oftree tree;
+	ulong addr;
+	int ret;
+
+	upl_get_test_data(&uts, upl);
+
+	log_debug("Writing UPL\n");
+	ret = upl_create_handoff_tree(upl, &tree);
+	if (ret) {
+		log_err("Failed to write (err=%dE)\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	log_debug("Flattening\n");
+	ret = oftree_to_fdt(tree, &buf);
+	if (ret) {
+		log_err("Failed to write (err=%dE)\n", ret);
+		return CMD_RET_FAILURE;
+	}
+	addr = map_to_sysmem(abuf_data(&buf));
+	printf("UPL handoff written to %lx size %lx\n", addr, abuf_size(&buf));
+	if (env_set_hex("upladdr", addr) ||
+	    env_set_hex("uplsize", abuf_size(&buf))) {
+		printf("Cannot set env var\n");
+		return CMD_RET_FAILURE;
+	}
+
+	log_debug("done\n");
+
+	return 0;
+}
+
+static int do_upl_read(struct cmd_tbl *cmdtp, int flag, int argc,
+		       char *const argv[])
+{
+	struct upl s_upl, *upl = &s_upl;
+	oftree tree;
+	ulong addr;
+	int ret;
+
+	if (argc < 1)
+		return CMD_RET_USAGE;
+	addr = hextoul(argv[1], NULL);
+
+	printf("Reading UPL at %lx\n", addr);
+	tree = oftree_from_fdt(map_sysmem(addr, 0));
+	ret = upl_read_handoff(upl, tree);
+	if (ret) {
+		log_err("Failed to read (err=%dE)\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	return 0;
+}
+
+U_BOOT_LONGHELP(upl,
+	"info [-v]     - Check UPL status\n"
+	"upl read <addr>   - Read handoff information\n"
+	"upl write         - Write handoff information");
+
+U_BOOT_CMD_WITH_SUBCMDS(upl, "Universal Payload support", upl_help_text,
+	U_BOOT_SUBCMD_MKENT(info, 2, 1, do_upl_info),
+	U_BOOT_SUBCMD_MKENT(read, 2, 1, do_upl_read),
+	U_BOOT_SUBCMD_MKENT(write, 1, 1, do_upl_write));
diff --git a/common/board_f.c b/common/board_f.c
index 29e1851..d71005d 100644
--- a/common/board_f.c
+++ b/common/board_f.c
@@ -40,6 +40,7 @@
 #include <sysreset.h>
 #include <timer.h>
 #include <trace.h>
+#include <upl.h>
 #include <video.h>
 #include <watchdog.h>
 #include <asm/cache.h>
@@ -859,6 +860,26 @@
 	return 0;
 }
 
+static int initf_upl(void)
+{
+	struct upl *upl;
+	int ret;
+
+	if (!IS_ENABLED(CONFIG_UPL_IN) || !(gd->flags & GD_FLG_UPL))
+		return 0;
+
+	upl = malloc(sizeof(struct upl));
+	if (upl)
+		ret = upl_read_handoff(upl, oftree_default());
+	if (ret) {
+		printf("UPL handoff: read failure (err=%dE)\n", ret);
+		return ret;
+	}
+	gd_set_upl(upl);
+
+	return 0;
+}
+
 static const init_fnc_t init_sequence_f[] = {
 	setup_mon_len,
 #ifdef CONFIG_OF_CONTROL
@@ -868,6 +889,7 @@
 	trace_early_init,
 #endif
 	initf_malloc,
+	initf_upl,
 	log_init,
 	initf_bootstage,	/* uses its own timer, so does not need DM */
 	event_init,
diff --git a/common/board_r.c b/common/board_r.c
index d4ba245..f445803 100644
--- a/common/board_r.c
+++ b/common/board_r.c
@@ -521,6 +521,8 @@
 		       uclass_count);
 		if (CONFIG_IS_ENABLED(OF_REAL))
 			printf(", devicetree: %s", fdtdec_get_srcname());
+		if (CONFIG_IS_ENABLED(UPL))
+			printf(", universal payload active");
 		printf("\n");
 		if (IS_ENABLED(CONFIG_OF_HAS_PRIOR_STAGE) &&
 		    (gd->fdt_src == FDTSRC_SEPARATE ||
diff --git a/common/spl/Makefile b/common/spl/Makefile
index 4809f9c..137b184 100644
--- a/common/spl/Makefile
+++ b/common/spl/Makefile
@@ -37,3 +37,5 @@
 obj-$(CONFIG_$(SPL_TPL_)RAM_SUPPORT) += spl_ram.o
 obj-$(CONFIG_$(SPL_TPL_)USB_SDP_SUPPORT) += spl_sdp.o
 endif
+
+obj-$(CONFIG_$(SPL_TPL_)UPL) += spl_upl.o
diff --git a/common/spl/spl.c b/common/spl/spl.c
index 7794ddc..d6a364d 100644
--- a/common/spl/spl.c
+++ b/common/spl/spl.c
@@ -810,6 +810,14 @@
 			printf(SPL_TPL_PROMPT
 			       "SPL hand-off write failed (err=%d)\n", ret);
 	}
+	if (CONFIG_IS_ENABLED(UPL_OUT) && (gd->flags & GD_FLG_UPL)) {
+		ret = spl_write_upl_handoff(&spl_image);
+		if (ret) {
+			printf(SPL_TPL_PROMPT
+			       "UPL hand-off write failed (err=%d)\n", ret);
+			hang();
+		}
+	}
 	if (CONFIG_IS_ENABLED(BLOBLIST)) {
 		ret = bloblist_finish();
 		if (ret)
diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c
index 2a097f4..1ad5a69 100644
--- a/common/spl/spl_fit.c
+++ b/common/spl/spl_fit.c
@@ -12,6 +12,7 @@
 #include <memalign.h>
 #include <mapmem.h>
 #include <spl.h>
+#include <upl.h>
 #include <sysinfo.h>
 #include <asm/global_data.h>
 #include <asm/io.h>
@@ -336,6 +337,8 @@
 			image_info->entry_point = FDT_ERROR;
 	}
 
+	upl_add_image(fit, node, load_addr, length);
+
 	return 0;
 }
 
@@ -847,6 +850,8 @@
 		spl_image->entry_point = spl_image->load_addr;
 
 	spl_image->flags |= SPL_FIT_FOUND;
+	upl_set_fit_info(map_to_sysmem(ctx.fit), ctx.conf_node,
+			 spl_image->entry_point);
 
 	return 0;
 }
@@ -941,6 +946,10 @@
 		if (ret < 0)
 			return ret;
 	}
+	spl_image->flags |= SPL_FIT_FOUND;
+
+	upl_set_fit_info(map_to_sysmem(header), conf_noffset,
+			 spl_image->entry_point);
 
 	return 0;
 }
diff --git a/common/spl/spl_upl.c b/common/spl/spl_upl.c
new file mode 100644
index 0000000..067d437
--- /dev/null
+++ b/common/spl/spl_upl.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * UPL handoff parsing
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <alist.h>
+#include <bloblist.h>
+#include <dm.h>
+#include <image.h>
+#include <mapmem.h>
+#include <serial.h>
+#include <spl.h>
+#include <upl.h>
+#include <video.h>
+#include <asm/global_data.h>
+#include <dm/read.h>
+#include <dm/uclass-internal.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+struct upl s_upl;
+
+void upl_set_fit_addr(ulong fit)
+{
+	struct upl *upl = &s_upl;
+
+	upl->fit = fit;
+}
+
+void upl_set_fit_info(ulong fit, int conf_offset, ulong entry_addr)
+{
+	struct upl *upl = &s_upl;
+
+	upl->fit = fit;
+	upl->conf_offset = conf_offset;
+	log_debug("upl: add fit %lx conf %x\n", fit, conf_offset);
+}
+
+int _upl_add_image(int node, ulong load_addr, ulong size, const char *desc)
+{
+	struct upl *upl = &s_upl;
+	struct upl_image img;
+
+	img.load = load_addr;
+	img.size = size;
+	img.offset = node;
+	img.description = desc;
+	if (!alist_add(&upl->image, img))
+		return -ENOMEM;
+	log_debug("upl: add image %s at %lx size %lx\n", desc, load_addr, size);
+
+	return 0;
+}
+
+static int write_serial(struct upl_serial *ser)
+{
+	struct udevice *dev = gd->cur_serial_dev;
+	struct serial_device_info info;
+	struct memregion region;
+	int ret;
+
+	if (!dev)
+		return log_msg_ret("ser", -ENOENT);
+	ret = serial_getinfo(dev, &info);
+	if (ret)
+		return log_msg_ret("inf", ret);
+
+	ser->compatible = ofnode_read_string(dev_ofnode(dev), "compatible");
+	ser->clock_frequency = info.clock;
+	ser->current_speed = gd->baudrate;
+	region.base = info.addr;
+	region.size = info.size;
+	alist_init_struct(&ser->reg, struct memregion);
+	if (!alist_add(&ser->reg, region))
+		return -ENOMEM;
+	ser->reg_io_shift = info.reg_shift;
+	ser->reg_offset = info.reg_offset;
+	ser->reg_io_width = info.reg_width;
+	ser->virtual_reg = 0;
+	ser->access_type = info.addr_space;
+
+	return 0;
+}
+
+static int write_graphics(struct upl_graphics *gra)
+{
+	struct video_uc_plat *plat;
+	struct video_priv *priv;
+	struct memregion region;
+	struct udevice *dev;
+
+	alist_init_struct(&gra->reg, struct memregion);
+	uclass_find_first_device(UCLASS_VIDEO, &dev);
+	if (!dev || !device_active(dev))
+		return log_msg_ret("vid", -ENOENT);
+
+	plat = dev_get_uclass_plat(dev);
+	region.base = plat->base;
+	region.size = plat->size;
+	if (!alist_add(&gra->reg, region))
+		return log_msg_ret("reg", -ENOMEM);
+
+	priv = dev_get_uclass_priv(dev);
+	gra->width = priv->xsize;
+	gra->height = priv->ysize;
+	gra->stride = priv->line_length;	/* private field */
+	switch (priv->format) {
+	case VIDEO_RGBA8888:
+	case VIDEO_X8R8G8B8:
+		gra->format = UPLGF_ARGB32;
+		break;
+	case VIDEO_X8B8G8R8:
+		gra->format = UPLGF_ABGR32;
+		break;
+	case VIDEO_X2R10G10B10:
+		log_debug("device '%s': VIDEO_X2R10G10B10 not supported\n",
+			  dev->name);
+		return log_msg_ret("for", -EPROTO);
+	case VIDEO_UNKNOWN:
+		log_debug("device '%s': Unknown video format\n", dev->name);
+		return log_msg_ret("for", -EPROTO);
+	}
+
+	return 0;
+}
+
+int spl_write_upl_handoff(struct spl_image_info *spl_image)
+{
+	struct upl *upl = &s_upl;
+	struct abuf buf;
+	ofnode root;
+	void *ptr;
+	int ret;
+
+	log_debug("UPL: Writing handoff - image_count=%d\n", upl->image.count);
+	upl->addr_cells = IS_ENABLED(CONFIG_PHYS_64BIT) ? 2 : 1;
+	upl->size_cells = IS_ENABLED(CONFIG_PHYS_64BIT) ? 2 : 1;
+	upl->bootmode = UPLBM_DEFAULT;
+	ret = write_serial(&upl->serial);
+	if (ret)
+		return log_msg_ret("ser", ret);
+	ret = write_graphics(&upl->graphics);
+	if (ret && ret != -ENOENT)
+		return log_msg_ret("gra", ret);
+
+	root = ofnode_root();
+	ret = upl_write_handoff(upl, root, true);
+	if (ret)
+		return log_msg_ret("wr", ret);
+
+	ret = oftree_to_fdt(oftree_default(), &buf);
+	if (ret)
+		return log_msg_ret("fdt", ret);
+	log_debug("FDT size %zx\n", abuf_size(&buf));
+
+	ptr = bloblist_add(BLOBLISTT_CONTROL_FDT, abuf_size(&buf), 0);
+	if (!ptr)
+		return log_msg_ret("blo", -ENOENT);
+	memcpy(ptr, abuf_data(&buf), abuf_size(&buf));
+
+	return 0;
+}
+
+void spl_upl_init(void)
+{
+	upl_init(&s_upl);
+}
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index dc5fcdb..484f9e1 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -16,6 +16,7 @@
 CONFIG_FIT_CIPHER=y
 CONFIG_FIT_VERBOSE=y
 CONFIG_BOOTMETH_ANDROID=y
+CONFIG_UPL=y
 CONFIG_LEGACY_IMAGE_FORMAT=y
 CONFIG_MEASURED_BOOT=y
 CONFIG_BOOTSTAGE=y
diff --git a/configs/sandbox_vpl_defconfig b/configs/sandbox_vpl_defconfig
index 72483d8..96e9211 100644
--- a/configs/sandbox_vpl_defconfig
+++ b/configs/sandbox_vpl_defconfig
@@ -27,6 +27,9 @@
 CONFIG_FIT_VERBOSE=y
 CONFIG_FIT_BEST_MATCH=y
 CONFIG_SPL_LOAD_FIT=y
+CONFIG_UPL=y
+CONFIG_UPL_IN=y
+CONFIG_SPL_UPL_OUT=y
 CONFIG_BOOTSTAGE=y
 CONFIG_BOOTSTAGE_REPORT=y
 CONFIG_BOOTSTAGE_FDT=y
@@ -35,6 +38,7 @@
 CONFIG_CONSOLE_RECORD=y
 CONFIG_CONSOLE_RECORD_OUT_SIZE=0x1000
 CONFIG_DISPLAY_BOARDINFO_LATE=y
+CONFIG_BLOBLIST_SIZE=0x5000
 CONFIG_SPL_NO_BSS_LIMIT=y
 CONFIG_HANDOFF=y
 CONFIG_SPL_BOARD_INIT=y
diff --git a/doc/usage/cmd/upl.rst b/doc/usage/cmd/upl.rst
new file mode 100644
index 0000000..8d6ea5d
--- /dev/null
+++ b/doc/usage/cmd/upl.rst
@@ -0,0 +1,186 @@
+.. SPDX-License-Identifier: GPL-2.0+:
+
+upl command
+===========
+
+Synopsis
+--------
+
+::
+
+    upl write
+    upl read <addr>
+    upl info [-v]
+
+Description
+-----------
+
+The *upl* command is used to test U-Boot's support for the Universal Payload
+Specification (UPL) firmware standard (see :doc:`../upl`). It allows creation of
+a fake handoff for use in testing.
+
+
+upl write
+~~~~~~~~~
+
+Write a fake UPL handoff structure. The `upladdr` environment variable is set to
+the address of this structure and `uplsize` is set to the size.
+
+
+upl read
+~~~~~~~~
+
+Read a UPL handoff structure into internal state. This allows testing that the
+handoff can be obtained.
+
+upl info
+~~~~~~~~
+
+Show basic information about usage of UPL:
+
+    UPL state
+        active or inactive (indicates whether U-Boot booted from UPL or not)
+
+    fit
+        Address of the FIT which was loaded
+
+    conf_offset 2a4
+        FIT offset of the chosen configuration
+
+For each image the following information is shown:
+
+    Image number
+        Images are numbered from 0
+
+    load
+        Address to which the image was loaded
+
+    size
+        Size of the loaded image
+
+    offset
+        FIT offset of the image
+
+    description
+        Description of the image
+
+
+Example
+-------
+
+This shows checking whether a UPL handoff was read at start-up::
+
+    => upl info
+    UPL state: active
+
+This shows how to use the command to write and display the handoff::
+
+    => upl write
+    UPL handoff written to bc8a5e0 size 662
+    => print upladdr
+    upladdr=bc8a5e0
+    => print uplsize
+    uplsize=662
+
+    > fdt addr ${upladdr}
+    Working FDT set to bc8a5e0
+    => fdt print
+    / {
+        #address-cells = <0x00000001>;
+        #size-cells = <0x00000001>;
+        options {
+            upl-params {
+                smbios = <0x00000123>;
+                acpi = <0x00000456>;
+                bootmode = "default", "s3";
+                addr-width = <0x0000002e>;
+                acpi-nvs-size = <0x00000100>;
+            };
+            upl-image {
+                fit = <0x00000789>;
+                conf-offset = <0x00000234>;
+                image-1 {
+                    load = <0x00000001>;
+                    size = <0x00000002>;
+                    offset = <0x00000003>;
+                    description = "U-Boot";
+                };
+                image-2 {
+                    load = <0x00000004>;
+                    size = <0x00000005>;
+                    offset = <0x00000006>;
+                    description = "ATF";
+                };
+            };
+        };
+        memory@0x10 {
+            reg = <0x00000010 0x00000020 0x00000030 0x00000040 0x00000050 0x00000060>;
+        };
+        memory@0x70 {
+            reg = <0x00000070 0x00000080>;
+            hotpluggable;
+        };
+        memory-map {
+            acpi@0x11 {
+                reg = <0x00000011 0x00000012 0x00000013 0x00000014 0x00000015 0x00000016 0x00000017 0x00000018 0x00000019 0x0000001a>;
+                usage = "acpi-reclaim";
+            };
+            u-boot@0x21 {
+                reg = <0x00000021 0x00000022>;
+                usage = "boot-data";
+            };
+            efi@0x23 {
+                reg = <0x00000023 0x00000024>;
+                usage = "runtime-code";
+            };
+            empty@0x25 {
+                reg = <0x00000025 0x00000026 0x00000027 0x00000028>;
+            };
+            acpi-things@0x2a {
+                reg = <0x0000002a 0x00000000>;
+                usage = "acpi-nvs", "runtime-code";
+            };
+        };
+        reserved-memory {
+            mmio@0x2b {
+                reg = <0x0000002b 0x0000002c>;
+            };
+            memory@0x2d {
+                reg = <0x0000002d 0x0000002e 0x0000002f 0x00000030>;
+                no-map;
+            };
+        };
+        serial@0xf1de0000 {
+            compatible = "ns16550a";
+            clock-frequency = <0x001c2000>;
+            current-speed = <0x0001c200>;
+            reg = <0xf1de0000 0x00000100>;
+            reg-io-shift = <0x00000002>;
+            reg-offset = <0x00000040>;
+            virtual-reg = <0x20000000>;
+            access-type = "mmio";
+        };
+        framebuffer@0xd0000000 {
+            compatible = "simple-framebuffer";
+            reg = <0xd0000000 0x10000000>;
+            width = <0x00000500>;
+            height = <0x00000500>;
+            stride = <0x00001400>;
+            format = "a8r8g8b8";
+        };
+    };
+    =>
+
+This showing reading the handoff into internal state::
+
+    => upl read bc8a5e0
+    Reading UPL at bc8a5e0
+    =>
+
+This shows getting basic information about UPL:
+
+    => upl info -v
+    UPL state: active
+    fit 1264000
+    conf_offset 2a4
+    image 0: load 200000 size 105f5c8 offset a4: U-Boot 2024.07-00770-g739ee12e8358 for sandbox board
diff --git a/doc/usage/index.rst b/doc/usage/index.rst
index 1f6518b..b058c22 100644
--- a/doc/usage/index.rst
+++ b/doc/usage/index.rst
@@ -15,6 +15,7 @@
    cmdline
    semihosting
    measured_boot
+   upl
 
 Shell commands
 --------------
@@ -114,6 +115,7 @@
    cmd/tftpput
    cmd/trace
    cmd/true
+   cmd/upl
    cmd/ums
    cmd/unbind
    cmd/ut
diff --git a/doc/usage/upl.rst b/doc/usage/upl.rst
new file mode 100644
index 0000000..3c4a10c
--- /dev/null
+++ b/doc/usage/upl.rst
@@ -0,0 +1,46 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Universal Payload
+-----------------
+
+`Universal Payload (UPL) <https://universalpayload.github.io/spec/index.html>`_
+is an Industry Standard for firmware components. UPL
+is designed to improve interoperability within the firmware industry, allowing
+mixing and matching of projects with less friction and fewer project-specific
+implementations. UPL is cross-platform, supporting ARM, x86 and RISC-V
+initially.
+
+UPL is defined in termns of two firmware components:
+
+`Platform Init`
+	Perhaps initial setup of the hardware and jumps to the payload.
+
+`Payload`
+	Selects the OS to boot
+
+In practice UPL can be used to handle any number of handoffs through the
+firmware startup process, with one program acting as platform init and another
+acting as the payload.
+
+UPL provides a standard for three main pieces:
+
+- file format for the payload, which may comprise multiple images to load
+- handoff format for the information the payload needs, such as serial port,
+  memory layout, etc.
+- machine state and register settings at the point of handoff
+
+See also the :doc:`cmd/upl`.
+
+UPL in U-Boot
+~~~~~~~~~~~~~
+
+U-Boot supports:
+
+- writing a UPL handoff (devicetree) in SPL
+- reading a UPL handoff in U-Boot proper
+- creating a FIT
+
+There are some new FIT features in UPL which are not yet supported in U-Boot.
+
+.. sectionauthor:: Simon Glass <sjg@chromium.org>
+.. July 2024
diff --git a/drivers/block/sandbox.c b/drivers/block/sandbox.c
index ec34f1a..6c74d66 100644
--- a/drivers/block/sandbox.c
+++ b/drivers/block/sandbox.c
@@ -25,7 +25,7 @@
 	struct udevice *host_dev = dev_get_parent(dev);
 	struct host_sb_plat *plat = dev_get_plat(host_dev);
 
-	if (os_lseek(plat->fd, start * desc->blksz, OS_SEEK_SET) == -1) {
+	if (os_lseek(plat->fd, start * desc->blksz, OS_SEEK_SET) < 0) {
 		printf("ERROR: Invalid block %lx\n", start);
 		return -1;
 	}
@@ -44,7 +44,7 @@
 	struct udevice *host_dev = dev_get_parent(dev);
 	struct host_sb_plat *plat = dev_get_plat(host_dev);
 
-	if (os_lseek(plat->fd, start * desc->blksz, OS_SEEK_SET) == -1) {
+	if (os_lseek(plat->fd, start * desc->blksz, OS_SEEK_SET) < 0) {
 		printf("ERROR: Invalid block %lx\n", start);
 		return -1;
 	}
diff --git a/drivers/usb/emul/sandbox_flash.c b/drivers/usb/emul/sandbox_flash.c
index 24420e3..b5176bb 100644
--- a/drivers/usb/emul/sandbox_flash.c
+++ b/drivers/usb/emul/sandbox_flash.c
@@ -196,7 +196,7 @@
 		   priv->fd != -1) {
 		offset = os_lseek(priv->fd, info->seek_block * info->block_size,
 				  OS_SEEK_SET);
-		if (offset == (off_t)-1)
+		if (offset < 0)
 			setup_fail_response(priv);
 		else
 			setup_response(priv);
diff --git a/fs/sandbox/sandboxfs.c b/fs/sandbox/sandboxfs.c
index 773b583..76f1a71 100644
--- a/fs/sandbox/sandboxfs.c
+++ b/fs/sandbox/sandboxfs.c
@@ -28,7 +28,7 @@
 	if (fd < 0)
 		return fd;
 	ret = os_lseek(fd, pos, OS_SEEK_SET);
-	if (ret == -1) {
+	if (ret < 0) {
 		os_close(fd);
 		return ret;
 	}
@@ -65,14 +65,14 @@
 	if (fd < 0)
 		return fd;
 	ret = os_lseek(fd, pos, OS_SEEK_SET);
-	if (ret == -1) {
+	if (ret < 0) {
 		os_close(fd);
 		return ret;
 	}
 	size = os_write(fd, buffer, towrite);
 	os_close(fd);
 
-	if (size == -1) {
+	if (size < 0) {
 		ret = -1;
 	} else {
 		ret = 0;
diff --git a/include/asm-generic/global_data.h b/include/asm-generic/global_data.h
index 27aa75e..19c66e1 100644
--- a/include/asm-generic/global_data.h
+++ b/include/asm-generic/global_data.h
@@ -30,6 +30,7 @@
 
 struct acpi_ctx;
 struct driver_rt;
+struct upl;
 
 typedef struct global_data gd_t;
 
@@ -491,6 +492,12 @@
 	 * @dmtag_list: List of DM tags
 	 */
 	struct list_head dmtag_list;
+#if CONFIG_IS_ENABLED(UPL)
+	/**
+	 * @upl: Universal Payload-handoff information
+	 */
+	struct upl *upl;
+#endif
 };
 #ifndef DO_DEPS_ONLY
 static_assert(sizeof(struct global_data) == GD_SIZE);
@@ -590,6 +597,14 @@
 #define gd_malloc_ptr()		0L
 #endif
 
+#if CONFIG_IS_ENABLED(UPL)
+#define gd_upl()		gd->upl
+#define gd_set_upl(_val)	gd->upl = (_val)
+#else
+#define gd_upl()		NULL
+#define gd_set_upl(val)
+#endif
+
 /**
  * enum gd_flags - global data flags
  *
@@ -701,6 +716,10 @@
 	 * @GD_FLG_HUSH_MODERN_PARSER: Use hush 2021 parser.
 	 */
 	GD_FLG_HUSH_MODERN_PARSER = 0x2000000,
+	/**
+	 * @GD_FLG_UPL: Read/write a Universal Payload (UPL) handoff
+	 */
+	GD_FLG_UPL = 0x4000000,
 };
 
 #endif /* __ASSEMBLY__ */
diff --git a/include/os.h b/include/os.h
index 877404a..4371270 100644
--- a/include/os.h
+++ b/include/os.h
@@ -29,7 +29,7 @@
  * @fd:		File descriptor as returned by os_open()
  * @buf:	Buffer to place data
  * @count:	Number of bytes to read
- * Return:	number of bytes read, or -1 on error
+ * Return:	number of bytes read, or -errno on error
  */
 ssize_t os_read(int fd, void *buf, size_t count);
 
@@ -39,7 +39,7 @@
  * @fd:		File descriptor as returned by os_open()
  * @buf:	Buffer containing data to write
  * @count:	Number of bytes to write
- * Return:	number of bytes written, or -1 on error
+ * Return:	number of bytes written, or -errno on error
  */
 ssize_t os_write(int fd, const void *buf, size_t count);
 
@@ -49,7 +49,7 @@
  * @fd:		File descriptor as returned by os_open()
  * @offset:	File offset (based on whence)
  * @whence:	Position offset is relative to (see below)
- * Return:	new file offset
+ * Return:	new file offset, or -errno on error
  */
 off_t os_lseek(int fd, off_t offset, int whence);
 
diff --git a/include/spl.h b/include/spl.h
index 1eebea3..f92089b 100644
--- a/include/spl.h
+++ b/include/spl.h
@@ -1073,4 +1073,20 @@
 {
 	return IS_ENABLED(CONFIG_SPL_GZIP) || IS_ENABLED(CONFIG_SPL_LZMA);
 }
+
+/**
+ * spl_write_upl_handoff() - Write a Universal Payload hand-off structure
+ *
+ * @spl_image: Information about the image being booted
+ * Return: 0 if OK, -ve on error
+ */
+int spl_write_upl_handoff(struct spl_image_info *spl_image);
+
+/**
+ * spl_upl_init() - Get UPL ready for information to be added
+ *
+ * This must be called before upl_add_image(), etc.
+ */
+void spl_upl_init(void);
+
 #endif
diff --git a/include/test/suites.h b/include/test/suites.h
index 365d5f2..2ceef57 100644
--- a/include/test/suites.h
+++ b/include/test/suites.h
@@ -63,5 +63,6 @@
 int do_ut_time(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
 int do_ut_unicode(struct cmd_tbl *cmdtp, int flag, int argc,
 		  char *const argv[]);
+int do_ut_upl(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
 
 #endif /* __TEST_SUITES_H__ */
diff --git a/include/upl.h b/include/upl.h
new file mode 100644
index 0000000..2ec5ef1
--- /dev/null
+++ b/include/upl.h
@@ -0,0 +1,382 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * UPL handoff generation
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#ifndef __UPL_WRITE_H
+#define __UPL_WRITE_H
+
+#ifndef USE_HOSTCC
+
+#include <alist.h>
+#include <image.h>
+#include <dm/ofnode_decl.h>
+
+struct unit_test_state;
+
+#define UPLP_ADDRESS_CELLS	"#address-cells"
+#define UPLP_SIZE_CELLS		"#size-cells"
+
+#define UPLN_OPTIONS		"options"
+#define UPLN_UPL_PARAMS		"upl-params"
+#define UPLP_SMBIOS		"smbios"
+#define UPLP_ACPI		"acpi"
+#define UPLP_BOOTMODE		"bootmode"
+#define UPLP_ADDR_WIDTH		"addr-width"
+#define UPLP_ACPI_NVS_SIZE	"acpi-nvs-size"
+
+#define UPLPATH_UPL_IMAGE	"/options/upl-image"
+#define UPLN_UPL_IMAGE		"upl-image"
+#define UPLN_IMAGE		"image"
+#define UPLP_FIT		"fit"
+#define UPLP_CONF_OFFSET	"conf-offset"
+#define UPLP_LOAD		"load"
+#define UPLP_SIZE		"size"
+#define UPLP_OFFSET		"offset"
+#define UPLP_DESCRIPTION	"description"
+
+#define UPLN_MEMORY		"memory"
+#define UPLP_HOTPLUGGABLE	"hotpluggable"
+
+#define UPLPATH_MEMORY_MAP	"/memory-map"
+#define UPLN_MEMORY_MAP		"memory-map"
+#define UPLP_USAGE		"usage"
+
+#define UPLN_MEMORY_RESERVED	"reserved-memory"
+#define UPLPATH_MEMORY_RESERVED	"/reserved-memory"
+#define UPLP_NO_MAP		"no-map"
+
+#define UPLN_SERIAL		"serial"
+#define UPLP_REG		"reg"
+#define UPLP_COMPATIBLE		"compatible"
+#define UPLP_CLOCK_FREQUENCY	"clock-frequency"
+#define UPLP_CURRENT_SPEED	"current-speed"
+#define UPLP_REG_IO_SHIFT	"reg-io-shift"
+#define UPLP_REG_OFFSET		"reg-offset"
+#define UPLP_REG_IO_WIDTH	"reg-io-width"
+#define UPLP_VIRTUAL_REG	"virtual-reg"
+#define UPLP_ACCESS_TYPE	"access-type"
+
+#define UPLN_GRAPHICS		"framebuffer"
+#define UPLC_GRAPHICS		"simple-framebuffer"
+#define UPLP_WIDTH		"width"
+#define UPLP_HEIGHT		"height"
+#define UPLP_STRIDE		"stride"
+#define UPLP_GRAPHICS_FORMAT	"format"
+
+/**
+ * enum upl_boot_mode - Encodes the boot mode
+ *
+ * Each is a bit number from the boot_mode mask
+ */
+enum upl_boot_mode {
+	UPLBM_FULL,
+	UPLBM_MINIMAL,
+	UPLBM_FAST,
+	UPLBM_DIAG,
+	UPLBM_DEFAULT,
+	UPLBM_S2,
+	UPLBM_S3,
+	UPLBM_S4,
+	UPLBM_S5,
+	UPLBM_FACTORY,
+	UPLBM_FLASH,
+	UPLBM_RECOVERY,
+
+	UPLBM_COUNT,
+};
+
+/**
+ * struct upl_image - UPL image informaiton
+ *
+ * @load: Address image was loaded to
+ * @size: Size of image in bytes
+ * @offset: Offset of the image in the FIT (0=none)
+ * @desc: Description of the iamge (taken from the FIT)
+ */
+struct upl_image {
+	ulong load;
+	ulong size;
+	uint offset;
+	const char *description;
+};
+
+/**
+ * struct memregion - Information about a region of memory
+ *
+ * @base: Base address
+ * @size: Size in bytes
+ */
+struct memregion {
+	ulong base;
+	ulong size;
+};
+
+/**
+ * struct upl_mem - Information about physical-memory layout
+ *
+ * TODO: Figure out initial-mapped-area
+ *
+ * @region: Memory region list (struct memregion)
+ * @hotpluggable: true if hotpluggable
+ */
+struct upl_mem {
+	struct alist region;
+	bool hotpluggable;
+};
+
+/**
+ * enum upl_usage - Encodes the usage
+ *
+ * Each is a bit number from the usage mask
+ */
+enum upl_usage {
+	UPLUS_ACPI_RECLAIM,
+	UPLUS_ACPI_NVS,
+	UPLUS_BOOT_CODE,
+	UPLUS_BOOT_DATA,
+	UPLUS_RUNTIME_CODE,
+	UPLUS_RUNTIME_DATA,
+	UPLUS_COUNT
+};
+
+/**
+ * struct upl_memmap - Information about logical-memory layout
+ *
+ * @name: Node name to use
+ * @region: Memory region list (struct memregion)
+ * @usage: Memory-usage mask (enum upl_usage)
+ */
+struct upl_memmap {
+	const char *name;
+	struct alist region;
+	uint usage;
+};
+
+/**
+ * struct upl_memres - Reserved memory
+ *
+ * @name: Node name to use
+ * @region: Reserved memory region list (struct memregion)
+ * @no_map: true to indicate that a virtual mapping must not be created
+ */
+struct upl_memres {
+	const char *name;
+	struct alist region;
+	bool no_map;
+};
+
+enum upl_serial_access_type {
+	UPLSAT_MMIO,
+	UPLSAT_IO,
+};
+
+/* serial defaults */
+enum {
+	UPLD_REG_IO_SHIFT	= 0,
+	UPLD_REG_OFFSET		= 0,
+	UPLD_REG_IO_WIDTH	= 1,
+};
+
+/**
+ * enum upl_access_type - Access types
+ *
+ * @UPLAT_MMIO: Memory-mapped I/O
+ * @UPLAT_IO: Separate I/O
+ */
+enum upl_access_type {
+	UPLAT_MMIO,
+	UPLAT_IO,
+};
+
+/**
+ * struct upl_serial - Serial console
+ *
+ * @compatible: Compatible string (NULL if there is no serial console)
+ * @clock_frequency: Input clock frequency of UART
+ * @current_speed: Current baud rate of UART
+ * @reg: List of base address and size of registers (struct memregion)
+ * @reg_shift_log2: log2 of distance between each register
+ * @reg_offset: Offset of registers from the base address
+ * @reg_width: Register width in bytes
+ * @virtual_reg: Virtual register access (0 for none)
+ * @access_type: Register access type to use
+ */
+struct upl_serial {
+	const char *compatible;
+	uint clock_frequency;
+	uint current_speed;
+	struct alist reg;
+	uint reg_io_shift;
+	uint reg_offset;
+	uint reg_io_width;
+	ulong virtual_reg;
+	enum upl_serial_access_type access_type;
+};
+
+/**
+ * enum upl_graphics_format - Graphics formats
+ *
+ * @UPLGF_ARGB32: 32bpp format using 0xaarrggbb
+ * @UPLGF_ABGR32: 32bpp format using 0xaabbggrr
+ * @UPLGF_ARGB64: 64bpp format using 0xaaaabbbbggggrrrr
+ */
+enum upl_graphics_format {
+	UPLGF_ARGB32,
+	UPLGF_ABGR32,
+	UPLGF_ABGR64,
+};
+
+/**
+ * @reg: List of base address and size of registers (struct memregion)
+ * @width: Width of display in pixels
+ * @height: Height of display in pixels
+ * @stride: Number of bytes from one line to the next
+ * @format: Pixel format
+ */
+struct upl_graphics {
+	struct alist reg;
+	uint width;
+	uint height;
+	uint stride;
+	enum upl_graphics_format format;
+};
+
+/*
+ * Information about the UPL state
+ *
+ * @addr_cells: Number of address cells used in the handoff
+ * @size_cells: Number of size cells used in the handoff
+ * @bootmode: Boot-mode mask (enum upl_boot_mode)
+ * @fit: Address of FIT image that was loaded
+ * @conf_offset: Offset in FIT of the configuration that was selected
+ * @addr_width: Adress-bus width of machine, e.g. 46 for 46 bits
+ * @acpi_nvs_size: Size of the ACPI non-volatile-storage area in bytes
+ * @image: Information about each image (struct upl_image)
+ * @mem: Information about physical-memory regions (struct upl_mem)
+ * @nennap: Information about logical-memory regions (struct upl_memmap)
+ * @nennap: Information about reserved-memory regions (struct upl_memres)
+ */
+struct upl {
+	int addr_cells;
+	int size_cells;
+
+	ulong smbios;
+	ulong acpi;
+	uint bootmode;
+	ulong fit;
+	uint conf_offset;
+	uint addr_width;
+	uint acpi_nvs_size;
+
+	struct alist image;
+	struct alist mem;
+	struct alist memmap;
+	struct alist memres;
+	struct upl_serial serial;
+	struct upl_graphics graphics;
+};
+
+/**
+ * upl_write_handoff() - Write a Unversal Payload handoff structure
+ *
+ * upl: UPL state to write
+ * @root: root node to write it to
+ * @skip_existing: Avoid recreating any nodes which already exist in the
+ * devicetree. For example, if there is a serial node, just leave it alone,
+ * since don't need to create a new one
+ * Return: 0 on success, -ve on error
+ */
+int upl_write_handoff(const struct upl *upl, ofnode root, bool skip_existing);
+
+/**
+ * upl_create_handoff_tree() - Write a Unversal Payload handoff structure
+ *
+ * upl: UPL state to write
+ * @treep: Returns a new tree containing the handoff
+ * Return: 0 on success, -ve on error
+ */
+int upl_create_handoff_tree(const struct upl *upl, oftree *treep);
+
+/**
+ * upl_read_handoff() - Read a Unversal Payload handoff structure
+ *
+ * upl: UPL state to read into
+ * @tree: Devicetree containing the data to read
+ * Return: 0 on success, -ve on error
+ */
+int upl_read_handoff(struct upl *upl, oftree tree);
+
+/**
+ * upl_get_test_data() - Fill a UPL with some test data
+ *
+ * @uts: Test state (can be uninited)
+ * @upl: Returns test data
+ * Return: 0 on success, 1 on error
+ */
+int upl_get_test_data(struct unit_test_state *uts, struct upl *upl);
+#endif /* USE_HOSTCC */
+
+#if CONFIG_IS_ENABLED(UPL) && defined(CONFIG_SPL_BUILD)
+
+/**
+ * upl_set_fit_info() - Set up basic info about the FIT
+ *
+ * @fit: Address of FIT
+ * @conf_offset: Configuration node being used
+ * @entry_addr: Entry address for next phase
+ */
+void upl_set_fit_info(ulong fit, int conf_offset, ulong entry_addr);
+
+/**
+ * upl_set_fit_addr() - Set up the address of the FIT
+ *
+ * @fit: Address of FIT
+ */
+void upl_set_fit_addr(ulong fit);
+
+#else
+static inline void upl_set_fit_addr(ulong fit) {}
+static inline void upl_set_fit_info(ulong fit, int conf_offset,
+				    ulong entry_addr) {}
+#endif /* UPL && SPL */
+
+/**
+ * _upl_add_image() - Internal function to add a new image to the UPL
+ *
+ * @node: Image node offset in FIT
+ * @load_addr: Address to which images was loaded
+ * @size: Image size in bytes
+ * @desc: Description of image
+ * Return: 0 if OK, -ENOMEM if out of memory
+ */
+int _upl_add_image(int node, ulong load_addr, ulong size, const char *desc);
+
+/**
+ * upl_add_image() - Add a new image to the UPL
+ *
+ * @fit: Pointer to FIT
+ * @node: Image node offset in FIT
+ * @load_addr: Address to which images was loaded
+ * @size: Image size in bytes
+ * Return: 0 if OK, -ENOMEM if out of memory
+ */
+static inline int upl_add_image(const void *fit, int node, ulong load_addr,
+				ulong size)
+{
+	if (CONFIG_IS_ENABLED(UPL) && IS_ENABLED(CONFIG_SPL_BUILD)) {
+		const char *desc = fdt_getprop(fit, node, FIT_DESC_PROP, NULL);
+
+		return _upl_add_image(node, load_addr, size, desc);
+	}
+
+	return 0;
+}
+
+/** upl_init() - Set up a UPL struct */
+void upl_init(struct upl *upl);
+
+#endif /* __UPL_WRITE_H */
diff --git a/lib/fdtdec.c b/lib/fdtdec.c
index 6865f78..5edc8dd 100644
--- a/lib/fdtdec.c
+++ b/lib/fdtdec.c
@@ -1685,6 +1685,7 @@
 				gd->fdt_src = FDTSRC_BLOBLIST;
 				log_debug("Devicetree is in bloblist at %p\n",
 					  gd->fdt_blob);
+				ret = 0;
 			} else {
 				log_debug("No FDT found in bloblist\n");
 				ret = -ENOENT;
diff --git a/test/boot/Makefile b/test/boot/Makefile
index 068522c..8ec5daa 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -13,3 +13,5 @@
 obj-$(CONFIG_BOOTMETH_VBE_SIMPLE) += vbe_simple.o
 endif
 obj-$(CONFIG_BOOTMETH_VBE) += vbe_fixup.o
+
+obj-$(CONFIG_UPL) += upl.o
diff --git a/test/boot/upl.c b/test/boot/upl.c
new file mode 100644
index 0000000..364fb05
--- /dev/null
+++ b/test/boot/upl.c
@@ -0,0 +1,437 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * UPL handoff testing
+ *
+ * Copyright 2024 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#include <abuf.h>
+#include <mapmem.h>
+#include <upl.h>
+#include <dm/ofnode.h>
+#include <test/suites.h>
+#include <test/test.h>
+#include <test/ut.h>
+#include "bootstd_common.h"
+
+/* Declare a new upl test */
+#define UPL_TEST(_name, _flags)	UNIT_TEST(_name, _flags, upl_test)
+
+static int add_region(struct unit_test_state *uts, struct alist *lst,
+		      ulong base, ulong size)
+{
+	struct memregion region;
+
+	region.base = base;
+	region.size = size;
+	ut_assertnonnull(alist_add(lst, region));
+
+	return 0;
+}
+
+int upl_get_test_data(struct unit_test_state *uts, struct upl *upl)
+{
+	struct upl_memmap memmap;
+	struct upl_memres memres;
+	struct upl_image img;
+	struct upl_mem mem;
+
+	upl_init(upl);
+
+	upl->addr_cells = 1;
+	upl->size_cells = 1;
+	upl->smbios = 0x123;
+	upl->acpi = 0x456;
+	upl->bootmode = BIT(UPLBM_DEFAULT) | BIT(UPLBM_S3);
+	upl->fit = 0x789;
+	upl->conf_offset = 0x234;
+	upl->addr_width = 46;
+	upl->acpi_nvs_size = 0x100;
+
+	/* image[0] */
+	img.load = 0x1;
+	img.size = 0x2;
+	img.offset = 0x3;
+	img.description = "U-Boot";
+	ut_assertnonnull(alist_add(&upl->image, img));
+
+	/* image[1] */
+	img.load = 0x4;
+	img.size = 0x5;
+	img.offset = 0x6;
+	img.description = "ATF";
+	ut_assertnonnull(alist_add(&upl->image, img));
+
+	/* mem[0] : 3 regions */
+	memset(&mem, '\0', sizeof(mem));
+	alist_init_struct(&mem.region, struct memregion);
+	ut_assertok(add_region(uts, &mem.region, 0x10, 0x20));
+	ut_assertok(add_region(uts, &mem.region, 0x30, 0x40));
+	ut_assertok(add_region(uts, &mem.region, 0x40, 0x50));
+	ut_assertnonnull(alist_add(&upl->mem, mem));
+
+	/* mem[0] : 1 region */
+	alist_init_struct(&mem.region, struct memregion);
+	ut_assertok(add_region(uts, &mem.region, 0x70, 0x80));
+	mem.hotpluggable = true;
+	ut_assertnonnull(alist_add(&upl->mem, mem));
+	mem.hotpluggable = false;
+
+	/* memmap[0] : 5 regions */
+	alist_init_struct(&memmap.region, struct memregion);
+	memmap.name = "acpi";
+	memmap.usage = BIT(UPLUS_ACPI_RECLAIM);
+	ut_assertok(add_region(uts, &memmap.region, 0x11, 0x12));
+	ut_assertok(add_region(uts, &memmap.region, 0x13, 0x14));
+	ut_assertok(add_region(uts, &memmap.region, 0x15, 0x16));
+	ut_assertok(add_region(uts, &memmap.region, 0x17, 0x18));
+	ut_assertok(add_region(uts, &memmap.region, 0x19, 0x1a));
+	ut_assertnonnull(alist_add(&upl->memmap, memmap));
+
+	/* memmap[1] : 1 region */
+	memmap.name = "u-boot";
+	memmap.usage = BIT(UPLUS_BOOT_DATA);
+	alist_init_struct(&memmap.region, struct memregion);
+	ut_assertok(add_region(uts, &memmap.region, 0x21, 0x22));
+	ut_assertnonnull(alist_add(&upl->memmap, memmap));
+
+	/* memmap[2] : 1 region */
+	alist_init_struct(&memmap.region, struct memregion);
+	memmap.name = "efi";
+	memmap.usage = BIT(UPLUS_RUNTIME_CODE);
+	ut_assertok(add_region(uts, &memmap.region, 0x23, 0x24));
+	ut_assertnonnull(alist_add(&upl->memmap, memmap));
+
+	/* memmap[3]: 2 regions */
+	alist_init_struct(&memmap.region, struct memregion);
+	memmap.name = "empty";
+	memmap.usage = 0;
+	ut_assertok(add_region(uts, &memmap.region, 0x25, 0x26));
+	ut_assertok(add_region(uts, &memmap.region, 0x27, 0x28));
+	ut_assertnonnull(alist_add(&upl->memmap, memmap));
+
+	/* memmap[4]: 1 region */
+	alist_init_struct(&memmap.region, struct memregion);
+	memmap.name = "acpi-things";
+	memmap.usage = BIT(UPLUS_RUNTIME_CODE) | BIT(UPLUS_ACPI_NVS);
+	ut_assertok(add_region(uts, &memmap.region, 0x29, 0x2a));
+	ut_assertnonnull(alist_add(&upl->memmap, memmap));
+
+	/* memres[0]: 1 region */
+	alist_init_struct(&memres.region, struct memregion);
+	memset(&memres, '\0', sizeof(memres));
+	memres.name = "mmio";
+	ut_assertok(add_region(uts, &memres.region, 0x2b, 0x2c));
+	ut_assertnonnull(alist_add(&upl->memres, memres));
+
+	/* memres[1]: 2 regions */
+	alist_init_struct(&memres.region, struct memregion);
+	memres.name = "memory";
+	ut_assertok(add_region(uts, &memres.region, 0x2d, 0x2e));
+	ut_assertok(add_region(uts, &memres.region, 0x2f, 0x30));
+	memres.no_map = true;
+	ut_assertnonnull(alist_add(&upl->memres, memres));
+
+	upl->serial.compatible = "ns16550a";
+	upl->serial.clock_frequency = 1843200;
+	upl->serial.current_speed = 115200;
+	alist_init_struct(&upl->serial.reg, struct memregion);
+	ut_assertok(add_region(uts, &upl->serial.reg, 0xf1de0000, 0x100));
+	upl->serial.reg_io_shift = 2;
+	upl->serial.reg_offset = 0x40;
+	upl->serial.reg_io_width = 1;
+	upl->serial.virtual_reg = 0x20000000;
+	upl->serial.access_type = UPLSAT_MMIO;
+
+	alist_init_struct(&upl->graphics.reg, struct memregion);
+	ut_assertok(add_region(uts, &upl->graphics.reg, 0xd0000000, 0x10000000));
+	upl->graphics.width = 1280;
+	upl->graphics.height = 1280;
+	upl->graphics.stride = upl->graphics.width * 4;
+	upl->graphics.format = UPLGF_ARGB32;
+
+	return 0;
+}
+
+static int compare_upl_image(struct unit_test_state *uts,
+			     const struct upl_image *base,
+			     const struct upl_image *cmp)
+{
+	ut_asserteq(base->load, cmp->load);
+	ut_asserteq(base->size, cmp->size);
+	ut_asserteq(base->offset, cmp->offset);
+	ut_asserteq_str(base->description, cmp->description);
+
+	return 0;
+}
+
+static int compare_upl_memregion(struct unit_test_state *uts,
+				 const struct memregion *base,
+				 const struct memregion *cmp)
+{
+	ut_asserteq(base->base, cmp->base);
+	ut_asserteq(base->size, cmp->size);
+
+	return 0;
+}
+
+static int compare_upl_mem(struct unit_test_state *uts,
+			   const struct upl_mem *base,
+			   const struct upl_mem *cmp)
+{
+	int i;
+
+	ut_asserteq(base->region.count, cmp->region.count);
+	ut_asserteq(base->hotpluggable, cmp->hotpluggable);
+	for (i = 0; i < base->region.count; i++) {
+		ut_assertok(compare_upl_memregion(uts,
+			alist_get(&base->region, i, struct memregion),
+			alist_get(&cmp->region, i, struct memregion)));
+	}
+
+	return 0;
+}
+
+static int check_device_name(struct unit_test_state *uts, const char *base,
+			     const char *cmp)
+{
+	const char *p;
+
+	p = strchr(cmp, '@');
+	if (p) {
+		ut_assertnonnull(p);
+		ut_asserteq_strn(base, cmp);
+		ut_asserteq(p - cmp, strlen(base));
+	} else {
+		ut_asserteq_str(base, cmp);
+	}
+
+	return 0;
+}
+
+static int compare_upl_memmap(struct unit_test_state *uts,
+			      const struct upl_memmap *base,
+			      const struct upl_memmap *cmp)
+{
+	int i;
+
+	ut_assertok(check_device_name(uts, base->name, cmp->name));
+	ut_asserteq(base->region.count, cmp->region.count);
+	ut_asserteq(base->usage, cmp->usage);
+	for (i = 0; i < base->region.count; i++)
+		ut_assertok(compare_upl_memregion(uts,
+			alist_get(&base->region, i, struct memregion),
+			alist_get(&cmp->region, i, struct memregion)));
+
+	return 0;
+}
+
+static int compare_upl_memres(struct unit_test_state *uts,
+			      const struct upl_memres *base,
+			      const struct upl_memres *cmp)
+{
+	int i;
+
+	ut_assertok(check_device_name(uts, base->name, cmp->name));
+	ut_asserteq(base->region.count, cmp->region.count);
+	ut_asserteq(base->no_map, cmp->no_map);
+	for (i = 0; i < base->region.count; i++)
+		ut_assertok(compare_upl_memregion(uts,
+			alist_get(&base->region, i, struct memregion),
+			alist_get(&cmp->region, i, struct memregion)));
+
+	return 0;
+}
+
+static int compare_upl_serial(struct unit_test_state *uts,
+			      struct upl_serial *base, struct upl_serial *cmp)
+{
+	int i;
+
+	ut_asserteq_str(base->compatible, cmp->compatible);
+	ut_asserteq(base->clock_frequency, cmp->clock_frequency);
+	ut_asserteq(base->current_speed, cmp->current_speed);
+	for (i = 0; i < base->reg.count; i++)
+		ut_assertok(compare_upl_memregion(uts,
+			alist_get(&base->reg, i, struct memregion),
+			alist_get(&cmp->reg, i, struct memregion)));
+	ut_asserteq(base->reg_io_shift, cmp->reg_io_shift);
+	ut_asserteq(base->reg_offset, cmp->reg_offset);
+	ut_asserteq(base->reg_io_width, cmp->reg_io_width);
+	ut_asserteq(base->virtual_reg, cmp->virtual_reg);
+	ut_asserteq(base->access_type, cmp->access_type);
+
+	return 0;
+}
+
+static int compare_upl_graphics(struct unit_test_state *uts,
+				struct upl_graphics *base,
+				struct upl_graphics *cmp)
+{
+	int i;
+
+	for (i = 0; i < base->reg.count; i++)
+		ut_assertok(compare_upl_memregion(uts,
+			alist_get(&base->reg, i, struct memregion),
+			alist_get(&cmp->reg, i, struct memregion)));
+	ut_asserteq(base->width, cmp->width);
+	ut_asserteq(base->height, cmp->height);
+	ut_asserteq(base->stride, cmp->stride);
+	ut_asserteq(base->format, cmp->format);
+
+	return 0;
+}
+
+static int compare_upl(struct unit_test_state *uts, struct upl *base,
+		       struct upl *cmp)
+{
+	int i;
+
+	ut_asserteq(base->addr_cells, cmp->addr_cells);
+	ut_asserteq(base->size_cells, cmp->size_cells);
+
+	ut_asserteq(base->smbios, cmp->smbios);
+	ut_asserteq(base->acpi, cmp->acpi);
+	ut_asserteq(base->bootmode, cmp->bootmode);
+	ut_asserteq(base->fit, cmp->fit);
+	ut_asserteq(base->conf_offset, cmp->conf_offset);
+	ut_asserteq(base->addr_width, cmp->addr_width);
+	ut_asserteq(base->acpi_nvs_size, cmp->acpi_nvs_size);
+
+	ut_asserteq(base->image.count, cmp->image.count);
+	for (i = 0; i < base->image.count; i++)
+		ut_assertok(compare_upl_image(uts,
+			alist_get(&base->image, i, struct upl_image),
+			alist_get(&cmp->image, i, struct upl_image)));
+
+	ut_asserteq(base->mem.count, cmp->mem.count);
+	for (i = 0; i < base->mem.count; i++)
+		ut_assertok(compare_upl_mem(uts,
+			alist_get(&base->mem, i, struct upl_mem),
+			alist_get(&cmp->mem, i, struct upl_mem)));
+
+	ut_asserteq(base->memmap.count, cmp->memmap.count);
+	for (i = 0; i < base->memmap.count; i++)
+		ut_assertok(compare_upl_memmap(uts,
+			alist_get(&base->memmap, i, struct upl_memmap),
+			alist_get(&cmp->memmap, i, struct upl_memmap)));
+
+	ut_asserteq(base->memres.count, cmp->memres.count);
+	for (i = 0; i < base->memres.count; i++)
+		ut_assertok(compare_upl_memres(uts,
+			alist_get(&base->memres, i, struct upl_memres),
+			alist_get(&cmp->memres, i, struct upl_memres)));
+
+	ut_assertok(compare_upl_serial(uts, &base->serial, &cmp->serial));
+	ut_assertok(compare_upl_graphics(uts, &base->graphics, &cmp->graphics));
+
+	return 0;
+}
+
+/* Basic test of writing and reading UPL handoff */
+static int upl_test_base(struct unit_test_state *uts)
+{
+	oftree tree, check_tree;
+	struct upl upl, check;
+	struct abuf buf;
+
+	if (!CONFIG_IS_ENABLED(OFNODE_MULTI_TREE))
+		return -EAGAIN;  /* skip test */
+	ut_assertok(upl_get_test_data(uts, &upl));
+
+	ut_assertok(upl_create_handoff_tree(&upl, &tree));
+	ut_assertok(oftree_to_fdt(tree, &buf));
+
+	/*
+	 * strings in check_tree and therefore check are only valid so long as
+	 * buf stays around. As soon as we call abuf_uninit they go away
+	 */
+	check_tree = oftree_from_fdt(abuf_data(&buf));
+	ut_assert(ofnode_valid(oftree_path(check_tree, "/")));
+
+	ut_assertok(upl_read_handoff(&check, check_tree));
+	ut_assertok(compare_upl(uts, &upl, &check));
+	abuf_uninit(&buf);
+
+	return 0;
+}
+UPL_TEST(upl_test_base, 0);
+
+/* Test 'upl info' command */
+static int upl_test_info(struct unit_test_state *uts)
+{
+	gd_set_upl(NULL);
+	ut_assertok(run_command("upl info", 0));
+	ut_assert_nextline("UPL state: inactive");
+	ut_assert_console_end();
+
+	gd_set_upl((struct upl *)uts);	/* set it to any non-zero value */
+	ut_assertok(run_command("upl info", 0));
+	ut_assert_nextline("UPL state: active");
+	ut_assert_console_end();
+	gd_set_upl(NULL);
+
+	return 0;
+}
+UPL_TEST(upl_test_info, UT_TESTF_CONSOLE_REC);
+
+/* Test 'upl read' and 'upl_write' commands */
+static int upl_test_read_write(struct unit_test_state *uts)
+{
+	ulong addr;
+
+	if (!CONFIG_IS_ENABLED(OFNODE_MULTI_TREE))
+		return -EAGAIN;  /* skip test */
+	ut_assertok(run_command("upl write", 0));
+
+	addr = env_get_hex("upladdr", 0);
+	ut_assert_nextline("UPL handoff written to %lx size %lx", addr,
+			   env_get_hex("uplsize", 0));
+	ut_assert_console_end();
+
+	ut_assertok(run_command("upl read ${upladdr}", 0));
+	ut_assert_nextline("Reading UPL at %lx", addr);
+	ut_assert_console_end();
+
+	return 0;
+}
+UPL_TEST(upl_test_read_write, UT_TESTF_CONSOLE_REC);
+
+/* Test UPL passthrough */
+static int upl_test_info_norun(struct unit_test_state *uts)
+{
+	const struct upl_image *img;
+	struct upl *upl = gd_upl();
+	const void *fit;
+
+	ut_assertok(run_command("upl info -v", 0));
+	ut_assert_nextline("UPL state: active");
+	ut_assert_nextline("fit %lx", upl->fit);
+	ut_assert_nextline("conf_offset %x", upl->conf_offset);
+	ut_assert_nextlinen("image 0");
+	ut_assert_nextlinen("image 1");
+	ut_assert_console_end();
+
+	/* check the offsets */
+	fit = map_sysmem(upl->fit, 0);
+	ut_asserteq_str("conf-1", fdt_get_name(fit, upl->conf_offset, NULL));
+
+	ut_asserteq(2, upl->image.count);
+
+	img = alist_get(&upl->image, 1, struct upl_image);
+	ut_asserteq_str("firmware-1", fdt_get_name(fit, img->offset, NULL));
+	ut_asserteq(CONFIG_TEXT_BASE, img->load);
+
+	return 0;
+}
+UPL_TEST(upl_test_info_norun, UT_TESTF_CONSOLE_REC | UT_TESTF_MANUAL);
+
+int do_ut_upl(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
+{
+	struct unit_test *tests = UNIT_TEST_SUITE_START(upl_test);
+	const int n_ents = UNIT_TEST_SUITE_COUNT(upl_test);
+
+	return cmd_ut_category("cmd_upl", "cmd_upl_", tests, n_ents, argc,
+			       argv);
+}
diff --git a/test/cmd_ut.c b/test/cmd_ut.c
index 4e4aa8f..38ba89e 100644
--- a/test/cmd_ut.c
+++ b/test/cmd_ut.c
@@ -133,6 +133,9 @@
 #ifdef CONFIG_CMD_SEAMA
 	U_BOOT_CMD_MKENT(seama, CONFIG_SYS_MAXARGS, 1, do_ut_seama, "", ""),
 #endif
+#ifdef CONFIG_CMD_UPL
+	U_BOOT_CMD_MKENT(upl, CONFIG_SYS_MAXARGS, 1, do_ut_upl, "", ""),
+#endif
 };
 
 static int do_ut_all(struct cmd_tbl *cmdtp, int flag, int argc,
diff --git a/test/image/spl_load_os.c b/test/image/spl_load_os.c
index 7d5fb9b..56105a5 100644
--- a/test/image/spl_load_os.c
+++ b/test/image/spl_load_os.c
@@ -10,63 +10,12 @@
 #include <test/spl.h>
 #include <test/ut.h>
 
-/* Context used for this test */
-struct text_ctx {
-	int fd;
-};
-
-static ulong read_fit_image(struct spl_load_info *load, ulong offset,
-			    ulong size, void *buf)
-{
-	struct text_ctx *text_ctx = load->priv;
-	off_t ret;
-	ssize_t res;
-
-	ret = os_lseek(text_ctx->fd, offset, OS_SEEK_SET);
-	if (ret != offset) {
-		printf("Failed to seek to %zx, got %zx (errno=%d)\n", offset,
-		       ret, errno);
-		return 0;
-	}
-
-	res = os_read(text_ctx->fd, buf, size);
-	if (res == -1) {
-		printf("Failed to read %lx bytes, got %ld (errno=%d)\n",
-		       size, res, errno);
-		return 0;
-	}
-
-	return size;
-}
-
 static int spl_test_load(struct unit_test_state *uts)
 {
 	struct spl_image_info image;
-	struct legacy_img_hdr *header;
-	struct text_ctx text_ctx;
-	struct spl_load_info load;
 	char fname[256];
-	int ret;
-	int fd;
-
-	memset(&load, '\0', sizeof(load));
-	spl_set_bl_len(&load, 512);
-	load.read = read_fit_image;
-
-	ret = sandbox_find_next_phase(fname, sizeof(fname), true);
-	if (ret)
-		ut_assertf(0, "%s not found, error %d\n", fname, ret);
-
-	header = spl_get_load_buffer(-sizeof(*header), sizeof(*header));
-
-	fd = os_open(fname, OS_O_RDONLY);
-	ut_assert(fd >= 0);
-	ut_asserteq(512, os_read(fd, header, 512));
-	text_ctx.fd = fd;
-
-	load.priv = &text_ctx;
 
-	ut_assertok(spl_load_simple_fit(&image, &load, 0, header));
+	ut_assertok(sandbox_spl_load_fit(fname, sizeof(fname), &image));
 
 	return 0;
 }
diff --git a/test/py/tests/test_upl.py b/test/py/tests/test_upl.py
new file mode 100644
index 0000000..3164bda
--- /dev/null
+++ b/test/py/tests/test_upl.py
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright 2024 Google LLC
+#
+# Test addition of Universal Payload
+
+import os
+
+import pytest
+import u_boot_utils
+
+@pytest.mark.boardspec('sandbox_vpl')
+def test_upl_handoff(u_boot_console):
+    """Test of UPL handoff
+
+    This works by starting up U-Boot VPL, which gets to SPL and then sets up a
+    UPL handoff using the FIT containing U-Boot proper. It then jumps to U-Boot
+    proper and runs a test to check that the parameters are correct.
+
+    The entire FIT is loaded into memory in SPL (in upl_load_from_image()) so
+    that it can be inpected in upl_test_info_norun
+    """
+    cons = u_boot_console
+    ram = os.path.join(cons.config.build_dir, 'ram.bin')
+    fdt = os.path.join(cons.config.build_dir, 'u-boot.dtb')
+
+    # Remove any existing RAM file, so we don't have old data present
+    if os.path.exists(ram):
+        os.remove(ram)
+    flags = ['-m', ram, '-d', fdt, '--upl']
+    cons.restart_uboot_with_flags(flags, use_dtb=False)
+
+    # Make sure that Universal Payload is detected in U-Boot proper
+    output = cons.run_command('upl info')
+    assert 'UPL state: active' == output
+
+    # Check the FIT offsets look correct
+    output = cons.run_command('ut upl -f upl_test_info_norun')
+    assert 'Failures: 0' in output