Peter Shih | fdf1768 | 2017-05-26 11:38:39 +0800 | [diff] [blame] | 1 | # pylint: disable=attribute-defined-outside-init |
Hung-Te Lin | 1990b74 | 2017-08-09 17:34:57 +0800 | [diff] [blame] | 2 | # Copyright 2012 The Chromium OS Authors. All rights reserved. |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 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 |
Hung-Te Lin | 76674b1 | 2017-02-15 11:34:53 +0800 | [diff] [blame] | 18 | import os |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 19 | import re |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 20 | import tempfile |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 21 | |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 22 | from cros.factory.gooftool import common |
Hung-Te Lin | 2e3c135 | 2018-04-03 00:12:53 +0800 | [diff] [blame] | 23 | from cros.factory.utils 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 | |
Hung-Te Lin | 76674b1 | 2017-02-15 11:34:53 +0800 | [diff] [blame] | 31 | CROS_PD_PATH = '/dev/cros_pd' |
| 32 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 33 | # Types of named tuples |
| 34 | WpStatus = collections.namedtuple('WpStatus', 'enabled offset size') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 35 | |
Hung-Te Lin | 14f084f | 2018-04-11 23:21:20 +0800 | [diff] [blame] | 36 | # All Chrome OS images are FMAP based. |
| 37 | FirmwareImage = fmap.FirmwareImage |
| 38 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 39 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 40 | class Flashrom: |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 41 | """Wrapper for calling system command flashrom(8).""" |
| 42 | |
| 43 | # flashrom(8) command line parameters |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 44 | _VALID_TARGETS = (TARGET_MAIN, TARGET_EC, TARGET_PD) |
| 45 | _TARGET_MAP = { |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 46 | TARGET_MAIN: '-p host', |
| 47 | TARGET_EC: '-p ec', |
Hung-Te Lin | d3b124c | 2016-10-20 22:22:31 +0800 | [diff] [blame] | 48 | TARGET_PD: '-p ec:type=pd', |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 49 | } |
Daniel Campello | f36a146 | 2021-04-27 07:08:02 -0600 | [diff] [blame] | 50 | _WRITE_FLAGS = '--noverify-all' |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 51 | _READ_FLAGS = '' |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 52 | |
| 53 | def __init__(self, target=None): |
| 54 | self._target = target or TARGET_MAIN |
| 55 | |
| 56 | def _InvokeCommand(self, param, ignore_status=False): |
| 57 | command = ' '.join(['flashrom', self._TARGET_MAP[self._target], param]) |
Hung-Te Lin | 76674b1 | 2017-02-15 11:34:53 +0800 | [diff] [blame] | 58 | |
| 59 | if self._target == TARGET_PD and not os.path.exists(CROS_PD_PATH): |
| 60 | # crbug.com/p/691901: 'flashrom' does not return PD information reliably |
| 61 | # using programmer "-p ec:type=pd". As a result, we want to only read PD |
| 62 | # information if /dev/cros_pd exists. |
| 63 | logging.debug('%s._InvokeCommand: Ignore command because %s does not ' |
| 64 | 'exist: [%s]', self.__class__, CROS_PD_PATH, command) |
| 65 | command = 'false' |
| 66 | else: |
| 67 | logging.debug('%s._InvokeCommand: %s', self.__class__, command) |
| 68 | |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 69 | result = common.Shell(command) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 70 | if not (ignore_status or result.success): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 71 | raise IOError('Failed in command: %s\n%s' % (command, result.stderr)) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 72 | return result |
| 73 | |
| 74 | def GetTarget(self): |
| 75 | """Gets current target (bus) to access.""" |
| 76 | return self._target |
| 77 | |
| 78 | def SetTarget(self, target): |
| 79 | """Sets current target (bus) to access.""" |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 80 | assert target in self._VALID_TARGETS, 'Unknown target: %s' % target |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 81 | self._target = target |
| 82 | |
| 83 | def GetSize(self): |
Wei-Han Chen | 6420479 | 2019-12-11 10:23:54 +0800 | [diff] [blame] | 84 | return int(self._InvokeCommand('--flash-size').stdout.splitlines()[-1], 0) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 85 | |
| 86 | def GetName(self): |
| 87 | """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] | 88 | results = self._InvokeCommand('--flash-name', ignore_status=True).stdout |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 89 | match_list = re.findall(r'\b(\w+)="([^"]*)"', results) |
| 90 | return dict(match_list) if match_list else None |
| 91 | |
| 92 | def Read(self, filename=None, sections=None): |
| 93 | """Reads whole image from selected flash chipset. |
| 94 | |
| 95 | Args: |
| 96 | filename: File name to receive image. None to use temporary file. |
| 97 | sections: List of sections to read. None to read whole image. |
| 98 | |
| 99 | Returns: |
| 100 | Image data read from flash chipset. |
| 101 | """ |
| 102 | if filename is None: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 103 | with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f: |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 104 | return self.Read(f.name) |
Yong Hong | 5b4f0b4 | 2017-08-30 16:53:56 +0800 | [diff] [blame] | 105 | sections_param = ['-i %s' % name for name in sections or []] |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 106 | self._InvokeCommand("-r '%s' %s %s" % (filename, ' '.join(sections_param), |
| 107 | self._READ_FLAGS)) |
| 108 | with open(filename, 'rb') as file_handle: |
| 109 | return file_handle.read() |
| 110 | |
| 111 | def Write(self, data=None, filename=None, sections=None): |
| 112 | """Writes image into selected flash chipset. |
| 113 | |
| 114 | Args: |
| 115 | data: Image data to write. None to write given file. |
| 116 | filename: File name of image to write if data is None. |
| 117 | sections: List of sections to write. None to write whole image. |
| 118 | """ |
Peter Shih | e6afab3 | 2018-09-11 17:16:48 +0800 | [diff] [blame] | 119 | assert ((data is None) ^ (filename is None)), ( |
| 120 | 'Either data or filename should be None.') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 121 | if data is not None: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 122 | with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f: |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 123 | f.write(data) |
| 124 | f.flush() |
Peter Shih | a78867d | 2018-02-26 14:17:51 +0800 | [diff] [blame] | 125 | self.Write(None, f.name) |
| 126 | return |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 127 | sections_param = [('-i %s' % name) for name in sections or []] |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 128 | self._InvokeCommand("-w '%s' %s %s" % (filename, ' '.join(sections_param), |
| 129 | self._WRITE_FLAGS)) |
| 130 | |
| 131 | def GetWriteProtectionStatus(self): |
| 132 | """Gets write protection status from selected flash chipset. |
| 133 | |
| 134 | Returns: A named tuple with (enabled, offset, size). |
| 135 | """ |
| 136 | # flashrom(8) output: WP: status: 0x80 |
| 137 | # WP: status.srp0: 1 |
| 138 | # WP: write protect is %s. (disabled/enabled) |
| 139 | # WP: write protect range: start=0x%8x, len=0x%08x |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 140 | results = self._InvokeCommand('--wp-status').stdout |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 141 | status = re.findall(r'WP: write protect is (\w+)\.', results) |
| 142 | if len(status) != 1: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 143 | raise IOError('Failed getting write protection status') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 144 | status = status[0] |
| 145 | if status not in ('enabled', 'disabled'): |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 146 | raise ValueError('Unknown write protection status: %s' % status) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 147 | |
| 148 | wp_range = re.findall(r'WP: write protect range: start=(\w+), len=(\w+)', |
| 149 | results) |
| 150 | if len(wp_range) != 1: |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 151 | raise IOError('Failed getting write protection range') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 152 | wp_range = wp_range[0] |
Stimim Chen | d044626 | 2020-06-30 15:32:51 +0800 | [diff] [blame] | 153 | return WpStatus(status == 'enabled', |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 154 | int(wp_range[0], 0), |
| 155 | int(wp_range[1], 0)) |
| 156 | |
| 157 | def EnableWriteProtection(self, offset, size): |
| 158 | """Enables write protection by specified range.""" |
Daniel Campello | 2a27fed | 2021-04-28 20:37:31 -0600 | [diff] [blame] | 159 | self._InvokeCommand('--wp-range 0x%06X,0x%06X --wp-enable' % (offset, size)) |
Fei Shao | 1b30de1 | 2019-11-20 15:14:08 +0800 | [diff] [blame] | 160 | result = self.GetWriteProtectionStatus() |
| 161 | if ((not result.enabled) or (result.offset != offset) or |
| 162 | (result.size != size)): |
| 163 | raise IOError('Failed to enabled write protection.') |
| 164 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 165 | # Try to verify write protection by attempting to disable it. |
Daniel Campello | 2a27fed | 2021-04-28 20:37:31 -0600 | [diff] [blame] | 166 | self._InvokeCommand('--wp-disable --wp-range 0,0', ignore_status=True) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 167 | # Verify the results |
| 168 | result = self.GetWriteProtectionStatus() |
| 169 | if ((not result.enabled) or (result.offset != offset) or |
| 170 | (result.size != size)): |
Fei Shao | 1b30de1 | 2019-11-20 15:14:08 +0800 | [diff] [blame] | 171 | raise IOError('Software write protection can be disabled. Please make ' |
| 172 | 'sure hardware write protection is enabled.') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 173 | |
| 174 | def DisableWriteProtection(self): |
| 175 | """Tries to Disable whole write protection range and status.""" |
Daniel Campello | 2a27fed | 2021-04-28 20:37:31 -0600 | [diff] [blame] | 176 | self._InvokeCommand('--wp-disable --wp-range 0,0') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 177 | result = self.GetWriteProtectionStatus() |
Hung-Te Lin | d44e129 | 2017-02-15 11:34:26 +0800 | [diff] [blame] | 178 | if result.enabled or (result.offset != 0) or (result.size != 0): |
| 179 | raise IOError('Failed to disable write protection.') |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 180 | |
| 181 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 182 | class FirmwareContent: |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 183 | """Wrapper around flashrom for a specific firmware target. |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 184 | |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 185 | This class keeps track of all the instances of itself that exist. |
| 186 | The goal being that only one instance ever gets created for each |
| 187 | target. This mapping of targets to instances is tracked by the |
| 188 | _target_cache class data member. |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 189 | """ |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 190 | |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 191 | # Cache of target:instance pairs. |
| 192 | _target_cache = {} |
| 193 | |
| 194 | @classmethod |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 195 | def Load(cls, target): |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 196 | """Create class instance for target, using cached copy if available.""" |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 197 | if target in cls._target_cache: |
| 198 | return cls._target_cache[target] |
| 199 | obj = cls() |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 200 | obj.target = target |
| 201 | obj.flashrom = Flashrom(target) |
Yong Hong | 5b4f0b4 | 2017-08-30 16:53:56 +0800 | [diff] [blame] | 202 | obj.cached_files = [] |
Tammo Spalink | 01e1172 | 2012-07-24 10:17:54 -0700 | [diff] [blame] | 203 | cls._target_cache[target] = obj |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 204 | return obj |
| 205 | |
| 206 | def GetChipId(self): |
| 207 | """Caching get of flashrom chip identifier. None if no chip is present.""" |
| 208 | if not hasattr(self, 'chip_id'): |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 209 | info = self.flashrom.GetName() |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 210 | self.chip_id = ' '.join([info['vendor'], info['name']]) if info else None |
| 211 | return self.chip_id |
| 212 | |
Yong Hong | 5b4f0b4 | 2017-08-30 16:53:56 +0800 | [diff] [blame] | 213 | def GetFileName(self, sections=None): |
| 214 | """Filename containing firmware data. None if no chip is present. |
| 215 | |
| 216 | Args: |
| 217 | sections: Restrict the sections of firmware data to be stored in the file. |
| 218 | |
| 219 | Returns: |
| 220 | Name of the file which contains the firmware data. |
| 221 | """ |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 222 | if self.GetChipId() is None: |
| 223 | return None |
Yong Hong | 5b4f0b4 | 2017-08-30 16:53:56 +0800 | [diff] [blame] | 224 | |
| 225 | sections = set(sections) if sections else None |
| 226 | |
| 227 | for (fileref, sections_in_file) in self.cached_files: |
| 228 | if sections_in_file is None or ( |
| 229 | sections is not None and sections.issubset(sections_in_file)): |
| 230 | return fileref.name |
| 231 | |
| 232 | fileref = tempfile.NamedTemporaryFile(prefix='fw_%s_' % self.target) |
| 233 | self.flashrom.Read(filename=fileref.name, sections=sections) |
| 234 | self.cached_files.append((fileref, sections)) |
| 235 | return fileref.name |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 236 | |
Yong Hong | 5d55309 | 2017-10-17 15:09:38 +0800 | [diff] [blame] | 237 | def Write(self, filename): |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 238 | """Call flashrom write for specific sections.""" |
Yong Hong | 5d55309 | 2017-10-17 15:09:38 +0800 | [diff] [blame] | 239 | for (fileref, sections_in_file) in self.cached_files: |
| 240 | if fileref.name == filename: |
| 241 | self.flashrom.Write(filename=filename, sections=sections_in_file) |
| 242 | return |
| 243 | raise ValueError('%r is not found in the cached files' % (filename,)) |
Tammo Spalink | 9a96b8a | 2012-04-03 11:10:41 +0800 | [diff] [blame] | 244 | |
Yong Hong | dad230a | 2017-08-30 22:25:19 +0800 | [diff] [blame] | 245 | def GetFirmwareImage(self, sections=None): |
Hung-Te Lin | 14f084f | 2018-04-11 23:21:20 +0800 | [diff] [blame] | 246 | """Returns a fmap.FirmwareImage instance. |
Yong Hong | dad230a | 2017-08-30 22:25:19 +0800 | [diff] [blame] | 247 | |
| 248 | Args: |
| 249 | sections: Restrict the sections of firmware data to be stored in the file. |
| 250 | |
| 251 | Returns: |
| 252 | An instance of FormwareImage. |
| 253 | """ |
| 254 | with open(self.GetFileName(sections=sections), 'rb') as image: |
Hung-Te Lin | 14f084f | 2018-04-11 23:21:20 +0800 | [diff] [blame] | 255 | return fmap.FirmwareImage(image.read()) |
Hung-Te Lin | dd708d4 | 2014-07-11 17:05:01 +0800 | [diff] [blame] | 256 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 257 | |
| 258 | def LoadEcFirmware(): |
| 259 | """Returns flashrom data from Embedded Controller chipset.""" |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 260 | return FirmwareContent.Load(TARGET_EC) |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 261 | |
| 262 | |
Ricky Liang | fb3d01b | 2014-08-27 12:02:20 +0800 | [diff] [blame] | 263 | def LoadPDFirmware(): |
| 264 | """Returns flashrom data from Power Delivery chipset.""" |
| 265 | return FirmwareContent.Load(TARGET_PD) |
| 266 | |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 267 | |
Hung-Te Lin | b33e564 | 2012-03-02 16:12:34 +0800 | [diff] [blame] | 268 | def LoadMainFirmware(): |
| 269 | """Returns flashrom data from main firmware (also known as BIOS).""" |
Tammo Spalink | cc87d83 | 2012-03-28 09:57:40 +0800 | [diff] [blame] | 270 | return FirmwareContent.Load(TARGET_MAIN) |