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