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