Skip to content

Commit 0a21a0b

Browse files
committed
openness_score: Use ifdtool to parse IFD regions
Signed-off-by: Pawel Langowski <pawel.langowski@3mdeb.com>
1 parent 3824bbb commit 0a21a0b

2 files changed

Lines changed: 126 additions & 10 deletions

File tree

openness_score/coreboot.py

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class DasharoCorebootImage:
3030
region_regexp = re.compile(''.join(region_patterns), re.MULTILINE)
3131
"""Regular expression variable used to extract the flashmap regions"""
3232

33+
ifdtool_pattern = r'^FLREG(?P<id>\d+):\s+(?P<reg_val>0x[0-9a-fA-F]+)\s*?\n\s+Flash Region \d+ \((?P<name>.+?)\): (?P<start>[0-9a-fA-F]+) - (?P<end>[0-9a-fA-F]+)(?: \((?P<status>unused)\))?'
34+
ifdtool_regexp = re.compile(ifdtool_pattern, re.MULTILINE)
35+
3336
# Regions to consider as data, they should not contain any code ever.
3437
# Some of the regions are used only by certain platforms and may not be met
3538
# on Dasharo builds.
@@ -38,9 +41,13 @@ class DasharoCorebootImage:
3841
'CONSOLE', 'RW_FWID_A', 'RW_FWID_B', 'VBLOCK_A', 'RO_VPD',
3942
'VBLOCK_B', 'HSPHY_FW', 'RW_ELOG', 'FMAP', 'RO_FRID',
4043
'RO_FRID_PAD', 'SPD_CACHE', 'FPF_STATUS', 'RO_LIMITS_CFG',
41-
'RW_DDR_TRAINING', 'GBB', 'BOOTORDER']
44+
'RW_DDR_TRAINING', 'GBB', 'BOOTORDER', 'RESERVED', 'BPA',
45+
'ROMHOLE']
4246
"""A list of region names known to contain data"""
4347

48+
IFD_DATA_REGIONS = ['Flash Descriptor', 'Platform Data', 'GbE']
49+
"""A list of IFD regions known to contain data"""
50+
4451
# Regions that are not CBFSes and may contain open-source code
4552
# Their whole size is counted as code.
4653
CODE_REGIONS = ['BOOTBLOCK']
@@ -53,6 +60,9 @@ class DasharoCorebootImage:
5360
'SIGN_CSE']
5461
"""A list of region names known to contain closed-source code"""
5562

