Home | History | Annotate | Download | only in utils
      1 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """ This module provides convenience routines to access Flash ROM (EEPROM)
      6 
      7 saft_flashrom_util is based on utility 'flashrom'.
      8 
      9 Original tool syntax:
     10     (read ) flashrom -r <file>
     11     (write) flashrom -l <layout_fn> [-i <image_name> ...] -w <file>
     12 
     13 The layout_fn is in format of
     14     address_begin:address_end image_name
     15     which defines a region between (address_begin, address_end) and can
     16     be accessed by the name image_name.
     17 
     18 Currently the tool supports multiple partial write but not partial read.
     19 
     20 In the saft_flashrom_util, we provide read and partial write abilities.
     21 For more information, see help(saft_flashrom_util.flashrom_util).
     22 """
     23 
     24 class TestError(Exception):
     25     pass
     26 
     27 
     28 class LayoutScraper(object):
     29     """Object of this class is used to retrieve layout from a BIOS file."""
     30 
     31     # The default conversion table for mosys.
     32     DEFAULT_CHROMEOS_FMAP_CONVERSION = {
     33         "Boot Stub": "FV_BSTUB",
     34         "GBB Area": "FV_GBB",
     35         "Recovery Firmware": "FVDEV",
     36         "RO VPD": "RO_VPD",
     37         "Firmware A Key": "VBOOTA",
     38         "Firmware A Data": "FVMAIN",
     39         "Firmware B Key": "VBOOTB",
     40         "Firmware B Data": "FVMAINB",
     41         "Log Volume": "FV_LOG",
     42         # New layout in Chrome OS Main Processor Firmware Specification,
     43         # used by all newer (>2011) platforms except Mario.
     44         "BOOT_STUB": "FV_BSTUB",
     45         "GBB": "FV_GBB",
     46         "RECOVERY": "FVDEV",
     47         "VBLOCK_A": "VBOOTA",
     48         "VBLOCK_B": "VBOOTB",
     49         "FW_MAIN_A": "FVMAIN",
     50         "FW_MAIN_B": "FVMAINB",
     51         "RW_FWID_A": "RW_FWID_A",
     52         "RW_FWID_B": "RW_FWID_B",
     53         # Memory Training data cache for recovery boots
     54         # Added on Nov 09, 2016
     55         "RECOVERY_MRC_CACHE": "RECOVERY_MRC_CACHE",
     56         # New sections in Depthcharge.
     57         "EC_MAIN_A": "ECMAINA",
     58         "EC_MAIN_B": "ECMAINB",
     59         # EC firmware layout
     60         "EC_RW": "EC_RW",
     61         "EC_RW_B": "EC_RW_B",
     62         "RW_FWID": "RW_FWID",
     63         }
     64 
     65     def __init__(self, os_if):
     66         self.image = None
     67         self.os_if = os_if
     68 
     69     def _get_text_layout(self, file_name):
     70         """Retrieve text layout from a firmware image file.
     71 
     72         This function uses the 'mosys' utility to scan the firmware image and
     73         retrieve the section layout information.
     74 
     75         The layout is reported as a set of lines with multiple
     76         "<name>"="value" pairs, all this output is passed to the caller.
     77         """
     78 
     79         mosys_cmd = 'mosys -k eeprom map %s' % file_name
     80         return self.os_if.run_shell_command_get_output(mosys_cmd)
     81 
     82     def _line_to_dictionary(self, line):
     83         """Convert a text layout line into a dictionary.
     84 
     85         Get a string consisting of single space separated "<name>"="value>"
     86         pairs and convert it into a dictionary where keys are the <name>
     87         fields, and values are the corresponding <value> fields.
     88 
     89         Return the dictionary to the caller.
     90         """
     91 
     92         rv = {}
     93 
     94         items = line.replace('" ', '"^').split('^')
     95         for item in items:
     96             pieces = item.split('=')
     97             if len(pieces) != 2:
     98                 continue
     99             rv[pieces[0]] = pieces[1].strip('"')
    100         return rv
    101 
    102     def check_layout(self, layout, file_size):
    103         """Verify the layout to be consistent.
    104 
    105         The layout is consistent if there is no overlapping sections and the
    106         section boundaries do not exceed the file size.
    107 
    108         Inputs:
    109           layout: a dictionary keyed by a string (the section name) with
    110                   values being two integers tuples, the first and the last
    111                   bites' offset in the file.
    112           file_size: and integer, the size of the file the layout describes
    113                      the sections in.
    114 
    115         Raises:
    116           TestError in case the layout is not consistent.
    117         """
    118 
    119         # Generate a list of section range tuples.
    120         ost = sorted([layout[section] for section in layout])
    121         base = -1
    122         for section_base, section_end in ost:
    123             if section_base <= base or section_end + 1 < section_base:
    124                 # Overlapped section is possible, like the fwid which is
    125                 # inside the main fw section.
    126                 self.os_if.log('overlapped section at 0x%x..0x%x' % (
    127                         section_base, section_end))
    128             base = section_end
    129         if base > file_size:
    130             raise TestError('Section end 0x%x exceeds file size %x' % (
    131                     base, file_size))
    132 
    133     def get_layout(self, file_name):
    134         """Generate layout for a firmware file.
    135 
    136         First retrieve the text layout as reported by 'mosys' and then convert
    137         it into a dictionary, replacing section names reported by mosys into
    138         matching names from DEFAULT_CHROMEOS_FMAP_CONVERSION dictionary above,
    139         using the names as keys in the layout dictionary. The elements of the
    140         layout dictionary are the offsets of the first ans last bytes of the
    141         section in the firmware file.
    142 
    143         Then verify the generated layout's consistency and return it to the
    144         caller.
    145         """
    146 
    147         layout_data = {} # keyed by the section name, elements - tuples of
    148                          # (<section start addr>, <section end addr>)
    149 
    150         for line in self._get_text_layout(file_name):
    151             d = self._line_to_dictionary(line)
    152             try:
    153                 name = self.DEFAULT_CHROMEOS_FMAP_CONVERSION[d['area_name']]
    154             except KeyError:
    155                 continue  # This line does not contain an area of interest.
    156 
    157             if name in layout_data:
    158                 raise TestError('%s duplicated in the layout' % name)
    159 
    160             offset = int(d['area_offset'], 0)
    161             size = int(d['area_size'], 0)
    162             layout_data[name] = (offset, offset + size - 1)
    163 
    164         self.check_layout(layout_data, self.os_if.get_file_size(file_name))
    165         return layout_data
    166 
    167 # flashrom utility wrapper
    168 class flashrom_util(object):
    169     """ a wrapper for "flashrom" utility.
    170 
    171     You can read, write, or query flash ROM size with this utility.
    172     Although you can do "partial-write", the tools always takes a
    173     full ROM image as input parameter.
    174 
    175     NOTE before accessing flash ROM, you may need to first "select"
    176     your target - usually BIOS or EC. That part is not handled by
    177     this utility. Please find other external script to do it.
    178 
    179     To perform a read, you need to:
    180      1. Prepare a flashrom_util object
    181         ex: flashrom = flashrom_util.flashrom_util()
    182      2. Perform read operation
    183         ex: image = flashrom.read_whole()
    184 
    185         When the contents of the flashrom is read off the target, it's map
    186         gets created automatically (read from the flashrom image using
    187         'mosys'). If the user wants this object to operate on some other file,
    188         he could either have the map for the file created explicitly by
    189         invoking flashrom.set_firmware_layout(filename), or supply his own map
    190         (which is a dictionary where keys are section names, and values are
    191         tuples of integers, base address of the section and the last address
    192         of the section).
    193 
    194     By default this object operates on the map retrieved from the image and
    195     stored locally, this map can be overwritten by an explicitly passed user
    196     map.
    197 
    198    To perform a (partial) write:
    199 
    200      1. Prepare a buffer storing an image to be written into the flashrom.
    201      2. Have the map generated automatically or prepare your own, for instance:
    202         ex: layout_map_all = { 'all': (0, rom_size - 1) }
    203         ex: layout_map = { 'ro': (0, 0xFFF), 'rw': (0x1000, rom_size-1) }
    204      4. Perform write operation
    205 
    206         ex using default map:
    207           flashrom.write_partial(new_image, (<section_name>, ...))
    208         ex using explicitly provided map:
    209           flashrom.write_partial(new_image, layout_map_all, ('all',))
    210 
    211     Attributes:
    212         keep_temp_files: boolean flag to control cleaning of temporary files
    213     """
    214 
    215     def __init__(self, os_if, keep_temp_files=False,
    216                  target_is_ec=False):
    217         """ constructor of flashrom_util. help(flashrom_util) for more info """
    218         self.os_if = os_if
    219         self.keep_temp_files = keep_temp_files
    220         self.firmware_layout = {}
    221         self._target_command = ''
    222         if target_is_ec:
    223             self._enable_ec_access()
    224         else:
    225             self._enable_bios_access()
    226 
    227     def _enable_bios_access(self):
    228         if not self.os_if.target_hosted():
    229             return
    230         self._target_command = '-p host'
    231 
    232     def _enable_ec_access(self):
    233         if not self.os_if.target_hosted():
    234             return
    235         self._target_command = '-p ec'
    236 
    237     def _get_temp_filename(self, prefix):
    238         """Returns name of a temporary file in /tmp."""
    239         return self.os_if.create_temp_file(prefix)
    240 
    241     def _remove_temp_file(self, filename):
    242         """Removes a temp file if self.keep_temp_files is false."""
    243         if self.keep_temp_files:
    244             return
    245         if self.os_if.path_exists(filename):
    246             self.os_if.remove_file(filename)
    247 
    248     def _create_layout_file(self, layout_map):
    249         """Creates a layout file based on layout_map.
    250 
    251         Returns the file name containing layout information.
    252         """
    253         layout_text = ['0x%08lX:0x%08lX %s' % (v[0], v[1], k)
    254             for k, v in layout_map.items()]
    255         layout_text.sort()  # XXX unstable if range exceeds 2^32
    256         tmpfn = self._get_temp_filename('lay_')
    257         self.os_if.write_file(tmpfn, '\n'.join(layout_text) + '\n')
    258         return tmpfn
    259 
    260     def get_section(self, base_image, section_name):
    261         """
    262         Retrieves a section of data based on section_name in layout_map.
    263         Raises error if unknown section or invalid layout_map.
    264         """
    265         if section_name not in self.firmware_layout:
    266             return []
    267         pos = self.firmware_layout[section_name]
    268         if pos[0] >= pos[1] or pos[1] >= len(base_image):
    269             raise TestError('INTERNAL ERROR: invalid layout map: %s.' %
    270                             section_name)
    271         blob = base_image[pos[0] : pos[1] + 1]
    272         # Trim down the main firmware body to its actual size since the
    273         # signing utility uses the size of the input file as the size of
    274         # the data to sign. Make it the same way as firmware creation.
    275         if section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB'):
    276             align = 4
    277             pad = blob[-1]
    278             blob = blob.rstrip(pad)
    279             blob = blob + ((align - 1) - (len(blob) - 1) % align) * pad
    280         return blob
    281 
    282     def put_section(self, base_image, section_name, data):
    283         """
    284         Updates a section of data based on section_name in firmware_layout.
    285         Raises error if unknown section.
    286         Returns the full updated image data.
    287         """
    288         pos = self.firmware_layout[section_name]
    289         if pos[0] >= pos[1] or pos[1] >= len(base_image):
    290             raise TestError('INTERNAL ERROR: invalid layout map.')
    291         if len(data) != pos[1] - pos[0] + 1:
    292             # Pad the main firmware body since we trimed it down before.
    293             if (len(data) < pos[1] - pos[0] + 1 and section_name in
    294                     ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB',
    295                      'RW_FWID')):
    296                 pad = base_image[pos[1]]
    297                 data = data + pad * (pos[1] - pos[0] + 1 - len(data))
    298             else:
    299                 raise TestError('INTERNAL ERROR: unmatched data size.')
    300         return base_image[0 : pos[0]] + data + base_image[pos[1] + 1 :]
    301 
    302     def get_size(self):
    303         """ Gets size of current flash ROM """
    304         # TODO(hungte) Newer version of tool (flashrom) may support --get-size
    305         # command which is faster in future. Right now we use back-compatible
    306         # method: read whole and then get length.
    307         image = self.read_whole()
    308         return len(image)
    309 
    310     def set_firmware_layout(self, file_name):
    311         """get layout read from the BIOS """
    312 
    313         scraper = LayoutScraper(self.os_if)
    314         self.firmware_layout = scraper.get_layout(file_name)
    315 
    316     def enable_write_protect(self):
    317         """Enable the write pretection of the flash chip."""
    318         cmd = 'flashrom %s --wp-enable' % self._target_command
    319         self.os_if.run_shell_command(cmd)
    320 
    321     def disable_write_protect(self):
    322         """Disable the write pretection of the flash chip."""
    323         cmd = 'flashrom %s --wp-disable' % self._target_command
    324         self.os_if.run_shell_command(cmd)
    325 
    326     def read_whole(self):
    327         """
    328         Reads whole flash ROM data.
    329         Returns the data read from flash ROM, or empty string for other error.
    330         """
    331         tmpfn = self._get_temp_filename('rd_')
    332         cmd = 'flashrom %s -r "%s"' % (self._target_command, tmpfn)
    333         self.os_if.log('flashrom_util.read_whole(): %s' % cmd)
    334         self.os_if.run_shell_command(cmd)
    335         result = self.os_if.read_file(tmpfn)
    336         self.set_firmware_layout(tmpfn)
    337 
    338         # clean temporary resources
    339         self._remove_temp_file(tmpfn)
    340         return result
    341 
    342     def write_partial(self, base_image, write_list, write_layout_map=None):
    343         """
    344         Writes data in sections of write_list to flash ROM.
    345         An exception is raised if write operation fails.
    346         """
    347 
    348         if write_layout_map:
    349             layout_map = write_layout_map
    350         else:
    351             layout_map = self.firmware_layout
    352 
    353         tmpfn = self._get_temp_filename('wr_')
    354         self.os_if.write_file(tmpfn, base_image)
    355         layout_fn = self._create_layout_file(layout_map)
    356 
    357         cmd = 'flashrom %s -l "%s" -i %s -w "%s"' % (
    358                 self._target_command, layout_fn, ' -i '.join(write_list), tmpfn)
    359         self.os_if.log('flashrom.write_partial(): %s' % cmd)
    360         self.os_if.run_shell_command(cmd)
    361 
    362         # flashrom write will reboot the ec after corruption
    363         # For Android, need to make sure ec is back online
    364         # before continuing, or adb command will cause test failure
    365         if self.os_if.is_android:
    366             self.os_if.wait_for_device(60)
    367 
    368         # clean temporary resources
    369         self._remove_temp_file(tmpfn)
    370         self._remove_temp_file(layout_fn)
    371 
    372     def write_whole(self, base_image):
    373         """Write the whole base image. """
    374         layout_map = { 'all': (0, len(base_image) - 1) }
    375         self.write_partial(base_image, ('all',), layout_map)
    376