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 5 """ This module provides convenience routines to access Flash ROM (EEPROM) 6 7 saft_flashrom_util is based on utility 'flashrom'. 8 9 Original tool syntax: 10 (read ) flashrom -r <file> 11 (write) flashrom -l <layout_fn> [-i <image_name> ...] -w <file> 12 13 The layout_fn is in format of 14 address_begin:address_end image_name 15 which defines a region between (address_begin, address_end) and can 16 be accessed by the name image_name. 17 18 Currently the tool supports multiple partial write but not partial read. 19 20 In the saft_flashrom_util, we provide read and partial write abilities. 21 For more information, see help(saft_flashrom_util.flashrom_util). 22 """ 23 24 class TestError(Exception): 25 pass 26 27 28 class LayoutScraper(object): 29 """Object of this class is used to retrieve layout from a BIOS file.""" 30 31 # The default conversion table for mosys. 32 DEFAULT_CHROMEOS_FMAP_CONVERSION = { 33 "Boot Stub": "FV_BSTUB", 34 "GBB Area": "FV_GBB", 35 "Recovery Firmware": "FVDEV", 36 "RO VPD": "RO_VPD", 37 "Firmware A Key": "VBOOTA", 38 "Firmware A Data": "FVMAIN", 39 "Firmware B Key": "VBOOTB", 40 "Firmware B Data": "FVMAINB", 41 "Log Volume": "FV_LOG", 42 # New layout in Chrome OS Main Processor Firmware Specification, 43 # used by all newer (>2011) platforms except Mario. 44 "BOOT_STUB": "FV_BSTUB", 45 "GBB": "FV_GBB", 46 "RECOVERY": "FVDEV", 47 "VBLOCK_A": "VBOOTA", 48 "VBLOCK_B": "VBOOTB", 49 "FW_MAIN_A": "FVMAIN", 50 "FW_MAIN_B": "FVMAINB", 51 "RW_FWID_A": "RW_FWID_A", 52 "RW_FWID_B": "RW_FWID_B", 53 # Memory Training data cache for recovery boots 54 # Added on Nov 09, 2016 55 "RECOVERY_MRC_CACHE": "RECOVERY_MRC_CACHE", 56 # New sections in Depthcharge. 57 "EC_MAIN_A": "ECMAINA", 58 "EC_MAIN_B": "ECMAINB", 59 # EC firmware layout 60 "EC_RW": "EC_RW", 61 "EC_RW_B": "EC_RW_B", 62 "RW_FWID": "RW_FWID", 63 } 64 65 def __init__(self, os_if): 66 self.image = None 67 self.os_if = os_if 68 69 def _get_text_layout(self, file_name): 70 """Retrieve text layout from a firmware image file. 71 72 This function uses the 'mosys' utility to scan the firmware image and 73 retrieve the section layout information. 74 75 The layout is reported as a set of lines with multiple 76 "<name>"="value" pairs, all this output is passed to the caller. 77 """ 78 79 mosys_cmd = 'mosys -k eeprom map %s' % file_name 80 return self.os_if.run_shell_command_get_output(mosys_cmd) 81 82 def _line_to_dictionary(self, line): 83 """Convert a text layout line into a dictionary. 84 85 Get a string consisting of single space separated "<name>"="value>" 86 pairs and convert it into a dictionary where keys are the <name> 87 fields, and values are the corresponding <value> fields. 88 89 Return the dictionary to the caller. 90 """ 91 92 rv = {} 93 94 items = line.replace('" ', '"^').split('^') 95 for item in items: 96 pieces = item.split('=') 97 if len(pieces) != 2: 98 continue 99 rv[pieces[0]] = pieces[1].strip('"') 100 return rv 101 102 def check_layout(self, layout, file_size): 103 """Verify the layout to be consistent. 104 105 The layout is consistent if there is no overlapping sections and the 106 section boundaries do not exceed the file size. 107 108 Inputs: 109 layout: a dictionary keyed by a string (the section name) with 110 values being two integers tuples, the first and the last 111 bites' offset in the file. 112 file_size: and integer, the size of the file the layout describes 113 the sections in. 114 115 Raises: 116 TestError in case the layout is not consistent. 117 """ 118 119 # Generate a list of section range tuples. 120 ost = sorted([layout[section] for section in layout]) 121 base = -1 122 for section_base, section_end in ost: 123 if section_base <= base or section_end + 1 < section_base: 124 # Overlapped section is possible, like the fwid which is 125 # inside the main fw section. 126 self.os_if.log('overlapped section at 0x%x..0x%x' % ( 127 section_base, section_end)) 128 base = section_end 129 if base > file_size: 130 raise TestError('Section end 0x%x exceeds file size %x' % ( 131 base, file_size)) 132 133 def get_layout(self, file_name): 134 """Generate layout for a firmware file. 135 136 First retrieve the text layout as reported by 'mosys' and then convert 137 it into a dictionary, replacing section names reported by mosys into 138 matching names from DEFAULT_CHROMEOS_FMAP_CONVERSION dictionary above, 139 using the names as keys in the layout dictionary. The elements of the 140 layout dictionary are the offsets of the first ans last bytes of the 141 section in the firmware file. 142 143 Then verify the generated layout's consistency and return it to the 144 caller. 145 """ 146 147 layout_data = {} # keyed by the section name, elements - tuples of 148 # (<section start addr>, <section end addr>) 149 150 for line in self._get_text_layout(file_name): 151 d = self._line_to_dictionary(line) 152 try: 153 name = self.DEFAULT_CHROMEOS_FMAP_CONVERSION[d['area_name']] 154 except KeyError: 155 continue # This line does not contain an area of interest. 156 157 if name in layout_data: 158 raise TestError('%s duplicated in the layout' % name) 159 160 offset = int(d['area_offset'], 0) 161 size = int(d['area_size'], 0) 162 layout_data[name] = (offset, offset + size - 1) 163 164 self.check_layout(layout_data, self.os_if.get_file_size(file_name)) 165 return layout_data 166 167 # flashrom utility wrapper 168 class flashrom_util(object): 169 """ a wrapper for "flashrom" utility. 170 171 You can read, write, or query flash ROM size with this utility. 172 Although you can do "partial-write", the tools always takes a 173 full ROM image as input parameter. 174 175 NOTE before accessing flash ROM, you may need to first "select" 176 your target - usually BIOS or EC. That part is not handled by 177 this utility. Please find other external script to do it. 178 179 To perform a read, you need to: 180 1. Prepare a flashrom_util object 181 ex: flashrom = flashrom_util.flashrom_util() 182 2. Perform read operation 183 ex: image = flashrom.read_whole() 184 185 When the contents of the flashrom is read off the target, it's map 186 gets created automatically (read from the flashrom image using 187 'mosys'). If the user wants this object to operate on some other file, 188 he could either have the map for the file created explicitly by 189 invoking flashrom.set_firmware_layout(filename), or supply his own map 190 (which is a dictionary where keys are section names, and values are 191 tuples of integers, base address of the section and the last address 192 of the section). 193 194 By default this object operates on the map retrieved from the image and 195 stored locally, this map can be overwritten by an explicitly passed user 196 map. 197 198 To perform a (partial) write: 199 200 1. Prepare a buffer storing an image to be written into the flashrom. 201 2. Have the map generated automatically or prepare your own, for instance: 202 ex: layout_map_all = { 'all': (0, rom_size - 1) } 203 ex: layout_map = { 'ro': (0, 0xFFF), 'rw': (0x1000, rom_size-1) } 204 4. Perform write operation 205 206 ex using default map: 207 flashrom.write_partial(new_image, (<section_name>, ...)) 208 ex using explicitly provided map: 209 flashrom.write_partial(new_image, layout_map_all, ('all',)) 210 211 Attributes: 212 keep_temp_files: boolean flag to control cleaning of temporary files 213 """ 214 215 def __init__(self, os_if, keep_temp_files=False, 216 target_is_ec=False): 217 """ constructor of flashrom_util. help(flashrom_util) for more info """ 218 self.os_if = os_if 219 self.keep_temp_files = keep_temp_files 220 self.firmware_layout = {} 221 self._target_command = '' 222 if target_is_ec: 223 self._enable_ec_access() 224 else: 225 self._enable_bios_access() 226 227 def _enable_bios_access(self): 228 if not self.os_if.target_hosted(): 229 return 230 self._target_command = '-p host' 231 232 def _enable_ec_access(self): 233 if not self.os_if.target_hosted(): 234 return 235 self._target_command = '-p ec' 236 237 def _get_temp_filename(self, prefix): 238 """Returns name of a temporary file in /tmp.""" 239 return self.os_if.create_temp_file(prefix) 240 241 def _remove_temp_file(self, filename): 242 """Removes a temp file if self.keep_temp_files is false.""" 243 if self.keep_temp_files: 244 return 245 if self.os_if.path_exists(filename): 246 self.os_if.remove_file(filename) 247 248 def _create_layout_file(self, layout_map): 249 """Creates a layout file based on layout_map. 250 251 Returns the file name containing layout information. 252 """ 253 layout_text = ['0x%08lX:0x%08lX %s' % (v[0], v[1], k) 254 for k, v in layout_map.items()] 255 layout_text.sort() # XXX unstable if range exceeds 2^32 256 tmpfn = self._get_temp_filename('lay_') 257 self.os_if.write_file(tmpfn, '\n'.join(layout_text) + '\n') 258 return tmpfn 259 260 def get_section(self, base_image, section_name): 261 """ 262 Retrieves a section of data based on section_name in layout_map. 263 Raises error if unknown section or invalid layout_map. 264 """ 265 if section_name not in self.firmware_layout: 266 return [] 267 pos = self.firmware_layout[section_name] 268 if pos[0] >= pos[1] or pos[1] >= len(base_image): 269 raise TestError('INTERNAL ERROR: invalid layout map: %s.' % 270 section_name) 271 blob = base_image[pos[0] : pos[1] + 1] 272 # Trim down the main firmware body to its actual size since the 273 # signing utility uses the size of the input file as the size of 274 # the data to sign. Make it the same way as firmware creation. 275 if section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB'): 276 align = 4 277 pad = blob[-1] 278 blob = blob.rstrip(pad) 279 blob = blob + ((align - 1) - (len(blob) - 1) % align) * pad 280 return blob 281 282 def put_section(self, base_image, section_name, data): 283 """ 284 Updates a section of data based on section_name in firmware_layout. 285 Raises error if unknown section. 286 Returns the full updated image data. 287 """ 288 pos = self.firmware_layout[section_name] 289 if pos[0] >= pos[1] or pos[1] >= len(base_image): 290 raise TestError('INTERNAL ERROR: invalid layout map.') 291 if len(data) != pos[1] - pos[0] + 1: 292 # Pad the main firmware body since we trimed it down before. 293 if (len(data) < pos[1] - pos[0] + 1 and section_name in 294 ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB', 295 'RW_FWID')): 296 pad = base_image[pos[1]] 297 data = data + pad * (pos[1] - pos[0] + 1 - len(data)) 298 else: 299 raise TestError('INTERNAL ERROR: unmatched data size.') 300 return base_image[0 : pos[0]] + data + base_image[pos[1] + 1 :] 301 302 def get_size(self): 303 """ Gets size of current flash ROM """ 304 # TODO(hungte) Newer version of tool (flashrom) may support --get-size 305 # command which is faster in future. Right now we use back-compatible 306 # method: read whole and then get length. 307 image = self.read_whole() 308 return len(image) 309 310 def set_firmware_layout(self, file_name): 311 """get layout read from the BIOS """ 312 313 scraper = LayoutScraper(self.os_if) 314 self.firmware_layout = scraper.get_layout(file_name) 315 316 def enable_write_protect(self): 317 """Enable the write pretection of the flash chip.""" 318 cmd = 'flashrom %s --wp-enable' % self._target_command 319 self.os_if.run_shell_command(cmd) 320 321 def disable_write_protect(self): 322 """Disable the write pretection of the flash chip.""" 323 cmd = 'flashrom %s --wp-disable' % self._target_command 324 self.os_if.run_shell_command(cmd) 325 326 def read_whole(self): 327 """ 328 Reads whole flash ROM data. 329 Returns the data read from flash ROM, or empty string for other error. 330 """ 331 tmpfn = self._get_temp_filename('rd_') 332 cmd = 'flashrom %s -r "%s"' % (self._target_command, tmpfn) 333 self.os_if.log('flashrom_util.read_whole(): %s' % cmd) 334 self.os_if.run_shell_command(cmd) 335 result = self.os_if.read_file(tmpfn) 336 self.set_firmware_layout(tmpfn) 337 338 # clean temporary resources 339 self._remove_temp_file(tmpfn) 340 return result 341 342 def write_partial(self, base_image, write_list, write_layout_map=None): 343 """ 344 Writes data in sections of write_list to flash ROM. 345 An exception is raised if write operation fails. 346 """ 347 348 if write_layout_map: 349 layout_map = write_layout_map 350 else: 351 layout_map = self.firmware_layout 352 353 tmpfn = self._get_temp_filename('wr_') 354 self.os_if.write_file(tmpfn, base_image) 355 layout_fn = self._create_layout_file(layout_map) 356 357 cmd = 'flashrom %s -l "%s" -i %s -w "%s"' % ( 358 self._target_command, layout_fn, ' -i '.join(write_list), tmpfn) 359 self.os_if.log('flashrom.write_partial(): %s' % cmd) 360 self.os_if.run_shell_command(cmd) 361 362 # flashrom write will reboot the ec after corruption 363 # For Android, need to make sure ec is back online 364 # before continuing, or adb command will cause test failure 365 if self.os_if.is_android: 366 self.os_if.wait_for_device(60) 367 368 # clean temporary resources 369 self._remove_temp_file(tmpfn) 370 self._remove_temp_file(layout_fn) 371 372 def write_whole(self, base_image): 373 """Write the whole base image. """ 374 layout_map = { 'all': (0, len(base_image) - 1) } 375 self.write_partial(base_image, ('all',), layout_map) 376