63+
IFD_BLOB_REGIONS = ['Intel ME', 'IE', 'PTT', '10GbE_0', '10GbE_1', 'EC']
64+
"""A list of closed-source code IFD regions"""
65+
5666
# Regions to not account for in calculations.
5767
# These are containers aggregating smaller regions.
5868
SKIP_REGIONS = ['RW_MISC', 'UNIFIED_MRC_CACHE', 'RW_SHARED', 'SI_ALL',
@@ -61,12 +71,20 @@ class DasharoCorebootImage:
6171
"""A list of region names known to be containers or aliases of other
6272
regions. These regions are skipped from classification."""
6373

74+
# Regions to not account for in calculations when ifdtool is used.
75+
# These regions will be classified based on their presence in IFD.
76+
IFD_SKIP_REGIONS = ['SI_DESC', 'SI_ME', 'SI_GBE', 'SI_PDR', 'SI_EC',
77+
'SI_DEVICEEXT', 'SI_BIOS2', 'SI_DEVICEEXT2',
78+
'SI_IE', 'SI_10GBE0', 'SI_10GBE1', 'SI_PTT']
79+
"""A list of region names to be skipped when ifdtool is used.
80+
These regions willbe classified by IFD region purpose."""
81+
6482
# Regions to count as empty/unused
6583
EMPTY_REGIONS = ['UNUSED', 'RW_UNUSED', 'SI_DEVICEEXT2']
6684
"""A list of region names known to be empty spaces, e.g. between IFD
6785
regions."""
6886

69-
def __init__(self, image_path, verbose=False):
87+
def __init__(self, image_path, verbose=False, microarch=""):
7088
"""DasharoCorebootImage class init method
7189
7290
Initializes the class fields for storing the firmware image components
@@ -83,14 +101,22 @@ def __init__(self, image_path, verbose=False):
83101
"""
84102
self.image_path = image_path
85103
"""Path to the image represented by DasharoCorebootImage class"""
104+
self.microarch = microarch
105+
"""CPU michroarchitecture supported by the firmware binary to be passed to ifdtool.
106+
For a complete list of supported microarchitectures, use 'ifdtool -h'.
107+
"""
86108
self.image_size = os.path.getsize(image_path)
87109
"""Image size in bytes"""
88110
self.fmap_regions = {}
89111
"""A dictionary holding the coreboot image flashmap regions"""
112+
self.ifdtool_regions = {}
113+
"""A dictionary holding regions found by ifdtool"""
90114
self.cbfs_images = []
91115
"""A list holding the regions with CBFS"""
92116
self.num_regions = 0
93117
"""Total number of flashmap regions"""
118+
self.num_ifdtool_regions = 0
119+
"""Total number of regions found by ifdtool"""
94120
self.num_cbfses = 0
95121
"""Total number of flashmap regions containing CBFSes"""
96122
self.open_code_size = 0
@@ -109,18 +135,31 @@ def __init__(self, image_path, verbose=False):
109135
"""A list holding flashmap regions filled with data"""
110136
self.empty_regions = []
111137
"""A list holding empty flashmap regions"""
138+
self.closed_code_regions_ifdtool = []
139+
"""A list holding ifdtool regions filled with closed-source code"""
140+
self.data_regions_ifdtool = []
141+
"""A list holding ifdtool regions filled with data"""
142+
self.empty_regions_ifdtool = []
143+
"""A list holding empty ifdtool regions"""
112144
# This type of regions will be counted as closed-source at the end of
113145
# metrics calculation. Keep them in separate array to export them into
114146
# CSV later for review.
115147
self.uncategorized_regions = []
116148
"""A list holding flashmap regions that could not be classified.
117149
Counted as closed-source code at the end of calculation process.
118150
"""
119-
151+
self.uncategorized_regions_ifdtool = []
152+
"""A list holding ifdtool regions that could not be classified.
153+
Counted as closed-source code at the end of calculation process.
154+
"""
120155
self.debug = verbose
121156
"""Used to enable verbose debug output from the parsing process"""
157+
self.use_ifdtool = bool(microarch)
158+
"""If `microarch` argument is set, use ifdtool"""
122159

123160
self._parse_cb_fmap_layout()
161+
if self.use_ifdtool:
162+
self._parse_ifdtool_regions(microarch)
124163
self._calculate_metrics()
125164

126165
def __len__(self):
@@ -256,6 +295,55 @@ def _validate_fmap_layout(self):
256295

257296
return hole_size
258297

298+
def _parse_ifdtool_regions(self, microarch):
299+
cmd = ['ifdtool', '-p', microarch, '-d', self.image_path]
300+
output = subprocess.run(cmd, text=True, capture_output=True)
301+
for match in re.finditer(self.ifdtool_regexp, output.stdout):
302+
# Do not add regions marked as unused
303+
if not bool(match.group('status')):
304+
self.ifdtool_regions[self.num_ifdtool_regions] = {
305+
'id': int(match.group('id')),
306+
'reg_val': match.group('reg_val'),
307+
'name': match.group('name'),
308+
'start': f"0x{match.group('start')}",
309+
'end': f"0x{match.group('end')}",
310+
}
311+
start_int = int(self.ifdtool_regions[self.num_ifdtool_regions]['start'], 16)
312+
end_int = int(self.ifdtool_regions[self.num_ifdtool_regions]['end'], 16)
313+
self.ifdtool_regions[self.num_ifdtool_regions]['size'] = end_int - start_int + 1
314+
self.num_ifdtool_regions += 1
315+
if self.debug:
316+
print('IFD regions:')
317+
[print(self.ifdtool_regions[i]) for i in range(self.num_ifdtool_regions)]
318+
319+
def _classify_ifdtool_region(self, region):
320+
if self._is_empty(int(region["start"], 16), int(region["end"],16)):
321+
self.empty_regions_ifdtool.append(region)
322+
return
323+
if region["name"] in self.IFD_BLOB_REGIONS:
324+
self.closed_code_regions_ifdtool.append(region)
325+
elif region["name"] in self.IFD_DATA_REGIONS:
326+
self.data_regions_ifdtool.append(region)
327+
elif region["name"] == "BIOS":
328+
return
329+
else:
330+
self.uncategorized_regions_ifdtool.append(region)
331+
332+
def _is_empty(self, start, end):
333+
"""Checks if a flash region is empty, where empty is defined as filled with 0x00 or 0xFF bytes.
334+
335+
:param: start: Start address of the region
336+
:type start: int
337+
:param end: End address of the region
338+
:type end: int
339+
340+
:rtype: bool
341+
"""
342+
with open(self.image_path, 'rb') as f:
343+
f.seek(start)
344+
region_data = f.read(end - start + 1)
345+
return all(b in (0x00, 0xFF) for b in region_data)
346+
259347
def _classify_region(self, region):
260348
"""Classifies the flashmap regions into basic categories
261349
@@ -296,6 +384,8 @@ def _classify_region(self, region):
296384
# Skip CBFSes because they have separate class and methods to
297385
# calculate metrics
298386
return
387+
elif self.use_ifdtool and region['name'] in self.IFD_SKIP_REGIONS:
388+
return
299389
elif region['name'] in self.SKIP_REGIONS:
300390
return
301391
elif region['name'] in self.CODE_REGIONS:
@@ -363,11 +453,15 @@ def _calculate_metrics(self):
363453
if fmap_hole > 0:
364454
self.closed_code_size += fmap_hole
365455

456+
if self.use_ifdtool:
457+
for i in range(self.num_ifdtool_regions):
458+
self._classify_ifdtool_region(self.ifdtool_regions[i])
459+
366460
self.open_code_size += self._sum_sizes(self.open_code_regions)
367-
self.closed_code_size += self._sum_sizes(self.closed_code_regions)
368-
self.data_size += self._sum_sizes(self.data_regions)
369-
self.empty_size += self._sum_sizes(self.empty_regions)
370-
self.closed_code_size += self._sum_sizes(self.uncategorized_regions)
461+
self.closed_code_size += self._sum_sizes(self.closed_code_regions) + self._sum_sizes(self.closed_code_regions_ifdtool)
462+
self.data_size += self._sum_sizes(self.data_regions) + self._sum_sizes(self.data_regions_ifdtool)
463+
self.empty_size += self._sum_sizes(self.empty_regions) + self._sum_sizes(self.empty_regions_ifdtool)
464+
self.closed_code_size += self._sum_sizes(self.uncategorized_regions) + self._sum_sizes(self.uncategorized_regions_ifdtool)
371465
if len(self.uncategorized_regions) != 0:
372466
print('INFO: Found %d uncategorized regions of total size %d bytes'
373467
% (len(self.uncategorized_regions),
@@ -407,8 +501,9 @@ def _normalize_sizes(self):
407501
# It may happen that the FMAP does not cover whole flash size and the
408502
# first region will start with non-zero offset. Check if first region
409503
# offset is zero, if not count all bytes from the start of flash to the
410-
# start of first region as closed source.
411-
if self.fmap_regions[0]['offset'] != 0:
504+
# start of first region as closed source. This is only done if ifdtool
505+
# is not used, because ifdtool will always parse those regions correctly.
506+
if self.fmap_regions[0]['offset'] != 0 and not self.use_ifdtool:
412507
self.closed_code_size += self.fmap_regions[0]['offset']
413508

414509
# Final check if all sizes are summing up to whole image size
@@ -448,6 +543,12 @@ def _export_regions_md(self, file, regions, category):
448543
region['name'], hex(region['offset']),
449544
hex(region['size']), category))
450545

546+
def _export_ifdtool_regions_md(self, file, regions, category):
547+
for region in regions:
548+
file.write('| {} | {} | {} | {} | {} |\n'.format(
549+
region['name'], region['start'], region['end'],
550+
hex(region['size']), category))
551+
451552
def export_markdown(self, file, mkdocs):
452553
"""Opens a file and saves the openness report in markdown format
453554
@@ -509,6 +610,19 @@ def export_markdown(self, file, mkdocs):
509610
self._export_regions_md(md, self.data_regions, 'data')
510611
self._export_regions_md(md, self.empty_regions, 'empty')
511612

613+
if self.use_ifdtool:
614+
if not mkdocs:
615+
md.write('\n## IFD regions\n\n')
616+
else:
617+
md.write('\n### IFD regions\n\n')
618+
619+
md.write('| IFD region | Start | End | Size | Category |\n')
620+
md.write('| -------------- | ----- | --- | ---- | -------- |\n')
621+
self._export_ifdtool_regions_md(md, self.closed_code_regions_ifdtool,
622+
'closed-source')
623+
self._export_ifdtool_regions_md(md, self.data_regions_ifdtool, 'data')
624+
self._export_ifdtool_regions_md(md, self.empty_regions_ifdtool, 'empty')
625+
512626
for cbfs in self.cbfs_images:
513627
md.write('\n')
514628
cbfs.export_markdown(md, mkdocs)

openness_score/openness_score.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ def OpennessScore():
136136
parser.add_argument('-h', '--help', action='help', help=SUPPRESS)
137137
parser.add_argument('-o', '--output', default='out/', help='\n'.join([
138138
'Specifies the directory where to store the results']))
139+
parser.add_argument('-a', '--microarch', default="", help='\n'.join([
140+
'CPU microarchitecture']))
139141
parser.add_argument('-v', '--verbose', help='\n'.join([
140142
'Print verbose information during the image parsing']),
141143
action='store_true')
@@ -159,7 +161,7 @@ def OpennessScore():
159161
if fw_is_cbfs:
160162
print('\'%s\' detected as Dasharo image' % args.file)
161163
print('\n\n\'%s\' Dasharo image statistics:' % args.file)
162-
DasharoCbImg = DasharoCorebootImage(args.file, args.verbose)
164+
DasharoCbImg = DasharoCorebootImage(args.file, args.verbose, args.microarch)
163165
print(DasharoCbImg)
164166
export_data(args, DasharoCbImg)
165167
elif fw_is_uefi:

0 commit comments

Comments
 (0)