blob: 8570bffc485e27d6745b653b7ae48f4fdf802d89 [file] [log] [blame]
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
""" This module provides convenience routines to access Flash ROM (EEPROM)
saft_flashrom_util is based on utility 'flashrom'.
Original tool syntax:
(read ) flashrom -r <file>
(write) flashrom -l <layout_fn> [-i <image_name> ...] -w <file>
The layout_fn is in format of
address_begin:address_end image_name
which defines a region between (address_begin, address_end) and can
be accessed by the name image_name.
Currently the tool supports multiple partial write but not partial read.
In the saft_flashrom_util, we provide read and partial write abilities.
For more information, see help(saft_flashrom_util.flashrom_util).
"""
import re
class TestError(Exception):
"""Represents an internal error, such as invalid arguments."""
pass
class LayoutScraper(object):
"""Object of this class is used to retrieve layout from a BIOS file."""
DEFAULT_CHROMEOS_FMAP_CONVERSION = {
"BOOT_STUB": "FV_BSTUB",
"RO_FRID": "RO_FRID",
"GBB": "FV_GBB",
"RECOVERY": "FVDEV",
"VBLOCK_A": "VBOOTA",
"VBLOCK_B": "VBOOTB",
"FW_MAIN_A": "FVMAIN",
"FW_MAIN_B": "FVMAINB",
"RW_FWID_A": "RW_FWID_A",
"RW_FWID_B": "RW_FWID_B",
# Intel CSME FW Update sections
"ME_RW_A": "ME_RW_A",
"ME_RW_B": "ME_RW_B",
# Memory Training data cache for recovery boots
# Added on Nov 09, 2016
"RECOVERY_MRC_CACHE": "RECOVERY_MRC_CACHE",
# New sections in Depthcharge.
"EC_MAIN_A": "ECMAINA",
"EC_MAIN_B": "ECMAINB",
# EC firmware layout
"EC_RW": "EC_RW",
"EC_RW_B": "EC_RW_B",
"RW_FWID": "RW_FWID",
"RW_LEGACY": "RW_LEGACY",
}
def __init__(self, os_if):
self.image = None
self.os_if = os_if
def check_layout(self, layout, file_size):
"""Verify the layout to be consistent.
The layout is consistent if there is no overlapping sections and the
section boundaries do not exceed the file size.
Inputs:
layout: a dictionary keyed by a string (the section name) with
values being two integers tuples, the first and the last
bites' offset in the file.
file_size: and integer, the size of the file the layout describes
the sections in.
Raises:
TestError in case the layout is not consistent.
"""
# Generate a list of section range tuples.
ost = sorted([layout[section] for section in layout])
base = -1
for section_base, section_end in ost:
if section_base <= base or section_end + 1 < section_base:
# Overlapped section is possible, like the fwid which is
# inside the main fw section.
self.os_if.log('overlapped section at 0x%x..0x%x' %
(section_base, section_end))
base = section_end
if base > file_size:
raise TestError('Section end 0x%x exceeds file size %x' %
(base, file_size))
def get_layout(self, file_name):
"""Generate layout for a firmware file.
Internally, this uses the "dump_fmap" command, and converts
the output into a dictionary mapping region names to 2-tuples
of the start and last addresses.
Then verify the generated layout's consistency and return it to the
caller.
"""
command = 'dump_fmap -p %s' % file_name
layout_data = {} # keyed by the section name, elements - tuples of
# (<section start addr>, <section end addr>)
for line in self.os_if.run_shell_command_get_output(command):
region_name, offset, size = line.split()
try:
name = self.DEFAULT_CHROMEOS_FMAP_CONVERSION[region_name]
except KeyError:
continue # This line does not contain an area of interest.
if name in layout_data:
raise TestError('%s duplicated in the layout' % name)
offset = int(offset)
size = int(size)
layout_data[name] = (offset, offset + size - 1)
self.check_layout(layout_data, self.os_if.get_file_size(file_name))
return layout_data
# flashrom utility wrapper
class flashrom_util(object):
""" a wrapper for "flashrom" utility.
You can read, write, or query flash ROM size with this utility.
Although you can do "partial-write", the tools always takes a
full ROM image as input parameter.
NOTE before accessing flash ROM, you may need to first "select"
your target - usually BIOS or EC. That part is not handled by
this utility. Please find other external script to do it.
To perform a read, you need to:
1. Prepare a flashrom_util object
ex: flashrom = flashrom_util.flashrom_util()
2. Perform read operation
ex: image = flashrom.read_whole()
When the contents of the flashrom is read off the target, it's map
gets created automatically (read from the flashrom image using
'dump_fmap'). If the user wants this object to operate on some other
file, they could either have the map for the file created explicitly by
invoking flashrom.set_firmware_layout(filename), or supply their own map
(which is a dictionary where keys are section names, and values are
tuples of integers, base address of the section and the last address
of the section).
By default this object operates on the map retrieved from the image and
stored locally, this map can be overwritten by an explicitly passed user
map.
To perform a (partial) write:
1. Prepare a buffer storing an image to be written into the flashrom.
2. Have the map generated automatically or prepare your own, for instance:
ex: layout_map_all = { 'all': (0, rom_size - 1) }
ex: layout_map = { 'ro': (0, 0xFFF), 'rw': (0x1000, rom_size-1) }
4. Perform write operation
ex using default map:
flashrom.write_partial(new_image, (<section_name>, ...))
ex using explicitly provided map:
flashrom.write_partial(new_image, layout_map_all, ('all',))
"""
def __init__(self, os_if, keep_temp_files=False, target_is_ec=False):
""" constructor of flashrom_util. help(flashrom_util) for more info
@param os_if: an object providing interface to OS services
@param keep_temp_files: if true, preserve temp files after operations
@param target_is_ec: if false, target is BIOS/AP
@type os_if: client.cros.faft.utils.os_interface.OSInterface
@type keep_temp_files: bool
@type target_is_ec: bool
"""
self.os_if = os_if
self.keep_temp_files = keep_temp_files
self.firmware_layout = {}
self._target_command = ''
if target_is_ec:
self._enable_ec_access()
else:
self._enable_bios_access()
def _enable_bios_access(self):
if self.os_if.test_mode or self.os_if.target_hosted():
self._target_command = '-p host'
def _enable_ec_access(self):
if self.os_if.test_mode or self.os_if.target_hosted():
self._target_command = '-p ec'
def _get_temp_filename(self, prefix):
"""Returns name of a temporary file in /tmp."""
return self.os_if.create_temp_file(prefix)
def _remove_temp_file(self, filename):
"""Removes a temp file if self.keep_temp_files is false."""
if self.keep_temp_files:
return
if self.os_if.path_exists(filename):
self.os_if.remove_file(filename)
def _create_layout_file(self, layout_map):
"""Creates a layout file based on layout_map.
Returns the file name containing layout information.
"""
layout_text = [
'0x%08lX:0x%08lX %s' % (v[0], v[1], k)
for k, v in layout_map.items()
]
layout_text.sort() # XXX unstable if range exceeds 2^32
tmpfn = self._get_temp_filename('lay_')
self.os_if.write_file(tmpfn, '\n'.join(layout_text) + '\n')
return tmpfn
def check_target(self):
"""Check if flashrom programmer is working, by specifying no commands.
The command executed is just 'flashrom -p <target>'.
@return: True if flashrom completed successfully
@raise autotest_lib.client.common_lib.error.CmdError: if flashrom failed
"""
cmd = 'flashrom %s' % self._target_command
self.os_if.run_shell_command(cmd)
return True
def get_section(self, base_image, section_name):
"""
Retrieves a section of data based on section_name in layout_map.
Raises error if unknown section or invalid layout_map.
"""
if section_name not in self.firmware_layout:
return ''
pos = self.firmware_layout[section_name]
if pos[0] >= pos[1] or pos[1] >= len(base_image):
raise TestError(
'INTERNAL ERROR: invalid layout map: %s.' % section_name)
blob = base_image[pos[0]:pos[1] + 1]
# Trim down the main firmware body to its actual size since the
# signing utility uses the size of the input file as the size of
# the data to sign. Make it the same way as firmware creation.
if section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB'):
align = 4
pad = blob[-1]
blob = blob.rstrip(pad)
blob = blob + ((align - 1) - (len(blob) - 1) % align) * pad
return blob
def put_section(self, base_image, section_name, data):
"""
Updates a section of data based on section_name in firmware_layout.
Raises error if unknown section.
Returns the full updated image data.
"""
pos = self.firmware_layout[section_name]
if pos[0] >= pos[1] or pos[1] >= len(base_image):
raise TestError('INTERNAL ERROR: invalid layout map.')
if len(data) != pos[1] - pos[0] + 1:
# Pad the main firmware body since we trimed it down before.
if (len(data) < pos[1] - pos[0] + 1
and section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA',
'ECMAINB', 'RW_FWID')):
pad = base_image[pos[1]]
data = data + pad * (pos[1] - pos[0] + 1 - len(data))
else:
raise TestError('INTERNAL ERROR: unmatched data size.')
return base_image[0:pos[0]] + data + base_image[pos[1] + 1:]
def get_size(self):
""" Gets size of current flash ROM """
# TODO(hungte) Newer version of tool (flashrom) may support --get-size
# command which is faster in future. Right now we use back-compatible
# method: read whole and then get length.
image = self.read_whole()
return len(image)
def set_firmware_layout(self, file_name):
"""get layout read from the BIOS """
scraper = LayoutScraper(self.os_if)
self.firmware_layout = scraper.get_layout(file_name)
def enable_write_protect(self):
"""Enable the write protection of the flash chip."""
# For MTD devices, this will fail: need both --wp-range and --wp-enable.
# See: https://crrev.com/c/275381
cmd = 'flashrom %s --verbose --wp-enable' % self._target_command
self.os_if.run_shell_command(cmd, modifies_device=True)
def disable_write_protect(self):
"""Disable the write protection of the flash chip."""
cmd = 'flashrom %s --verbose --wp-disable' % self._target_command
self.os_if.run_shell_command(cmd, modifies_device=True)
def set_write_protect_region(self, image_file, region, enabled=None):
"""
Set write protection region, using specified image's layout.
The name should match those seen in `futility dump_fmap <image>`, and
is not checked against self.firmware_layout, due to different naming.
@param image_file: path of the image file to read regions from
@param region: Region to set (usually WP_RO)
@param enabled: if True, run --wp-enable; if False, run --wp-disable.
"""
cmd = 'flashrom %s --verbose --image %s:%s --wp-region %s' % (
self._target_command, region, image_file, region)
if enabled is not None:
cmd += ' '
cmd += '--wp-enable' if enabled else '--wp-disable'
self.os_if.run_shell_command(cmd, modifies_device=True)
def set_write_protect_range(self, start, length, enabled=None):
"""
Set write protection range by offset, using current image's layout.
@param start: offset (bytes) from start of flash to start of range
@param length: offset (bytes) from start of range to end of range
@param enabled: If True, run --wp-enable; if False, run --wp-disable.
If None (default), don't specify either one.
"""
cmd = 'flashrom %s --verbose --wp-range %s %s' % (
self._target_command, start, length)
if enabled is not None:
cmd += ' '
cmd += '--wp-enable' if enabled else '--wp-disable'
self.os_if.run_shell_command(cmd, modifies_device=True)
def get_write_protect_status(self):
"""Get a dict describing the status of the write protection
@return: {'enabled': True/False, 'start': '0x0', 'length': '0x0', ...}
@rtype: dict
"""
# https://crrev.com/8ebbd500b5d8da9f6c1b9b44b645f99352ef62b4/writeprotect.c
status_pattern = re.compile(
r'WP: status: (.*)')
enabled_pattern = re.compile(
r'WP: write protect is (\w+)\.?')
range_pattern = re.compile(
r'WP: write protect range: start=(\w+), len=(\w+)')
range_err_pattern = re.compile(
r'WP: write protect range: (.+)')
output = self.os_if.run_shell_command_get_output(
'flashrom %s --wp-status' % self._target_command)
wp_status = {}
for line in output:
if not line.startswith('WP: '):
continue
found_enabled = re.match(enabled_pattern, line)
if found_enabled:
status_word = found_enabled.group(1)
wp_status['enabled'] = (status_word == 'enabled')
continue
found_range = re.match(range_pattern, line)
if found_range:
(start, length) = found_range.groups()
wp_status['start'] = int(start, 16)
wp_status['length'] = int(length, 16)
continue
found_range_err = re.match(range_err_pattern, line)
if found_range_err:
# WP: write protect range: (cannot resolve the range)
wp_status['error'] = found_range_err.group(1)
continue
found_status = re.match(status_pattern, line)
if found_status:
wp_status['status'] = found_status.group(1)
continue
return wp_status
def dump_flash(self, filename):
"""Read the flash device's data into a file, but don't parse it."""
cmd = 'flashrom %s -r "%s"' % (self._target_command, filename)
self.os_if.log('flashrom_util.dump_flash(): %s' % cmd)
self.os_if.run_shell_command(cmd)
def read_whole(self):
"""
Reads whole flash ROM data.
Returns the data read from flash ROM, or empty string for other error.
"""
tmpfn = self._get_temp_filename('rd_')
cmd = 'flashrom %s -r "%s"' % (self._target_command, tmpfn)
self.os_if.log('flashrom_util.read_whole(): %s' % cmd)
self.os_if.run_shell_command(cmd)
result = self.os_if.read_file(tmpfn)
self.set_firmware_layout(tmpfn)
# clean temporary resources
self._remove_temp_file(tmpfn)
return result
def write_partial(self, base_image, write_list, write_layout_map=None):
"""
Writes data in sections of write_list to flash ROM.
An exception is raised if write operation fails.
"""
if write_layout_map:
layout_map = write_layout_map
else:
layout_map = self.firmware_layout
tmpfn = self._get_temp_filename('wr_')
self.os_if.write_file(tmpfn, base_image)
layout_fn = self._create_layout_file(layout_map)
write_cmd = 'flashrom %s -l "%s" -i %s -w "%s"' % (
self._target_command, layout_fn, ' -i '.join(write_list),
tmpfn)
self.os_if.log('flashrom.write_partial(): %s' % write_cmd)
self.os_if.run_shell_command(write_cmd, modifies_device=True)
# clean temporary resources
self._remove_temp_file(tmpfn)
self._remove_temp_file(layout_fn)
def write_whole(self, base_image):
"""Write the whole base image. """
layout_map = {'all': (0, len(base_image) - 1)}
self.write_partial(base_image, ('all', ), layout_map)
def get_write_cmd(self, image=None):
"""Get the command to write the whole image (no layout handling)
@param image: the filename (empty to use current handler data)
"""
return 'flashrom %s -w "%s"' % (self._target_command, image)