From da272c5168dfe918e0adb43f8973d52b57d51dfb Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 14:02:34 -0700 Subject: [PATCH 1/9] tools: add an x86 FSP UPD decoder driven by the FspmUpd.h offset comments --- tools/x86_fsp/decode_fsp_upd.py | 204 ++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100755 tools/x86_fsp/decode_fsp_upd.py diff --git a/tools/x86_fsp/decode_fsp_upd.py b/tools/x86_fsp/decode_fsp_upd.py new file mode 100755 index 0000000000..5401bcf31f --- /dev/null +++ b/tools/x86_fsp/decode_fsp_upd.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Decode an Intel FSP UPD block into named fields. + +The UPD region is a flat C struct whose only authoritative layout is the +FspmUpd.h / FspsUpd.h that ships with the FSP, where each member is annotated +with its byte offset. This parses that header and uses it to decode either the +hex block WOLFBOOT_DUMP_FSP_UPD prints over the UART, or a UPD region in a +flash image. --diff reports only the fields that differ between two captures. + +Examples: + decode_fsp_upd.py --header include/x86/fsp/FspmUpd.h --log boot.log + decode_fsp_upd.py --header ... --bin image.bin --at 0x1F3A384 + decode_fsp_upd.py --header ... --diff ours.txt intel.txt +""" + +import argparse +import re +import struct +import sys + +# Offsets in these headers are relative to the start of the outer UPD struct +# (FSPM_UPD), not to the inner config struct, so a single flat table works. +# The declaration must immediately follow its comment, or a struct-typed member +# in between (FSPM_UPD has three) mis-pairs the offset with the next scalar. +DECL = re.compile( + r"/\*\*\s*Offset\s+(0x[0-9A-Fa-f]{4})\s*-?\s*([^\n*]*?)\s*\n" # offset + title + r"(.*?)" # body + r"\*\*/[ \t]*\n" + r"[ \t]*(\w+)[ \t]+(\w+)[ \t]*(?:\[\s*(\d+)\s*\])?[ \t]*;", + re.S) + +WIDTH = {"UINT8": 1, "UINT16": 2, "UINT32": 4, "UINT64": 8, + "INT8": 1, "INT16": 2, "INT32": 4, "INT64": 8} +FMT = {1: "B", 2: " nxt: + problems.append(" overlap: %s ends 0x%04X, next (%s) starts 0x%04X" + % (name, end, fields[i + 1][2], nxt)) + return problems + + +def hex_from_log(path): + """Pull every UPD hex dump out of a wolfBoot console capture.""" + blocks, cur = [], [] + for raw in open(path, "r", errors="replace"): + line = raw.strip() + if re.fullmatch(r"[0-9A-Fa-f]{2,32}", line) and len(line) % 2 == 0: + cur.append(line) + else: + if cur: + blocks.append("".join(cur)) + cur = [] + if cur: + blocks.append("".join(cur)) + # A stray hex-looking log line is not a dump; keep only plausible ones. + return [bytes.fromhex(b) for b in blocks if len(b) >= 256] + + +def value_of(data, off, ctype, count): + w = WIDTH[ctype] + if off + w * count > len(data): + return None + if count == 1: + return struct.unpack_from(FMT[w], data, off)[0] + return [struct.unpack_from(FMT[w], data, off + i * w)[0] for i in range(count)] + + +def fmt_value(v): + if isinstance(v, list): + if len(v) > 16: + return "[" + ", ".join(str(x) for x in v[:16]) + ", ...]" + return "[" + ", ".join(str(x) for x in v) + "]" + return str(v) + + +def decode(fields, data, show_all): + out = [] + for off, ctype, name, count, title in fields: + v = value_of(data, off, ctype, count) + if v is None: + continue + if not show_all and name.startswith(("UnusedUpdSpace", "Reserved")): + continue + out.append("0x%04X %-40s %s" % (off, name, fmt_value(v))) + return out + + +def load_decoded(path): + """Read a previously written decode for --diff.""" + vals = {} + for line in open(path, "r", errors="replace"): + m = re.match(r"0x([0-9A-Fa-f]{4})\s+(\S+)\s+(.*)", line.rstrip()) + if m: + vals[m.group(2)] = (m.group(1), m.group(3)) + return vals + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--header", help="FspmUpd.h / FspsUpd.h to take the layout from") + ap.add_argument("--log", help="wolfBoot console capture containing a hex dump") + ap.add_argument("--bin", help="binary file holding a UPD region") + ap.add_argument("--at", help="offset of the UPD inside --bin (hex or decimal)") + ap.add_argument("--which", type=int, default=-1, + help="which dump from --log to decode (default: the last)") + ap.add_argument("--all", action="store_true", + help="include Reserved/UnusedUpdSpace padding") + ap.add_argument("--diff", nargs=2, metavar=("A", "B"), + help="diff two files previously produced by this tool") + args = ap.parse_args() + + if args.diff: + a, b = load_decoded(args.diff[0]), load_decoded(args.diff[1]) + common = [n for n in a if n in b] + names = [n for n in common if a[n][1] != b[n][1]] + only = [n for n in a if n not in b] + [n for n in b if n not in a] + print("%-40s %-28s %s" % ("FIELD", args.diff[0], args.diff[1])) + for n in sorted(names, key=lambda n: a[n][0]): + print("%-40s %-28s %s" % (n, a[n][1], b[n][1])) + print("\n%d of %d common fields differ" % (len(names), len(common))) + if only: + print("fields present in only one side: %s" % ", ".join(sorted(only))) + return 0 + + if not args.header: + ap.error("--header is required unless --diff is used") + fields = parse_header(args.header) + if not fields: + print("no offset-annotated fields found in %s" % args.header, file=sys.stderr) + return 1 + problems = check_layout(fields) + if problems: + print("WARNING: parsed layout is not self-consistent:", file=sys.stderr) + for p in problems[:10]: + print(p, file=sys.stderr) + + if args.log: + blocks = hex_from_log(args.log) + if not blocks: + print("no UPD hex dump found in %s" % args.log, file=sys.stderr) + return 1 + idx = args.which % len(blocks) + print("# %s: %d dump(s) found, decoding #%d (%d bytes)" + % (args.log, len(blocks), idx, len(blocks[idx]))) + data = blocks[idx] + elif args.bin: + raw = open(args.bin, "rb").read() + at = int(args.at, 0) if args.at else 0 + data = raw[at:at + 0x1000] + print("# %s at 0x%X" % (args.bin, at)) + else: + ap.error("one of --log, --bin or --diff is required") + + sig = data[:8].decode("ascii", "replace") + print("# signature %r size %d bytes" % (sig, len(data))) + for line in decode(fields, data, args.all): + print(line) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ff92f35777ed2ab9a5cc07dad95d1ce5efc25eab Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 14:56:15 -0700 Subject: [PATCH 2/9] x86 FSP: honour NotifyPhase reset requests, including the Tiger Lake global reset --- include/x86/common.h | 3 +++ include/x86/fsp.h | 12 +++++++++++ src/boot_x86_fsp.c | 3 +-- src/x86/common.c | 27 ++++++++++++++++++++++++ src/x86/fsp.c | 49 +++++++++++++++++++++++++++++++++----------- 5 files changed, 80 insertions(+), 14 deletions(-) diff --git a/include/x86/common.h b/include/x86/common.h index 5a40701226..954ae07a6f 100644 --- a/include/x86/common.h +++ b/include/x86/common.h @@ -60,6 +60,9 @@ uint16_t io_read16(uint16_t port); void io_write32(uint16_t port, uint32_t value); uint32_t io_read32(uint16_t port); void reset(uint8_t warm); +#ifdef WOLFBOOT_TGL +int global_reset(void); +#endif void delay(int msec); __attribute__((noreturn)) void panic(void); void cpuid(uint32_t eax_param, diff --git a/include/x86/fsp.h b/include/x86/fsp.h index ef7cf21fca..b404f2689e 100644 --- a/include/x86/fsp.h +++ b/include/x86/fsp.h @@ -29,4 +29,16 @@ int fsp_get_image_revision(struct fsp_info_header *h, int *build, void print_fsp_image_revision(struct fsp_info_header *h); void fsp_init_silicon(void); +/* Reset requests (not failures) from FspMemInit and NotifyPhase. */ +#define FSP_STATUS_RESET_REQUIRED_COLD 0x40000001 +#define FSP_STATUS_RESET_REQUIRED_WARM 0x40000002 +/* Codes 3..8 are platform-defined. On Intel client SoCs FSP-S returns _3 to + * request a global reset (host + CSME), needed for the ChipsetInit sync. */ +#define FSP_STATUS_RESET_REQUIRED_3 0x40000003 +#define FSP_STATUS_RESET_REQUIRED_4 0x40000004 +#define FSP_STATUS_RESET_REQUIRED_5 0x40000005 +#define FSP_STATUS_RESET_REQUIRED_6 0x40000006 +#define FSP_STATUS_RESET_REQUIRED_7 0x40000007 +#define FSP_STATUS_RESET_REQUIRED_8 0x40000008 + #endif /* FSP_H */ diff --git a/src/boot_x86_fsp.c b/src/boot_x86_fsp.c index 5b4596f1e9..79b5694ba5 100644 --- a/src/boot_x86_fsp.c +++ b/src/boot_x86_fsp.c @@ -78,8 +78,7 @@ const uint8_t __attribute__((section(".sig_wolfboot_raw"))) /* offset of the header from the base image */ #define FSP_INFO_HEADER_OFFSET 0x94 #define EFI_SUCCESS 0x0 -#define FSP_STATUS_RESET_REQUIRED_COLD 0x40000001 -#define FSP_STATUS_RESET_REQUIRED_WARM 0x40000002 +/* FSP_STATUS_RESET_REQUIRED_* are defined in x86/fsp.h */ #define MEMORY_4GB (4ULL * 1024 * 1024 * 1024) #define ENDLINE "\r\n" /* Standard PCI capabilities live in conventional config space at 0x40-0xFC, diff --git a/src/x86/common.c b/src/x86/common.c index 7e0db43797..1da8926c5f 100644 --- a/src/x86/common.c +++ b/src/x86/common.c @@ -251,6 +251,33 @@ void reset(uint8_t warm) while(1){}; } +#ifdef WOLFBOOT_TGL +/* PMC ETR3 (PWRMBASE 0xFE000000 + 0x1048). CF9_GLB_RST makes the next 0xCF9 + * full reset a global reset (host + CSME); CF9_LOCK makes ETR3 read-only. */ +#define TGL_PCH_PWRM_BASE 0xFE000000u +#define TGL_PWRM_ETR3 0x1048u +#define ETR3_CF9_GLB_RST (1u << 20) +#define ETR3_CF9_LOCK (1u << 31) + +/* Global reset (host + CSME), which FSP-S needs after a ChipsetInit sync. + * Returns non-zero if ETR3 is locked and the global bit could not be set; + * otherwise does not return. */ +int global_reset(void) +{ + volatile uint32_t *etr3 = + (volatile uint32_t *)(uintptr_t)(TGL_PCH_PWRM_BASE + TGL_PWRM_ETR3); + uint32_t v; + + v = *etr3; + if ((v & ETR3_CF9_LOCK) != 0) { + return -1; + } + *etr3 = v | ETR3_CF9_GLB_RST; + reset(0); + return 0; +} +#endif /* WOLFBOOT_TGL */ + /** * @brief Delay the execution for a specified number of milliseconds. * diff --git a/src/x86/fsp.c b/src/x86/fsp.c index 84787f65bb..bc27fa350a 100644 --- a/src/x86/fsp.c +++ b/src/x86/fsp.c @@ -19,6 +19,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ #include +#include #include #include #include @@ -99,6 +100,39 @@ void print_fsp_image_revision(struct fsp_info_header *h) wolfBoot_printf("%x.%x.%x build %x\r\n", maj, min, rev, build); } +/* Act on a reset request returned by an FSP NotifyPhase call. WARM/COLD do the + * matching reset; RESET_REQUIRED_3 is the Tiger Lake global reset (host + CSME) + * needed after the FSP-S ChipsetInit sync. A non-success, non-reset status is + * fatal. Returns only on EFI_SUCCESS. */ +static void notify_phase_handle_reset(uint32_t status) +{ + if (status == FSP_STATUS_RESET_REQUIRED_WARM) { + wolfBoot_printf("notify phase: warm reset required\n"); + reset(1); + } + if (status == FSP_STATUS_RESET_REQUIRED_COLD) { + wolfBoot_printf("notify phase: cold reset required\n"); + reset(0); + } + if (status == FSP_STATUS_RESET_REQUIRED_3) { + wolfBoot_printf("notify phase: global reset required\n"); +#ifdef WOLFBOOT_TGL + /* global_reset() returns only if ETR3 is locked and the global bit + * could not be armed. A plain reset would not satisfy the FSP-S + * request and would loop forever, so halt instead (fail-secure). */ + global_reset(); + wolfBoot_printf("ETR3 is locked, cannot arm a global reset\n"); + panic(); +#else + reset(0); +#endif + } + if (status != EFI_SUCCESS) { + wolfBoot_printf("notify phase failed %d\n", status); + panic(); + } +} + void fsp_init_silicon(void) { uint8_t silicon_init_parameter[FSP_S_PARAM_SIZE]; @@ -167,20 +201,11 @@ void fsp_init_silicon(void) notify_phase = _start_fsp_s + notify_phase_off; param.Phase = EnumInitPhaseAfterPciEnumeration; status = x86_run_fsp_32bit(notify_phase, ¶m); - if (status != EFI_SUCCESS) { - wolfBoot_printf("notify phase failed %d\n", status); - panic(); - } + notify_phase_handle_reset(status); param.Phase = EnumInitPhaseReadyToBoot; status = x86_run_fsp_32bit(notify_phase, ¶m); - if (status != EFI_SUCCESS) { - wolfBoot_printf("notify phase failed %d\n", status); - panic(); - } + notify_phase_handle_reset(status); param.Phase = EnumInitPhaseEndOfFirmware; status = x86_run_fsp_32bit(notify_phase, ¶m); - if (status != EFI_SUCCESS) { - wolfBoot_printf("notify phase failed %d\n", status); - panic(); - } + notify_phase_handle_reset(status); } From 1213beb2a855f0abc3f0651e4d327889386baa59 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 14:56:37 -0700 Subject: [PATCH 3/9] x86 FSP: add the 64-bit Linux boot protocol to the bzImage loader --- src/x86/linux_loader.c | 62 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/x86/linux_loader.c b/src/x86/linux_loader.c index 2dcdbdf251..99527c815b 100644 --- a/src/x86/linux_loader.c +++ b/src/x86/linux_loader.c @@ -18,7 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA * * - * Linux x86 32bit protocol implementation + * Linux x86 boot protocol implementation (32-bit and 64-bit entry) */ #include @@ -27,6 +27,9 @@ #include #include +#ifdef WOLFBOOT_64BIT +#include +#endif #ifdef WOLFBOOT_FSP #include @@ -35,22 +38,32 @@ #define ENDLINE "\r\n" -#ifdef WOLFBOOT_64BIT -#error "Linux loader 64bit is not supported" -#endif -static void jump_to_linux(uint32_t kernel_addr, struct boot_params *p) -{ - (void)kernel_addr; - (void)p; +/* XLF_KERNEL_64: the kernel has a 64-bit entry point (boot protocol >= 2.12, + * xloadflags bit 0). Without it a 64-bit loader has nothing to jump to. */ +#define XLF_KERNEL_64 (1u << 0) -#if !defined(WOLFBOOT_64BIT) +static void jump_to_linux(uint64_t kernel_addr, struct boot_params *p) +{ +#if defined(WOLFBOOT_64BIT) + /* 64-bit boot protocol: in long mode already, so pass the zero page in RSI, + * clear RDI as the protocol requires, and jump to the 64-bit entry (0x200 + * past the loaded kernel), interrupts off. The kernel sets up its own GDT + * and stack. */ + __asm__ __volatile__("cli\n\t" + "movq %0, %%rsi\n\t" + "xorl %%edi, %%edi\n\t" + "jmp *%1" + : + : "r"(p), "r"(kernel_addr) + : "rsi", "rdi", "memory"); +#else __asm__ __volatile__("movl %0, %%esi\n\t" "xorl %%ebp, %%ebp\n\t" "xorl %%edi, %%edi\n\t" "xorl %%ebx, %%ebx\n\t" "jmp *%1" : - : "r"(p), "r"(kernel_addr) + : "r"((uint32_t)p), "r"((uint32_t)kernel_addr) : "esi", "edi", "ebx"); #endif /* WOLFBOOT_64BIT */ } @@ -145,8 +158,11 @@ void load_linux(uint8_t *linux_image, void *params, const char *cmd_line) uint8_t *image_boot_param; uint16_t end_of_header_off; uint8_t *_cmd_line; - (void)cmd_line; int ret; +#if defined(WOLFBOOT_64BIT) + uint32_t map_size; +#endif + (void)cmd_line; wolfBoot_printf("linux payload" ENDLINE); @@ -185,6 +201,30 @@ void load_linux(uint8_t *linux_image, void *params, const char *cmd_line) memcpy((uint8_t *)KERNEL_LOAD_ADDRESS, linux_image + param_size, kernel_size); +#if defined(WOLFBOOT_64BIT) + /* A 64-bit build needs the kernel's 64-bit entry point. */ + if ((param.hdr.xloadflags & XLF_KERNEL_64) == 0) { + wolfBoot_printf("kernel has no 64-bit entry (xloadflags 0x%x)" ENDLINE, + param.hdr.xloadflags); + wolfBoot_panic(); + } + + /* Identity-map the load region (init_size bytes of scratch), the command + * line and the zero page; the kernel cannot fault these in itself. */ + map_size = param.hdr.init_size; + if (kernel_size > map_size) { + map_size = kernel_size; + } + x86_paging_map_memory(KERNEL_LOAD_ADDRESS, KERNEL_LOAD_ADDRESS, map_size); + x86_paging_map_memory(KERNEL_CMDLINE_ADDRESS, KERNEL_CMDLINE_ADDRESS, 0x1000); + x86_paging_map_memory((uint64_t)(uintptr_t)¶m, + (uint64_t)(uintptr_t)¶m, sizeof(param)); + + /* 64-bit entry point: 0x200 past the loaded protected-mode kernel. */ + wolfBoot_printf("booting (64-bit entry)..." ENDLINE); + jump_to_linux((uint64_t)KERNEL_LOAD_ADDRESS + 0x200, ¶m); +#else wolfBoot_printf("booting..." ENDLINE); jump_to_linux(param.hdr.code32_start, ¶m); +#endif } From 9df4d448fbc229882632c72c489234ef3dc392a5 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 14:56:37 -0700 Subject: [PATCH 4/9] x86 FSP: initialise hobList before FspMemInit --- src/boot_x86_fsp.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/boot_x86_fsp.c b/src/boot_x86_fsp.c index 79b5694ba5..b681918b42 100644 --- a/src/boot_x86_fsp.c +++ b/src/boot_x86_fsp.c @@ -533,7 +533,9 @@ void start(uint32_t stack_base, uint32_t stack_top, uint64_t timestamp, struct stage2_ptr_holder stage2_holder; struct stage2_parameter temp_params; uint8_t *fsp_m_base, done = 0; - struct efi_hob *hobList, *it; + /* FspMemInit writes hobList only on EFI_SUCCESS; init so the reset-required + * and error paths never carry a stale pointer. */ + struct efi_hob *hobList = NULL, *it; memory_init_cb MemoryInit; uint64_t top_address = MEMORY_4GB; uint32_t new_stack; From de7a11587e3d920cdcf70e96bf7c18ceff17a2d2 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 14:56:37 -0700 Subject: [PATCH 5/9] x86 FSP: measure the disk OS image into a configurable PCR --- options.mk | 3 +++ src/update_disk.c | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/options.mk b/options.mk index ce6915d098..340234c006 100644 --- a/options.mk +++ b/options.mk @@ -113,6 +113,9 @@ ifeq ($(MEASURED_BOOT),1) WOLFTPM:=1 CFLAGS+=-D"WOLFBOOT_MEASURED_BOOT" CFLAGS+=-D"WOLFBOOT_MEASURED_PCR_A=$(MEASURED_PCR_A)" + ifneq ($(MEASURED_PCR_OS),) + CFLAGS+=-D"WOLFBOOT_MEASURED_PCR_OS=$(MEASURED_PCR_OS)" + endif ifeq ($(MEASURED_BOOT_APP_PARTITION),1) CFLAGS+=-D"WOLFBOOT_MEASURED_BOOT_APP_PARTITION" endif diff --git a/src/update_disk.c b/src/update_disk.c index 0d77858c6d..1f13618f07 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -43,6 +43,11 @@ #include "spi_flash.h" #include "printf.h" #include "wolfboot/wolfboot.h" +#include "tpm.h" +#if defined(WOLFBOOT_MEASURED_BOOT) && defined(WOLFBOOT_MEASURED_PCR_OS) && \ + (WOLFBOOT_SHA_DIGEST_SIZE > WOLFBOOT_TPM_PCR_DIG_SZ) +#include +#endif #include "disk.h" #ifdef WOLFBOOT_DISK_FS #include "disk_fs.h" @@ -484,6 +489,22 @@ void RAMFUNCTION wolfBoot_start(void) uintptr_t bl31_entry = 0; #endif char part_name[4] = {'P', ':', 'X', '\0'}; +#if defined(WOLFBOOT_MEASURED_BOOT) && defined(WOLFBOOT_MEASURED_PCR_OS) + int measure_ret; +#if defined(WOLFBOOT_SKIP_BOOT_VERIFY) +#error "measured boot: WOLFBOOT_MEASURED_PCR_OS needs the OS digest, which WOLFBOOT_SKIP_BOOT_VERIFY does not produce" +#endif +#if WOLFBOOT_SHA_DIGEST_SIZE < WOLFBOOT_TPM_PCR_DIG_SZ +#error "measured boot: image digest narrower than the PCR bank is not supported" +#endif +#if WOLFBOOT_SHA_DIGEST_SIZE > WOLFBOOT_TPM_PCR_DIG_SZ +#if WOLFBOOT_TPM_PCR_DIG_SZ != 32 +#error "measured boot: the re-hash path only implements a SHA-256 PCR bank" +#endif + uint8_t os_pcr[WOLFBOOT_TPM_PCR_DIG_SZ]; + wc_Sha256 os_sha; +#endif +#endif BENCHMARK_DECLARE(); #ifdef DISK_ENCRYPT @@ -814,6 +835,40 @@ void RAMFUNCTION wolfBoot_start(void) * disk_close(BOOT_DISK) is deferred to just before hal_prepare_boot(). */ wolfBoot_printf("Firmware Valid.\r\n"); +#if defined(WOLFBOOT_MEASURED_BOOT) && defined(WOLFBOOT_MEASURED_PCR_OS) + /* Measure the verified OS image into its own PCR, separate from the + * firmware measurement in WOLFBOOT_MEASURED_PCR_A. PCR4 is the TCG slot for + * the boot payload the boot manager launches. Re-hash to the PCR bank + * algorithm when the image digest is wider, so a SHA-256 bank is not + * extended with a truncated wider digest. */ + /* sha_hash is set only by a successful verify; NULL under + * WOLFBOOT_SKIP_BOOT_VERIFY. Fail-secure rather than dereference it. */ + if (os_image.sha_hash == NULL) { + wolfBoot_printf("No OS digest available to measure\r\n"); + wolfBoot_panic(); + } +#if WOLFBOOT_SHA_DIGEST_SIZE > WOLFBOOT_TPM_PCR_DIG_SZ + if (wc_InitSha256(&os_sha) != 0 || + wc_Sha256Update(&os_sha, os_image.sha_hash, + WOLFBOOT_SHA_DIGEST_SIZE) != 0 || + wc_Sha256Final(&os_sha, os_pcr) != 0) { + wolfBoot_printf("Failed to re-hash the OS digest\r\n"); + wolfBoot_panic(); + } + measure_ret = wolfBoot_tpm2_extend(WOLFBOOT_MEASURED_PCR_OS, os_pcr, + __LINE__); +#else + measure_ret = wolfBoot_tpm2_extend(WOLFBOOT_MEASURED_PCR_OS, + os_image.sha_hash, __LINE__); +#endif + if (measure_ret != 0) { + /* Fail-secure, as stage1 does for its own measurement: a working + * TPM that cannot record the OS measurement must not boot it. */ + wolfBoot_printf("Failed to measure the OS image into its PCR\r\n"); + wolfBoot_panic(); + } +#endif /* WOLFBOOT_MEASURED_BOOT && WOLFBOOT_MEASURED_PCR_OS */ + load_address = (uint32_t*)os_image.fw_base; #ifdef WOLFBOOT_FDT From 630c05c29752c7f68085c66bc4cee0685e5e8de9 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 15:28:06 -0700 Subject: [PATCH 6/9] tpm: build wolfBoot_print_hexstr for measured boot, not only seal or keystore --- src/tpm.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tpm.c b/src/tpm.c index e240946e86..e60adcccfc 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -56,7 +56,10 @@ int NOINLINEFUNCTION wolfBoot_constant_compare(const uint8_t* a, const uint8_t* return (diff != 0U) ? 1 : 0; } +#endif +#if defined(WOLFBOOT_TPM_SEAL) || defined(WOLFBOOT_TPM_KEYSTORE) || \ + defined(WOLFBOOT_MEASURED_BOOT) void wolfBoot_print_hexstr(const unsigned char* bin, unsigned long sz, unsigned long maxLine) { From 3e03c17385bfff208ff7c23a47e54dfffb712c14 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 15:49:07 -0700 Subject: [PATCH 7/9] x86 FSP: load an optional initrd alongside the Linux bzImage payload --- include/stage2_params.h | 3 + include/x86/linux_loader.h | 3 +- src/boot_x86_fsp_payload.c | 3 +- src/update_disk.c | 5 ++ src/x86/linux_loader.c | 92 ++++++++++++++++++++- tools/unit-tests/Makefile | 6 ++ tools/unit-tests/unit-linux-loader-initrd.c | 78 +++++++++++++++++ 7 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tools/unit-tests/unit-linux-loader-initrd.c diff --git a/include/stage2_params.h b/include/stage2_params.h index 69608cd7f2..ef3c6e2c81 100644 --- a/include/stage2_params.h +++ b/include/stage2_params.h @@ -28,6 +28,9 @@ struct stage2_parameter { uint32_t hobList; uint32_t page_table; uint32_t tolum; + /* Verified payload length, set by the disk loader before do_boot so the + * Linux loader can bound the signed container header against the image. */ + uint32_t payload_size; #ifdef WOLFBOOT_TPM_SEAL uint32_t tpm_policy; uint16_t tpm_policy_size; diff --git a/include/x86/linux_loader.h b/include/x86/linux_loader.h index 5edc0ef89c..7ad0dda037 100644 --- a/include/x86/linux_loader.h +++ b/include/x86/linux_loader.h @@ -120,6 +120,7 @@ struct boot_params { uint8_t _pad9[276]; } __attribute__((packed)); -void load_linux(uint8_t *linux_image, void *params, const char *cmd_line); +void load_linux(uint8_t *linux_image, uint32_t image_size, void *params, + const char *cmd_line); #endif /* LINUX_LOADER_H */ diff --git a/src/boot_x86_fsp_payload.c b/src/boot_x86_fsp_payload.c index a0a1f384e5..d7fb793ea1 100644 --- a/src/boot_x86_fsp_payload.c +++ b/src/boot_x86_fsp_payload.c @@ -105,7 +105,8 @@ void do_boot(const uint32_t *app) stage2_params = stage2_get_parameters(); #if defined(WOLFBOOT_LINUX_PAYLOAD) mptable_setup(); - load_linux((uint8_t *)app, stage2_params, cmdline); + load_linux((uint8_t *)app, stage2_params->payload_size, stage2_params, + cmdline); #elif defined(WOLFBOOT_ELF) int r; uint64_t e; diff --git a/src/update_disk.c b/src/update_disk.c index 1f13618f07..f5d9e18cf7 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -1038,6 +1038,11 @@ void RAMFUNCTION wolfBoot_start(void) zynqmp_atf_handoff(bl31_entry, (uintptr_t)load_address, (uintptr_t)dts_addr, ZYNQMP_ATF_EL2); } +#endif +#ifdef WOLFBOOT_FSP + /* Hand the verified payload length to the Linux loader (via do_boot) so it + * can bound the signed container header against the image. */ + stage2_params->payload_size = (uint32_t)os_image.fw_size; #endif do_boot((uint32_t*)load_address #if defined(MMU) || defined(WOLFBOOT_FDT) diff --git a/src/x86/linux_loader.c b/src/x86/linux_loader.c index 99527c815b..ef7327c850 100644 --- a/src/x86/linux_loader.c +++ b/src/x86/linux_loader.c @@ -38,6 +38,17 @@ #define ENDLINE "\r\n" +/* Optional container so one signed image can carry a kernel plus an initrd: + * a linux_payload_hdr, then the bzImage, then the initrd. Without the magic the + * image is a bare bzImage (no initrd), so existing payloads still boot. */ +#define LINUX_PAYLOAD_MAGIC 0x3150584Cu /* "LXP1" */ +struct linux_payload_hdr { + uint32_t magic; + uint32_t kernel_size; + uint32_t initrd_size; + uint32_t reserved; +}; + /* XLF_KERNEL_64: the kernel has a 64-bit entry point (boot protocol >= 2.12, * xloadflags bit 0). Without it a 64-bit loader has nothing to jump to. */ #define XLF_KERNEL_64 (1u << 0) @@ -151,9 +162,39 @@ static int linux_kernel_size(uint32_t syssize, uint32_t load_limit, return 0; } -void load_linux(uint8_t *linux_image, void *params, const char *cmd_line) +/* Pick the initrd load address: as high as possible, page-aligned, below the + * smaller of the kernel's initrd_addr_max and the top of usable low RAM + * (ram_limit, i.e. tolum; 0 if unknown), without overlapping the kernel. + * Rejects a zero size and any size that cannot fit, so the placement can never + * underflow past the fit test. Returns 0 and *out on success, -1 otherwise. */ +static int linux_initrd_place(uint32_t initrd_addr_max, uint64_t ram_limit, + uint32_t initrd_size, uint64_t kernel_end, + uint64_t *out) +{ + uint64_t limit; /* first byte past the highest allowed placement */ + + if (initrd_size == 0) + return -1; + limit = (initrd_addr_max != 0) ? ((uint64_t)initrd_addr_max + 1) + : 0x38000000ULL; /* pre-2.03 default + 1 */ + if (ram_limit != 0 && ram_limit < limit) + limit = ram_limit; + if ((uint64_t)initrd_size > limit) + return -1; + *out = (limit - initrd_size) & ~0xfffULL; + if (*out < kernel_end) + return -1; + return 0; +} + +void load_linux(uint8_t *linux_image, uint32_t image_size, void *params, + const char *cmd_line) { struct boot_params param = { 0 }; + struct linux_payload_hdr *phdr; + uint8_t *kernel_image; + uint8_t *initrd_image = NULL; + uint32_t initrd_size = 0; uint32_t kernel_size, param_size, load_limit; uint8_t *image_boot_param; uint16_t end_of_header_off; @@ -166,8 +207,28 @@ void load_linux(uint8_t *linux_image, void *params, const char *cmd_line) wolfBoot_printf("linux payload" ENDLINE); - image_boot_param = linux_image + 0x1f1; - end_of_header_off = *(linux_image + 0x201) + 0x202; + /* Unwrap the optional kernel+initrd container; a bare bzImage has no magic. */ + kernel_image = linux_image; + phdr = (struct linux_payload_hdr *)linux_image; + if (phdr->magic == LINUX_PAYLOAD_MAGIC) { + /* Bound the trusted header fields against the verified image so a + * malformed container cannot make the kernel or initrd reads run past + * it (or wrap the pointer arithmetic). Sum in 64-bit. */ + uint64_t need = (uint64_t)sizeof(*phdr) + phdr->kernel_size + + phdr->initrd_size; + if (image_size == 0 || phdr->kernel_size == 0 || + need > (uint64_t)image_size) { + wolfBoot_printf("invalid linux payload container" ENDLINE); + wolfBoot_panic(); + } + kernel_image = linux_image + sizeof(*phdr); + initrd_image = kernel_image + phdr->kernel_size; + initrd_size = phdr->initrd_size; + wolfBoot_printf("initrd: %d bytes" ENDLINE, initrd_size); + } + + image_boot_param = kernel_image + 0x1f1; + end_of_header_off = *(kernel_image + 0x201) + 0x202; memcpy((uint8_t*)¶m.hdr, image_boot_param, sizeof(struct setup_header)); @@ -198,9 +259,32 @@ void load_linux(uint8_t *linux_image, void *params, const char *cmd_line) wolfBoot_printf("invalid kernel size" ENDLINE); wolfBoot_panic(); } - memcpy((uint8_t *)KERNEL_LOAD_ADDRESS, linux_image + param_size, + memcpy((uint8_t *)KERNEL_LOAD_ADDRESS, kernel_image + param_size, kernel_size); + /* Place the initrd high in usable low RAM, copy it there, and hand its + * address to the kernel via the ramdisk fields. */ + if (initrd_size != 0) { + uint64_t initrd_addr; + /* The kernel occupies init_size (decompression + BSS + heap), which is + * normally larger than the compressed kernel_size; floor above both. */ + uint32_t kernel_span = (param.hdr.init_size > kernel_size) + ? param.hdr.init_size : kernel_size; + if (linux_initrd_place(param.hdr.initrd_addr_max, (uint64_t)load_limit, + initrd_size, + (uint64_t)KERNEL_LOAD_ADDRESS + kernel_span, + &initrd_addr) != 0) { + wolfBoot_printf("initrd does not fit in usable RAM" ENDLINE); + wolfBoot_panic(); + } +#if defined(WOLFBOOT_64BIT) + x86_paging_map_memory(initrd_addr, initrd_addr, initrd_size); +#endif + memcpy((uint8_t *)(uintptr_t)initrd_addr, initrd_image, initrd_size); + param.hdr.ramdisk_image = (uint32_t)initrd_addr; + param.hdr.ramdisk_size = initrd_size; + } + #if defined(WOLFBOOT_64BIT) /* A 64-bit build needs the kernel's 64-bit entry point. */ if ((param.hdr.xloadflags & XLF_KERNEL_64) == 0) { diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9306819684..05508e8d01 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -200,6 +200,7 @@ ENABLE_32BIT_TESTS ?= $(HAVE_M32) ifeq ($(ENABLE_32BIT_TESTS),1) TESTS+=unit-linux-loader-e820 TESTS+=unit-linux-loader-syssize +TESTS+=unit-linux-loader-initrd TESTS+=unit-sama5d3-ext-read else $(info Skipping 32-bit x86 unit tests (linux-loader, sama5d3-ext-read): 'gcc -m32' unavailable (set ENABLE_32BIT_TESTS=1 to force)) @@ -756,6 +757,11 @@ unit-linux-loader-syssize: ../../include/target.h unit-linux-loader-syssize.c -g -DUNIT_TEST -DWOLFBOOT_FSP -DUCODE0_ADDRESS=0 \ -DWOLFBOOT_LOAD_BASE=0x100000 +unit-linux-loader-initrd: ../../include/target.h unit-linux-loader-initrd.c + gcc -m32 -o $@ unit-linux-loader-initrd.c -I. -I../../src -I../../include \ + -g -DUNIT_TEST -DWOLFBOOT_FSP -DUCODE0_ADDRESS=0 \ + -DWOLFBOOT_LOAD_BASE=0x100000 + unit-boot-x86-fsp: ../../include/target.h unit-boot-x86_fsp.c gcc -o $@ $^ $(CFLAGS) -DWOLFBOOT_LOAD_BASE=0x100000 -DWOLFBOOT_FSP \ -DUCODE0_ADDRESS=0 -ffunction-sections -fdata-sections $(LDFLAGS) \ diff --git a/tools/unit-tests/unit-linux-loader-initrd.c b/tools/unit-tests/unit-linux-loader-initrd.c new file mode 100644 index 0000000000..8794ea857a --- /dev/null +++ b/tools/unit-tests/unit-linux-loader-initrd.c @@ -0,0 +1,78 @@ +/* unit-linux-loader-initrd.c + * + * Tests linux_initrd_place(), the pure helper that chooses the initrd load + * address for the Linux bzImage loader. It must place the initrd as high as + * possible, page-aligned, below the smaller of the kernel's initrd_addr_max and + * the top of usable low RAM, without overlapping the kernel, and must never + * underflow past its own fit check. + * + * Built for x86 32bit (the only target supported by linux_loader.c), without + * the check framework, since 32bit libcheck is not generally available. + */ + +#include +#include +#include +#include + +#include "x86/hob.h" +#include "x86/linux_loader.h" + +#include "../../src/x86/hob.c" +#include "../../src/x86/linux_loader.c" + +static int fail(const char *msg) +{ + printf("FAIL: %s\n", msg); + return 1; +} + +int main(void) +{ + uint64_t out; + + /* Zero size is rejected. */ + if (linux_initrd_place(0x7fffffffu, 0, 0, 0x100000u, &out) == 0) + return fail("zero size accepted"); + + /* Size larger than the limit is rejected (no underflow). */ + if (linux_initrd_place(0x00000fffu, 0, 0x2000u, 0x1000u, &out) == 0) + return fail("size larger than limit accepted"); + + /* Normal fit: placed as high as possible below initrd_addr_max+1, + * page-aligned. limit 0x80000000 - 0x100000 -> 0x7ff00000. */ + out = 0; + if (linux_initrd_place(0x7fffffffu, 0, 0x100000u, 0x200000u, &out) != 0) + return fail("valid placement rejected"); + if (out != 0x7ff00000ull) + return fail("wrong high placement"); + if ((out & 0xfffu) != 0) + return fail("placement not page aligned"); + + /* initrd_addr_max == 0 falls back to the pre-2.03 default 0x38000000. */ + out = 0; + if (linux_initrd_place(0, 0, 0x1000u, 0x100000u, &out) != 0) + return fail("default-max placement rejected"); + if (out != 0x37fff000ull) + return fail("wrong default-max placement"); + + /* ram_limit (tolum) below initrd_addr_max clamps the placement. */ + out = 0; + if (linux_initrd_place(0x7fffffffu, 0x10000000ull, 0x1000u, 0x100000u, + &out) != 0) + return fail("ram-limited placement rejected"); + if (out != 0x0ffff000ull) + return fail("ram_limit not honoured"); + + /* A placement that would land below the kernel end is rejected. */ + if (linux_initrd_place(0x00300000u, 0, 0x100000u, 0x00280000u, &out) == 0) + return fail("kernel-overlapping placement accepted"); + + /* Size exactly equal to the limit yields address 0, rejected by the + * kernel-end floor. */ + if (linux_initrd_place(0x00000fffu, 0, 0x1000u, 0x1000u, &out) == 0) + return fail("zero-address placement accepted"); + + printf("PASS\n"); + return 0; +} From f238a2b4946508df6c5785f83f4b0cb8e20ffea1 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 17 Sep 2026 11:31:37 -0700 Subject: [PATCH 8/9] docs: document the x86 FSP Linux bzImage/initrd payload, OS PCR measurement, and UPD decoder --- docs/Targets.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/Targets.md b/docs/Targets.md index 3289930bcb..3855ceda3c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -8457,6 +8457,52 @@ Note: - This feature requires `NASM` to be installed on the machine building wolfBoot. +### Booting a Linux bzImage payload (with an optional initrd) + +Set `LINUX_PAYLOAD=1` to boot a signed Linux `bzImage` from the disk A/B slot +instead of an ELF/Multiboot2 image. Both the 32-bit and the 64-bit (`64BIT=1`) +boot protocols are supported; a 64-bit build requires a kernel with a 64-bit +entry point (`xloadflags` bit 0). + +The kernel must be able to reach its root filesystem. A monolithic kernel with +the storage and filesystem drivers built in needs nothing further. To boot a +modular distribution kernel that relies on an initramfs, wrap the kernel and +the initrd into a single signed image using the container header below, then +sign that image as usual (all fields little-endian): + +``` +magic u32 0x3150584C ("LXP1") +kernel_size u32 exact bzImage byte length +initrd_size u32 exact initrd byte length +reserved u32 0 + + +``` + +wolfBoot detects the magic, loads the kernel, places the initrd below the +kernel's `initrd_addr_max` (and below the top of usable RAM), and hands its +address to the kernel. An image without the magic is treated as a bare bzImage +with no initrd, so existing payloads are unaffected. + +### Measuring the OS image into a PCR + +With `MEASURED_BOOT=1`, set `MEASURED_PCR_OS=` to extend the verified disk OS +image digest into PCR ``, in addition to the firmware measurement in +`MEASURED_PCR_A`. PCR 4 follows the TCG PC Client convention for the boot +payload the boot manager launches. The digest is re-hashed into the PCR bank +algorithm when it is wider than the bank. A failed extend is fail-secure: the +image is not booted. + +### Debugging the FSP UPD configuration + +`tools/x86_fsp/decode_fsp_upd.py` decodes an FSP UPD block into named fields, +using the offset comments that ship in `FspmUpd.h` / `FspsUpd.h`. It accepts +either the hex block a bootloader prints over the debug UART or a UPD region +lifted from a flash image, and `--diff` reports only the fields that differ +between two captures. This is useful when tuning the memory or silicon +configuration for a new board. + + ### Running on 64-bit QEMU Two example configuration files are available: `config/examples/x86_fsp_qemu.config` and `config/examples/x86_fsp_qemu_seal.config`. From 833eef46ed5752e41a9a0e61648cc0c423da9234 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 13:15:10 -0700 Subject: [PATCH 9/9] x86/FSP: select the FSP debug UART controller with X86_UART_NUMBER --- src/x86/tgl_fsp.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/x86/tgl_fsp.c b/src/x86/tgl_fsp.c index 357646b0d6..011794d496 100644 --- a/src/x86/tgl_fsp.c +++ b/src/x86/tgl_fsp.c @@ -158,6 +158,15 @@ SI_PCH_DEVICE_INTERRUPT_CONFIG mPchHDevIntConfig[] = { {30, 0, SiPchIntA, 16}, }; +/* Which PCH LPSS SerialIo UART the FSP brings up for debug output. This is a + * controller index, not an address: the FSP maps whichever one is selected at + * PcdSerialIoUartDebugMmioBase, which we set from X86_UART_BASE. The two are + * independent, and getting the index wrong yields a silent console on a board + * whose UART base is perfectly correct. */ +#ifndef X86_UART_NUMBER +#define X86_UART_NUMBER 0 +#endif + #if defined(BUILD_LOADER_STAGE1) #define FIT_NUM_ENTRIES 2 __attribute__((__section__(".boot"))) const struct fit_table_entry fit_table[FIT_NUM_ENTRIES] = @@ -421,7 +430,10 @@ static int fsp_set_memory_cfg(FSPM_UPD *udp) mem_cfg->PcieClkSrcUsage[15] = 128; mem_cfg->PcieRpEnableMask = 1520787455; mem_cfg->PcdDebugInterfaceFlags = 16; - mem_cfg->SerialIoUartDebugControllerNumber = 0; + /* FSP-M and FSP-S each carry their own debug UART selector; both must be + * set from X86_UART_NUMBER or the console changes between phases - output + * appears for part of the boot and then stops, reading as a hang. */ + mem_cfg->SerialIoUartDebugControllerNumber = X86_UART_NUMBER; mem_cfg->MrcSafeConfig = 1; mem_cfg->TcssItbtPcie0En = 0; mem_cfg->TcssItbtPcie1En = 0; @@ -557,7 +569,7 @@ static void fsp_set_silicon_cfg(FSPS_UPD *fsps) upd->SerialIoUartTxPinMuxPolicy[4] = 0; upd->SerialIoUartTxPinMuxPolicy[5] = 0; upd->SerialIoUartTxPinMuxPolicy[6] = 0; - upd->SerialIoDebugUartNumber = 0; + upd->SerialIoDebugUartNumber = X86_UART_NUMBER; upd->SerialIoI2cMode[0] = 0; upd->SerialIoI2cMode[1] = 0; upd->SerialIoI2cMode[2] = 0; @@ -956,7 +968,7 @@ int fsp_machine_update_s_parameters(uint8_t *default_s_params) upd->EnableMultiPhaseSiliconInit = 0; upd->SerialIoUartMode[1] = upd->SerialIoUartMode[2] = 0x1; - upd->SerialIoDebugUartNumber = 0x0; + upd->SerialIoDebugUartNumber = X86_UART_NUMBER; memset(upd->PcieRpHotPlug, 0, sizeof(upd->PcieRpHotPlug)); memset(upd->CpuPcieRpHotPlug, 0, sizeof(upd->CpuPcieRpHotPlug));