• 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, 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