Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 1 | # pylint: disable=W0201 |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 2 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """ChromeOS Firmware Utilities |
| 7 | |
| 8 | This modules provides easy access to ChromeOS firmware. |
| 9 | |
| 10 | To access the contents of a firmware image, use FimwareImage(). |
| 11 | To access the flash chipset containing firmware, use Flashrom(). |
| 12 | To get the content of (cacheable) firmware, use LoadMainFirmware() or |
| 13 | LoadEcFirmware(). |
| 14 | """ |
| 15 | |
| 16 | import collections |
| 17 | import logging |
| 18 | import re |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 19 | import tempfile |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 20 | |
Tammo Spalink | a40293e | 2012-07-04 14:58:56 +0800 | [diff] [blame] | 21 | import factory_common # pylint: disable=W0611 |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 22 | from cros.factory.gooftool import common |
| 23 | from cros.factory.gooftool import fmap |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 24 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 25 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 26 | # Names to select target bus. |
| 27 | TARGET_MAIN = 'main' |
| 28 | TARGET_EC = 'ec' |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 29 | TARGET_PD = 'pd' |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 30 | |
| 31 | # Types of named tuples |
| 32 | WpStatus = collections.namedtuple('WpStatus', 'enabled offset size') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 33 | |
| 34 | |
| 35 | class Flashrom(object): |
| 36 | """Wrapper for calling system command flashrom(8).""" |
| 37 | |
| 38 | # flashrom(8) command line parameters |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 39 | _VALID_TARGETS = (TARGET_MAIN, TARGET_EC, TARGET_PD) |
| 40 | _TARGET_MAP = { |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 41 | TARGET_MAIN: '-p host', |
| 42 | TARGET_EC: '-p ec', |
Hung-Te Lin | d3b124c | 2016-10-20 22:22:31 +0800 | [diff] [blame] | 43 | TARGET_PD: '-p ec:type=pd', |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 44 | } |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 45 | _WRITE_FLAGS = '--fast-verify' |
| 46 | _READ_FLAGS = '' |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 47 | |
| 48 | def __init__(self, target=None): |
| 49 | self._target = target or TARGET_MAIN |
| 50 | |
| 51 | def _InvokeCommand(self, param, ignore_status=False): |
| 52 | command = ' '.join(['flashrom', self._TARGET_MAP[self._target], param]) |
| 53 | logging.debug('Flashrom._InvokeCommand: %s', command) |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 54 | result = common.Shell(command) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 55 | if not (ignore_status or result.success): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 56 | raise IOError('Failed in command: %s\n%s' % (command, result.stderr)) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 57 | return result |
| 58 | |
| 59 | def GetTarget(self): |
| 60 | """Gets current target (bus) to access.""" |
| 61 | return self._target |
| 62 | |
| 63 | def SetTarget(self, target): |
| 64 | """Sets current target (bus) to access.""" |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 65 | assert target in self._VALID_TARGETS, 'Unknown target: %s' % target |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 66 | self._target = target |
| 67 | |
| 68 | def GetSize(self): |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 69 | return int(self._InvokeCommand('--get-size').stdout.splitlines()[-1], 0) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 70 | |
| 71 | def GetName(self): |
| 72 | """Returns a key-value dict for chipset info, or None for any failure.""" |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 73 | results = self._InvokeCommand('--flash-name', ignore_status=True).stdout |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 74 | match_list = re.findall(r'\b(\w+)="([^"]*)"', results) |
| 75 | return dict(match_list) if match_list else None |
| 76 | |
| 77 | def Read(self, filename=None, sections=None): |
| 78 | """Reads whole image from selected flash chipset. |
| 79 | |
| 80 | Args: |
| 81 | filename: File name to receive image. None to use temporary file. |
| 82 | sections: List of sections to read. None to read whole image. |
| 83 | |
| 84 | Returns: |
| 85 | Image data read from flash chipset. |
| 86 | """ |
| 87 | if filename is None: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 88 | with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f: |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 89 | return self.Read(f.name) |
| 90 | sections_param = [name % '-i %s' for name in sections or []] |
| 91 | self._InvokeCommand("-r '%s' %s %s" % (filename, ' '.join(sections_param), |
| 92 | self._READ_FLAGS)) |
| 93 | with open(filename, 'rb') as file_handle: |
| 94 | return file_handle.read() |
| 95 | |
| 96 | def Write(self, data=None, filename=None, sections=None): |
| 97 | """Writes image into selected flash chipset. |
| 98 | |
| 99 | Args: |
| 100 | data: Image data to write. None to write given file. |
| 101 | filename: File name of image to write if data is None. |
| 102 | sections: List of sections to write. None to write whole image. |
| 103 | """ |
| 104 | assert (((data is not None) and (filename is None)) or |
| 105 | ((data is None) and (filename is not None))), \ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 106 | 'Either data or filename should be None.' |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 107 | if data is not None: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 108 | with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f: |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 109 | f.write(data) |
| 110 | f.flush() |
| 111 | return self.Write(None, f.name) |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 112 | sections_param = [('-i %s' % name) for name in sections or []] |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 113 | self._InvokeCommand("-w '%s' %s %s" % (filename, ' '.join(sections_param), |
| 114 | self._WRITE_FLAGS)) |
| 115 | |
| 116 | def GetWriteProtectionStatus(self): |
| 117 | """Gets write protection status from selected flash chipset. |
| 118 | |
| 119 | Returns: A named tuple with (enabled, offset, size). |
| 120 | """ |
| 121 | # flashrom(8) output: WP: status: 0x80 |
| 122 | # WP: status.srp0: 1 |
| 123 | # WP: write protect is %s. (disabled/enabled) |
| 124 | # WP: write protect range: start=0x%8x, len=0x%08x |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 125 | results = self._InvokeCommand('--wp-status').stdout |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 126 | status = re.findall(r'WP: write protect is (\w+)\.', results) |
| 127 | if len(status) != 1: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 128 | raise IOError('Failed getting write protection status') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 129 | status = status[0] |
| 130 | if status not in ('enabled', 'disabled'): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 131 | raise ValueError('Unknown write protection status: %s' % status) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 132 | |
| 133 | wp_range = re.findall(r'WP: write protect range: start=(\w+), len=(\w+)', |
| 134 | results) |
| 135 | if len(wp_range) != 1: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 136 | raise IOError('Failed getting write protection range') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 137 | wp_range = wp_range[0] |
| 138 | return WpStatus(True if status == 'enabled' else False, |
| 139 | int(wp_range[0], 0), |
| 140 | int(wp_range[1], 0)) |
| 141 | |
| 142 | def EnableWriteProtection(self, offset, size): |
| 143 | """Enables write protection by specified range.""" |
| 144 | self._InvokeCommand('--wp-range 0x%06X 0x%06X --wp-enable' % (offset, size)) |
| 145 | # Try to verify write protection by attempting to disable it. |
| 146 | self._InvokeCommand('--wp-disable --wp-range 0 0', ignore_status=True) |
| 147 | # Verify the results |
| 148 | result = self.GetWriteProtectionStatus() |
| 149 | if ((not result.enabled) or (result.offset != offset) or |
| 150 | (result.size != size)): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 151 | raise IOError('Failed to enabled write protection.') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 152 | |
| 153 | def DisableWriteProtection(self): |
| 154 | """Tries to Disable whole write protection range and status.""" |
| 155 | self._InvokeCommand('--wp-disable --wp-range 0 0') |
| 156 | result = self.GetWriteProtectionStatus() |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 157 | if result.enabled or (result.offset != 0) or (result.size != 0): |
| 158 | raise IOError('Failed to disable write protection.') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 159 | |
| 160 | |
| 161 | class FirmwareImage(object): |
| 162 | """Provides access to firmware image via FMAP sections.""" |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 163 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 164 | def __init__(self, image_source): |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 165 | self._image = image_source |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 166 | self._fmap = fmap.fmap_decode(self._image) |
| 167 | self._areas = dict( |
| 168 | (entry['name'], [entry['offset'], entry['size']]) |
| 169 | for entry in self._fmap['areas']) |
| 170 | |
| 171 | def get_size(self): |
| 172 | """Returns the size of associate firmware image.""" |
| 173 | return len(self._image) |
| 174 | |
| 175 | def has_section(self, name): |
| 176 | """Returns if specified section is available in image.""" |
| 177 | return name in self._areas |
| 178 | |
| 179 | def get_section_area(self, name): |
| 180 | """Returns the area (offset, size) information of given section.""" |
| 181 | if not self.has_section(name): |
| 182 | raise ValueError('get_section_area: invalid section: %s' % name) |
| 183 | return self._areas[name] |
| 184 | |
| 185 | def get_section(self, name): |
| 186 | """Returns the content of specified section.""" |
| 187 | area = self.get_section_area(name) |
| 188 | return self._image[area[0]:(area[0] + area[1])] |
| 189 | |
| 190 | def get_section_offset(self, name): |
| 191 | area = self.get_section_area(name) |
| 192 | return self._image[area[0]:(area[0] + area[1])] |
| 193 | |
| 194 | def put_section(self, name, value): |
| 195 | """Updates content of specified section in image.""" |
| 196 | area = self.get_section_area(name) |
| 197 | if len(value) != area[1]: |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 198 | raise ValueError('Value size (%d) does not fit into section (%s, %d)' % |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 199 | (len(value), name, area[1])) |
| 200 | self._image = (self._image[0:area[0]] + |
| 201 | value + |
| 202 | self._image[(area[0] + area[1]):]) |
| 203 | return True |
| 204 | |
| 205 | def get_fmap_blob(self): |
| 206 | """Returns the re-encoded fmap blob from firmware image.""" |
| 207 | return fmap.fmap_encode(self._fmap) |
| 208 | |
| 209 | |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 210 | class FirmwareContent(object): |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 211 | """Wrapper around flashrom for a specific firmware target. |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 212 | |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 213 | This class keeps track of all the instances of itself that exist. |
| 214 | The goal being that only one instance ever gets created for each |
| 215 | target. This mapping of targets to instances is tracked by the |
| 216 | _target_cache class data member. |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 217 | """ |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 218 | |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 219 | # Cache of target:instance pairs. |
| 220 | _target_cache = {} |
| 221 | |
| 222 | @classmethod |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 223 | def Load(cls, target): |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 224 | """Create class instance for target, using cached copy if available.""" |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 225 | if target in cls._target_cache: |
| 226 | return cls._target_cache[target] |
| 227 | obj = cls() |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 228 | obj.target = target |
| 229 | obj.flashrom = Flashrom(target) |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 230 | cls._target_cache[target] = obj |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 231 | return obj |
| 232 | |
| 233 | def GetChipId(self): |
| 234 | """Caching get of flashrom chip identifier. None if no chip is present.""" |
| 235 | if not hasattr(self, 'chip_id'): |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 236 | info = self.flashrom.GetName() |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 237 | self.chip_id = ' '.join([info['vendor'], info['name']]) if info else None |
| 238 | return self.chip_id |
| 239 | |
| 240 | def GetFileName(self): |
| 241 | """Filename containing firmware data. None if no chip is present.""" |
| 242 | if self.GetChipId() is None: |
| 243 | return None |
| 244 | if not hasattr(self, 'filename'): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame^] | 245 | fileref = tempfile.NamedTemporaryFile(prefix='fw_%s_' % self.target) |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 246 | self.flashrom.Read(filename=fileref.name) |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 247 | self.fileref = fileref |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 248 | self.filename = fileref.name |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 249 | return self.filename |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 250 | |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 251 | def Write(self, sections=None): |
| 252 | """Call flashrom write for specific sections.""" |
| 253 | self.flashrom.Write(filename=self.GetFileName(), sections=sections) |
| 254 | |
Hung-Te Lin | dd708d4 | 2014-07-11 17:05:01 +0800 | [diff] [blame] | 255 | def GetFirmwareImage(self): |
| 256 | """Returns a FirmwareImage instance.""" |
| 257 | with open(self.GetFileName(), 'rb') as image: |
| 258 | return FirmwareImage(image.read()) |
| 259 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 260 | |
| 261 | def LoadEcFirmware(): |
| 262 | """Returns flashrom data from Embedded Controller chipset.""" |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 263 | return FirmwareContent.Load(TARGET_EC) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 264 | |
| 265 | |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 266 | def LoadPDFirmware(): |
| 267 | """Returns flashrom data from Power Delivery chipset.""" |
| 268 | return FirmwareContent.Load(TARGET_PD) |
| 269 | |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 270 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 271 | def LoadMainFirmware(): |
| 272 | """Returns flashrom data from main firmware (also known as BIOS).""" |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 273 | return FirmwareContent.Load(TARGET_MAIN) |