Skip to content

Commit 6310e1e

Browse files
Javagedesmakubacki
andauthored
ImageValidation: Add default configuration (#1100)
## Description 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"] } ``` - [ ] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [x] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [ ] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [x] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## 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 <joey.vagedes@gmail.com> Co-authored-by: Michael Kubacki <michael.kubacki@microsoft.com>
1 parent 1906ad9 commit 6310e1e

3 files changed

Lines changed: 279 additions & 96 deletions

File tree

.pytool/Plugin/ImageValidation/ImageValidation.py

Lines changed: 131 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,65 @@
1010
from pathlib import Path
1111
from pefile import PE
1212
from edk2toolext.environment.plugintypes.uefi_build_plugin import IUefiBuildPlugin
13-
from edk2toolext.image_validation import *
14-
from edk2toollib.uefi.edk2.path_utilities import Edk2Path
15-
from edk2toollib.uefi.edk2.parsers.inf_parser import InfParser
13+
from edk2toolext.image_validation import (
14+
Result, TestManager, TestInterface, TestWriteExecuteFlags,
15+
TestSectionAlignment, MACHINE_TYPE
16+
)
1617
from edk2toollib.uefi.edk2.parsers.fdf_parser import FdfParser
1718
from edk2toollib.uefi.edk2.parsers.dsc_parser import DscParser
18-
from edk2toollib.uefi.edk2.parsers.dsc_parser import *
19-
import json
19+
import yaml
2020
from typing import List
2121
import logging
2222
from datetime import datetime
2323

24+
DEFAULT_CONFIG_FILE_PATH = Path(__file__).parent.resolve() / "image_validation.cfg"
25+
26+
class TestImageBase(TestInterface):
27+
"""Image base verification test.
28+
29+
Checks the image base of the binary by accessing the optional
30+
header, then the image base. This value must be the same value
31+
as specified in the config file.
32+
33+
Output:
34+
@Success: Image base matches the expected value
35+
@Skip: Image base requirement not set in the config file
36+
@Warn: Image Alignment value is not found in the Optional Header
37+
@Fail: Image base does not match the expected value
38+
"""
39+
def name(self) -> str:
40+
"""Returns the name of the test."""
41+
return 'Image Base verification'
42+
43+
def execute(self, pe: PE, config_data: dict) -> Result:
44+
"""Executes the test on the pefile.
45+
46+
Arguments:
47+
pe (PE): a parsed PE/COFF image file
48+
config_data (dict): the configuration data for the test
49+
50+
Returns:
51+
(Result): SKIP, WARN, FAIL, PASS
52+
"""
53+
target_requirements = config_data["TARGET_REQUIREMENTS"]
54+
55+
required_base = target_requirements.get("IMAGE_BASE")
56+
if required_base is None or required_base == -1:
57+
return Result.SKIP
58+
59+
try:
60+
image_base = pe.OPTIONAL_HEADER.ImageBase
61+
except Exception:
62+
logging.warning("Image Base not found in Optional Header")
63+
return Result.WARN
64+
65+
if image_base != required_base:
66+
logging.error(
67+
f'[{Result.FAIL}]: Image Base address Expected: {hex(required_base)}, Found: {hex(image_base)}'
68+
)
69+
return Result.FAIL
70+
return Result.PASS
71+
2472

2573
class ImageValidation(IUefiBuildPlugin):
2674
def __init__(self):
@@ -29,7 +77,7 @@ def __init__(self):
2977
# Default tests provided by edk2toolext.image_validation
3078
self.test_manager.add_test(TestWriteExecuteFlags())
3179
self.test_manager.add_test(TestSectionAlignment())
32-
self.test_manager.add_test(TestSubsystemValue())
80+
self.test_manager.add_test(TestImageBase())
3381
# Add additional Tests here
3482

3583
def do_post_build(self, thebuilder):
@@ -46,16 +94,33 @@ def do_post_build(self, thebuilder):
4694
config_path = thebuilder.env.GetValue("PE_VALIDATION_PATH", None)
4795
tool_chain_tag = thebuilder.env.GetValue("TOOL_CHAIN_TAG")
4896
if config_path is None:
49-
logging.info(
50-
"PE_VALIDATION_PATH not set, PE Image Validation Skipped")
51-
return 0 # Path not set, Plugin skipped
52-
53-
if not os.path.isfile(config_path):
97+
logging.info("PE_VALIDATION_PATH not set, Using default configuration")
98+
logging.info("Review ImageValidation/Readme.md for configuration options.")
99+
elif not os.path.isfile(config_path):
54100
logging.error("Invalid PE_VALIDATION_PATH. File not Found")
55101
return 1
56102

57-
with open(config_path) as jsonfile:
58-
config_data = json.load(jsonfile)
103+
# Use the default configuration. If a configuration file is provided, merge the two
104+
# At the top level entries, with the provided configuration taking precedence.
105+
if not DEFAULT_CONFIG_FILE_PATH.is_file():
106+
logging.error("Default configuration file not found.")
107+
return 1
108+
try:
109+
with open(DEFAULT_CONFIG_FILE_PATH) as f:
110+
config_data = yaml.safe_load(f)
111+
except Exception as e:
112+
logging.error(f"Error parsing {DEFAULT_CONFIG_FILE_PATH}: [{e}]")
113+
return 1
114+
115+
try:
116+
if config_path:
117+
with open(config_path) as f:
118+
config_data = ImageValidation.merge_config(
119+
config_data, yaml.safe_load(f))
120+
121+
except Exception as e:
122+
logging.error(f"Error parsing {config_path}: [{e}]")
123+
return 1
59124

60125
self.test_manager.config_data = config_data
61126
self.config_data = config_data
@@ -100,15 +165,16 @@ def do_post_build(self, thebuilder):
100165
efi_path = self._resolve_vars(thebuilder, efi_path)
101166
efi_path = edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
102167
efi_path)
103-
if efi_path == None:
168+
if efi_path is None:
104169
logging.warning(
105170
"Unable to parse the path to the pre-compiled efi")
106171
continue
107172
if os.path.basename(efi_path) in self.ignore_list:
108173
continue
109-
logging.info(
174+
logging.debug(
110175
f'Performing Image Verification ... {os.path.basename(efi_path)}')
111176
if self._validate_image(efi_path, fv_file["type"]) == Result.FAIL:
177+
logging.error(f'{os.path.basename(efi_path)} Failed Image Validation.')
112178
result = Result.FAIL
113179
count += 1
114180
# End Pre-Compiled Image Verification
@@ -125,30 +191,31 @@ def do_post_build(self, thebuilder):
125191

126192
# Perform Image Verification on any output efi's
127193
# Grab profile from makefile
128-
if efi_path.__contains__("OUTPUT"):
194+
if "OUTPUT" in efi_path:
129195
try:
130-
if tool_chain_tag.__contains__("VS"):
196+
if "VS" in tool_chain_tag:
131197
profile = self._get_profile_from_makefile(
132198
f'{Path(efi_path).parent.parent}/Makefile')
133199

134-
elif tool_chain_tag.__contains__("GCC"):
200+
elif "GCC" in tool_chain_tag:
135201
profile = self._get_profile_from_makefile(
136202
f'{Path(efi_path).parent.parent}/GNUmakefile')
137203

138-
elif tool_chain_tag.__contains__("CLANG"):
204+
elif "CLANG" in tool_chain_tag:
139205
profile = self._get_profile_from_makefile(
140206
f'{Path(efi_path).parent.parent}/GNUmakefile')
141207
else:
142208
logging.warning("Unexpected TOOL_CHAIN_TAG... Cannot parse makefile. Using DEFAULT profile.")
143209
profile = "DEFAULT"
144-
except:
210+
except Exception:
145211
logging.warning(f'Failed to parse makefile at [{Path(efi_path).parent.parent}/GNUmakefile]')
146-
logging.warning(f'Using DEFAULT profile')
212+
logging.warning('Using DEFAULT profile')
147213
profile = "DEFAULT"
148214

149-
logging.info(
215+
logging.debug(
150216
f'Performing Image Verification ... {os.path.basename(efi_path)}')
151217
if self._validate_image(efi_path, profile) == Result.FAIL:
218+
logging.error(f'{os.path.basename(efi_path)} Failed Image Validation.')
152219
result = Result.FAIL
153220
count += 1
154221
# End Built Time Compiled Image Verification
@@ -186,7 +253,7 @@ def _validate_image(self, efi_path, profile="DEFAULT"):
186253
def _get_profile_from_makefile(self, makefile):
187254
with open(makefile) as file:
188255
for line in file.readlines():
189-
if line.__contains__('MODULE_TYPE'):
256+
if "MODULE_TYPE" in line:
190257
line = line.split('=')
191258
module_type = line[1]
192259
module_type = module_type.strip()
@@ -198,8 +265,8 @@ def _get_profile_from_makefile(self, makefile):
198265
# Fallback architectures can be added here
199266
def _try_convert_full_arch(self, arch):
200267
full_arch = self.arch_dict.get(arch)
201-
if full_arch == None:
202-
if arch.__contains__("ARM"):
268+
if full_arch is None:
269+
if "ARM" in arch:
203270
full_arch = "IMAGE_FILE_MACHINE_ARM"
204271
# Add other Arches
205272
return full_arch
@@ -212,8 +279,8 @@ def _resolve_vars(self, thebuilder, s):
212279
for match in var_pattern.findall(s):
213280
var_name = match[2:-1]
214281
env_var = env.GetValue(var_name) if env.GetValue(
215-
var_name) != None else env.GetBuildValue(var_name)
216-
if env_var == None:
282+
var_name) is not None else env.GetBuildValue(var_name)
283+
if env_var is None:
217284
pass
218285
rs = rs.replace(match, env_var)
219286
return rs
@@ -272,3 +339,40 @@ def _walk_directory_for_extension(self, extensionlist: List[str], directory: os.
272339
returnlist.append(os.path.join(Root, File))
273340

274341
return returnlist
342+
343+
# Merged two configuration dictionaries, with the provided configuration taking precedence
344+
# config = { **default, **provided } is shallow and merged only top level entries. We want
345+
# to be able to replace individual profiles per architecture.
346+
def merge_config(default: dict, provided: dict) -> dict:
347+
348+
ret_dict = {}
349+
350+
# Take these top level entries from the provided configuration if available
351+
ret_dict["TARGET_ARCH"] = provided.get("TARGET_ARCH", default["TARGET_ARCH"])
352+
ret_dict["IGNORE_LIST"] = provided.get("IGNORE_LIST", default["IGNORE_LIST"])
353+
354+
# Take all configuration profiles for each architecture, from the default but allow
355+
# for overrides per profile (DEFAULT, SEC, DEX_DRIVER, etc.)
356+
ret_dict["IMAGE_FILE_MACHINE_AMD64"] = default["IMAGE_FILE_MACHINE_AMD64"]
357+
ret_dict["IMAGE_FILE_MACHINE_ARM64"] = default["IMAGE_FILE_MACHINE_ARM64"]
358+
ret_dict["IMAGE_FILE_MACHINE_I386"] = default["IMAGE_FILE_MACHINE_I386"]
359+
ret_dict["IMAGE_FILE_MACHINE_ARM"] = default["IMAGE_FILE_MACHINE_ARM"]
360+
361+
# Update the default configuration with the provided configuration
362+
ret_dict["IMAGE_FILE_MACHINE_AMD64"].update(
363+
provided.get("IMAGE_FILE_MACHINE_AMD64", provided.get("X64", {}))
364+
)
365+
366+
ret_dict["IMAGE_FILE_MACHINE_ARM64"].update(
367+
provided.get("IMAGE_FILE_MACHINE_ARM64", provided.get("AARCH64", {}))
368+
)
369+
370+
ret_dict["IMAGE_FILE_MACHINE_I386"].update(
371+
provided.get("IMAGE_FILE_MACHINE_I386", provided.get("IA32", {}))
372+
)
373+
374+
ret_dict["IMAGE_FILE_MACHINE_ARM"].update(
375+
provided.get("IMAGE_FILE_MACHINE_ARM", provided.get("ARM", {}))
376+
)
377+
378+
return ret_dict

0 commit comments

Comments
 (0)