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 6import os 7 8from autotest_lib.client.common_lib import error 9from autotest_lib.server.cros.faft.firmware_test import FirmwareTest 10from autotest_lib.server.cros.faft.firmware_test import ConnectionError 11 12 13BIOS = 'bios' 14EC = 'ec' 15 16 17class firmware_WriteProtectFunc(FirmwareTest): 18 """ 19 This test checks whether the SPI flash write-protection functionally works 20 """ 21 version = 1 22 23 def initialize(self, host, cmdline_args, dev_mode=False): 24 """Initialize the test""" 25 super(firmware_WriteProtectFunc, self).initialize(host, cmdline_args) 26 self.switcher.setup_mode('dev' if dev_mode else 'normal') 27 if self.faft_config.chrome_ec: 28 self._targets = (BIOS, EC) 29 else: 30 self._targets = (BIOS, ) 31 self._rpcs = {BIOS: self.faft_client.bios, 32 EC: self.faft_client.ec} 33 self._flashrom_targets = {BIOS: 'host', EC: 'ec'} 34 self._original_sw_wps = {} 35 for target in self._targets: 36 sw_wp_dict = self._rpcs[target].get_write_protect_status() 37 self._original_sw_wps[target] = sw_wp_dict['enabled'] 38 self._original_hw_wp = 'on' in self.servo.get('fw_wp_state') 39 self.backup_firmware() 40 self.work_path = self.faft_client.system.create_temp_dir( 41 'flashrom_', '/mnt/stateful_partition/') 42 43 def cleanup(self): 44 """Cleanup the test""" 45 try: 46 if self.is_firmware_saved(): 47 self.restore_firmware() 48 except ConnectionError: 49 logging.error("ERROR: DUT did not come up after firmware restore!") 50 51 try: 52 # Recover SW WP status. 53 if hasattr(self, '_original_sw_wps'): 54 # If HW WP is enabled, we have to disable it first so that 55 # SW WP can be changed. 56 current_hw_wp = 'on' in self.servo.get('fw_wp_state') 57 if current_hw_wp: 58 self.set_ap_write_protect_and_reboot(False) 59 for target, original_sw_wp in self._original_sw_wps.items(): 60 self._set_write_protect(target, original_sw_wp) 61 self.set_ap_write_protect_and_reboot(current_hw_wp) 62 # Recover HW WP status. 63 if hasattr(self, '_original_hw_wp'): 64 self.set_ap_write_protect_and_reboot(self._original_hw_wp) 65 except Exception as e: 66 logging.error('Caught exception: %s', str(e)) 67 68 self.faft_client.system.remove_dir(self.work_path) 69 super(firmware_WriteProtectFunc, self).cleanup() 70 71 def _set_write_protect(self, target, enable): 72 """ 73 Set write_protect to `enable` for the specified target. 74 75 @param target: Which firmware to toggle the write-protect for, 76 either 'bios' or 'ec' 77 @type target: string 78 @param enable: Whether to enable or disable write-protect 79 @type enable: bool 80 """ 81 assert target in (BIOS, EC) 82 if target == BIOS: 83 # Unlock registers to alter the region/range 84 self.set_ap_write_protect_and_reboot(False) 85 self.faft_client.bios.set_write_protect_region('WP_RO', enable) 86 if enable: 87 self.set_ap_write_protect_and_reboot(True) 88 elif target == EC: 89 self.switcher.mode_aware_reboot('custom', 90 lambda:self.set_ec_write_protect_and_reboot(enable)) 91 92 def _get_relative_path(self, target): 93 """ 94 Send an RPC.updater call to get the relative path for the target. 95 96 @param target: Which firmware to get the relative path to, 97 either 'bios' or 'ec'. 98 @type target: string 99 @return: The relative path of the bios/ec image in the shellball. 100 """ 101 assert target in (BIOS, EC) 102 if target == BIOS: 103 return self.faft_client.updater.get_bios_relative_path() 104 elif target == EC: 105 return self.faft_client.updater.get_ec_relative_path() 106 107 def run_cmd(self, command, checkfor=''): 108 """ 109 Log and execute command and return the output. 110 111 @param command: Command to execute on device. 112 @param checkfor: If not empty, make the test fail when this param 113 is not found in the command output. 114 @returns the output of command. 115 """ 116 command = command + ' 2>&1' 117 logging.info('Execute %s', command) 118 output = self.faft_client.system.run_shell_command_get_output(command) 119 logging.info('Output >>> %s <<<', output) 120 if checkfor and checkfor not in '\n'.join(output): 121 raise error.TestFail('Expect %s in output of cmd <%s>:\n\t%s' % 122 (checkfor, command, '\n\t'.join(output))) 123 return output 124 125 def get_wp_ro_firmware_section(self, firmware_file, wp_ro_firmware_file): 126 """ 127 Read out WP_RO section from the firmware file. 128 129 @param firmware_file: The AP or EC firmware binary to be parsed. 130 @param wp_ro_firmware_file: The file path for the WP_RO section 131 dumped from the firmware_file. 132 @returns the output of the dd command. 133 """ 134 cmd_output = self.run_cmd( 135 'futility dump_fmap -p %s WP_RO'% firmware_file) 136 if cmd_output: 137 unused_name, offset, size = cmd_output[0].split() 138 139 return self.run_cmd('dd bs=1 skip=%s count=%s if=%s of=%s' % 140 (offset, size, firmware_file, wp_ro_firmware_file)) 141 142 def run_once(self): 143 """Runs a single iteration of the test.""" 144 # Enable WP 145 for target in self._targets: 146 self._set_write_protect(target, True) 147 148 # Check WP is properly enabled at the start 149 for target in self._targets: 150 sw_wp_dict = self._rpcs[target].get_write_protect_status() 151 if not sw_wp_dict['enabled']: 152 raise error.TestFail('Failed to enable %s SW WP at ' 153 'test start' % target.upper()) 154 155 reboots = (('shutdown cmd', lambda:self.run_shutdown_process( 156 lambda:self.run_shutdown_cmd())), 157 ('reboot cmd', lambda:self.run_cmd('reboot')), 158 ('power button', lambda:self.full_power_off_and_on())) 159 160 if self.faft_config.chrome_ec: 161 reboots += (('ec reboot', lambda:self.sync_and_ec_reboot('hard')), ) 162 163 # Check if enabled SW WP can stay preserved across reboots. 164 for (reboot_name, reboot_method) in reboots: 165 self.switcher.mode_aware_reboot('custom', reboot_method) 166 for target in self._targets: 167 sw_wp_dict = self._rpcs[target].get_write_protect_status() 168 if not sw_wp_dict['enabled']: 169 raise error.TestFail('%s SW WP can not stay preserved ' 170 'accross %s' % 171 (target.upper(), reboot_name)) 172 173 work_path = self.work_path 174 # Check if RO FW really can't be overwritten when WP is enabled. 175 for target in self._targets: 176 # Current firmware image as read from flash 177 ro_before = os.path.join(work_path, '%s_ro_before.bin' % target) 178 # Current firmware image with modification to test writing 179 ro_test = os.path.join(work_path, '%s_ro_test.bin' % target) 180 # Firmware as read after writing flash 181 ro_after = os.path.join(work_path, '%s_ro_after.bin' % target) 182 183 # Fetch firmware from flash. This serves as the base of ro_test 184 self.run_cmd( 185 'flashrom -p %s -r -i WP_RO:%s ' % 186 (self._flashrom_targets[target], ro_before), 'SUCCESS') 187 188 lines = self.run_cmd('dump_fmap -p %s' % ro_before) 189 FMAP_AREA_NAMES = ['name', 'offset', 'size'] 190 191 modified = False 192 wpro_offset = -1 193 for line in lines: 194 region = dict(zip(FMAP_AREA_NAMES, line.split())) 195 if region['name'] == 'WP_RO': 196 wpro_offset = int(region['offset']) 197 if wpro_offset == -1: 198 raise error.TestFail('WP_RO not found in fmap') 199 for line in lines: 200 region = dict(zip(FMAP_AREA_NAMES, line.split())) 201 if region['name'] == 'RO_FRID': 202 modified = True 203 self.run_cmd('cp %s %s' % (ro_before, ro_test)) 204 self.run_cmd( 205 'dd if=%s bs=1 count=%d skip=%d ' 206 '| tr "[a-zA-Z]" "[A-Za-z]" ' 207 '| dd of=%s bs=1 count=%d seek=%d conv=notrunc' % 208 (ro_test, int(region['size']), 209 int(region['offset']) - wpro_offset, ro_test, 210 int(region['size']), 211 int(region['offset']) - wpro_offset)) 212 213 if not modified: 214 raise error.TestFail('Could not find RO_FRID in %s' % 215 target.upper()) 216 217 # Writing WP_RO section is expected to fail. 218 self.run_cmd('flashrom -p %s -w -i WP_RO:%s' % 219 (self._flashrom_targets[target], ro_test), 220 'FAIL') 221 self.run_cmd('flashrom -p %s -r -i WP_RO:%s' % 222 (self._flashrom_targets[target], ro_after), 223 'SUCCESS') 224 225 self.switcher.mode_aware_reboot(reboot_type='cold') 226 227 # The WP_RO section on the DUT should not change. 228 cmp_output = self.run_cmd('cmp %s %s' % (ro_before, ro_after)) 229 if ''.join(cmp_output) != '': 230 raise error.TestFail('%s RO changes when WP is on!' % 231 target.upper()) 232 233 # Disable WP 234 for target in self._targets: 235 self._set_write_protect(target, False) 236 237 # Check if RO FW can be overwritten when WP is disabled. 238 for target in self._targets: 239 ro_after = os.path.join(work_path, '%s_ro_after.bin' % target) 240 ro_test = os.path.join(work_path, '%s_ro_test.bin' % target) 241 242 # Writing WP_RO section is expected to succeed. 243 self.run_cmd('flashrom -p %s -w -i WP_RO:%s' % 244 (self._flashrom_targets[target], ro_test), 245 'SUCCESS') 246 self.run_cmd('flashrom -p %s -r -i WP_RO:%s' % 247 (self._flashrom_targets[target], ro_after), 248 'SUCCESS') 249 250 # The DUT's WP_RO section should be the same as the test firmware. 251 cmp_output = self.run_cmd('cmp %s %s' % (ro_test, ro_after)) 252 if ''.join(cmp_output) != '': 253 raise error.TestFail('%s RO is not flashed correctly' 254 'when WP is off!' % target.upper()) 255