xref: /aosp_15_r20/external/autotest/server/site_tests/firmware_CsmeFwUpdate/firmware_CsmeFwUpdate.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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