## @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.
# # 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 #include 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 #include 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 { 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_/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()