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