• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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""" This module provides convenience routines to access Flash ROM (EEPROM)
5
6saft_flashrom_util is based on utility 'flashrom'.
7
8Original tool syntax:
9    (read ) flashrom -r <file>
10    (write) flashrom -l <layout_fn> [-i <image_name> ...] -w <file>
11
12The layout_fn is in format of
13    address_begin:address_end image_name
14    which defines a region between (address_begin, address_end) and can
15    be accessed by the name image_name.
16
17Currently the tool supports multiple partial write but not partial read.
18
19In the saft_flashrom_util, we provide read and partial write abilities.
20For more information, see help(saft_flashrom_util.flashrom_util).
21"""
22import re
23import logging
24
25
26class TestError(Exception):
27    """Represents an internal error, such as invalid arguments."""
28    pass
29
30
31class LayoutScraper(object):
32    """Object of this class is used to retrieve layout from a BIOS file."""
33
34    DEFAULT_CHROMEOS_FMAP_CONVERSION = {
35            "BOOT_STUB": "FV_BSTUB",
36            "RO_FRID": "RO_FRID",
37            "GBB": "FV_GBB",
38            "RECOVERY": "FVDEV",
39            "VBLOCK_A": "VBOOTA",
40            "VBLOCK_B": "VBOOTB",
41            "FW_MAIN_A": "FVMAIN",
42            "FW_MAIN_B": "FVMAINB",
43            "RW_FWID_A": "RW_FWID_A",
44            "RW_FWID_B": "RW_FWID_B",
45            # Intel CSME FW Update sections
46            "ME_RW_A": "ME_RW_A",
47            "ME_RW_B": "ME_RW_B",
48            # Memory Training data cache for recovery boots
49            # Added on Nov 09, 2016
50            "RECOVERY_MRC_CACHE": "RECOVERY_MRC_CACHE",
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            "EC_RW_B": "EC_RW_B",
57            "RW_FWID": "RW_FWID",
58            "RW_LEGACY": "RW_LEGACY",
59    }
60
61    def __init__(self, os_if):
62        self.image = None
63        self.os_if = os_if
64
65    def check_layout(self, layout, file_size):
66        """Verify the layout to be consistent.
67
68        The layout is consistent if there is no overlapping sections and the
69        section boundaries do not exceed the file size.
70
71        Inputs:
72          layout: a dictionary keyed by a string (the section name) with
73                  values being two integers tuples, the first and the last
74                  bites' offset in the file.
75          file_size: and integer, the size of the file the layout describes
76                     the sections in.
77
78        Raises:
79          TestError in case the layout is not consistent.
80        """
81
82        # Generate a list of section range tuples.
83        ost = sorted([layout[section] for section in layout])
84        base = -1
85        for section_base, section_end in ost:
86            if section_base <= base or section_end + 1 < section_base:
87                # Overlapped section is possible, like the fwid which is
88                # inside the main fw section.
89                logging.info('overlapped section at 0x%x..0x%x', section_base,
90                             section_end)
91            base = section_end
92        if base > file_size:
93            raise TestError('Section end 0x%x exceeds file size %x' %
94                            (base, file_size))
95
96    def get_layout(self, file_name):
97        """Generate layout for a firmware file.
98
99        Internally, this uses the "dump_fmap" command, and converts
100        the output into a dictionary mapping region names to 2-tuples
101        of the start and last addresses.
102
103        Then verify the generated layout's consistency and return it to the
104        caller.
105        """
106        command = 'dump_fmap -p %s' % file_name
107        layout_data = {}  # keyed by the section name, elements - tuples of
108        # (<section start addr>, <section end addr>)
109
110        for line in self.os_if.run_shell_command_get_output(command):
111            region_name, offset, size = line.split()
112
113            try:
114                name = self.DEFAULT_CHROMEOS_FMAP_CONVERSION[region_name]
115            except KeyError:
116                continue  # This line does not contain an area of interest.
117
118            if name in layout_data:
119                raise TestError('%s duplicated in the layout' % name)
120
121            offset = int(offset)
122            size = int(size)
123            layout_data[name] = (offset, offset + size - 1)
124
125        self.check_layout(layout_data, self.os_if.get_file_size(file_name))
126        return layout_data
127
128
129# flashrom utility wrapper
130class flashrom_util(object):
131    """ a wrapper for "flashrom" utility.
132
133    You can read, write, or query flash ROM size with this utility.
134    Although you can do "partial-write", the tools always takes a
135    full ROM image as input parameter.
136
137    NOTE before accessing flash ROM, you may need to first "select"
138    your target - usually BIOS or EC. That part is not handled by
139    this utility. Please find other external script to do it.
140
141    To perform a read, you need to:
142     1. Prepare a flashrom_util object
143        ex: flashrom = flashrom_util.flashrom_util()
144     2. Perform read operation
145        ex: image = flashrom.read_whole()
146
147        When the contents of the flashrom is read off the target, it's map
148        gets created automatically (read from the flashrom image using
149        'dump_fmap'). If the user wants this object to operate on some other
150        file, they could either have the map for the file created explicitly by
151        invoking flashrom.set_firmware_layout(filename), or supply their own map
152        (which is a dictionary where keys are section names, and values are
153        tuples of integers, base address of the section and the last address
154        of the section).
155
156    By default this object operates on the map retrieved from the image and
157    stored locally, this map can be overwritten by an explicitly passed user
158    map.
159
160    To perform a (partial) write:
161
162     1. Prepare a buffer storing an image to be written into the flashrom.
163     2. Have the map generated automatically or prepare your own, for instance:
164        ex: layout_map_all = { 'all': (0, rom_size - 1) }
165        ex: layout_map = { 'ro': (0, 0xFFF), 'rw': (0x1000, rom_size-1) }
166     4. Perform write operation
167
168        ex using default map:
169          flashrom.write_partial(new_image, (<section_name>, ...))
170        ex using explicitly provided map:
171          flashrom.write_partial(new_image, layout_map_all, ('all',))
172    """
173
174    def __init__(self, os_if, keep_temp_files=False, target_is_ec=False):
175        """ constructor of flashrom_util. help(flashrom_util) for more info
176
177        @param os_if: an object providing interface to OS services
178        @param keep_temp_files: if true, preserve temp files after operations
179        @param target_is_ec: if false, target is BIOS/AP
180
181        @type os_if: client.cros.faft.utils.os_interface.OSInterface
182        @type keep_temp_files: bool
183        @type target_is_ec: bool
184        """
185
186        self.os_if = os_if
187        self.keep_temp_files = keep_temp_files
188        self.firmware_layout = {}
189        self._target_command = ''
190        if target_is_ec:
191            self._enable_ec_access()
192        else:
193            self._enable_bios_access()
194
195    def _enable_bios_access(self):
196        if self.os_if.test_mode or self.os_if.target_hosted():
197            self._target_command = '-p host'
198
199    def _enable_ec_access(self):
200        if self.os_if.test_mode or self.os_if.target_hosted():
201            self._target_command = '-p ec'
202
203    def _get_temp_filename(self, prefix):
204        """Returns name of a temporary file in /tmp."""
205        return self.os_if.create_temp_file(prefix)
206
207    def _remove_temp_file(self, filename):
208        """Removes a temp file if self.keep_temp_files is false."""
209        if self.keep_temp_files:
210            return
211        if self.os_if.path_exists(filename):
212            self.os_if.remove_file(filename)
213
214    def _create_layout_file(self, layout_map):
215        """Creates a layout file based on layout_map.
216
217        Returns the file name containing layout information.
218        """
219        layout_text = [
220                '0x%08lX:0x%08lX %s' % (v[0], v[1], k)
221                for k, v in layout_map.items()
222        ]
223        layout_text.sort()  # XXX unstable if range exceeds 2^32
224        tmpfn = self._get_temp_filename('lay_')
225        with open(tmpfn, "w") as file:
226            file.write('\n'.join(layout_text) + '\n')
227        return tmpfn
228
229    def check_target(self):
230        """Check if flashrom programmer is working, by specifying no commands.
231
232        The command executed is just 'flashrom -p <target>'.
233
234        @return: True if flashrom completed successfully
235        @raise autotest_lib.client.common_lib.error.CmdError: if flashrom failed
236        """
237        cmd = 'flashrom %s' % self._target_command
238        self.os_if.run_shell_command(cmd)
239        return True
240
241    def get_section(self, base_image, section_name):
242        """
243        Retrieves a section of data based on section_name in layout_map.
244        Raises error if unknown section or invalid layout_map.
245        """
246        if section_name not in self.firmware_layout:
247            return ''
248        pos = self.firmware_layout[section_name]
249        if pos[0] >= pos[1] or pos[1] >= len(base_image):
250            raise TestError(
251                    'INTERNAL ERROR: invalid layout map: %s.' % section_name)
252        blob = base_image[pos[0]:pos[1] + 1]
253        # Trim down the main firmware body to its actual size since the
254        # signing utility uses the size of the input file as the size of
255        # the data to sign. Make it the same way as firmware creation.
256        if section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB'):
257            align = 4
258            pad = blob[-1:]
259            blob = blob.rstrip(pad)
260            blob = blob + ((align - 1) - (len(blob) - 1) % align) * pad
261        return blob
262
263    def put_section(self, base_image, section_name, data):
264        """
265        Updates a section of data based on section_name in firmware_layout.
266        Raises error if unknown section.
267        Returns the full updated image data.
268        """
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.')
272        if len(data) != pos[1] - pos[0] + 1:
273            # Pad the main firmware body since we trimed it down before.
274            if (len(data) < pos[1] - pos[0] + 1
275                        and section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA',
276                                             'ECMAINB', 'RW_FWID')):
277                pad = base_image[pos[1]:pos[1] + 1]
278                data = data + pad * (pos[1] - pos[0] + 1 - len(data))
279            else:
280                raise TestError('INTERNAL ERROR: unmatched data size.')
281        return base_image[0:pos[0]] + data + base_image[pos[1] + 1:]
282
283    def get_size(self):
284        """ Gets size of current flash ROM """
285        # TODO(hungte) Newer version of tool (flashrom) may support --get-size
286        # command which is faster in future. Right now we use back-compatible
287        # method: read whole and then get length.
288        image = self.read_whole()
289        return len(image)
290
291    def set_firmware_layout(self, file_name):
292        """get layout read from the BIOS """
293
294        scraper = LayoutScraper(self.os_if)
295        self.firmware_layout = scraper.get_layout(file_name)
296
297    def enable_write_protect(self):
298        """Enable the write protection of the flash chip."""
299
300        # For MTD devices, this will fail: need both --wp-range and --wp-enable.
301        # See: https://crrev.com/c/275381
302
303        cmd = 'flashrom %s --verbose --wp-enable' % self._target_command
304        self.os_if.run_shell_command(cmd, modifies_device=True)
305
306    def disable_write_protect(self):
307        """Disable the write protection of the flash chip."""
308        cmd = 'flashrom %s --verbose --wp-disable' % self._target_command
309        self.os_if.run_shell_command(cmd, modifies_device=True)
310
311    def set_write_protect_region(self, image_file, region, enabled=None):
312        """
313        Set write protection region, using specified image's layout.
314
315        The name should match those seen in `futility dump_fmap <image>`, and
316        is not checked against self.firmware_layout, due to different naming.
317
318        @param image_file: path of the image file to read regions from
319        @param region: Region to set (usually WP_RO)
320        @param enabled: if True, run --wp-enable; if False, run --wp-disable.
321        """
322        cmd = 'flashrom %s --verbose --image %s:%s --wp-region %s' % (
323                self._target_command, region, image_file, region)
324        if enabled is not None:
325            cmd += ' '
326            cmd += '--wp-enable' if enabled else '--wp-disable'
327
328        self.os_if.run_shell_command(cmd, modifies_device=True)
329
330    def set_write_protect_range(self, start, length, enabled=None):
331        """
332        Set write protection range by offset, using current image's layout.
333
334        @param start: offset (bytes) from start of flash to start of range
335        @param length: offset (bytes) from start of range to end of range
336        @param enabled: If True, run --wp-enable; if False, run --wp-disable.
337                        If None (default), don't specify either one.
338        """
339        cmd = 'flashrom %s --verbose --wp-range %s,%s' % (
340                self._target_command, start, length)
341        if enabled is not None:
342            cmd += ' '
343            cmd += '--wp-enable' if enabled else '--wp-disable'
344
345        self.os_if.run_shell_command(cmd, modifies_device=True)
346
347    def get_write_protect_status(self):
348        """Get a dict describing the status of the write protection
349
350        @return: {'enabled': True/False, 'start': '0x0', 'length': '0x0', ...}
351        @rtype: dict
352        """
353        # https://crrev.com/8ebbd500b5d8da9f6c1b9b44b645f99352ef62b4/writeprotect.c
354
355        status_pattern = re.compile(
356                r'WP: status: (.*)')
357        enabled_pattern = re.compile(
358                r'WP: write protect is (\w+)\.?')
359        range_pattern = re.compile(
360                r'WP: write protect range: start=(\w+), len=(\w+)')
361        range_err_pattern = re.compile(
362                r'WP: write protect range: (.+)')
363
364        output = self.os_if.run_shell_command_get_output(
365                'flashrom %s --wp-status' % self._target_command)
366        logging.debug('`flashrom %s --wp-status` returned %s',
367                      self._target_command, output)
368
369        wp_status = {}
370        for line in output:
371            if not line.startswith('WP: '):
372                continue
373
374            found_enabled = re.match(enabled_pattern, line)
375            if found_enabled:
376                status_word = found_enabled.group(1)
377                wp_status['enabled'] = (status_word == 'enabled')
378                continue
379
380            found_range = re.match(range_pattern, line)
381            if found_range:
382                (start, length) = found_range.groups()
383                wp_status['start'] = int(start, 16)
384                wp_status['length'] = int(length, 16)
385                continue
386
387            found_range_err = re.match(range_err_pattern, line)
388            if found_range_err:
389                # WP: write protect range: (cannot resolve the range)
390                wp_status['error'] = found_range_err.group(1)
391                continue
392
393            found_status = re.match(status_pattern, line)
394            if found_status:
395                wp_status['status'] = found_status.group(1)
396                continue
397
398        return wp_status
399
400    def dump_flash(self, filename):
401        """Read the flash device's data into a file, but don't parse it."""
402        cmd = 'flashrom %s -r "%s"' % (self._target_command, filename)
403        logging.info('flashrom_util.dump_flash(): %s', cmd)
404        self.os_if.run_shell_command(cmd)
405
406    def read_whole(self):
407        """
408        Reads whole flash ROM data.
409        Returns the data read from flash ROM, or empty string for other error.
410        """
411        tmpfn = self._get_temp_filename('rd_')
412        cmd = 'flashrom %s -r "%s"' % (self._target_command, tmpfn)
413        logging.info('flashrom_util.read_whole(): %s', cmd)
414        self.os_if.run_shell_command(cmd)
415        result = self.os_if.read_file(tmpfn)
416        self.set_firmware_layout(tmpfn)
417
418        # clean temporary resources
419        self._remove_temp_file(tmpfn)
420        return result
421
422    def write_partial(self, base_image, write_list, write_layout_map=None):
423        """
424        Writes data in sections of write_list to flash ROM.
425        An exception is raised if write operation fails.
426        """
427
428        if write_layout_map:
429            layout_map = write_layout_map
430        else:
431            layout_map = self.firmware_layout
432
433        tmpfn = self._get_temp_filename('wr_')
434        self.os_if.write_file(tmpfn, base_image)
435        layout_fn = self._create_layout_file(layout_map)
436
437        write_cmd = 'flashrom %s -l "%s" -i %s -w "%s"' % (
438                self._target_command, layout_fn, ' -i '.join(write_list),
439                tmpfn)
440        logging.info('flashrom.write_partial(): %s', write_cmd)
441        self.os_if.run_shell_command(write_cmd, modifies_device=True)
442
443        # clean temporary resources
444        self._remove_temp_file(tmpfn)
445        self._remove_temp_file(layout_fn)
446
447    def write_whole(self, base_image):
448        """Write the whole base image. """
449        layout_map = {'all': (0, len(base_image) - 1)}
450        self.write_partial(base_image, ('all', ), layout_map)
451
452    def get_write_cmd(self, image=None):
453        """Get the command to write the whole image (no layout handling)
454
455        @param image: the filename (empty to use current handler data)
456        """
457        return 'flashrom %s -w "%s"' % (self._target_command, image)
458