1 # Copyright (c) 2012 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 """A module to support automatic firmware update. 6 7 See FirmwareUpdater object below. 8 """ 9 10 import os 11 import re 12 13 from autotest_lib.client.common_lib.cros import chip_utils 14 from autotest_lib.client.cros.faft.utils import (common, 15 flashrom_handler, 16 saft_flashrom_util, 17 shell_wrapper) 18 19 20 class FirmwareUpdaterError(Exception): 21 """Error in the FirmwareUpdater module.""" 22 23 24 class FirmwareUpdater(object): 25 """An object to support firmware update. 26 27 This object will create a temporary directory in /var/tmp/faft/autest with 28 two subdirectory keys/ and work/. You can modify the keys in keys/ 29 directory. If you want to provide a given shellball to do firmware update, 30 put shellball under /var/tmp/faft/autest with name chromeos-firmwareupdate. 31 """ 32 33 DAEMON = 'update-engine' 34 CBFSTOOL = 'cbfstool' 35 HEXDUMP = 'hexdump -v -e \'1/1 "0x%02x\\n"\'' 36 37 def __init__(self, os_if): 38 self.os_if = os_if 39 self._temp_path = '/var/tmp/faft/autest' 40 self._cbfs_work_path = os.path.join(self._temp_path, 'cbfs') 41 self._keys_path = os.path.join(self._temp_path, 'keys') 42 self._work_path = os.path.join(self._temp_path, 'work') 43 self._bios_path = 'bios.bin' 44 self._ec_path = 'ec.bin' 45 pubkey_path = os.path.join(self._keys_path, 'root_key.vbpubk') 46 self._bios_handler = common.LazyInitHandlerProxy( 47 flashrom_handler.FlashromHandler, 48 saft_flashrom_util, 49 os_if, 50 pubkey_path, 51 self._keys_path, 52 'bios') 53 self._ec_handler = common.LazyInitHandlerProxy( 54 flashrom_handler.FlashromHandler, 55 saft_flashrom_util, 56 os_if, 57 pubkey_path, 58 self._keys_path, 59 'ec') 60 61 # _detect_image_paths always needs to run during initialization 62 # or after extract_shellball is called. 63 # 64 # If we are setting up the temp dir from scratch, we'll transitively 65 # call _detect_image_paths since extract_shellball is called. 66 # Otherwise, we need to scan the existing temp directory. 67 if not self.os_if.is_dir(self._temp_path): 68 self._setup_temp_dir() 69 else: 70 self._detect_image_paths() 71 72 def _setup_temp_dir(self): 73 """Setup temporary directory. 74 75 Devkeys are copied to _key_path. Then, shellball (default: 76 /usr/sbin/chromeos-firmwareupdate) is extracted to _work_path. 77 """ 78 self.cleanup_temp_dir() 79 80 self.os_if.create_dir(self._temp_path) 81 self.os_if.create_dir(self._cbfs_work_path) 82 self.os_if.create_dir(self._work_path) 83 self.os_if.copy_dir('/usr/share/vboot/devkeys', self._keys_path) 84 85 original_shellball = '/usr/sbin/chromeos-firmwareupdate' 86 working_shellball = os.path.join(self._temp_path, 87 'chromeos-firmwareupdate') 88 self.os_if.copy_file(original_shellball, working_shellball) 89 self.extract_shellball() 90 91 def cleanup_temp_dir(self): 92 """Cleanup temporary directory.""" 93 if self.os_if.is_dir(self._temp_path): 94 self.os_if.remove_dir(self._temp_path) 95 96 def stop_daemon(self): 97 """Stop update-engine daemon.""" 98 self.os_if.log('Stopping %s...' % self.DAEMON) 99 cmd = 'status %s | grep stop || stop %s' % (self.DAEMON, self.DAEMON) 100 self.os_if.run_shell_command(cmd) 101 102 def start_daemon(self): 103 """Start update-engine daemon.""" 104 self.os_if.log('Starting %s...' % self.DAEMON) 105 cmd = 'status %s | grep start || start %s' % (self.DAEMON, self.DAEMON) 106 self.os_if.run_shell_command(cmd) 107 108 def retrieve_fwid(self): 109 """Retrieve shellball's fwid tuple. 110 111 This method should be called after _setup_temp_dir. 112 113 Returns: 114 Shellball's fwid tuple (ro_fwid, rw_fwid). 115 """ 116 self._bios_handler.new_image( 117 os.path.join(self._work_path, self._bios_path)) 118 # Remove the tailing null characters 119 ro_fwid = self._bios_handler.get_section_fwid('ro').rstrip('\0') 120 rw_fwid = self._bios_handler.get_section_fwid('a').rstrip('\0') 121 return (ro_fwid, rw_fwid) 122 123 def retrieve_ecid(self): 124 """Retrieve shellball's ecid. 125 126 This method should be called after _setup_temp_dir. 127 128 Returns: 129 Shellball's ecid. 130 """ 131 self._ec_handler.new_image( 132 os.path.join(self._work_path, self._ec_path)) 133 fwid = self._ec_handler.get_section_fwid('rw') 134 # Remove the tailing null characters 135 return fwid.rstrip('\0') 136 137 def retrieve_ec_hash(self): 138 """Retrieve the hex string of the EC hash.""" 139 return self._ec_handler.get_section_hash('rw') 140 141 def modify_ecid_and_flash_to_bios(self): 142 """Modify ecid, put it to AP firmware, and flash it to the system. 143 144 This method is used for testing EC software sync for EC EFS (Early 145 Firmware Selection). It creates a slightly different EC RW image 146 (a different EC fwid) in AP firmware, in order to trigger EC 147 software sync on the next boot (a different hash with the original 148 EC RW). 149 150 The steps of this method: 151 * Modify the EC fwid by appending a '~', like from 152 'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'. 153 * Resign the EC image. 154 * Store the modififed EC RW image to CBFS component 'ecrw' of the 155 AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash. 156 * Resign the AP image. 157 * Flash the modified AP image back to the system. 158 """ 159 self.cbfs_setup_work_dir() 160 161 fwid = self.retrieve_ecid() 162 if fwid.endswith('~'): 163 raise FirmwareUpdaterError('The EC fwid is already modified') 164 165 # Modify the EC FWID and resign 166 fwid = fwid[:-1] + '~' 167 self._ec_handler.set_section_fwid('rw', fwid) 168 self._ec_handler.resign_ec_rwsig() 169 170 # Replace ecrw to the new one 171 ecrw_bin_path = os.path.join(self._cbfs_work_path, 172 chip_utils.ecrw.cbfs_bin_name) 173 self._ec_handler.dump_section_body('rw', ecrw_bin_path) 174 175 # Replace ecrw.hash to the new one 176 ecrw_hash_path = os.path.join(self._cbfs_work_path, 177 chip_utils.ecrw.cbfs_hash_name) 178 with open(ecrw_hash_path, 'w') as f: 179 f.write(self.retrieve_ec_hash()) 180 181 # Store the modified ecrw and its hash to cbfs 182 self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='') 183 184 # Resign and flash the AP firmware back to the system 185 self.cbfs_sign_and_flash() 186 187 def resign_firmware(self, version=None, work_path=None): 188 """Resign firmware with version. 189 190 Args: 191 version: new firmware version number, default to no modification. 192 work_path: work path, default to the updater work path. 193 """ 194 if work_path is None: 195 work_path = self._work_path 196 self.os_if.run_shell_command( 197 '/usr/share/vboot/bin/resign_firmwarefd.sh ' 198 '%s %s %s %s %s %s %s %s' % ( 199 os.path.join(work_path, self._bios_path), 200 os.path.join(self._temp_path, 'output.bin'), 201 os.path.join(self._keys_path, 'firmware_data_key.vbprivk'), 202 os.path.join(self._keys_path, 'firmware.keyblock'), 203 os.path.join(self._keys_path, 204 'dev_firmware_data_key.vbprivk'), 205 os.path.join(self._keys_path, 'dev_firmware.keyblock'), 206 os.path.join(self._keys_path, 'kernel_subkey.vbpubk'), 207 ('%d' % version) if version is not None else '')) 208 self.os_if.copy_file('%s' % os.path.join(self._temp_path, 'output.bin'), 209 '%s' % os.path.join( 210 work_path, self._bios_path)) 211 212 def _detect_image_paths(self): 213 """Scans shellball to find correct bios and ec image paths.""" 214 def _extract_path_from_match(match_result, model): 215 """Extract a path from a matched line of setvars.sh. 216 217 Args: 218 match_result: Match object: group 1 contains the quoted filename. 219 model: Name of model to use to resolve ${MODEL_DIR} in the filename. 220 221 Returns: 222 pathname to firmware file (e.g. 'models/grunt/bios.bin'). 223 """ 224 pathname = match_result.group(1).replace('"', '') 225 pathname = pathname.replace('${MODEL_DIR}', 'models/' + model) 226 return pathname 227 228 model_result = self.os_if.run_shell_command_get_output( 229 'mosys platform model') 230 if model_result: 231 model = model_result[0] 232 search_path = os.path.join( 233 self._work_path, 'models', model, 'setvars.sh') 234 grep_result = self.os_if.run_shell_command_get_output( 235 'grep IMAGE_MAIN= %s' % search_path) 236 if grep_result: 237 match = re.match('IMAGE_MAIN=(.*)', grep_result[0]) 238 if match: 239 self._bios_path = _extract_path_from_match(match, model) 240 grep_result = self.os_if.run_shell_command_get_output( 241 'grep IMAGE_EC= %s' % search_path) 242 if grep_result: 243 match = re.match('IMAGE_EC=(.*)', grep_result[0]) 244 if match: 245 self._ec_path = _extract_path_from_match(match, model) 246 247 def _update_target_fwid(self): 248 """Update target fwid/ecid in the setvars.sh.""" 249 model_result = self.os_if.run_shell_command_get_output( 250 'mosys platform model') 251 if model_result: 252 model = model_result[0] 253 setvars_path = os.path.join( 254 self._work_path, 'models', model, 'setvars.sh') 255 if self.os_if.path_exists(setvars_path): 256 ro_fwid, rw_fwid = self.retrieve_fwid() 257 args = ['-i'] 258 args.append( 259 '"s/TARGET_FWID=\\".*\\"/TARGET_FWID=\\"%s\\"/g"' 260 % rw_fwid) 261 args.append(setvars_path) 262 cmd = 'sed %s' % ' '.join(args) 263 self.os_if.run_shell_command(cmd) 264 265 args = ['-i'] 266 args.append( 267 '"s/TARGET_RO_FWID=\\".*\\"/TARGET_RO_FWID=\\"%s\\"/g"' 268 % ro_fwid) 269 args.append(setvars_path) 270 cmd = 'sed %s' % ' '.join(args) 271 self.os_if.run_shell_command(cmd) 272 273 # Only update ECID if an EC image is found 274 if self.get_ec_relative_path(): 275 ecid = self.retrieve_ecid() 276 args = ['-i'] 277 args.append( 278 '"s/TARGET_ECID=\\".*\\"/TARGET_ECID=\\"%s\\"/g"' 279 % ecid) 280 args.append(setvars_path) 281 cmd = 'sed %s' % ' '.join(args) 282 self.os_if.run_shell_command(cmd) 283 284 def extract_shellball(self, append=None): 285 """Extract the working shellball. 286 287 Args: 288 append: decide which shellball to use with format 289 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate' 290 if append is None. 291 """ 292 working_shellball = os.path.join(self._temp_path, 293 'chromeos-firmwareupdate') 294 if append: 295 working_shellball = working_shellball + '-%s' % append 296 297 self.os_if.run_shell_command('sh %s --sb_extract %s' % ( 298 working_shellball, self._work_path)) 299 300 self._detect_image_paths() 301 302 def repack_shellball(self, append=None): 303 """Repack shellball with new fwid. 304 305 New fwid follows the rule: [orignal_fwid]-[append]. 306 307 Args: 308 append: save the new shellball with a suffix, for example, 309 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate' 310 if append is None. 311 """ 312 self._update_target_fwid(); 313 314 working_shellball = os.path.join(self._temp_path, 315 'chromeos-firmwareupdate') 316 if append: 317 self.os_if.copy_file(working_shellball, 318 working_shellball + '-%s' % append) 319 working_shellball = working_shellball + '-%s' % append 320 321 self.os_if.run_shell_command('sh %s --sb_repack %s' % ( 322 working_shellball, self._work_path)) 323 324 if append: 325 args = ['-i'] 326 args.append( 327 '"s/TARGET_FWID=\\"\\(.*\\)\\"/TARGET_FWID=\\"\\1.%s\\"/g"' 328 % append) 329 args.append(working_shellball) 330 cmd = 'sed %s' % ' '.join(args) 331 self.os_if.run_shell_command(cmd) 332 333 args = ['-i'] 334 args.append('"s/TARGET_UNSTABLE=\\".*\\"/TARGET_UNSTABLE=\\"\\"/g"') 335 args.append(working_shellball) 336 cmd = 'sed %s' % ' '.join(args) 337 self.os_if.run_shell_command(cmd) 338 339 def run_firmwareupdate(self, mode, updater_append=None, options=[]): 340 """Do firmwareupdate with updater in temp_dir. 341 342 Args: 343 updater_append: decide which shellball to use with format 344 chromeos-firmwareupdate-[append]. Use'chromeos-firmwareupdate' 345 if updater_append is None. 346 mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'... 347 options: ex. ['--noupdate_ec', '--force'] or [] for 348 no option. 349 """ 350 if updater_append: 351 updater = os.path.join( 352 self._temp_path, 'chromeos-firmwareupdate-%s' % updater_append) 353 else: 354 updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate') 355 command = '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options)) 356 357 if mode == 'bootok': 358 # Since CL:459837, bootok is moved to chromeos-setgoodfirmware. 359 new_command = '/usr/sbin/chromeos-setgoodfirmware' 360 command = 'if [ -e %s ]; then %s; else %s; fi' % ( 361 new_command, new_command, command) 362 363 self.os_if.run_shell_command(command) 364 365 def cbfs_setup_work_dir(self): 366 """Sets up cbfs on DUT. 367 368 Finds bios.bin on the DUT and sets up a temp dir to operate on 369 bios.bin. If a bios.bin was specified, it is copied to the DUT 370 and used instead of the native bios.bin. 371 372 Returns: 373 The cbfs work directory path. 374 """ 375 376 self.os_if.remove_dir(self._cbfs_work_path) 377 self.os_if.copy_dir(self._work_path, self._cbfs_work_path) 378 379 return self._cbfs_work_path 380 381 def cbfs_extract_chip(self, fw_name, extension='.bin'): 382 """Extracts chip firmware blob from cbfs. 383 384 For a given chip type, looks for the corresponding firmware 385 blob and hash in the specified bios. The firmware blob and 386 hash are extracted into self._cbfs_work_path. 387 388 The extracted blobs will be <fw_name><extension> and 389 <fw_name>.hash located in cbfs_work_path. 390 391 Args: 392 fw_name: Chip firmware name to be extracted. 393 extension: Extension of the name of the cbfs component. 394 395 Returns: 396 Boolean success status. 397 """ 398 399 bios = os.path.join(self._cbfs_work_path, self._bios_path) 400 fw = fw_name 401 cbfs_extract = '%s %s extract -r FW_MAIN_A -n %s%%s -f %s%%s' % ( 402 self.CBFSTOOL, 403 bios, 404 fw, 405 os.path.join(self._cbfs_work_path, fw)) 406 407 cmd = cbfs_extract % (extension, extension) 408 if self.os_if.run_shell_command_get_status(cmd) != 0: 409 return False 410 411 cmd = cbfs_extract % ('.hash', '.hash') 412 if self.os_if.run_shell_command_get_status(cmd) != 0: 413 return False 414 415 return True 416 417 def cbfs_get_chip_hash(self, fw_name): 418 """Returns chip firmware hash blob. 419 420 For a given chip type, returns the chip firmware hash blob. 421 Before making this request, the chip blobs must have been 422 extracted from cbfs using cbfs_extract_chip(). 423 The hash data is returned as hexadecimal string. 424 425 Args: 426 fw_name: 427 Chip firmware name whose hash blob to get. 428 429 Returns: 430 Boolean success status. 431 432 Raises: 433 shell_wrapper.ShellError: Underlying remote shell 434 operations failed. 435 """ 436 437 hexdump_cmd = '%s %s.hash' % ( 438 self.HEXDUMP, 439 os.path.join(self._cbfs_work_path, fw_name)) 440 hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd) 441 return hashblob 442 443 def cbfs_replace_chip(self, fw_name, extension='.bin'): 444 """Replaces chip firmware in CBFS (bios.bin). 445 446 For a given chip type, replaces its firmware blob and hash in 447 bios.bin. All files referenced are expected to be in the 448 directory set up using cbfs_setup_work_dir(). 449 450 Args: 451 fw_name: Chip firmware name to be replaced. 452 extension: Extension of the name of the cbfs component. 453 454 Returns: 455 Boolean success status. 456 457 Raises: 458 shell_wrapper.ShellError: Underlying remote shell 459 operations failed. 460 """ 461 462 bios = os.path.join(self._cbfs_work_path, self._bios_path) 463 rm_hash_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s.hash' % ( 464 self.CBFSTOOL, bios, fw_name) 465 rm_bin_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s%s' % ( 466 self.CBFSTOOL, bios, fw_name, extension) 467 expand_cmd = '%s %s expand -r FW_MAIN_A,FW_MAIN_B' % ( 468 self.CBFSTOOL, bios) 469 add_hash_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c none ' 470 '-f %s.hash -n %s.hash') % ( 471 self.CBFSTOOL, 472 bios, 473 os.path.join(self._cbfs_work_path, fw_name), 474 fw_name) 475 add_bin_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c lzma ' 476 '-f %s%s -n %s%s') % ( 477 self.CBFSTOOL, 478 bios, 479 os.path.join(self._cbfs_work_path, fw_name), 480 extension, 481 fw_name, 482 extension) 483 truncate_cmd = '%s %s truncate -r FW_MAIN_A,FW_MAIN_B' % ( 484 self.CBFSTOOL, bios) 485 486 self.os_if.run_shell_command(rm_hash_cmd) 487 self.os_if.run_shell_command(rm_bin_cmd) 488 try: 489 self.os_if.run_shell_command(expand_cmd) 490 except shell_wrapper.ShellError: 491 self.os_if.log(('%s may be too old, ' 492 'continuing without "expand" support') % 493 self.CBFSTOOL) 494 495 self.os_if.run_shell_command(add_hash_cmd) 496 self.os_if.run_shell_command(add_bin_cmd) 497 try: 498 self.os_if.run_shell_command(truncate_cmd) 499 except shell_wrapper.ShellError: 500 self.os_if.log(('%s may be too old, ' 501 'continuing without "truncate" support') % 502 self.CBFSTOOL) 503 504 return True 505 506 def cbfs_sign_and_flash(self): 507 """Signs CBFS (bios.bin) and flashes it.""" 508 self.resign_firmware(work_path=self._cbfs_work_path) 509 self._bios_handler.new_image( 510 os.path.join(self._cbfs_work_path, self._bios_path)) 511 self._bios_handler.write_whole() 512 return True 513 514 def get_temp_path(self): 515 """Get temp directory path.""" 516 return self._temp_path 517 518 def get_keys_path(self): 519 """Get keys directory path.""" 520 return self._keys_path 521 522 def get_cbfs_work_path(self): 523 """Get cbfs work directory path.""" 524 return self._cbfs_work_path 525 526 def get_work_path(self): 527 """Get work directory path.""" 528 return self._work_path 529 530 def get_bios_relative_path(self): 531 """Gets the relative path of the bios image in the shellball.""" 532 return self._bios_path 533 534 def get_ec_relative_path(self): 535 """Gets the relative path of the ec image in the shellball.""" 536 return self._ec_path 537