1# Copyright 2020 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 5import logging, re 6import os 7import six 8 9from autotest_lib.client.common_lib import error 10from autotest_lib.client.common_lib import utils 11from autotest_lib.server.cros.faft.firmware_test import FirmwareTest 12 13 14class firmware_CsmeFwUpdate(FirmwareTest): 15 """ 16 This tests csme rw firmware update feature by changing the me_rw 17 image in firmware main regions with a different version 18 19 Accepted --args names: 20 old_bios = specify this argument to use a different bios 21 than shellball default for downgrade 22 23 """ 24 version = 1 25 ORIGINAL_BIOS = "/usr/local/tmp/bios_original.bin" 26 DOWNGRADE_BIOS = "/usr/local/tmp/bios_downgrade.bin" 27 # Region to use for flashrom wp-region commands 28 WP_REGION = 'WP_RO' 29 MODE = 'recovery' 30 CBFSTOOL = 'cbfstool' 31 CMPTOOL = 'cmp' 32 33 def initialize(self, host, cmdline_args, dev_mode = False): 34 # Parse arguments from command line 35 dict_args = utils.args_to_dict(cmdline_args) 36 super(firmware_CsmeFwUpdate, self).initialize(host, cmdline_args) 37 38 self.bios_input = None 39 self.restore_required = False 40 self.downgrade_bios = None 41 self.spi_bios = None 42 self._orig_sw_wp = None 43 self._original_hw_wp = None 44 arg_name = "old_bios" 45 arg_value = dict_args.get(arg_name) 46 if arg_value: 47 logging.info('%s=%s', arg_name, arg_value) 48 image_path = os.path.expanduser(arg_value) 49 if not os.path.isfile(image_path): 50 raise error.TestError( 51 "Specified file does not exist: %s=%s" 52 % (arg_name, image_path)) 53 self.bios_input = image_path 54 else: 55 logging.info("No bios specified. Using default " \ 56 "shellball bios for downgrade") 57 58 self.backup_firmware() 59 self.switcher.setup_mode('dev' if dev_mode else 'normal', 60 allow_gbb_force=True) 61 62 # Save write protect configuration and enable it 63 logging.info("Enabling Write protection") 64 self._orig_sw_wp = self.faft_client.bios.get_write_protect_status() 65 self._original_hw_wp = 'on' in self.servo.get('fw_wp_state') 66 self.set_ap_write_protect_and_reboot(False) 67 self.faft_client.bios.set_write_protect_region(self.WP_REGION, True) 68 self.set_ap_write_protect_and_reboot(True) 69 70 # Make sure that the shellball is retained over subsequent power cycles 71 self.blocking_sync() 72 73 def cleanup(self): 74 """ 75 Flash the backed up firmware at the end of test 76 77 """ 78 self.faft_client.system.remove_file(self.ORIGINAL_BIOS) 79 self.faft_client.system.remove_file(self.DOWNGRADE_BIOS) 80 self.set_ap_write_protect_and_reboot(False) 81 82 try: 83 if self.is_firmware_saved() and self.restore_required: 84 logging.info("Restoring Original Image") 85 self.restore_firmware() 86 except (EnvironmentError, six.moves.xmlrpc_client.Fault, 87 error.AutoservError, error.TestBaseException): 88 logging.error("Problem restoring firmware:", exc_info=True) 89 90 try: 91 # Restore the old write-protection value at the end of the test. 92 logging.info("Restoring write protection configuration") 93 if self._orig_sw_wp: 94 self.faft_client.bios.set_write_protect_range( 95 self._orig_sw_wp['start'], 96 self._orig_sw_wp['length'], 97 self._orig_sw_wp['enabled']) 98 except (EnvironmentError, six.moves.xmlrpc_client.Fault, 99 error.AutoservError, error.TestBaseException): 100 logging.error("Problem restoring software write-protect:", 101 exc_info = True) 102 103 if self._original_hw_wp is not None: 104 self.set_ap_write_protect_and_reboot(self._original_hw_wp) 105 106 self.switcher.mode_aware_reboot(reboot_type = 'cold') 107 super(firmware_CsmeFwUpdate, self).cleanup() 108 109 def read_current_bios_and_save(self): 110 """ 111 Dumps current bios from spi to two file.(working copy and backup) 112 113 @returns the working copy file path 114 115 """ 116 # Dump the current spi bios to file 117 self.spi_bios = self.ORIGINAL_BIOS 118 logging.info("Copying current bios image to %s for upgrade " \ 119 "test", self.spi_bios) 120 self.faft_client.bios.dump_whole(self.spi_bios) 121 122 # Get the downgrade bios image from user or from shellball 123 self.downgrade_bios = self.DOWNGRADE_BIOS 124 if self.bios_input: 125 logging.info("Copying user given bios image to %s for downgrade " \ 126 "test", self.downgrade_bios) 127 self._client.send_file(self.bios_input, self.downgrade_bios) 128 else: 129 logging.info("Copying bios image from update shellball to %s " \ 130 "for downgrade test", self.downgrade_bios) 131 self.faft_client.updater.extract_shellball() 132 cbfs_work_dir = self.faft_client.updater.cbfs_setup_work_dir() 133 shellball_bios = os.path.join(cbfs_work_dir, 134 self.faft_client.updater.get_bios_relative_path()) 135 command = "cp %s %s" % (shellball_bios, self.downgrade_bios) 136 self.faft_client.system.run_shell_command(command) 137 138 def check_fmap_format(self, image_path): 139 """ 140 Checks FMAP format used by the Image for CSME update 141 142 @param image_path: path of the image 143 @returns the fmap format string 144 145 """ 146 # Check if ME_RW_A is present in the image 147 logging.info("Checking if seperate CBFS is used for CSE RW in " \ 148 "image : %s", image_path) 149 command = "futility dump_fmap -F %s | grep ME_RW_A" % image_path 150 output = self.faft_client.system.run_shell_command_get_output( 151 command, True) 152 if output: 153 logging.info("Image uses seperate CBFS for CSE RW") 154 return "CSE_RW_SEPARATE_CBFS" 155 else: 156 return "DEFAULT" 157 158 def check_if_me_blob_exist_in_image(self, image_path): 159 """ 160 Checks if me_blob exists in FW MAIN section of an image 161 162 @param image_path: path of the image 163 @returns True if present else False 164 165 """ 166 # Check if me_rw.version present FW_MAIN region 167 logging.info("Checking if me_rw.version file " \ 168 "present in image : %s", image_path ) 169 command = "cbfstool %s print -r FW_MAIN_A " \ 170 "| grep me_rw.version" % image_path 171 output = self.faft_client.system.run_shell_command_get_output( 172 command, True) 173 if output: 174 available = True 175 logging.info("me_rw.version present in image") 176 else: 177 available = False 178 logging.info("me_rw.version not present in image") 179 180 return available 181 182 def extract_me_rw_version_from_bin(self, me_blob, version_offset = 0): 183 """ 184 Extract me_rw version from given me_rw blob. Version is first 8 185 bytes in the blob 186 187 @param me_blob: me_rw blob (old fmap) or me_rw.version blob 188 @param version_offset: version filed offset in the blob 189 @returns the CSME RW version string 190 191 """ 192 ver_res = "" 193 logging.info("Extracting version field from ME blob") 194 195 command = ("hexdump -C %s | cut -c 9- | cut -d'|' -f 2"%me_blob ) 196 output = self.faft_client.system.run_shell_command_get_output( 197 command, True) 198 ver_res = output[0].strip(".") 199 logging.info("Version : %s", ver_res) 200 return ver_res 201 202 def get_image_fwmain_me_rw_version(self, 203 bios, 204 region = "FW_MAIN_A"): 205 """ 206 Extract CSME RW version of the me_rw blob of the given 207 region in the given bios 208 209 @param bios: Bios path 210 @param region: region which contains me_rw blob 211 @returns the CSME RW version string 212 213 """ 214 # Extract me_rw.version and check version. 215 cbfs_name = "me_rw.version" 216 temp_dir = self.faft_client.system.create_temp_dir() 217 me_blob = os.path.join(temp_dir, cbfs_name) 218 219 cmd_status = self.faft_client.updater.cbfs_extract(cbfs_name, 220 '',(region, ), 221 me_blob,'x86',bios) 222 223 if cmd_status is None: 224 self.faft_client.system.remove_dir(temp_dir) 225 raise error.TestError("Failed to extract ME blob from " \ 226 "the given bios : %s" % bios) 227 228 version = self.extract_me_rw_version_from_bin(me_blob) 229 self.faft_client.system.remove_dir(temp_dir) 230 return version 231 232 def get_current_me_rw_version(self): 233 """ 234 Reads the current active CSME RW Version from coreboot logs 235 236 @returns the CSME RW version string 237 238 """ 239 logging.info("Extracting cselite version info from coreboot logs") 240 command = "cbmem -1 | grep 'cse_lite:'" 241 output = self.faft_client.system.run_shell_command_get_output( 242 command, True) 243 logging.info(output) 244 # Offset of rw portion in ME region 245 me_cse_rw_info = re.search(r"(cse_lite: RW version = )" \ 246 "([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*)","".join(output)) 247 248 if me_cse_rw_info: 249 me_version = me_cse_rw_info.group(2) 250 else: 251 raise error.TestError("cse_lite RW info not" 252 " found in coreboot logs!") 253 return me_version 254 255 def verify_me_version(self, expected_version, expected_slot): 256 """ 257 Reads the current active CSME RW Version from coreboot logs 258 and compares with expected version 259 260 @param expected_version: Expected CSME RW Version string 261 @returns True is matching else False 262 263 """ 264 me_version = self.get_current_me_rw_version() 265 command = "crossystem mainfw_act" 266 output = self.faft_client.system.run_shell_command_get_output( 267 command, True) 268 main_fw_act = output[0] 269 270 logging.info("Expected mainfw_act : %s\n" \ 271 "Current mainfw_act : %s\n" \ 272 "Expected ME RW Version : %s\n" \ 273 "Current ME RW Version : %s\n", 274 expected_slot, main_fw_act, expected_version, me_version) 275 276 if (expected_version not in me_version) or \ 277 (expected_slot not in main_fw_act): 278 return False 279 else: 280 return True 281 282 def cmp_local_files(self, 283 local_filename_1, 284 local_filename_2): 285 """ 286 Compare two local files 287 288 @param local_filename_1: Path to first local file to compare 289 @param local_filename_2: Path to second local file to compare 290 291 @returns "None" if files are identical, or 292 string response from "cmp" command if files differ. 293 """ 294 compare_cmd = ('%s %s %s' % 295 (self.CMPTOOL, local_filename_1, local_filename_2)) 296 try: 297 return self.faft_client.system.run_shell_command_get_output( 298 compare_cmd, True) 299 except error.CmdError: 300 # already logged by run_shell_command() 301 return None 302 303 def cbfs_read(self, 304 filename, 305 extension, 306 region='ME_RW_A', 307 local_filename=None, 308 arch=None, 309 bios=None): 310 """ 311 Reads an arbitrary file from cbfs. 312 313 @param filename: Filename in cbfs, including extension 314 @param extension: Extension of the file, including '.' 315 @param region: region (the default is just 'ME_RW_A') 316 @param local_filename: Path to use on the DUT, overriding the default in 317 the cbfs work dir. 318 @param arch: Specific machine architecture to extract (default unset) 319 @param bios: Image from which the cbfs file to be read 320 @return: The full path of the read file, or None 321 """ 322 if bios is None: 323 bios = os.path.join(self._cbfs_work_path, self._bios_path) 324 325 cbfs_filename = filename + extension 326 if local_filename is None: 327 local_filename = os.path.join(self._cbfs_work_path, 328 filename + extension) 329 330 extract_cmd = ('%s %s extract -r %s -n %s%s -f %s' % 331 (self.CBFSTOOL, bios, region, filename, 332 extension, local_filename)) 333 if arch: 334 extract_cmd += ' -m %s' % arch 335 336 try: 337 self.faft_client.system.run_shell_command(extract_cmd) 338 return os.path.abspath(local_filename) 339 except error.CmdError: 340 # already logged by run_shell_command() 341 return None 342 343 def abort_if_me_rw_blobs_identical(self, 344 downgrade_bios, 345 spi_bios): 346 """ 347 Determine if the CSME RW blob in the downgrade bios image is 348 different from the CSME RW blob in the spi bios image. 349 350 @param downgrade_bios: Downgrade bios path 351 @param spi_bios: Bios from spi flash path 352 @returns "None" if CSME RW blobs are identical, or 353 string response from "diff" command if blobs differ. 354 """ 355 # Extract me_rw blobs 356 cbfs_name = "me_rw" 357 downgrade_name = "me_rw" 358 spi_me_a_name = "me_rw_spi_a" 359 spi_me_b_name = "me_rw_spi_b" 360 temp_dir = self.faft_client.system.create_temp_dir() 361 downgrade_me_blob = os.path.join(temp_dir, downgrade_name) 362 spi_me_a_blob = os.path.join(temp_dir, spi_me_a_name) 363 spi_me_b_blob = os.path.join(temp_dir, spi_me_b_name) 364 365 downgrade_rw_path = self.cbfs_read(cbfs_name, '','ME_RW_A', 366 downgrade_me_blob, 'x86', 367 downgrade_bios) 368 if downgrade_rw_path is None: 369 self.faft_client.system.remove_dir(temp_dir) 370 raise error.TestError("Failed to read %s me_rw blob from " \ 371 "the downgrade bios %s" % (downgrade_me_blob, 372 downgrade_bios)) 373 374 spi_rw_a_path = self.cbfs_read(cbfs_name, '','ME_RW_A', spi_me_a_blob, 375 'x86', spi_bios) 376 if spi_rw_a_path is None: 377 self.faft_client.system.remove_dir(temp_dir) 378 raise error.TestError("Failed to read %s me_rw_a blob from " \ 379 "the downgrade bios %s" % (spi_me_a_blob, 380 spi_bios)) 381 382 spi_rw_b_path = self.cbfs_read(cbfs_name, '','ME_RW_B', spi_me_b_blob, 383 'x86', spi_bios) 384 if spi_rw_b_path is None: 385 self.faft_client.system.remove_dir(temp_dir) 386 raise error.TestError("Failed to read %s me_rw_b blob from " \ 387 "the spi bios %s" % (spi_me_b_blob, spi_bios)) 388 389 # Are the blobs different? 390 diff_a = self.cmp_local_files(downgrade_rw_path, spi_rw_a_path) 391 diff_b = self.cmp_local_files(downgrade_rw_path, spi_rw_b_path) 392 if diff_a and diff_b: 393 logging.info("CSME RW version is same, but downgrade me_rw " \ 394 "differs from both me_rw blobs in spi flash.") 395 elif diff_a: 396 logging.info("CSME RW version is same, but downgrade me_rw and " \ 397 "FW_MAIN_A me_rw differ.") 398 elif diff_b: 399 logging.info("CSME RW version is same, but downgrade me_rw and " \ 400 "FW_MAIN_B me_rw differ.") 401 else: 402 # Blobs are the same 403 self.faft_client.system.remove_dir(temp_dir) 404 raise error.TestNAError("CSME RW blobs are the same in downgrade " \ 405 "and spi bios. Test skipped.") 406 407 self.faft_client.system.remove_dir(temp_dir) 408 409 def prepare_shellball(self, bios_image, append = None): 410 """Prepare a shellball with the given bios image. 411 412 @param bios_image: bios image with shellball to be created 413 @param append: string to be updated with shellball name 414 """ 415 logging.info("Preparing shellball with %s", bios_image) 416 self.faft_client.updater.reset_shellball() 417 # Copy the given bois to shellball 418 extract_dir = self.faft_client.updater.get_work_path() 419 bios_rel = self.faft_client.updater.get_bios_relative_path() 420 bios_shell = os.path.join(extract_dir, bios_rel) 421 command = "cp %s %s" % (bios_image, bios_shell) 422 output = self.faft_client.system.run_shell_command_get_output( 423 command, True) 424 if output: 425 raise error.TestError("File not found!: %s" % bios_image) 426 # Reload and repack the shellball 427 self.faft_client.updater.reload_images() 428 self.faft_client.updater.repack_shellball(append) 429 430 def run_shellball(self, append): 431 """Run chromeos-firmwareupdate 432 433 @param append: additional piece to add to shellball name 434 """ 435 436 # make sure we restore firmware after the test, if it tried to flash. 437 self.restore_required = True 438 439 # Update only host firmware 440 options = ['--host_only', '--wp=1'] 441 logging.info("Updating RW firmware using " \ 442 "chromeos_firmwareupdate") 443 logging.info( 444 "Update command : chromeos_firmwareupdate-%s --mode=%s " 445 " %s", append, self.MODE, ' '.join(options)) 446 result = self.run_chromeos_firmwareupdate( 447 self.MODE, append, options, ignore_status = True) 448 449 if result.exit_status == 255: 450 raise error.TestError("DUT network dropped during update.") 451 elif result.exit_status != 0: 452 if ('Good. It seems nothing was changed.' in result.stdout): 453 logging.info("DUT already matched the image; updater aborted.") 454 else: 455 raise error.TestError("Firmware updater unexpectedly" \ 456 "failed (rc=%s)" % result.exit_status) 457 458 def run_once(self): 459 if not ('x86' in self.faft_config.ec_capability): 460 raise error.TestNAError("The firmware_CsmeFwUpdate test is only " \ 461 "applicable to Intel platforms. Skipping " \ 462 "test.") 463 464 # Read current bios from SPI and create a backup copy 465 self.read_current_bios_and_save() 466 467 if not self.check_if_me_blob_exist_in_image(self.spi_bios): 468 raise error.TestNAError("The me_rw blob is not present in the " \ 469 "current bios. Skipping test.") 470 471 # Check fmap scheme of the bios read from SPI 472 spi_bios_fmap_ver = self.check_fmap_format(self.spi_bios) 473 474 # Check fmap scheme of the default bios in shellball 475 downgrade_bios_fmap = self.check_fmap_format(self.downgrade_bios) 476 477 # Check if me_rw blob is present in FW_MAIN 478 if not self.check_if_me_blob_exist_in_image(self.downgrade_bios): 479 raise error.TestNAError("Test setup issue : me_rw blob is not " \ 480 "present in downgrade bios.") 481 482 # Check if both of the bios versions use same fmap structure for me_rw 483 if downgrade_bios_fmap not in spi_bios_fmap_ver: 484 raise error.TestError("Test setup issue : FMAP format is " \ 485 "different in current and downgrade bios.") 486 487 # Get the version of me_rw in the downgrade bios 488 downgrade_me_version = self.get_image_fwmain_me_rw_version( \ 489 self.downgrade_bios) 490 491 # Get the version of me_rw in the spi bios 492 spi_me_version = self.get_image_fwmain_me_rw_version(self.spi_bios) 493 494 # Get active CSME RW version from cbmem -1 495 active_csme_rw_version = self.get_current_me_rw_version() 496 497 logging.info("Active CSME RW Version : %s\n" \ 498 "FW main CSME RW Version SPI Image : %s\n" \ 499 "FW main CSME RW Version downgrade Image: %s\n", 500 active_csme_rw_version, spi_me_version, 501 downgrade_me_version) 502 503 # Abort if downgrade me_rw version is same as spi me_rw version 504 if (spi_me_version in downgrade_me_version): 505 # Version is the same, abort test if blob content is the same 506 self.abort_if_me_rw_blobs_identical(self.downgrade_bios, 507 self.spi_bios) 508 509 for slot in ["A", "B"]: 510 operation = "downgrade" 511 # Create a shellball with downgrade bios 512 self.prepare_shellball(self.downgrade_bios, operation) 513 514 logging.info("Downgrading RW section. Downgrade ME " \ 515 "Version: %s", downgrade_me_version) 516 # Run firmware updater downgrade the bios RW 517 self.run_shellball(operation) 518 519 # Set fw_try_next to slot and reboot to trigger csme update 520 logging.info("Setting fw_try_next to %s: ", slot) 521 self.faft_client.system.set_fw_try_next(slot) 522 self.switcher.mode_aware_reboot(reboot_type = 'cold') 523 524 # Check if the Active CSME RW version changed to downgrade version 525 if not self.verify_me_version(downgrade_me_version, slot): 526 raise error.TestError("CSME RW Downgrade using " 527 "FW_MAIN_%s is Failed!" % slot) 528 logging.info("CSME RW Downgrade using FW_MAIN_%s is " 529 "successful", slot) 530 531 operation = "upgrade" 532 # Create a shellball with the original spi bios 533 self.prepare_shellball(self.spi_bios, operation) 534 535 logging.info("Upgrading RW Section. Upgrade ME " \ 536 "Version: %s", spi_me_version) 537 # Run firmware updater and update RW section with shellball 538 self.run_shellball(operation) 539 540 # Set fw_try_next to slot and reboot to trigger csme update 541 logging.info("Setting fw_try_next to %s: ", slot) 542 self.faft_client.system.set_fw_try_next(slot) 543 self.switcher.mode_aware_reboot(reboot_type = 'cold') 544 545 # Check if the Active CSME RW version changed to original version 546 if not self.verify_me_version(spi_me_version, slot): 547 raise error.TestError("CSME RW Upgrade using " 548 "FW_MAIN_%s is Failed!" % slot) 549 logging.info("CSME RW Upgrade using FW_MAIN_%s is " 550 "successful", slot) 551