blob: 5a4ad154b3edd4dee05d547081adb8bb9f6686fd [file]
## @file
# Unit and functional tests for GenFv XIP rebase behavior
#
# Tests the ,XIP suffix generation in Python GenFds and the
# ForceRebase/XIP decision logic in C GenFv.
#
# Test Plan Summary:
#
# FfsRebase() in GenFvInternalLib.c decides whether to rebase each PE/COFF
# image in an FV based on three inputs: ForceRebase, BaseAddress, and XipFile[].
#
# ForceRebase BaseAddress XipFileCount XipFile[] Result
# ----------- ----------- ------------ --------- ---------------------------------
# -1 (unset) 0 any any No rebase (early return)
# 0 (FALSE) any any any No rebase (early return)
# 1 (TRUE) any 0 any Rebase ALL files (legacy compat)
# 1 (TRUE) any > 0 FALSE No rebase (skip non-XIP file)
# 1 (TRUE) any > 0 TRUE Rebase (XIP file selected)
# -1 (unset) != 0 any any Rebase ALL files (legacy path)
#
# Unit Tests (TestDetermineXipEnabled):
# 11 parameterized subtests calling FfsInfStatement.DetermineXipEnabled()
# with RuleComplexFile and RuleSimpleFile objects to verify Xip attribute
# parsing (TRUE/FALSE/None, case insensitive, boolean vs string).
#
# Functional Tests (TestFunctionalBuildXipRebase):
# 8 test cases using real edk2 builds with generated DSC/FDF files
# containing a test package (2 PEIMs + 1 DXE driver). Each test
# verifies:
# 1. FV INF file has correct ,XIP suffix on EFI_FILE_NAME entries
# 2. FV map file shows correct rebase status (Fixed Flash Address)
# 3. PE/COFF ImageBase in the FV binary matches expected value
#
# TC1: ForceRebase=unset, Base=0 -> no rebase
# TC2: ForceRebase=unset, Base!=0 -> rebase all (legacy)
# TC3: ForceRebase=FALSE, Base!=0 -> no rebase
# TC4: ForceRebase=TRUE, all Xip=TRUE -> rebase all
# TC5: ForceRebase=TRUE, selective Xip -> rebase only Xip=TRUE
# TC6: ForceRebase=TRUE, no Xip -> rebase all (legacy compat)
# TC7: ForceRebase=TRUE, mixed Xip -> rebase Xip=TRUE only
# TC8: ForceRebase=TRUE, Base=0, Xip -> rebase (force overrides)
#
# Copyright (c) 2026, Intel Corporation. All rights reserved.<BR>
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
#
import ctypes
import os
import re
import shutil
import subprocess
import sys
import threading
import unittest
from pathlib import Path
# Add BaseTools Python source to path
_TESTS_DIR = Path(__file__).resolve().parent
_PYTHON_SRC = str(_TESTS_DIR.parent / 'Source' / 'Python')
if _PYTHON_SRC not in sys.path:
sys.path.insert(0, _PYTHON_SRC)
from GenFds.RuleComplexFile import RuleComplexFile
from GenFds.RuleSimpleFile import RuleSimpleFile
from GenFds.EfiSection import EfiSection
from GenFds.FfsInfStatement import FfsInfStatement
from FirmwareStorageFormat.FvHeader import EFI_FIRMWARE_VOLUME_HEADER
from FirmwareStorageFormat.FfsFileHeader import EFI_FFS_FILE_HEADER
from FirmwareStorageFormat.SectionHeader import (
EFI_COMMON_SECTION_HEADER,
EFI_SECTION_PE32,
)
from FirmwareStorageFormat.PECOFFHeader import (
EFI_IMAGE_DOS_HEADER,
EFI_IMAGE_DOS_SIGNATURE,
EFI_IMAGE_NT_HEADERS32,
EFI_IMAGE_NT_HEADERS64,
EFI_IMAGE_NT_OPTIONAL_HDR64_MAGIC,
EFI_IMAGE_NT_SIGNATURE,
)
class TestDetermineXipEnabled(unittest.TestCase):
"""Test FfsInfStatement.DetermineXipEnabled() with real Rule objects.
This calls the actual production code that determines whether XIP
is enabled based on a Rule object's Xip attribute(s).
"""
_UNSET = object() # sentinel: do not set the Xip attribute
# (rule_class, section_xip_list, rule_xip, expected, description)
#
# For RuleComplexFile:
# section_xip_list is a list of Xip values per EfiSection (_UNSET = no attr).
# rule_xip is ignored (_UNSET).
#
# For RuleSimpleFile:
# section_xip_list is None (no SectionList used).
# rule_xip is the value to assign to rule.Xip (_UNSET = leave default).
TEST_CASES = [
# RuleComplexFile: one section with Xip='TRUE'.
# DetermineXipEnabled iterates SectionList looking for any section with
# Xip set to 'TRUE' (case-insensitive string match).
# A single section set to 'TRUE' should return True (XIP-eligible).
(RuleComplexFile, ['TRUE'], _UNSET, True,
'Complex: section Xip=TRUE'),
# RuleComplexFile: Xip='true' (lowercase).
# The FDF parser normalizes keywords but DetermineXipEnabled uses
# case-insensitive matching. Verifies 'true' is equivalent to 'TRUE'.
(RuleComplexFile, ['true'], _UNSET, True,
'Complex: section Xip=true (case insensitive)'),
# RuleComplexFile: one section with Xip='FALSE'.
# An explicit 'FALSE' must not be confused with 'TRUE'. Confirms the
# string 'FALSE' does not accidentally match the 'TRUE' comparison.
(RuleComplexFile, ['FALSE'], _UNSET, False,
'Complex: section Xip=FALSE'),
# RuleComplexFile: section with no Xip attribute set.
# EfiSection.__init__ does not set Xip by default. Verifies that when
# the FDF rule omits the Xip keyword, DetermineXipEnabled returns False
# without raising an AttributeError.
(RuleComplexFile, [_UNSET], _UNSET, False,
'Complex: section with no Xip attribute'),
# RuleComplexFile: two sections, only the second has Xip=TRUE.
# DetermineXipEnabled should return True if ANY section in the list has
# Xip=TRUE, not just the first. Tests iteration with Xip=TRUE at index 1.
(RuleComplexFile, [_UNSET, 'TRUE'], _UNSET, True,
'Complex: multiple sections, one Xip=TRUE'),
# RuleComplexFile: empty SectionList.
# Edge case: a complex rule with no sections should safely return False
# without raising an exception from iterating an empty list.
(RuleComplexFile, [], _UNSET, False,
'Complex: empty section list'),
# RuleSimpleFile: Xip='TRUE' (string).
# Unlike RuleComplexFile, RuleSimpleFile stores Xip directly on the
# rule object. Verifies the string 'TRUE' path via isinstance check.
(RuleSimpleFile, None, 'TRUE', True,
'Simple: Xip=TRUE (string)'),
# RuleSimpleFile: Xip=True (Python boolean).
# The FDF parser may set Xip as a boolean True rather than the string
# 'TRUE'. Verifies that a Python boolean True is recognized as XIP.
(RuleSimpleFile, None, True, True,
'Simple: Xip=True (boolean)'),
# RuleSimpleFile: Xip='FALSE' (string).
# Verifies that an explicit 'FALSE' string causes DetermineXipEnabled
# to return False. Complement of the 'TRUE' string test.
(RuleSimpleFile, None, 'FALSE', False,
'Simple: Xip=FALSE'),
# RuleSimpleFile: default Xip value from RuleClassObject.__init__.
# When constructed without setting Xip, the default is False (boolean).
# Represents the common case where the FDF rule omits the Xip keyword.
(RuleSimpleFile, None, _UNSET, False,
'Simple: default Xip (not set)'),
# RuleSimpleFile: Xip=None.
# Edge case: if code or a parser bug sets Xip=None, it should be
# treated as falsy and return False rather than raising a TypeError.
(RuleSimpleFile, None, None, False,
'Simple: Xip=None'),
]
def test_determine_xip_enabled(self) -> None:
"""Parameterized test for DetermineXipEnabled with Rule objects."""
for rule_class, section_xip_list, rule_xip, expected, desc in self.TEST_CASES:
with self.subTest(desc):
rule = rule_class()
if section_xip_list is not None:
# RuleComplexFile: build SectionList
rule.SectionList = []
for xip_val in section_xip_list:
sect = EfiSection()
if xip_val is not self._UNSET:
sect.Xip = xip_val
rule.SectionList.append(sect)
elif rule_xip is not self._UNSET:
# RuleSimpleFile: set Xip on rule
rule.Xip = rule_xip
self.assertEqual(
FfsInfStatement.DetermineXipEnabled(rule), expected
)
class TestFunctionalBuildXipRebase(unittest.TestCase):
"""Functional tests that build with generated DSC/FDF files to exercise
all ForceRebase/BaseAddress/Xip combinations.
Prerequisites:
- edksetup has been run (sets WORKSPACE and puts build in PATH)
- BaseTools C binaries built (GenFv.exe, etc.)
- A working compiler toolchain (VS2022, GCC5, etc.)
Each test case verifies:
1. FV INF file has correct ,XIP suffix on EFI_FILE_NAME entries
2. FV map file shows correct rebase status (Fixed Flash Address)
3. PE/COFF ImageBase in the FV binary matches expected value
"""
WORKSPACE = None
TOOLCHAIN = None
BUILD_AVAILABLE = False
# Module names in FV file order (matches INF listing in FDF_TEMPLATE)
_MODULE_NAMES = ('TestPeim', 'TestPeim2', 'TestDxeDriver')
# --- File content constants / templates ---
DEC_CONTENT = """\
[Defines]
DEC_SPECIFICATION = 0x00010005
PACKAGE_NAME = TestXipRebasePkg
PACKAGE_GUID = FC530350-34AA-4498-88F5-BF71987785B2
PACKAGE_VERSION = 1.0
"""
PEIM_C_TEMPLATE = """\
#include <PiPei.h>
#include <Library/PeimEntryPoint.h>
EFI_STATUS
EFIAPI
{entry_point} (
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES **PeiServices
)
{{
return EFI_SUCCESS;
}}
"""
PEIM_INF_TEMPLATE = """\
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = {base_name}
FILE_GUID = {file_guid}
MODULE_TYPE = PEIM
VERSION_STRING = 1.0
ENTRY_POINT = {entry_point}
[Sources]
{source_file}
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
PeimEntryPoint
[Depex]
TRUE
"""
DXE_C_SOURCE = """\
#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
EFI_STATUS
EFIAPI
TestDxeDriverEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
return EFI_SUCCESS;
}
"""
DXE_INF = """\
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = TestDxeDriver
FILE_GUID = FEC0E1C9-544F-493E-9BD1-91263C7970FC
MODULE_TYPE = DXE_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = TestDxeDriverEntry
[Sources]
TestDxeDriver.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiDriverEntryPoint
[Depex]
TRUE
"""
DSC_CONTENT = """\
[Defines]
PLATFORM_NAME = TestXipRebase
PLATFORM_GUID = A44E9966-C1A1-47E8-8B21-8B773705DD79
PLATFORM_VERSION = 1.0
DSC_SPECIFICATION = 0x00010005
OUTPUT_DIRECTORY = Build/TestXipRebase
SUPPORTED_ARCHITECTURES = X64
BUILD_TARGETS = DEBUG
SKUID_IDENTIFIER = DEFAULT
!include MdePkg/MdeLibs.dsc.inc
[LibraryClasses]
PeimEntryPoint|MdePkg/Library/PeimEntryPoint/PeimEntryPoint.inf
UefiDriverEntryPoint|MdePkg/Library/UefiDriverEntryPoint/UefiDriverEntryPoint.inf
BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf
DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf
PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf
UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
[Components]
TestXipRebasePkg/TestPeim/TestPeim.inf
TestXipRebasePkg/TestPeim2/TestPeim2.inf
TestXipRebasePkg/TestDxeDriver/TestDxeDriver.inf {
<BuildOptions>
MSFT:*_*_*_DLINK_FLAGS = /ALIGN:4096 /FILEALIGN:4096
GCC:*_*_*_DLINK_FLAGS = -z common-page-size=0x1000
}
"""
PEIM2_RULE_TEMPLATE = """
[Rule.Common.PEIM.PEIM2RULE]
FILE PEIM = $(NAMED_GUID) {{
PE32 PE32 Align=Auto{peim2_xip_clause} $(INF_OUTPUT)/$(MODULE_NAME).efi
}}
"""
FDF_TEMPLATE = """\
[FV.{fv_name}]
FvNameGuid = 15942B69-82DC-41DC-9F01-D60162870C4A
{base_line}\
{force_line}\
BlockSize = 0x10000
NumBlocks = 0x10
FvAlignment = 16
ERASE_POLARITY = 1
MEMORY_MAPPED = TRUE
STICKY_WRITE = TRUE
LOCK_CAP = TRUE
LOCK_STATUS = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP = TRUE
WRITE_STATUS = TRUE
WRITE_LOCK_CAP = TRUE
WRITE_LOCK_STATUS = TRUE
READ_DISABLED_CAP = TRUE
READ_ENABLED_CAP = TRUE
READ_STATUS = TRUE
READ_LOCK_CAP = TRUE
READ_LOCK_STATUS = TRUE
INF TestXipRebasePkg/TestPeim/TestPeim.inf
{peim2_inf_line}
INF TestXipRebasePkg/TestDxeDriver/TestDxeDriver.inf
[Rule.Common.PEIM]
FILE PEIM = $(NAMED_GUID) {{
PE32 PE32 Align=Auto{peim1_xip_clause} $(INF_OUTPUT)/$(MODULE_NAME).efi
}}
{peim2_rule}\
[Rule.Common.DXE_DRIVER]
FILE DRIVER = $(NAMED_GUID) {{
PE32 PE32{dxe_xip_clause} $(INF_OUTPUT)/$(MODULE_NAME).efi
}}
"""
@classmethod
def _detect_toolchain(cls) -> str:
"""Get toolchain from --toolchain command line option (default: VS2022).
Returns:
Toolchain tag string (e.g. 'VS2022', 'GCC5').
"""
for i, arg in enumerate(sys.argv):
if arg == '--toolchain' and i + 1 < len(sys.argv):
return sys.argv[i + 1]
if arg.startswith('--toolchain='):
return arg.split('=', 1)[1]
return 'VS2022'
@classmethod
def setUpClass(cls) -> None:
cls.WORKSPACE = os.environ.get('WORKSPACE')
if cls.WORKSPACE is None:
return
cls.TOOLCHAIN = cls._detect_toolchain()
try:
result = subprocess.run(
'build --version', capture_output=True, text=True,
timeout=30, cwd=cls.WORKSPACE, shell=True
)
cls.BUILD_AVAILABLE = result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
@classmethod
def tearDownClass(cls) -> None:
"""Clean up generated test package directory."""
if cls.WORKSPACE:
pkg_dir = Path(cls.WORKSPACE, 'TestXipRebasePkg')
if pkg_dir.is_dir():
shutil.rmtree(pkg_dir, ignore_errors=True)
def setUp(self) -> None:
if self.WORKSPACE is None:
self.fail("WORKSPACE environment variable is not set. "
"Run edksetup before running tests.")
if not self.BUILD_AVAILABLE:
self.fail("edk2 'build' command not found in PATH. "
"Run edksetup before running tests.")
# --- Test package generation ---
def _create_test_package(self) -> str:
"""Create a minimal test package with two PEIMs and one DXE driver.
Returns:
Absolute path to the created package directory.
"""
pkg = Path(self.WORKSPACE, 'TestXipRebasePkg')
pkg.mkdir(parents=True, exist_ok=True)
(pkg / 'TestXipRebasePkg.dec').write_text(self.DEC_CONTENT)
# Generate PEIM modules from template
for name, guid, entry in [
('TestPeim', 'F43835C3-245F-4951-9D35-8684B98328DA', 'TestPeimEntry'),
('TestPeim2', '816C5A1F-23A8-485F-B005-D565FD01E303', 'TestPeim2Entry'),
]:
mod = pkg / name
mod.mkdir(parents=True, exist_ok=True)
(mod / f'{name}.c').write_text(
self.PEIM_C_TEMPLATE.format(entry_point=entry))
(mod / f'{name}.inf').write_text(
self.PEIM_INF_TEMPLATE.format(
base_name=name, file_guid=guid,
entry_point=entry, source_file=f'{name}.c'))
# DXE driver (different includes/signature, not templated)
dxe = pkg / 'TestDxeDriver'
dxe.mkdir(parents=True, exist_ok=True)
(dxe / 'TestDxeDriver.c').write_text(self.DXE_C_SOURCE)
(dxe / 'TestDxeDriver.inf').write_text(self.DXE_INF)
return str(pkg)
def _generate_fdf(self, pkg_dir: str, fv_name: str,
base_address: str | None, force_rebase: str | None,
peim1_xip: str | None, peim2_xip: str | None,
dxe_xip: str | None) -> str:
"""Generate an FDF file with the specified FV settings.
Args:
pkg_dir: Package directory path.
fv_name: Name for the firmware volume.
base_address: Hex string (e.g. '0xFFF00000') or None.
force_rebase: 'TRUE', 'FALSE', or None.
peim1_xip: 'TRUE', 'FALSE', or None (omit Xip keyword).
peim2_xip: 'TRUE', 'FALSE', or None.
dxe_xip: 'TRUE', 'FALSE', or None.
Returns:
Absolute path to the generated FDF file.
"""
def xip_clause(setting):
return f' Xip={setting}' if setting is not None else ''
base_line = f'FvBaseAddress = {base_address}\n' if base_address is not None else ''
force_line = f'FvForceRebase = {force_rebase}\n' if force_rebase is not None else ''
# Use RuleOverride when PEIM2 needs a different Xip than PEIM1
if peim2_xip != peim1_xip:
peim2_inf_line = 'INF RuleOverride=PEIM2RULE TestXipRebasePkg/TestPeim2/TestPeim2.inf'
peim2_rule = self.PEIM2_RULE_TEMPLATE.format(
peim2_xip_clause=xip_clause(peim2_xip))
else:
peim2_inf_line = 'INF TestXipRebasePkg/TestPeim2/TestPeim2.inf'
peim2_rule = ''
fdf_content = self.FDF_TEMPLATE.format(
fv_name=fv_name, base_line=base_line, force_line=force_line,
peim2_inf_line=peim2_inf_line,
peim1_xip_clause=xip_clause(peim1_xip),
peim2_rule=peim2_rule,
dxe_xip_clause=xip_clause(dxe_xip),
)
fdf_path = Path(pkg_dir, 'TestXipRebase.fdf')
fdf_path.write_text(fdf_content)
return str(fdf_path)
def _run_build(self, dsc_path: Path, fdf_path: str) -> tuple[int, str, str]:
"""Run the edk2 build command and return (returncode, stdout, stderr).
When verbose mode is enabled (-v / --verbose), streams build output
in real-time.
Args:
dsc_path: Path to the DSC platform description file.
fdf_path: Path to the FDF flash description file.
Returns:
Tuple of (returncode, stdout, stderr) from the build process.
"""
rel_dsc = os.path.relpath(dsc_path, self.WORKSPACE)
rel_fdf = os.path.relpath(fdf_path, self.WORKSPACE)
cmd = (
f'build -p {rel_dsc} -f {rel_fdf}'
f' -a X64 -b DEBUG -t {self.TOOLCHAIN} --quiet'
)
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1, cwd=self.WORKSPACE, shell=True,
)
stdout_lines, stderr_lines = [], []
verbose = '-v' in sys.argv or '--verbose' in sys.argv
def reader(pipe, sink, stream):
for line in pipe:
if stream:
stream.write(line)
stream.flush()
sink.append(line)
threads = [
threading.Thread(target=reader,
args=(proc.stdout, stdout_lines,
sys.stdout if verbose else None)),
threading.Thread(target=reader,
args=(proc.stderr, stderr_lines,
sys.stderr if verbose else None)),
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=300)
proc.stdout.close()
proc.stderr.close()
proc.wait(timeout=300)
return proc.returncode, ''.join(stdout_lines), ''.join(stderr_lines)
# --- Verification helpers ---
def _fv_output_path(self, filename: str) -> Path:
"""Return the path to a file in the FV output directory.
Args:
filename: Name of the file (e.g. 'TESTFV1.Fv', 'TESTFV1.inf').
Returns:
Full path to the file under Build/TestXipRebase/DEBUG_<TOOLCHAIN>/FV/.
"""
return Path(
self.WORKSPACE, 'Build', 'TestXipRebase',
f'DEBUG_{self.TOOLCHAIN}', 'FV', filename
)
def _read_fv_file(self, filename: str) -> str | None:
"""Read a generated FV output file as text, or None if missing.
Args:
filename: Name of the file in the FV output directory.
Returns:
File contents as a string, or None if the file does not exist.
"""
path = self._fv_output_path(filename)
return path.read_text() if path.is_file() else None
def _check_module_rebased(self, map_content: str, module_name: str,
expect_rebased: bool) -> None:
"""Assert a module's rebase status in the FV map file.
A rebased module has '(Fixed Flash Address, BaseAddress=0x...' in
its map entry. A non-rebased module lacks this marker.
Args:
map_content: Text content of the FV .map file.
module_name: Module base name to search for (e.g. 'TestPeim').
expect_rebased: True if the module should have been rebased.
"""
self.assertIsNotNone(map_content, "FV map file not found")
found = bool(re.search(
rf'{re.escape(module_name)}.*\(Fixed Flash Address',
map_content, re.IGNORECASE
))
verb = "to be" if expect_rebased else "NOT to be"
self.assertEqual(
found, expect_rebased,
f"Expected {module_name} {verb} rebased.\n"
f"Map excerpt: {map_content[:500]}"
)
def _get_pe_image_bases(self, fv_name: str) -> list[tuple[int, int]] | None:
"""Extract (fv_offset, image_base) for each PE/COFF image in the FV.
Walks the FV binary using ctypes structures:
EFI_FIRMWARE_VOLUME_HEADER -> EFI_FFS_FILE_HEADER ->
EFI_COMMON_SECTION_HEADER -> EFI_IMAGE_DOS_HEADER ->
EFI_IMAGE_OPTIONAL_HEADER32 / EFI_IMAGE_OPTIONAL_HEADER64.
Args:
fv_name: Firmware volume name (e.g. 'TESTFV1').
Returns:
Sorted list of (fv_offset, image_base) tuples, or None if the
FV file does not exist.
"""
fv_path = self._fv_output_path(f'{fv_name}.Fv')
if not fv_path.is_file():
return None
fv_data = fv_path.read_bytes()
fv_hdr = EFI_FIRMWARE_VOLUME_HEADER.from_buffer_copy(fv_data)
ffs_offset = fv_hdr.HeaderLength
results = []
# Walk FFS files within the FV
while ffs_offset + ctypes.sizeof(EFI_FFS_FILE_HEADER) <= len(fv_data):
ffs_offset = (ffs_offset + 7) & ~7 # FFS 8-byte alignment
if ffs_offset + ctypes.sizeof(EFI_FFS_FILE_HEADER) > len(fv_data):
break
ffs_hdr = EFI_FFS_FILE_HEADER.from_buffer_copy(fv_data, ffs_offset)
file_size = ffs_hdr.FFS_FILE_SIZE
if file_size in (0, 0xFFFFFF):
break # End of FFS files or pad
file_end = ffs_offset + file_size
sect_offset = ffs_offset + ffs_hdr.HeaderLength
# Walk sections within this FFS file
while sect_offset + ctypes.sizeof(EFI_COMMON_SECTION_HEADER) <= file_end:
sect_offset = (sect_offset + 3) & ~3 # Section 4-byte alignment
if sect_offset + ctypes.sizeof(EFI_COMMON_SECTION_HEADER) > file_end:
break
sect_hdr = EFI_COMMON_SECTION_HEADER.from_buffer_copy(
fv_data, sect_offset)
sect_size = sect_hdr.SECTION_SIZE
if sect_size == 0:
break
if sect_hdr.Type == EFI_SECTION_PE32:
pe_offset = sect_offset + sect_hdr.Common_Header_Size()
image_base = self._parse_pe_image_base(
fv_data, pe_offset)
if image_base is not None:
results.append((pe_offset, image_base))
sect_offset += sect_size
ffs_offset += file_size
results.sort(key=lambda x: x[0])
return results
@staticmethod
def _parse_pe_image_base(data: bytes, offset: int) -> int | None:
"""Parse ImageBase from a PE/COFF image at the given offset.
Uses EFI_IMAGE_DOS_HEADER to locate the PE signature, then reads
EFI_IMAGE_NT_HEADERS32 or EFI_IMAGE_NT_HEADERS64 to extract ImageBase.
Args:
data: Raw bytes of the FV binary.
offset: Byte offset where the PE/COFF image starts.
Returns:
ImageBase value (int), or None if the image cannot be parsed.
"""
if offset + ctypes.sizeof(EFI_IMAGE_DOS_HEADER) > len(data):
return None
dos_hdr = EFI_IMAGE_DOS_HEADER.from_buffer_copy(data, offset)
if dos_hdr.e_magic != EFI_IMAGE_DOS_SIGNATURE:
return None
nt_offset = offset + dos_hdr.e_lfanew
# Read as NT_HEADERS32 first (smaller); check magic to decide format
if nt_offset + ctypes.sizeof(EFI_IMAGE_NT_HEADERS32) > len(data):
return None
nt32 = EFI_IMAGE_NT_HEADERS32.from_buffer_copy(data, nt_offset)
if nt32.Signature != EFI_IMAGE_NT_SIGNATURE:
return None
if nt32.OptionalHeader.Magic == EFI_IMAGE_NT_OPTIONAL_HDR64_MAGIC:
# PE32+: re-read with the larger NT_HEADERS64 structure
if nt_offset + ctypes.sizeof(EFI_IMAGE_NT_HEADERS64) > len(data):
return None
nt64 = EFI_IMAGE_NT_HEADERS64.from_buffer_copy(data, nt_offset)
return nt64.OptionalHeader.ImageBase
return nt32.OptionalHeader.ImageBase
def _check_pe_image_base(self, fv_name: str, base_address: str | None,
file_index: int, expect_rebased: bool) -> None:
"""Assert that a PE/COFF image in the FV has the correct ImageBase.
A rebased image has ImageBase = FvBaseAddress + fv_offset.
A non-rebased image retains its link-time ImageBase of 0.
Args:
fv_name: Firmware volume name (e.g. 'TESTFV1').
base_address: FvBaseAddress hex string or None.
file_index: Zero-based index of the PE image in FV file order.
expect_rebased: True if the image should have been rebased.
"""
pe_images = self._get_pe_image_bases(fv_name)
self.assertIsNotNone(pe_images, f"Could not read FV for {fv_name}")
self.assertGreater(
len(pe_images), file_index,
f"FV {fv_name} has {len(pe_images)} PE images, need >= {file_index + 1}")
fv_offset, image_base = pe_images[file_index]
fv_base = int(base_address, 16) if isinstance(base_address, str) else (base_address or 0)
expected = (fv_base + fv_offset) if expect_rebased else 0
self.assertEqual(
image_base, expected,
f"File #{file_index} @ FV+0x{fv_offset:X}: "
f"ImageBase=0x{image_base:X}, expected 0x{expected:X}"
f"{'' if expect_rebased else ' (not rebased)'}"
)
def _build_and_verify(self, fv_name: str, base_address: str | None,
force_rebase: str | None, peim1_xip: str | None,
peim2_xip: str | None, dxe_xip: str | None,
expect_rebase: tuple[bool, bool, bool]) -> None:
"""Build an FV with the given configuration and verify all outputs.
Args:
fv_name: Firmware volume name.
base_address: FvBaseAddress hex string or None.
force_rebase: FvForceRebase setting ('TRUE', 'FALSE', or None).
peim1_xip: Xip= keyword for TestPeim ('TRUE', 'FALSE', or None).
peim2_xip: Xip= keyword for TestPeim2 ('TRUE', 'FALSE', or None).
dxe_xip: Xip= keyword for TestDxeDriver ('TRUE', 'FALSE', or None).
expect_rebase: Tuple of 3 bools (peim1, peim2, dxe) indicating
whether each module should be rebased.
"""
pkg_dir = self._create_test_package()
dsc_path = Path(pkg_dir, 'TestXipRebase.dsc')
dsc_path.write_text(self.DSC_CONTENT)
fdf_path = self._generate_fdf(
pkg_dir, fv_name, base_address, force_rebase,
peim1_xip, peim2_xip, dxe_xip)
rc, stdout, stderr = self._run_build(dsc_path, fdf_path)
self.assertEqual(rc, 0,
f"Build failed (rc={rc}).\nstdout:\n{stdout}\nstderr:\n{stderr}")
# Verify ,XIP suffix count in FV INF file
# (,XIP suffix is present when the FDF rule has Xip=TRUE)
inf_content = self._read_fv_file(f'{fv_name}.inf')
self.assertIsNotNone(inf_content, f"FV INF file not found for {fv_name}")
efi_lines = [l for l in inf_content.splitlines() if 'EFI_FILE_NAME' in l]
xip_count = sum(1 for l in efi_lines if l.rstrip().endswith(',XIP'))
expected_xip = sum(x == 'TRUE' for x in (peim1_xip, peim2_xip, dxe_xip))
self.assertEqual(xip_count, expected_xip,
f"Expected {expected_xip} ,XIP lines, got {xip_count}.\n"
f"INF content:\n{inf_content}")
# Verify rebase status in map file and PE/COFF ImageBase in FV binary
map_content = self._read_fv_file(f'{fv_name}.Fv.map')
self.assertIsNotNone(map_content, f"FV map file not found for {fv_name}")
for idx, (name, rebased) in enumerate(
zip(self._MODULE_NAMES, expect_rebase)):
self._check_module_rebased(map_content, name, rebased)
self._check_pe_image_base(fv_name, base_address, idx, rebased)
# ===================================================================
# Test case table (matches behavior matrix from test plan header)
# ===================================================================
# (fv_name, base_address, force_rebase,
# peim1_xip, peim2_xip, dxe_xip,
# (expect_peim1_rebase, expect_peim2_rebase, expect_dxe_rebase),
# description)
TEST_CASES = [
# TC1: ForceRebase not specified, BaseAddress defaults to 0.
# Early return path: (BaseAddress==0 && ForceRebase==-1).
# No files are rebased. PEIMs have ,XIP suffix (Xip=TRUE in rule).
('TESTFV1', None, None, 'TRUE', 'TRUE', None,
(False, False, False), 'TC1: ForceRebase=unset, Base=0 -> no rebase'),
# TC2: ForceRebase not specified, BaseAddress!=0.
# Legacy path: (BaseAddress!=0 && ForceRebase==-1).
# ALL files are rebased regardless of XIP status.
('TESTFV2', '0x00800000', None, 'TRUE', 'TRUE', None,
(True, True, True), 'TC2: ForceRebase=unset, Base!=0 -> rebase all (legacy)'),
# TC3: ForceRebase=FALSE with BaseAddress!=0.
# Early return path: (ForceRebase==0).
# No files are rebased even though all have Xip=TRUE.
('TESTFV3', '0x00800000', 'FALSE', 'TRUE', 'TRUE', 'TRUE',
(False, False, False), 'TC3: ForceRebase=FALSE -> no rebase'),
# TC4: ForceRebase=TRUE, all three files have Xip=TRUE.
# All files match (ForceRebase==1 && XipFile[i]==TRUE) and are rebased.
('TESTFV4', '0x00800000', 'TRUE', 'TRUE', 'TRUE', 'TRUE',
(True, True, True), 'TC4: ForceRebase=TRUE, all Xip=TRUE -> rebase all'),
# TC5: ForceRebase=TRUE, PEIMs have Xip=TRUE, DXE has no Xip.
# Selective rebase: PEIMs rebased, DXE skipped.
('TESTFV5', '0x00800000', 'TRUE', 'TRUE', 'TRUE', None,
(True, True, False), 'TC5: ForceRebase=TRUE, selective Xip -> rebase Xip only'),
# TC6: ForceRebase=TRUE, no files have Xip keyword.
# XipFileCount==0, so legacy behavior is preserved: rebase all files.
('TESTFV6', '0x00800000', 'TRUE', None, None, None,
(True, True, True), 'TC6: ForceRebase=TRUE, no Xip -> rebase all (legacy compat)'),
# TC7: ForceRebase=TRUE, PEIM1 has Xip=TRUE, PEIM2 has Xip=FALSE.
# Mixed XIP within same module type via RuleOverride.
# Only PEIM1 is rebased; PEIM2 and DXE are skipped.
('TESTFV7', '0x00800000', 'TRUE', 'TRUE', 'FALSE', None,
(True, False, False), 'TC7: ForceRebase=TRUE, mixed Xip -> rebase Xip=TRUE only'),
# TC8: ForceRebase=TRUE, BaseAddress=0, PEIMs have Xip=TRUE.
# ForceRebase=TRUE overrides the (BaseAddress==0) early return.
# PEIMs are rebased to offset 0+fv_offset; DXE is skipped (no Xip).
('TESTFV8', '0x0', 'TRUE', 'TRUE', 'TRUE', None,
(True, True, False), 'TC8: ForceRebase=TRUE, Base=0, Xip -> rebase (force overrides)'),
]
def test_xip_rebase_behavior(self) -> None:
"""Parameterized test covering all ForceRebase/BaseAddress/Xip combos."""
for (fv_name, base_address, force_rebase,
peim1_xip, peim2_xip, dxe_xip,
expect_rebase, description) in self.TEST_CASES:
with self.subTest(description):
self._build_and_verify(
fv_name, base_address, force_rebase,
peim1_xip, peim2_xip, dxe_xip, expect_rebase)
class TestFdfParserPe32KeywordOrder(unittest.TestCase):
"""Test that the FDF parser accepts PE32 section keywords in any order.
The keywords Align, Xip, and RELOCS_STRIPPED/RELOCS_RETAINED should be
accepted in any permutation within a PE32 section statement in a [Rule].
"""
@classmethod
def setUpClass(cls):
"""Set up environment for FdfParser imports."""
import tempfile
cls._tmpdir = tempfile.mkdtemp(prefix='fdf_parser_test_')
# Set WORKSPACE so the parser doesn't crash
os.environ.setdefault('WORKSPACE', cls._tmpdir)
from GenFds.GenFdsGlobalVariable import GenFdsGlobalVariable
GenFdsGlobalVariable.WorkSpaceDir = cls._tmpdir
from Common import GlobalData
GlobalData.gFdfParser = None
GlobalData.gWorkspace = cls._tmpdir
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls._tmpdir, ignore_errors=True)
def _parse_rule(self, pe32_section_line):
"""Parse a [Rule] with the given PE32 section line and return the EfiSection."""
import tempfile
from GenFds.FdfParser import FdfParser
from Common import GlobalData
fdf_content = (
"[Rule.Common.PEIM]\n"
" FILE PEIM = $(NAMED_GUID) {\n"
" " + pe32_section_line + "\n"
" }\n"
)
fdf_path = os.path.join(self._tmpdir, 'test_order.fdf')
with open(fdf_path, 'w') as f:
f.write(fdf_content)
parser = FdfParser(fdf_path)
# Manually set up parser state for rule parsing
parser.CurrentLineNumber = 3
parser.CurrentOffsetWithinLine = 4
# Re-read the profile to refresh file lines
parser.Profile.FileLinesList = fdf_content.splitlines(True)
# Create a RuleComplexFile as the container object
from GenFds.RuleComplexFile import RuleComplexFile
obj = RuleComplexFile()
obj.FvFileType = 'PEIM'
obj.KeepReloc = None
obj.SectionList = []
result = parser._GetEfiSection(obj)
self.assertTrue(result, f"Parser failed to parse: {pe32_section_line}")
self.assertEqual(len(obj.SectionList), 1)
return obj.SectionList[0]
# (pe32_line, expected_alignment, expected_xip, expected_keep_reloc, description)
TEST_CASES = [
# Align before Xip (original supported order)
('PE32 PE32 Align=8 Xip=TRUE',
'8', 'TRUE', None,
'Align then Xip'),
# Xip before Align (previously caused stack trace)
('PE32 PE32 Xip=TRUE Align=8',
'8', 'TRUE', None,
'Xip then Align'),
# Align before RELOCS_STRIPPED
('PE32 PE32 Align=16 RELOCS_STRIPPED',
'16', None, False,
'Align then RELOCS_STRIPPED'),
# RELOCS_STRIPPED before Align
('PE32 PE32 RELOCS_STRIPPED Align=16',
'16', None, False,
'RELOCS_STRIPPED then Align'),
# Xip before RELOCS_STRIPPED
('PE32 PE32 Xip=TRUE RELOCS_STRIPPED',
None, 'TRUE', False,
'Xip then RELOCS_STRIPPED'),
# RELOCS_STRIPPED before Xip
('PE32 PE32 RELOCS_STRIPPED Xip=TRUE',
None, 'TRUE', False,
'RELOCS_STRIPPED then Xip'),
# All three: Align, Xip, RELOCS_STRIPPED
('PE32 PE32 Align=16 Xip=TRUE RELOCS_STRIPPED',
'16', 'TRUE', False,
'Align then Xip then RELOCS_STRIPPED'),
# All three: Xip, Align, RELOCS_STRIPPED
('PE32 PE32 Xip=TRUE Align=16 RELOCS_STRIPPED',
'16', 'TRUE', False,
'Xip then Align then RELOCS_STRIPPED'),
# All three: RELOCS_STRIPPED, Align, Xip
('PE32 PE32 RELOCS_STRIPPED Align=16 Xip=TRUE',
'16', 'TRUE', False,
'RELOCS_STRIPPED then Align then Xip'),
# All three: RELOCS_STRIPPED, Xip, Align
('PE32 PE32 RELOCS_STRIPPED Xip=TRUE Align=16',
'16', 'TRUE', False,
'RELOCS_STRIPPED then Xip then Align'),
# All three: Xip, RELOCS_STRIPPED, Align
('PE32 PE32 Xip=TRUE RELOCS_STRIPPED Align=16',
'16', 'TRUE', False,
'Xip then RELOCS_STRIPPED then Align'),
# All three: Align, RELOCS_STRIPPED, Xip
('PE32 PE32 Align=16 RELOCS_STRIPPED Xip=TRUE',
'16', 'TRUE', False,
'Align then RELOCS_STRIPPED then Xip'),
# Xip=FALSE
('PE32 PE32 Xip=FALSE Align=8',
'8', 'FALSE', None,
'Xip=FALSE then Align'),
# RELOCS_RETAINED variant
('PE32 PE32 Xip=TRUE RELOCS_RETAINED Align=8',
'8', 'TRUE', True,
'Xip then RELOCS_RETAINED then Align'),
# Only Xip (no Align, no Reloc)
('PE32 PE32 Xip=TRUE',
None, 'TRUE', None,
'Xip only'),
# Only Align (no Xip, no Reloc)
('PE32 PE32 Align=32',
'32', None, None,
'Align only'),
# Only RELOCS_STRIPPED (no Align, no Xip)
('PE32 PE32 RELOCS_STRIPPED',
None, None, False,
'RELOCS_STRIPPED only'),
]
def test_pe32_keyword_order(self) -> None:
"""Parameterized test verifying PE32 section keywords in any order."""
for (pe32_line, exp_align, exp_xip, exp_keep_reloc, desc) in self.TEST_CASES:
with self.subTest(desc):
section = self._parse_rule(pe32_line)
self.assertEqual(section.SectionType, 'PE32')
if exp_align is not None:
self.assertEqual(section.Alignment, exp_align)
else:
self.assertIn(section.Alignment, (None, ''))
if exp_xip is not None:
self.assertEqual(section.Xip, exp_xip)
else:
self.assertFalse(
hasattr(section, 'Xip') and section.Xip,
f"Expected no Xip but got {getattr(section, 'Xip', None)}"
)
if exp_keep_reloc is not None:
self.assertEqual(section.KeepReloc, exp_keep_reloc)
else:
self.assertIsNone(
getattr(section, 'KeepReloc', None),
f"Expected no KeepReloc but got {section.KeepReloc}"
)
class TestFdfParserFvKeywordOrder(unittest.TestCase):
"""Test that the FDF parser accepts [FV] keywords in any order.
FvForceRebase, FvBaseAddress, FvAlignment, and FV attributes like
ERASE_POLARITY, MEMORY_MAPPED should be accepted in any order.
Previously, FvForceRebase between two FV attributes (e.g. between
ERASE_POLARITY and MEMORY_MAPPED) caused a Python stack trace.
"""
@classmethod
def setUpClass(cls):
"""Set up environment for FdfParser imports."""
import tempfile
cls._tmpdir = tempfile.mkdtemp(prefix='fdf_fv_parser_test_')
os.environ.setdefault('WORKSPACE', cls._tmpdir)
from GenFds.GenFdsGlobalVariable import GenFdsGlobalVariable
GenFdsGlobalVariable.WorkSpaceDir = cls._tmpdir
from Common import GlobalData
GlobalData.gFdfParser = None
GlobalData.gWorkspace = cls._tmpdir
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls._tmpdir, ignore_errors=True)
def _parse_fv_section(self, fv_body):
"""Parse an [FV] section and return the FV object."""
from GenFds.FdfParser import FdfParser
from Common import GlobalData
fdf_content = (
"[FV.TESTFV]\n"
+ fv_body + "\n"
)
fdf_path = os.path.join(self._tmpdir, 'test_fv_order.fdf')
with open(fdf_path, 'w') as f:
f.write(fdf_content)
parser = FdfParser(fdf_path)
parser.Profile.FileLinesList = fdf_content.splitlines(True)
# Position parser at start of FV body (line 2, offset 0)
parser.CurrentLineNumber = 2
parser.CurrentOffsetWithinLine = 0
# Create FV object and parse the attributes/keywords
from GenFds.Fv import FV
fv_obj = FV(Name='TESTFV')
# Use the same while loop the real parser uses
while True:
parser._GetSetStatements(fv_obj)
if not (parser._GetBlockStatement(fv_obj) or
parser._GetFvBaseAddress(fv_obj) or
parser._GetFvForceRebase(fv_obj) or
parser._GetFvAlignment(fv_obj) or
parser._GetFvAttributes(fv_obj) or
parser._GetFvNameGuid(fv_obj) or
parser._GetFvExtEntryStatement(fv_obj) or
parser._GetFvNameString(fv_obj)):
break
return fv_obj
# (fv_body, expected_attrs, expected_force_rebase, description)
TEST_CASES = [
# FvForceRebase after all attributes (original working order)
("ERASE_POLARITY = 1\nMEMORY_MAPPED = TRUE\nFvForceRebase = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, True,
'FvForceRebase after all attributes'),
# FvForceRebase before all attributes
("FvForceRebase = TRUE\nERASE_POLARITY = 1\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, True,
'FvForceRebase before all attributes'),
# FvForceRebase between ERASE_POLARITY and MEMORY_MAPPED
("ERASE_POLARITY = 1\nFvForceRebase = TRUE\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, True,
'FvForceRebase between attributes (previously crashed)'),
# FvForceRebase=FALSE between attributes
("ERASE_POLARITY = 1\nFvForceRebase = FALSE\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, False,
'FvForceRebase=FALSE between attributes'),
# Multiple attributes, FvForceRebase in the middle
("ERASE_POLARITY = 1\nSTICKY_WRITE = TRUE\nFvForceRebase = TRUE\n"
"MEMORY_MAPPED = TRUE\nLOCK_CAP = TRUE\n",
{'ERASE_POLARITY': '1', 'STICKY_WRITE': 'TRUE',
'MEMORY_MAPPED': 'TRUE', 'LOCK_CAP': 'TRUE'}, True,
'FvForceRebase in middle of many attributes'),
# FvBaseAddress between attributes
("ERASE_POLARITY = 1\nFvBaseAddress = 0x00800000\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, None,
'FvBaseAddress between attributes'),
# FvAlignment between attributes
("ERASE_POLARITY = 1\nFvAlignment = 16\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'MEMORY_MAPPED': 'TRUE'}, None,
'FvAlignment between attributes'),
# All interleaved: attr, FvForceRebase, attr, FvBaseAddress, attr
("ERASE_POLARITY = 1\nFvForceRebase = TRUE\nSTICKY_WRITE = TRUE\n"
"FvBaseAddress = 0x00800000\nMEMORY_MAPPED = TRUE\n",
{'ERASE_POLARITY': '1', 'STICKY_WRITE': 'TRUE', 'MEMORY_MAPPED': 'TRUE'}, True,
'Multiple keywords interleaved with attributes'),
]
def test_fv_keyword_order(self) -> None:
"""Parameterized test verifying FV keywords accepted in any order."""
for (fv_body, exp_attrs, exp_force_rebase, desc) in self.TEST_CASES:
with self.subTest(desc):
fv_obj = self._parse_fv_section(fv_body)
for attr_name, attr_val in exp_attrs.items():
self.assertIn(attr_name, fv_obj.FvAttributeDict,
f"Missing attribute {attr_name}")
self.assertEqual(fv_obj.FvAttributeDict[attr_name], attr_val)
if exp_force_rebase is not None:
self.assertEqual(fv_obj.FvForceRebase, exp_force_rebase)
if __name__ == '__main__':
unittest.main()