blob: 4abf6114485d63c4d6a7bb081b58226754f083f5 [file] [log] [blame]
Peter Shihfdf17682017-05-26 11:38:39 +08001# pylint: disable=attribute-defined-outside-init
Hung-Te Lin1990b742017-08-09 17:34:57 +08002# Copyright 2012 The Chromium OS Authors. All rights reserved.
Hung-Te Linb33e5642012-03-02 16:12:34 +08003# 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
8This modules provides easy access to ChromeOS firmware.
9
10To access the contents of a firmware image, use FimwareImage().
11To access the flash chipset containing firmware, use Flashrom().
12To get the content of (cacheable) firmware, use LoadMainFirmware() or
13 LoadEcFirmware().
14"""
15
16import collections
17import logging
Hung-Te Lin76674b12017-02-15 11:34:53 +080018import os
Hung-Te Linb33e5642012-03-02 16:12:34 +080019import re
Hung-Te Lind44e1292017-02-15 11:34:26 +080020import tempfile
Hung-Te Linb33e5642012-03-02 16:12:34 +080021
Hung-Te Lind44e1292017-02-15 11:34:26 +080022from cros.factory.gooftool import common
Hung-Te Lin2e3c1352018-04-03 00:12:53 +080023from cros.factory.utils import fmap
Tammo Spalink9a96b8a2012-04-03 11:10:41 +080024
Hung-Te Linb33e5642012-03-02 16:12:34 +080025
Hung-Te Linb33e5642012-03-02 16:12:34 +080026# Names to select target bus.
27TARGET_MAIN = 'main'
28TARGET_EC = 'ec'
Ricky Liangfb3d01b2014-08-27 12:02:20 +080029TARGET_PD = 'pd'
Hung-Te Linb33e5642012-03-02 16:12:34 +080030
Hung-Te Lin76674b12017-02-15 11:34:53 +080031CROS_PD_PATH = '/dev/cros_pd'
32
Hung-Te Linb33e5642012-03-02 16:12:34 +080033# Types of named tuples
34WpStatus = collections.namedtuple('WpStatus', 'enabled offset size')
Hung-Te Linb33e5642012-03-02 16:12:34 +080035
Hung-Te Lin14f084f2018-04-11 23:21:20 +080036# All Chrome OS images are FMAP based.
37FirmwareImage = fmap.FirmwareImage
38
Hung-Te Linb33e5642012-03-02 16:12:34 +080039
Fei Shaobd07c9a2020-06-15 19:04:50 +080040class Flashrom:
Hung-Te Linb33e5642012-03-02 16:12:34 +080041 """Wrapper for calling system command flashrom(8)."""
42
43 # flashrom(8) command line parameters
Ricky Liangfb3d01b2014-08-27 12:02:20 +080044 _VALID_TARGETS = (TARGET_MAIN, TARGET_EC, TARGET_PD)
45 _TARGET_MAP = {
Hung-Te Lin56b18402015-01-16 14:52:30 +080046 TARGET_MAIN: '-p host',
47 TARGET_EC: '-p ec',
Hung-Te Lind3b124c2016-10-20 22:22:31 +080048 TARGET_PD: '-p ec:type=pd',
Ricky Liangfb3d01b2014-08-27 12:02:20 +080049 }
Daniel Campellof36a1462021-04-27 07:08:02 -060050 _WRITE_FLAGS = '--noverify-all'
Hung-Te Lin56b18402015-01-16 14:52:30 +080051 _READ_FLAGS = ''
Hung-Te Linb33e5642012-03-02 16:12:34 +080052
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 Lin76674b12017-02-15 11:34:53 +080058
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 Lind44e1292017-02-15 11:34:26 +080069 result = common.Shell(command)
Hung-Te Linb33e5642012-03-02 16:12:34 +080070 if not (ignore_status or result.success):
Hung-Te Lind44e1292017-02-15 11:34:26 +080071 raise IOError('Failed in command: %s\n%s' % (command, result.stderr))
Hung-Te Linb33e5642012-03-02 16:12:34 +080072 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 Lin56b18402015-01-16 14:52:30 +080080 assert target in self._VALID_TARGETS, 'Unknown target: %s' % target
Hung-Te Linb33e5642012-03-02 16:12:34 +080081 self._target = target
82
83 def GetSize(self):
Wei-Han Chen64204792019-12-11 10:23:54 +080084 return int(self._InvokeCommand('--flash-size').stdout.splitlines()[-1], 0)
Hung-Te Linb33e5642012-03-02 16:12:34 +080085
86 def GetName(self):
87 """Returns a key-value dict for chipset info, or None for any failure."""
Hung-Te Lin56b18402015-01-16 14:52:30 +080088 results = self._InvokeCommand('--flash-name', ignore_status=True).stdout
Hung-Te Linb33e5642012-03-02 16:12:34 +080089 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 Lind44e1292017-02-15 11:34:26 +0800103 with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f:
Hung-Te Linb33e5642012-03-02 16:12:34 +0800104 return self.Read(f.name)
Yong Hong5b4f0b42017-08-30 16:53:56 +0800105 sections_param = ['-i %s' % name for name in sections or []]
Hung-Te Linb33e5642012-03-02 16:12:34 +0800106 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 Shihe6afab32018-09-11 17:16:48 +0800119 assert ((data is None) ^ (filename is None)), (
120 'Either data or filename should be None.')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800121 if data is not None:
Hung-Te Lind44e1292017-02-15 11:34:26 +0800122 with tempfile.NamedTemporaryFile(prefix='fw_%s_' % self._target) as f:
Hung-Te Linb33e5642012-03-02 16:12:34 +0800123 f.write(data)
124 f.flush()
Peter Shiha78867d2018-02-26 14:17:51 +0800125 self.Write(None, f.name)
126 return
Hung-Te Lind44e1292017-02-15 11:34:26 +0800127 sections_param = [('-i %s' % name) for name in sections or []]
Hung-Te Linb33e5642012-03-02 16:12:34 +0800128 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 Lin56b18402015-01-16 14:52:30 +0800140 results = self._InvokeCommand('--wp-status').stdout
Hung-Te Linb33e5642012-03-02 16:12:34 +0800141 status = re.findall(r'WP: write protect is (\w+)\.', results)
142 if len(status) != 1:
Hung-Te Lind44e1292017-02-15 11:34:26 +0800143 raise IOError('Failed getting write protection status')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800144 status = status[0]
145 if status not in ('enabled', 'disabled'):
Hung-Te Lind44e1292017-02-15 11:34:26 +0800146 raise ValueError('Unknown write protection status: %s' % status)
Hung-Te Linb33e5642012-03-02 16:12:34 +0800147
148 wp_range = re.findall(r'WP: write protect range: start=(\w+), len=(\w+)',
149 results)
150 if len(wp_range) != 1:
Hung-Te Lind44e1292017-02-15 11:34:26 +0800151 raise IOError('Failed getting write protection range')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800152 wp_range = wp_range[0]
Stimim Chend0446262020-06-30 15:32:51 +0800153 return WpStatus(status == 'enabled',
Hung-Te Linb33e5642012-03-02 16:12:34 +0800154 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 Campello2a27fed2021-04-28 20:37:31 -0600159 self._InvokeCommand('--wp-range 0x%06X,0x%06X --wp-enable' % (offset, size))
Fei Shao1b30de12019-11-20 15:14:08 +0800160 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 Linb33e5642012-03-02 16:12:34 +0800165 # Try to verify write protection by attempting to disable it.
Daniel Campello2a27fed2021-04-28 20:37:31 -0600166 self._InvokeCommand('--wp-disable --wp-range 0,0', ignore_status=True)
Hung-Te Linb33e5642012-03-02 16:12:34 +0800167 # Verify the results
168 result = self.GetWriteProtectionStatus()
169 if ((not result.enabled) or (result.offset != offset) or
170 (result.size != size)):
Fei Shao1b30de12019-11-20 15:14:08 +0800171 raise IOError('Software write protection can be disabled. Please make '
172 'sure hardware write protection is enabled.')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800173
174 def DisableWriteProtection(self):
175 """Tries to Disable whole write protection range and status."""
Daniel Campello2a27fed2021-04-28 20:37:31 -0600176 self._InvokeCommand('--wp-disable --wp-range 0,0')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800177 result = self.GetWriteProtectionStatus()
Hung-Te Lind44e1292017-02-15 11:34:26 +0800178 if result.enabled or (result.offset != 0) or (result.size != 0):
179 raise IOError('Failed to disable write protection.')
Hung-Te Linb33e5642012-03-02 16:12:34 +0800180
181
Fei Shaobd07c9a2020-06-15 19:04:50 +0800182class FirmwareContent:
Tammo Spalink9a96b8a2012-04-03 11:10:41 +0800183 """Wrapper around flashrom for a specific firmware target.
Hung-Te Linb33e5642012-03-02 16:12:34 +0800184
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800185 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 Linb33e5642012-03-02 16:12:34 +0800189 """
Hung-Te Linb33e5642012-03-02 16:12:34 +0800190
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800191 # Cache of target:instance pairs.
192 _target_cache = {}
193
194 @classmethod
Tammo Spalink01e11722012-07-24 10:17:54 -0700195 def Load(cls, target):
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800196 """Create class instance for target, using cached copy if available."""
Tammo Spalink01e11722012-07-24 10:17:54 -0700197 if target in cls._target_cache:
198 return cls._target_cache[target]
199 obj = cls()
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800200 obj.target = target
201 obj.flashrom = Flashrom(target)
Yong Hong5b4f0b42017-08-30 16:53:56 +0800202 obj.cached_files = []
Tammo Spalink01e11722012-07-24 10:17:54 -0700203 cls._target_cache[target] = obj
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800204 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 Spalink9a96b8a2012-04-03 11:10:41 +0800209 info = self.flashrom.GetName()
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800210 self.chip_id = ' '.join([info['vendor'], info['name']]) if info else None
211 return self.chip_id
212
Yong Hong5b4f0b42017-08-30 16:53:56 +0800213 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 Spalinkcc87d832012-03-28 09:57:40 +0800222 if self.GetChipId() is None:
223 return None
Yong Hong5b4f0b42017-08-30 16:53:56 +0800224
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 Linb33e5642012-03-02 16:12:34 +0800236
Yong Hong5d553092017-10-17 15:09:38 +0800237 def Write(self, filename):
Tammo Spalink9a96b8a2012-04-03 11:10:41 +0800238 """Call flashrom write for specific sections."""
Yong Hong5d553092017-10-17 15:09:38 +0800239 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 Spalink9a96b8a2012-04-03 11:10:41 +0800244
Yong Hongdad230a2017-08-30 22:25:19 +0800245 def GetFirmwareImage(self, sections=None):
Hung-Te Lin14f084f2018-04-11 23:21:20 +0800246 """Returns a fmap.FirmwareImage instance.
Yong Hongdad230a2017-08-30 22:25:19 +0800247
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 Lin14f084f2018-04-11 23:21:20 +0800255 return fmap.FirmwareImage(image.read())
Hung-Te Lindd708d42014-07-11 17:05:01 +0800256
Hung-Te Linb33e5642012-03-02 16:12:34 +0800257
258def LoadEcFirmware():
259 """Returns flashrom data from Embedded Controller chipset."""
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800260 return FirmwareContent.Load(TARGET_EC)
Hung-Te Linb33e5642012-03-02 16:12:34 +0800261
262
Ricky Liangfb3d01b2014-08-27 12:02:20 +0800263def LoadPDFirmware():
264 """Returns flashrom data from Power Delivery chipset."""
265 return FirmwareContent.Load(TARGET_PD)
266
Hung-Te Lin56b18402015-01-16 14:52:30 +0800267
Hung-Te Linb33e5642012-03-02 16:12:34 +0800268def LoadMainFirmware():
269 """Returns flashrom data from main firmware (also known as BIOS)."""
Tammo Spalinkcc87d832012-03-28 09:57:40 +0800270 return FirmwareContent.Load(TARGET_MAIN)