• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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