From 87e39fa46ffdd6be176b615a81635da3b81a572d Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Mon, 12 Aug 2024 11:46:50 -0700 Subject: [PATCH] [REBASE&FF][CHERRY-PICK] ImageValidation: Add default configuration (#1104) Previously, ImageValidation was an "opt-in" plugin by setting a build variable `PE_VALIDATION_PATH`, however with this pull request, Image Validation will be on by default, with some default configuration that can be changed with a custom configuration yaml file. The default requirements are: 1. All efi binaries must not be both write and execute 2. All efi binaries must have an image base of 0x0 3. All dxe phase binaries must be 4k section aligned, with the one exception of AARCH64 DXE_RUNTIME_DRIVERS, which must be 64k aligned. compiled binaries that need to be opted out of, can do so by adding an `IGNORE_LIST` in the configuration file ```json { "IGNORE_LIST": ["Shell.efi", "etc"] } ``` A cherry-pick of #1100 into release/202311 - [ ] Impacts functionality? - [ ] Impacts security? - [x] Breaking change? - [ ] Includes tests? - [x] Includes documentation? ## How This Was Tested Confirmed successful execution of the plugin on Windows with QemuQ35 and Ubuntu with QemuSbsa ## Integration Instructions Platforms that begin to fail this test will need to generate a configuration yaml file, and set a stuart build variable, `PE_VALIDATION_PATH` to it. It is suggested to do this in the Platform's `PlatformBuild.py`. **The Correct Integration** is to evaluate the binary and why it is not meeting the requirements. The platform can elect to update the compilation of the binary to meet the requirements, add or override validation rules for certain MODULE_TYPEs, or simply add the binary to the ignore list. Please review the Plugin's readme.md file for more details on doing any of these things. Signed-off-by: Joey Vagedes Co-authored-by: Michael Kubacki --- .../Plugin/ImageValidation/ImageValidation.py | 652 ++++++++++-------- .pytool/Plugin/ImageValidation/ReadMe.md | 147 ++-- .../ImageValidation/image_validation.cfg | 70 ++ 3 files changed, 526 insertions(+), 343 deletions(-) create mode 100644 .pytool/Plugin/ImageValidation/image_validation.cfg diff --git a/.pytool/Plugin/ImageValidation/ImageValidation.py b/.pytool/Plugin/ImageValidation/ImageValidation.py index f04b00ae78..910db9d744 100644 --- a/.pytool/Plugin/ImageValidation/ImageValidation.py +++ b/.pytool/Plugin/ImageValidation/ImageValidation.py @@ -1,274 +1,378 @@ -# @file ImageValidation.py -# Plugin to validate any PE images against a set of requirements -## -# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: BSD-2-Clause-Patent -## - -import os -import re -from pathlib import Path -from pefile import PE -from edk2toolext.environment.plugintypes.uefi_build_plugin import IUefiBuildPlugin -from edk2toolext.image_validation import * -from edk2toollib.uefi.edk2.path_utilities import Edk2Path -from edk2toollib.uefi.edk2.parsers.inf_parser import InfParser -from edk2toollib.uefi.edk2.parsers.fdf_parser import FdfParser -from edk2toollib.uefi.edk2.parsers.dsc_parser import DscParser -from edk2toollib.uefi.edk2.parsers.dsc_parser import * -import json -from typing import List -import logging -from datetime import datetime - - -class ImageValidation(IUefiBuildPlugin): - def __init__(self): - self.test_manager = TestManager() - - # Default tests provided by edk2toolext.image_validation - self.test_manager.add_test(TestWriteExecuteFlags()) - self.test_manager.add_test(TestSectionAlignment()) - self.test_manager.add_test(TestSubsystemValue()) - # Add additional Tests here - - def do_post_build(self, thebuilder): - - starttime = datetime.now() - logging.info( - "---------------------------------------------------------") - logging.info( - "-----------Postbuild Image Validation Starting-----------") - logging.info( - "---------------------------------------------------------") - - # Load Configuration Data - config_path = thebuilder.env.GetValue("PE_VALIDATION_PATH", None) - tool_chain_tag = thebuilder.env.GetValue("TOOL_CHAIN_TAG") - if config_path is None: - logging.info( - "PE_VALIDATION_PATH not set, PE Image Validation Skipped") - return 0 # Path not set, Plugin skipped - - if not os.path.isfile(config_path): - logging.error("Invalid PE_VALIDATION_PATH. File not Found") - return 1 - - with open(config_path) as jsonfile: - config_data = json.load(jsonfile) - - self.test_manager.config_data = config_data - self.config_data = config_data - self.ignore_list = config_data["IGNORE_LIST"] - self.arch_dict = config_data["TARGET_ARCH"] - - count = 0 - - # Start Pre-Compiled Image Verification - fdf_parser = FdfParser() - dsc_parser = DscParser() - - edk2 = thebuilder.edk2path - - ActiveDsc = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( - thebuilder.env.GetValue("ACTIVE_PLATFORM")) - ActiveFdf = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( - thebuilder.env.GetValue("FLASH_DEFINITION")) - - if ActiveFdf is None: - logging.info("No FDF found - PE Image Validation skipped") - return 0 - - # parse the DSC and the FDF - env_vars = thebuilder.env.GetAllBuildKeyValues() - dsc_parser.SetEdk2Path(edk2) - dsc_parser.SetInputVars(env_vars).ParseFile(ActiveDsc) - - env_vars.update(dsc_parser.LocalVars) - fdf_parser.SetEdk2Path(edk2) - fdf_parser.SetInputVars(env_vars).ParseFile(ActiveFdf) - - # Test all pre-compiled efis described in the fdf - result = Result.PASS - for FV_name in fdf_parser.FVs: # Get all Firmware volumes - FV_files = fdf_parser.FVs[FV_name]["Files"] - for fv_file_name in FV_files: # Iterate over each file in the firmware volume - fv_file = FV_files[fv_file_name] - if "PE32" in fv_file: # Any PE32 section in the FV contains a path to the efi - # could have multiple PE32 sections - for efi_path in fv_file["PE32"]: - efi_path = self._resolve_vars(thebuilder, efi_path) - efi_path = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( - efi_path) - if efi_path == None: - logging.warning( - "Unable to parse the path to the pre-compiled efi") - continue - if os.path.basename(efi_path) in self.ignore_list: - continue - logging.info( - f'Performing Image Verification ... {os.path.basename(efi_path)}') - if self._validate_image(efi_path, fv_file["type"]) == Result.FAIL: - result = Result.FAIL - count += 1 - # End Pre-Compiled Image Verification - - # Start Build Time Compiled Image Verification - result = Result.PASS - for arch in thebuilder.env.GetValue("TARGET_ARCH").split(): - efi_path_list = self._walk_directory_for_extension( - ['.efi'], f'{thebuilder.env.GetValue("BUILD_OUTPUT_BASE")}/{arch}') - - for efi_path in efi_path_list: - if os.path.basename(efi_path) in self.ignore_list: - continue - - # Perform Image Verification on any output efi's - # Grab profile from makefile - if efi_path.__contains__("OUTPUT"): - try: - if tool_chain_tag.__contains__("VS"): - profile = self._get_profile_from_makefile( - f'{Path(efi_path).parent.parent}/Makefile') - - elif tool_chain_tag.__contains__("GCC"): - profile = self._get_profile_from_makefile( - f'{Path(efi_path).parent.parent}/GNUmakefile') - - elif tool_chain_tag.__contains__("CLANG"): - profile = self._get_profile_from_makefile( - f'{Path(efi_path).parent.parent}/GNUmakefile') - else: - logging.warning("Unexpected TOOL_CHAIN_TAG... Cannot parse makefile. Using DEFAULT profile.") - profile = "DEFAULT" - except: - logging.warning(f'Failed to parse makefile at [{Path(efi_path).parent.parent}/GNUmakefile]') - logging.warning(f'Using DEFAULT profile') - profile = "DEFAULT" - - logging.info( - f'Performing Image Verification ... {os.path.basename(efi_path)}') - if self._validate_image(efi_path, profile) == Result.FAIL: - result = Result.FAIL - count += 1 - # End Built Time Compiled Image Verification - - endtime = datetime.now() - delta = endtime - starttime - logging.info( - "---------------------------------------------------------") - logging.info( - "-----------Postbuild Image Validation Finished-----------") - logging.info( - "------------------{:04d} Images Verified-------------------".format(count)) - logging.info( - "-------------- Running Time (mm:ss): {0[0]:02}:{0[1]:02} --------------".format(divmod(delta.seconds, 60))) - logging.info( - "---------------------------------------------------------") - - if result == Result.FAIL: - return 1 - else: - return 0 - - # Executes run_tests() on the efi - def _validate_image(self, efi_path, profile="DEFAULT"): - pe = PE(efi_path) - - target_config = self.config_data[MACHINE_TYPE[pe.FILE_HEADER.Machine]].get( - profile) - if target_config == {}: # The target_config is present, but empty, therefore, override to default - profile = "DEFAULT" - - return self.test_manager.run_tests(pe, profile) - - # Reads the Makefile of an efi, if present, to determine profile - def _get_profile_from_makefile(self, makefile): - with open(makefile) as file: - for line in file.readlines(): - if line.__contains__('MODULE_TYPE'): - line = line.split('=') - module_type = line[1] - module_type = module_type.strip() - return module_type - return "DEFAULT" - - # Attempts to convert shorthand arch such as X64 to the - # Fully describe architecture. Additional support for - # Fallback architectures can be added here - def _try_convert_full_arch(self, arch): - full_arch = self.arch_dict.get(arch) - if full_arch == None: - if arch.__contains__("ARM"): - full_arch = "IMAGE_FILE_MACHINE_ARM" - # Add other Arches - return full_arch - - # Resolves variable names matching the $(...) pattern. - def _resolve_vars(self, thebuilder, s): - var_pattern = re.compile(r'\$\([^)]*\)') # Detect $(...) pattern - env = thebuilder.env - rs = s - for match in var_pattern.findall(s): - var_name = match[2:-1] - env_var = env.GetValue(var_name) if env.GetValue( - var_name) != None else env.GetBuildValue(var_name) - if env_var == None: - pass - rs = rs.replace(match, env_var) - return rs - - def _walk_directory_for_extension(self, extensionlist: List[str], directory: os.PathLike, - ignorelist: List[str] = None) -> List[os.PathLike]: - ''' Walks a file directory recursively for all items ending in certain extension - @extensionlist: List[str] list of file extensions - @directory: Path - absolute path to directory to start looking - @ignorelist: List[str] or None. optional - default is None: a list of case insensitive filenames to ignore - @returns a List of file paths to matching files - ''' - if not isinstance(extensionlist, list): - logging.critical("Expected list but got " + - str(type(extensionlist))) - raise TypeError("extensionlist must be a list") - - if directory is None: - logging.critical("No directory given") - raise TypeError("directory is None") - - if not os.path.isabs(directory): - logging.critical("Directory not abs path") - raise ValueError("directory is not an absolute path") - - if not os.path.isdir(directory): - logging.critical("Invalid find directory to walk") - raise ValueError("directory is not a valid directory path") - - if ignorelist is not None: - if not isinstance(ignorelist, list): - logging.critical("Expected list but got " + - str(type(ignorelist))) - raise TypeError("ignorelist must be a list") - - ignorelist_lower = list() - for item in ignorelist: - ignorelist_lower.append(item.lower()) - - extensionlist_lower = list() - for item in extensionlist: - extensionlist_lower.append(item.lower()) - - returnlist = list() - for Root, Dirs, Files in os.walk(directory): - for File in Files: - for Extension in extensionlist_lower: - if File.lower().endswith(Extension): - ignoreIt = False - if(ignorelist is not None): - for c in ignorelist_lower: - if(File.lower().startswith(c)): - ignoreIt = True - break - if not ignoreIt: - returnlist.append(os.path.join(Root, File)) - - return returnlist +# @file ImageValidation.py +# Plugin to validate any PE images against a set of requirements +## +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +import os +import re +from pathlib import Path +from pefile import PE +from edk2toolext.environment.plugintypes.uefi_build_plugin import IUefiBuildPlugin +from edk2toolext.image_validation import ( + Result, TestManager, TestInterface, TestWriteExecuteFlags, + TestSectionAlignment, MACHINE_TYPE +) +from edk2toollib.uefi.edk2.parsers.fdf_parser import FdfParser +from edk2toollib.uefi.edk2.parsers.dsc_parser import DscParser +import yaml +from typing import List +import logging +from datetime import datetime + +DEFAULT_CONFIG_FILE_PATH = Path(__file__).parent.resolve() / "image_validation.cfg" + +class TestImageBase(TestInterface): + """Image base verification test. + + Checks the image base of the binary by accessing the optional + header, then the image base. This value must be the same value + as specified in the config file. + + Output: + @Success: Image base matches the expected value + @Skip: Image base requirement not set in the config file + @Warn: Image Alignment value is not found in the Optional Header + @Fail: Image base does not match the expected value + """ + def name(self) -> str: + """Returns the name of the test.""" + return 'Image Base verification' + + def execute(self, pe: PE, config_data: dict) -> Result: + """Executes the test on the pefile. + + Arguments: + pe (PE): a parsed PE/COFF image file + config_data (dict): the configuration data for the test + + Returns: + (Result): SKIP, WARN, FAIL, PASS + """ + target_requirements = config_data["TARGET_REQUIREMENTS"] + + required_base = target_requirements.get("IMAGE_BASE") + if required_base is None or required_base == -1: + return Result.SKIP + + try: + image_base = pe.OPTIONAL_HEADER.ImageBase + except Exception: + logging.warning("Image Base not found in Optional Header") + return Result.WARN + + if image_base != required_base: + logging.error( + f'[{Result.FAIL}]: Image Base address Expected: {hex(required_base)}, Found: {hex(image_base)}' + ) + return Result.FAIL + return Result.PASS + + +class ImageValidation(IUefiBuildPlugin): + def __init__(self): + self.test_manager = TestManager() + + # Default tests provided by edk2toolext.image_validation + self.test_manager.add_test(TestWriteExecuteFlags()) + self.test_manager.add_test(TestSectionAlignment()) + self.test_manager.add_test(TestImageBase()) + # Add additional Tests here + + def do_post_build(self, thebuilder): + + starttime = datetime.now() + logging.info( + "---------------------------------------------------------") + logging.info( + "-----------Postbuild Image Validation Starting-----------") + logging.info( + "---------------------------------------------------------") + + # Load Configuration Data + config_path = thebuilder.env.GetValue("PE_VALIDATION_PATH", None) + tool_chain_tag = thebuilder.env.GetValue("TOOL_CHAIN_TAG") + if config_path is None: + logging.info("PE_VALIDATION_PATH not set, Using default configuration") + logging.info("Review ImageValidation/Readme.md for configuration options.") + elif not os.path.isfile(config_path): + logging.error("Invalid PE_VALIDATION_PATH. File not Found") + return 1 + + # Use the default configuration. If a configuration file is provided, merge the two + # At the top level entries, with the provided configuration taking precedence. + if not DEFAULT_CONFIG_FILE_PATH.is_file(): + logging.error("Default configuration file not found.") + return 1 + try: + with open(DEFAULT_CONFIG_FILE_PATH) as f: + config_data = yaml.safe_load(f) + except Exception as e: + logging.error(f"Error parsing {DEFAULT_CONFIG_FILE_PATH}: [{e}]") + return 1 + + try: + if config_path: + with open(config_path) as f: + config_data = ImageValidation.merge_config( + config_data, yaml.safe_load(f)) + + except Exception as e: + logging.error(f"Error parsing {config_path}: [{e}]") + return 1 + + self.test_manager.config_data = config_data + self.config_data = config_data + self.ignore_list = config_data["IGNORE_LIST"] + self.arch_dict = config_data["TARGET_ARCH"] + + count = 0 + + # Start Pre-Compiled Image Verification + fdf_parser = FdfParser() + dsc_parser = DscParser() + + edk2 = thebuilder.edk2path + + ActiveDsc = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( + thebuilder.env.GetValue("ACTIVE_PLATFORM")) + ActiveFdf = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( + thebuilder.env.GetValue("FLASH_DEFINITION")) + + if ActiveFdf is None: + logging.info("No FDF found - PE Image Validation skipped") + return 0 + + # parse the DSC and the FDF + env_vars = thebuilder.env.GetAllBuildKeyValues() + dsc_parser.SetEdk2Path(edk2) + dsc_parser.SetInputVars(env_vars).ParseFile(ActiveDsc) + + env_vars.update(dsc_parser.LocalVars) + fdf_parser.SetEdk2Path(edk2) + fdf_parser.SetInputVars(env_vars).ParseFile(ActiveFdf) + + # Test all pre-compiled efis described in the fdf + result = Result.PASS + for FV_name in fdf_parser.FVs: # Get all Firmware volumes + FV_files = fdf_parser.FVs[FV_name]["Files"] + for fv_file_name in FV_files: # Iterate over each file in the firmware volume + fv_file = FV_files[fv_file_name] + if "PE32" in fv_file: # Any PE32 section in the FV contains a path to the efi + # could have multiple PE32 sections + for efi_path in fv_file["PE32"]: + efi_path = self._resolve_vars(thebuilder, efi_path) + efi_path = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath( + efi_path) + if efi_path is None: + logging.warning( + "Unable to parse the path to the pre-compiled efi") + continue + if os.path.basename(efi_path) in self.ignore_list: + continue + logging.debug( + f'Performing Image Verification ... {os.path.basename(efi_path)}') + if self._validate_image(efi_path, fv_file["type"]) == Result.FAIL: + logging.error(f'{os.path.basename(efi_path)} Failed Image Validation.') + result = Result.FAIL + count += 1 + # End Pre-Compiled Image Verification + + # Start Build Time Compiled Image Verification + result = Result.PASS + for arch in thebuilder.env.GetValue("TARGET_ARCH").split(): + efi_path_list = self._walk_directory_for_extension( + ['.efi'], f'{thebuilder.env.GetValue("BUILD_OUTPUT_BASE")}/{arch}') + + for efi_path in efi_path_list: + if os.path.basename(efi_path) in self.ignore_list: + continue + + # Perform Image Verification on any output efi's + # Grab profile from makefile + if "OUTPUT" in efi_path: + try: + if "VS" in tool_chain_tag: + profile = self._get_profile_from_makefile( + f'{Path(efi_path).parent.parent}/Makefile') + + elif "GCC" in tool_chain_tag: + profile = self._get_profile_from_makefile( + f'{Path(efi_path).parent.parent}/GNUmakefile') + + elif "CLANG" in tool_chain_tag: + profile = self._get_profile_from_makefile( + f'{Path(efi_path).parent.parent}/GNUmakefile') + else: + logging.warning("Unexpected TOOL_CHAIN_TAG... Cannot parse makefile. Using DEFAULT profile.") + profile = "DEFAULT" + except Exception: + logging.warning(f'Failed to parse makefile at [{Path(efi_path).parent.parent}/GNUmakefile]') + logging.warning('Using DEFAULT profile') + profile = "DEFAULT" + + logging.debug( + f'Performing Image Verification ... {os.path.basename(efi_path)}') + if self._validate_image(efi_path, profile) == Result.FAIL: + logging.error(f'{os.path.basename(efi_path)} Failed Image Validation.') + result = Result.FAIL + count += 1 + # End Built Time Compiled Image Verification + + endtime = datetime.now() + delta = endtime - starttime + logging.info( + "---------------------------------------------------------") + logging.info( + "-----------Postbuild Image Validation Finished-----------") + logging.info( + "------------------{:04d} Images Verified-------------------".format(count)) + logging.info( + "-------------- Running Time (mm:ss): {0[0]:02}:{0[1]:02} --------------".format(divmod(delta.seconds, 60))) + logging.info( + "---------------------------------------------------------") + + if result == Result.FAIL: + return 1 + else: + return 0 + + # Executes run_tests() on the efi + def _validate_image(self, efi_path, profile="DEFAULT"): + pe = PE(efi_path) + + target_config = self.config_data[MACHINE_TYPE[pe.FILE_HEADER.Machine]].get( + profile) + if target_config == {}: # The target_config is present, but empty, therefore, override to default + profile = "DEFAULT" + + return self.test_manager.run_tests(pe, profile) + + # Reads the Makefile of an efi, if present, to determine profile + def _get_profile_from_makefile(self, makefile): + with open(makefile) as file: + for line in file.readlines(): + if "MODULE_TYPE" in line: + line = line.split('=') + module_type = line[1] + module_type = module_type.strip() + return module_type + return "DEFAULT" + + # Attempts to convert shorthand arch such as X64 to the + # Fully describe architecture. Additional support for + # Fallback architectures can be added here + def _try_convert_full_arch(self, arch): + full_arch = self.arch_dict.get(arch) + if full_arch is None: + if "ARM" in arch: + full_arch = "IMAGE_FILE_MACHINE_ARM" + # Add other Arches + return full_arch + + # Resolves variable names matching the $(...) pattern. + def _resolve_vars(self, thebuilder, s): + var_pattern = re.compile(r'\$\([^)]*\)') # Detect $(...) pattern + env = thebuilder.env + rs = s + for match in var_pattern.findall(s): + var_name = match[2:-1] + env_var = env.GetValue(var_name) if env.GetValue( + var_name) is not None else env.GetBuildValue(var_name) + if env_var is None: + pass + rs = rs.replace(match, env_var) + return rs + + def _walk_directory_for_extension(self, extensionlist: List[str], directory: os.PathLike, + ignorelist: List[str] = None) -> List[os.PathLike]: + ''' Walks a file directory recursively for all items ending in certain extension + @extensionlist: List[str] list of file extensions + @directory: Path - absolute path to directory to start looking + @ignorelist: List[str] or None. optional - default is None: a list of case insensitive filenames to ignore + @returns a List of file paths to matching files + ''' + if not isinstance(extensionlist, list): + logging.critical("Expected list but got " + + str(type(extensionlist))) + raise TypeError("extensionlist must be a list") + + if directory is None: + logging.critical("No directory given") + raise TypeError("directory is None") + + if not os.path.isabs(directory): + logging.critical("Directory not abs path") + raise ValueError("directory is not an absolute path") + + if not os.path.isdir(directory): + logging.critical("Invalid find directory to walk") + raise ValueError("directory is not a valid directory path") + + if ignorelist is not None: + if not isinstance(ignorelist, list): + logging.critical("Expected list but got " + + str(type(ignorelist))) + raise TypeError("ignorelist must be a list") + + ignorelist_lower = list() + for item in ignorelist: + ignorelist_lower.append(item.lower()) + + extensionlist_lower = list() + for item in extensionlist: + extensionlist_lower.append(item.lower()) + + returnlist = list() + for Root, Dirs, Files in os.walk(directory): + for File in Files: + for Extension in extensionlist_lower: + if File.lower().endswith(Extension): + ignoreIt = False + if(ignorelist is not None): + for c in ignorelist_lower: + if(File.lower().startswith(c)): + ignoreIt = True + break + if not ignoreIt: + returnlist.append(os.path.join(Root, File)) + + return returnlist + + # Merged two configuration dictionaries, with the provided configuration taking precedence + # config = { **default, **provided } is shallow and merged only top level entries. We want + # to be able to replace individual profiles per architecture. + def merge_config(default: dict, provided: dict) -> dict: + + ret_dict = {} + + # Take these top level entries from the provided configuration if available + ret_dict["TARGET_ARCH"] = provided.get("TARGET_ARCH", default["TARGET_ARCH"]) + ret_dict["IGNORE_LIST"] = provided.get("IGNORE_LIST", default["IGNORE_LIST"]) + + # Take all configuration profiles for each architecture, from the default but allow + # for overrides per profile (DEFAULT, SEC, DEX_DRIVER, etc.) + ret_dict["IMAGE_FILE_MACHINE_AMD64"] = default["IMAGE_FILE_MACHINE_AMD64"] + ret_dict["IMAGE_FILE_MACHINE_ARM64"] = default["IMAGE_FILE_MACHINE_ARM64"] + ret_dict["IMAGE_FILE_MACHINE_I386"] = default["IMAGE_FILE_MACHINE_I386"] + ret_dict["IMAGE_FILE_MACHINE_ARM"] = default["IMAGE_FILE_MACHINE_ARM"] + + # Update the default configuration with the provided configuration + ret_dict["IMAGE_FILE_MACHINE_AMD64"].update( + provided.get("IMAGE_FILE_MACHINE_AMD64", provided.get("X64", {})) + ) + + ret_dict["IMAGE_FILE_MACHINE_ARM64"].update( + provided.get("IMAGE_FILE_MACHINE_ARM64", provided.get("AARCH64", {})) + ) + + ret_dict["IMAGE_FILE_MACHINE_I386"].update( + provided.get("IMAGE_FILE_MACHINE_I386", provided.get("IA32", {})) + ) + + ret_dict["IMAGE_FILE_MACHINE_ARM"].update( + provided.get("IMAGE_FILE_MACHINE_ARM", provided.get("ARM", {})) + ) + + return ret_dict diff --git a/.pytool/Plugin/ImageValidation/ReadMe.md b/.pytool/Plugin/ImageValidation/ReadMe.md index 1799382be0..ecd50dfd35 100644 --- a/.pytool/Plugin/ImageValidation/ReadMe.md +++ b/.pytool/Plugin/ImageValidation/ReadMe.md @@ -7,13 +7,64 @@ file path is provided via the command line as `PE_VALIDATION_PATH=` or can be configured in the the PlatformBuild.py within the `SetPlatformEnv()` method using `self.env.SetValue("PE_VALIDATION_PATH", , "Platform Hardcoded")`. A -profile is equivalent to the file types defined in the platform's fdf. All -profiles must be defined, forcing the developer to acklowedge each, however +profile is equivalent to the binary's MODULE_TYPE as defined in it's inf. All +profiles must be defined, forcing the developer to acknowledge each, however requirements for each profile do not need to be specified... If one or more requirement does not exist, the "DEFAULT" profile requirements will be used. The developer can have default requirements via the "DEFAULT" profile then override those requirements in other profiles. An example of a full config -file can be seen at the bootom of the readme. +file can be seen at the bottom of the readme. + +A default set of requirements are provided by the tool that apply to all +X64 and AARCH64 binaries. These requirements are: + +1. IMAGE_BASE = 0x0 +2. 4k Section Alignment (X64, AARCH64), 64k Section Alignment (AARCH64-DXE_RUNTIME_DRIVER) +3. No Section can be both Write and Execute. + +Image Base is verified to be 0x0 for EFIs as some PE loaders (including edk2's) +will attempt to load the image at that address, which is generally not needed in a +UEFI environment. Section alignment is verified at 4k as it is required for DXE +memory protections, exclduing AARCH64 DXE_RUNTIME_DRIVER binaries, which are +required to be 64K per the UEFI specifcation. Sections are verified **not** to be +both Write and Execute as it is required by DXE [Enhanced Memory Protections](https://microsoft.github.io/mu/WhatAndWhy/enhancedmemoryprotection/). + +These requirements can be expanded to IA32, ARM, etc, or overwritten for X64 +and AARCH64 as defined in the configuration file provided via PE_VALIDATION_PATH. +Individual profiles for individual architectures can be overwritten in this file. + +Configuration supports poth `json` and `yaml` formats. Here is a small example for setting +an ignore list, and overriding configuration for a specific module type. Read below for all +customization options. + +```json +{ + "IGNORE_LIST": ["Driver.efi"], + "X64": { + "DXE_DRIVER": { + ... + } + } +} +``` + +```yaml +IGNORE_LIST: + - Driver.efi +X64: + DXE_DRIVER: + ... + +``` + +**See Example below for customizing Image Validation** + +## Ignoring Files + +If your platform deems a particular binary does not, and cannot meet the +requirements set by the Image Validation plugin, or the platform's custom +config, it can be ignored by adding a `IGNORE_LIST = [...]` section to the +configuration file provided via PE_VALIDATION_PATH. ## Common Errors @@ -23,7 +74,7 @@ PROFILE_NAME is not specified in the configuration file, but is defined in the platform's fdf. The profile needs to be added to the configuration file, even if the requirements are the same as the DEFAULT requirements. This was a design choice to ensure the platform is not -accidently passing due to falling back to the DEFAULT profile if a profile +accidentally passing due to falling back to the DEFAULT profile if a profile is missing. ### Test specific failures @@ -40,7 +91,9 @@ write-able and execute-able. Sections can only be one or the other characteristics label for the Write Mask (0x80000000) and Execute Mask (0x20000000). -- JSON File Requirements: ```"DATA_CODE_SEPARATION": ``` +- Default Requirement: `DATA_CODE_SEPARATION: True` for X64 and AARCH64 + +- Config File Requirements: ```"DATA_CODE_SEPARATION": ``` - Output: - @Success: Only one (or neither) of the two masks (Write, Execute) are present @@ -54,7 +107,9 @@ is either Write-able or Read-able, but not both. - Description: Checks the section alignment value found in the optional header. This value must meet the requirements specified in the config file. -- JSON File Requirements: An array of dictionaries that contain a Comparison +- Default Requirement: 4K for X64 and AARCH64. 64k For AARCH64 DXE_RUNTIME_DRIVER + +- Config File Requirements: An array of dictionaries that contain a Comparison and a value for the particular MachineType and FV file type. See the below example. Can optionally describe an Alignment logic separator when doing multiple comparisons. @@ -84,55 +139,14 @@ multiple comparisons. - Possible Solution: Update the section alignment of the binary to match the requirements specified in the config file. -### Target Subsystem Type Verification - -- Description: Checks the subsystem value by accessing the optional header, -then subsystem value. This value must match the subsystem described in the -config file. +### Target Base Address Validation -- JSON File Requirements: An updated list of allowed subsystems, using the -offical name. See the below example. - -```json -"ALLOWED_SUBSYSTEMS" : [ - "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER", - "IMAGE_SUBSYSTEM_EFI_ROM" -] -``` - -- Output: - - @Success: Subsystem type found in the optional header matches the - subsystem type described in the config file - - @Warn : Subsystem type is not found in the optional header - - @Fail : Subsystem type found in the optional header does not match - the subsystem type described in the config file - -- Possible Solution: Verify which of the two subsystem type's is incorrect. If -it is the subsystem type found in the config file, update the config file. If -it is the subsystem type found in the binary, update the machine type in the -source code and re-compile. +- Description: Checks the base address value found in the optional header. +This value must meet the requirements specified in the config file. -### Example +- Default Requirement: `IMAGE_BASE: 0` -```json -"IMAGE_FILE_MACHINE_ARM64" : { - "BASE" : { - "ALIGNMENT" : [ - { - "COMPARISON" : ">=", - "VALUE" : 4096 - }, - { - "COMPARISON" : "!=", - "VALUE" : 65536 - } - ] - }, - "SEC" : { - "ALIGNMENT" : [] - }, -} -``` +- Config File Requirements: ```IMAGE_BASE: ``` ### Writing your own tests @@ -154,7 +168,7 @@ The parser is the parsed pe file that you are testing. Documentation on how to use the parser can be found by looking up the documentation for the pefile module. The config_data provided to the test will is the filtered data from the config file based upon the compilation target and profile. As an example, -looking at the above json file, if a pe that is being validated is of type +looking at the configuration file, if a pe that is being validated is of type IMAGE_FILE_MACHINE_ARM64 and profile BASE, the config_data provided will be: ```json @@ -201,15 +215,8 @@ no profile parameter is provided, "DEFAULT" is used. The current allowed settings are as follows: ### Top Level Settings - -#### TARGET_ARCH -This defines a dictionary between the build name using by stuart and the actual -Image File Machine Constant name found at . - -```json -TARGET_ARCH : {"" : ""} -``` +**WARNING: The Configuration file must start with brackets, i.e. `{ }`** #### IGNORE_LIST @@ -235,8 +242,8 @@ This will be any number of supported Image File Machine Constants that are suppo This will be any number of supported profiles for the particular Image File Machine Constant. This will not be a list (using [ ]), rather a comma separated list of all machine constants. ```json -"Profile1" : {""}, -"Profile2" : {""} +"DXE_CORE" : {""}, +"UEFI_APPLICATION" : {""} ``` ### Profile Level Settings @@ -282,16 +289,18 @@ This setting is only used if the alignment requirements specify multiple require "ALIGNMENT_LOGIC_SEP" : "" ``` +#### IMAGE_BASE + +This setting is used to specify what the base address of an image should be + +```json +"IMAGE_BASE": +``` + ### Full Configuration File Example ```json { - "TARGET_ARCH" : { - "X64" : "IMAGE_FILE_MACHINE_AMD64", - "IA32" : "IMAGE_FILE_MACHINE_I386", - "AARCH64" : "IMAGE_FILE_MACHINE_ARM64", - "ARM" : "IMAGE_FILE_MACHINE_ARM" - }, "IGNORE_LIST" : ["Shell.efi"], "IMAGE_FILE_MACHINE_AMD64" : { "DEFAULT" : { diff --git a/.pytool/Plugin/ImageValidation/image_validation.cfg b/.pytool/Plugin/ImageValidation/image_validation.cfg new file mode 100644 index 0000000000..da453f21cc --- /dev/null +++ b/.pytool/Plugin/ImageValidation/image_validation.cfg @@ -0,0 +1,70 @@ +TARGET_ARCH: + X64: IMAGE_FILE_MACHINE_AMD64 + IA32: IMAGE_FILE_MACHINE_I386 + AARCH64: IMAGE_FILE_MACHINE_ARM64 + ARM: IMAGE_FILE_MACHINE_ARM + +IGNORE_LIST: [] + +IMAGE_FILE_MACHINE_AMD64: + DEFAULT: + IMAGE_BASE: 0 + DATA_CODE_SEPARATION: true + ALIGNMENT: + - COMPARISON: "==" + VALUE: 0x1000 + APPLICATION: {} + DXE_CORE: {} + DXE_DRIVER: {} + DXE_RUNTIME_DRIVER: {} + DXE_SMM_DRIVER: {} + MM_CORE_STANDALONE: {} + MM_STANDALONE: {} + PEI_CORE: + ALIGNMENT: [] + PEIM: + ALIGNMENT: [] + SEC: + ALIGNMENT: [] + UEFI_APPLICATION: {} + UEFI_DRIVER: {} + USER_DEFINED: {} + +IMAGE_FILE_MACHINE_ARM64: + DEFAULT: + DATA_CODE_SEPARATION: true + ALIGNMENT: + - COMPARISON: == + VALUE: 0x1000 + APPLICATION: {} + DXE_CORE: {} + DXE_DRIVER: {} + DXE_RUNTIME_DRIVER: + ALIGNMENT: + - COMPARISON: == + VALUE: 0x10000 + DXE_SMM_DRIVER: {} + MM_CORE_STANDALONE: {} + MM_STANDALONE: {} + PEI_CORE: + ALIGNMENT: [] + PEIM: + ALIGNMENT: [] + SEC: + ALIGNMENT: [] + UEFI_APPLICATION: {} + UEFI_DRIVER: {} + USER_DEFINED: {} + +IMAGE_FILE_MACHINE_I386: + DEFAULT: + DATA_CODE_SEPARATION: false + PEIM: {} + PEI_CORE: {} + SEC: {} + USER_DEFINED: {} + +IMAGE_FILE_MACHINE_ARM: + DEFAULT: + DATA_CODE_SEPARATION: false + USER_DEFINED: {}