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 xmlrpclib 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 31 def initialize(self, host, cmdline_args, dev_mode = False): 32 # Parse arguments from command line 33 dict_args = utils.args_to_dict(cmdline_args) 34 super(firmware_CsmeFwUpdate, self).initialize(host, cmdline_args) 35 36 self.bios_input = None 37 self.restore_required = False 38 self.downgrade_bios = None 39 self.spi_bios = None 40 self._orig_sw_wp = None 41 self._original_hw_wp = None 42 arg_name = "old_bios" 43 arg_value = dict_args.get(arg_name) 44 if arg_value: 45 logging.info('%s=%s', arg_name, arg_value) 46 image_path = os.path.expanduser(arg_value) 47 if not os.path.isfile(image_path): 48 raise error.TestError( 49 "Specified file does not exist: %s=%s" 50 % (arg_name, image_path)) 51 self.bios_input = image_path 52 else: 53 logging.info("No bios specified. Using default " \ 54 "shellball bios for downgrade") 55 56 self.backup_firmware() 57 self.switcher.setup_mode('dev' if dev_mode else 'normal') 58 59 # Save write protect configuration and enable it 60 logging.info("Enabling Write protection") 61 self._orig_sw_wp = self.faft_client.bios.get_write_protect_status() 62 self._original_hw_wp = 'on' in self.servo.get('fw_wp_state') 63 self.set_ap_write_protect_and_reboot(False) 64 self.faft_client.bios.set_write_protect_region(self.WP_REGION, True) 65 self.set_ap_write_protect_and_reboot(True) 66 67 # Make sure that the shellball is retained over subsequent power cycles 68 self.blocking_sync() 69 70 def cleanup(self): 71 """ 72 Flash the backed up firmware at the end of test 73 74 """ 75 self.faft_client.system.remove_file(self.ORIGINAL_BIOS) 76 self.faft_client.system.remove_file(self.DOWNGRADE_BIOS) 77 self.set_ap_write_protect_and_reboot(False) 78 79 try: 80 if self.is_firmware_saved() and self.restore_required: 81 logging.info("Restoring Original Image") 82 self.restore_firmware() 83 except (EnvironmentError, xmlrpclib.Fault, 84 error.AutoservError, error.TestBaseException): 85 logging.error("Problem restoring firmware:", exc_info=True) 86 87 try: 88 # Restore the old write-protection value at the end of the test. 89 logging.info("Restoring write protection configuration") 90 if self._orig_sw_wp: 91 self.faft_client.bios.set_write_protect_range( 92 self._orig_sw_wp['start'], 93 self._orig_sw_wp['length'], 94 self._orig_sw_wp['enabled']) 95 except (EnvironmentError, xmlrpclib.Fault, 96 error.AutoservError, error.TestBaseException): 97 logging.error("Problem restoring software write-protect:", 98 exc_info = True) 99 100 if self._original_hw_wp is not None: 101 self.set_ap_write_protect_and_reboot(self._original_hw_wp) 102 103 self.switcher.mode_aware_reboot(reboot_type = 'cold') 104 super(firmware_CsmeFwUpdate, self).cleanup() 105 106 def read_current_bios_and_save(self): 107 """ 108 Dumps current bios from spi to two file.(working copy and backup) 109 110 @returns the working copy file path 111 112 """ 113 # Dump the current spi bios to file 114 self.spi_bios = self.ORIGINAL_BIOS 115 logging.info("Copying current bios image to %s for upgrade " \ 116 "test" % self.spi_bios) 117 self.faft_client.bios.dump_whole(self.spi_bios) 118 119 # Get the downgrade bios image from user or from shellball 120 self.downgrade_bios = self.DOWNGRADE_BIOS 121 if self.bios_input: 122 logging.info("Copying user given bios image to %s for downgrade " \ 123 "test" % self.downgrade_bios) 124 self._client.send_file(self.bios_input, self.downgrade_bios) 125 else: 126 logging.info("Copying bios image from update shellball to %s " \ 127 "for downgrade test" % self.downgrade_bios) 128 self.faft_client.updater.extract_shellball() 129 cbfs_work_dir = self.faft_client.updater.cbfs_setup_work_dir() 130 shellball_bios = os.path.join(cbfs_work_dir, 131 self.faft_client.updater.get_bios_relative_path()) 132 command = "cp %s %s" % (shellball_bios, self.downgrade_bios) 133 self.faft_client.system.run_shell_command(command) 134 135 def check_fmap_format(self, image_path): 136 """ 137 Checks FMAP format used by the Image for CSME update 138 139 @param image_path: path of the image 140 @returns the fmap format string 141 142 """ 143 # Check if ME_RW_A is present in the image 144 logging.info("Checking if seperate CBFS is used for CSE RW in " \ 145 "image : %s" % image_path) 146 command = "futility dump_fmap -F %s | grep ME_RW_A" % image_path 147 output = self.faft_client.system.run_shell_command_get_output( 148 command, True) 149 if output: 150 logging.info("Image uses seperate CBFS for CSE RW") 151 return "CSE_RW_SEPARATE_CBFS" 152 else: 153 return "DEFAULT" 154 155 def check_if_me_blob_exist_in_image(self, image_path): 156 """ 157 Checks if me_blob exists in FW MAIN section of an image 158 159 @param image_path: path of the image 160 @returns True if present else False 161 162 """ 163 # Check if me_rw.metadata present FW_MAIN region 164 logging.info("Checking if me_rw.metadata file " \ 165 "present in image : %s" % image_path ) 166 command = "cbfstool %s print -r FW_MAIN_A " \ 167 "| grep me_rw.metadata" % image_path 168 output = self.faft_client.system.run_shell_command_get_output( 169 command, True) 170 if output: 171 available = True 172 logging.info("me_rw.metadata present in image") 173 else: 174 available = False 175 logging.info("me_rw.metadata not present in image") 176 177 return available 178 179 def extract_me_rw_version_from_bin(self, me_blob, version_offset = 0): 180 """ 181 Extract me_rw version from given me_rw blob. Version is first 8 182 bytes in the blob 183 184 @param me_blob: me_rw blob (old fmap) or me_rw_metadata blob 185 @param version_offset: version filed offset in the blob 186 @returns the CSME RW version string 187 188 """ 189 ver_res = "" 190 logging.info("Extracting version field from ME blob") 191 command = ("hexdump -n 8 -s %s %s | cut -c 9- |sed 's/ //g' |" \ 192 "sed 's/.\{4\}/&./g;s/ $//' | head -c19" % ( \ 193 str(int(version_offset)), me_blob)) 194 output = self.faft_client.system.run_shell_command_get_output( 195 command, True) 196 for each_word in output[0].split("."): 197 version = (int(each_word, 16)) 198 ver_res = "".join((ver_res, "".join((str(version),".")))) 199 ver_res = ver_res[:-1] 200 logging.info("Version : %s" % ver_res) 201 return ver_res 202 203 def get_image_fwmain_me_rw_version(self, 204 bios, 205 region = "FW_MAIN_A"): 206 """ 207 Extract CSME RW version of the me_rw blob of the given 208 region in the given bios 209 210 @param bios: Bios path 211 @param region: region which contains me_rw blob 212 @returns the CSME RW version string 213 214 """ 215 # Extract me_rw.metadata and check version. 216 cbfs_name = "me_rw.metadata" 217 temp_dir = self.faft_client.system.create_temp_dir() 218 me_blob = os.path.join(temp_dir, cbfs_name) 219 220 cmd_status = self.faft_client.updater.cbfs_extract(cbfs_name, 221 '',(region, ), 222 me_blob,'x86',bios) 223 224 if cmd_status is None: 225 self.faft_client.system.remove_dir(temp_dir) 226 raise error.TestError("Failed to extract ME blob from " \ 227 "the given bios : %s" % bios) 228 229 version = self.extract_me_rw_version_from_bin(me_blob) 230 self.faft_client.system.remove_dir(temp_dir) 231 return version 232 233 def get_current_me_rw_version(self): 234 """ 235 Reads the current active CSME RW Version from coreboot logs 236 237 @returns the CSME RW version string 238 239 """ 240 logging.info("Extracting cselite version info from coreboot logs") 241 command = "cbmem -1 | grep 'cse_lite:'" 242 output = self.faft_client.system.run_shell_command_get_output( 243 command, True) 244 logging.info(output) 245 # Offset of rw portion in ME region 246 me_cse_rw_info = re.search(r"(cse_lite: RW version = )" \ 247 "([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*)","".join(output)) 248 249 if me_cse_rw_info: 250 me_version = me_cse_rw_info.group(2) 251 else: 252 raise error.TestError("cse_lite RW info not" 253 " found in coreboot logs!") 254 return me_version 255 256 def verify_me_version(self, expected_version, expected_slot): 257 """ 258 Reads the current active CSME RW Version from coreboot logs 259 and compares with expected version 260 261 @param expected_version: Expected CSME RW Version string 262 @returns True is matching else False 263 264 """ 265 me_version = self.get_current_me_rw_version() 266 command = "crossystem mainfw_act" 267 output = self.faft_client.system.run_shell_command_get_output( 268 command, True) 269 main_fw_act = output[0] 270 271 logging.info("Expected mainfw_act : %s\n" \ 272 "Current mainfw_act : %s\n" \ 273 "Expected ME RW Version : %s\n" \ 274 "Current ME RW Version : %s\n" % ( 275 expected_slot, main_fw_act, 276 expected_version, me_version)) 277 278 if (expected_version not in me_version) or \ 279 (expected_slot not in main_fw_act): 280 return False 281 else: 282 return True 283 284 def prepare_shellball(self, bios_image, append = None): 285 """Prepare a shellball with the given bios image. 286 287 @param bios_image: bios image with shellball to be created 288 @param append: string to be updated with shellball name 289 """ 290 logging.info("Preparing shellball with %s" % bios_image) 291 self.faft_client.updater.reset_shellball() 292 # Copy the given bois to shellball 293 extract_dir = self.faft_client.updater.get_work_path() 294 bios_rel = self.faft_client.updater.get_bios_relative_path() 295 bios_shell = os.path.join(extract_dir, bios_rel) 296 command = "cp %s %s" % (bios_image, bios_shell) 297 output = self.faft_client.system.run_shell_command_get_output( 298 command, True) 299 if output: 300 raise error.TestError("File not found!: %s" % bios_image) 301 # Reload and repack the shellball 302 self.faft_client.updater.reload_images() 303 self.faft_client.updater.repack_shellball(append) 304 305 def run_shellball(self, append): 306 """Run chromeos-firmwareupdate 307 308 @param append: additional piece to add to shellball name 309 """ 310 311 # make sure we restore firmware after the test, if it tried to flash. 312 self.restore_required = True 313 314 # Update only host firmware 315 options = ['--host_only', '--wp=1'] 316 logging.info("Updating RW firmware using " \ 317 "chromeos_firmwareupdate") 318 logging.info("Update command : chromeos_firmwareupdate-%s --mode=%s " 319 " %s" % (append,self.MODE,' '.join(options))) 320 result = self.run_chromeos_firmwareupdate( 321 self.MODE, append, options, ignore_status = True) 322 323 if result.exit_status == 255: 324 raise error.TestError("DUT network dropped during update.") 325 elif result.exit_status != 0: 326 if ('Good. It seems nothing was changed.' in result.stdout): 327 logging.info("DUT already matched the image; updater aborted.") 328 else: 329 raise error.TestError("Firmware updater unexpectedly" \ 330 "failed (rc=%s)" % result.exit_status) 331 332 def run_once(self): 333 if not self.faft_config.intel_cse_lite: 334 raise error.TestNAError("CSELite feature not supported " \ 335 "on this device. Test Skipped") 336 337 # Read current bios from SPI and create a backup copy 338 self.read_current_bios_and_save() 339 340 # Check fmap scheme of the bios read from SPI 341 spi_bios_fmap_ver = self.check_fmap_format(self.spi_bios) 342 343 if not self.check_if_me_blob_exist_in_image(self.spi_bios): 344 raise error.TestError("Test setup issue : me_rw blob is not " \ 345 "present in the current bios.!") 346 347 # Check fmap scheme of the default bios in shellball 348 downgrade_bios_fmap = self.check_fmap_format(self.downgrade_bios) 349 350 # Check if me_rw blob is present in FW_MAIN 351 if not self.check_if_me_blob_exist_in_image(self.downgrade_bios): 352 raise error.TestError("Test setup issue : me_rw blob is not " \ 353 "present in downgrade bios.") 354 355 # Check if both of the bios versions use same fmap structure for me_rw 356 if downgrade_bios_fmap not in spi_bios_fmap_ver: 357 raise error.TestError("Test setup issue : FMAP format is " \ 358 "different in current and downgrade bios.") 359 360 # Get the version of me_rw in the downgrade bios 361 downgrade_me_version = self.get_image_fwmain_me_rw_version( \ 362 self.downgrade_bios) 363 364 # Get the version of me_rw in the spi bios 365 spi_me_version = self.get_image_fwmain_me_rw_version(self.spi_bios) 366 367 # Get active CSME RW version from cbmem -1 368 active_csme_rw_version = self.get_current_me_rw_version() 369 370 logging.info("Active CSME RW Version : %s\n" \ 371 "FW main CSME RW Version SPI Image : %s\n" \ 372 "FW main CSME RW Version downgrade Image: %s\n" % ( 373 active_csme_rw_version, spi_me_version, 374 downgrade_me_version )) 375 376 # Abort if downgrade me_rw version is same as spi me_rw version 377 if (spi_me_version in downgrade_me_version): 378 raise error.TestError("Test setup issue : CSME RW version is " \ 379 "same in both of the images.") 380 381 for slot in ["A", "B"]: 382 operation = "downgrade" 383 # Create a shellball with downgrade bios 384 self.prepare_shellball(self.downgrade_bios, operation) 385 386 logging.info("Downgrading RW section. Downgrade ME " \ 387 "Version: %s" % downgrade_me_version) 388 # Run firmware updater downgrade the bios RW 389 self.run_shellball(operation) 390 391 # Set fw_try_next to slot and reboot to trigger csme update 392 logging.info("Setting fw_try_next to %s: " % slot) 393 self.faft_client.system.set_fw_try_next(slot) 394 self.switcher.mode_aware_reboot(reboot_type = 'cold') 395 396 # Check if the Active CSME RW version changed to downgrade version 397 if not self.verify_me_version(downgrade_me_version, slot): 398 raise error.TestError("CSME RW Downgrade using " 399 "FW_MAIN_%s is Failed!" % slot) 400 logging.info("CSME RW Downgrade using FW_MAIN_%s is " 401 "successful" % slot) 402 403 operation = "upgrade" 404 # Create a shellball with the original spi bios 405 self.prepare_shellball(self.spi_bios, operation) 406 407 logging.info("Upgrading RW Section. Upgrade ME " \ 408 "Version: %s" % spi_me_version) 409 # Run firmware updater and update RW section with shellball 410 self.run_shellball(operation) 411 412 # Set fw_try_next to slot and reboot to trigger csme update 413 logging.info("Setting fw_try_next to %s: " % slot) 414 self.faft_client.system.set_fw_try_next(slot) 415 self.switcher.mode_aware_reboot(reboot_type = 'cold') 416 417 # Check if the Active CSME RW version changed to original version 418 if not self.verify_me_version(spi_me_version, slot): 419 raise error.TestError("CSME RW Upgrade using " 420 "FW_MAIN_%s is Failed!" % slot) 421 logging.info("CSME RW Upgrade using FW_MAIN_%s is " 422 "successful" % slot) 423