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