blob: 14f0174c7e7dcb5a79f22022d48c01168a4bfc69 [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import pprint
import re
import time
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import cr50_utils
from autotest_lib.server.cros.faft.cr50_test import Cr50Test
class firmware_GSCAPROV1Trigger(Cr50Test):
"""Verify GSC response after triggering AP RO V1 verification."""
version = 1
# This only verifies V1 output right now.
TEST_AP_RO_VER = 1
# DBG image has to be able to set the AP RO hash with the board id set.
MIN_DBG_VER = '1.6.100'
MIN_RELEASE_MAJOR = 5
MIN_RELEASE_MINOR = 141
APRO_PASS = 6
APRO_FAIL = 2
APRO_IN_PROGRESS = 7
# Regex to search for the end of the AP RO output. Every run should end with
# "AP RO PASS!" or "AP RO FAIL! evt - status". Collect all output up until
# that point.
APRO_OUTPUT_RE = r'.*(AP RO ([\S]*)![^\n]|do_ap_ro_check: unsupported)'
# Cr50 tries to recalculate the hash with 8 of the common factory flags.
FACTORY_FLAG_COUNT = 8
TIMEOUT_SINGLE_RUN = 60
TIMEOUT_ALL_FLAGS = FACTORY_FLAG_COUNT * TIMEOUT_SINGLE_RUN
# If the flags are wrong, verification should fail quickly.
TIMEOUT_FLAG_FAILURE = 10
# Delay the gsctool command that starts verification, so the test can start
# looking for the AP RO verify command output before the gsctool command
# runs.
START_DELAY = 5
DIGEST_RE = r' digest ([0-9a-f]{64})'
CALCULATED_DIGEST_RE = 'Calculated' + DIGEST_RE
STORED_DIGEST_RE = 'Stored' + DIGEST_RE
# The FAFT GBB flags are innocuous. Check that the DUT is using those or
# doesn't have any flags set.
SUPPORTED_FLAGS = [0, 0x140]
TEST_RO_VPD_KEY = 'faft_apro_test_key'
# 0x42b9 is the last value in the cr50 factory flag list. Use it to verify
# cr50 can regenerate the hash with all factory flags in a reasonable time.
LAST_FACTORY_FLAG_VAL = 0x42b9
FLAG_0 = 0
FLAGS_NONE = None
FLAG_42B9 = '0x%x' % LAST_FACTORY_FLAG_VAL
FLAG_0 = '0x%x' % FLAG_0
FLAG_NA = 'na'
# GBB status bits
GBBS_INJECT_FLAGS = 1
GBBS_FLAGS_IN_HASH = 2
GBBD_INVALID = 'na (10)'
GBBD_SAVED = 'ok (%d)'
# If the flags are 0, then cr50 won't try to inject them.
STATUS_0 = GBBS_FLAGS_IN_HASH
# If the gbb flags are in the hash and they were non-zero when the hash
# was generated, then cr50 should try to inject them when calculating
# the hash.
STATUS_LAST_FLAG = (GBBS_FLAGS_IN_HASH | GBBS_INJECT_FLAGS)
# The flags aren't in the hash, so they're not injected. Cr50 should save
# the status as 0.
STATUS_OUTSIDE_HASH = 0
GBBD_SAVED_0 = GBBD_SAVED % STATUS_0
GBBD_SAVED_LAST_FLAG = GBBD_SAVED % STATUS_LAST_FLAG
GBBD_SAVED_FLAGS_NOT_IN_HASH = GBBD_SAVED % STATUS_OUTSIDE_HASH
# These messages should never happen. Fail if they show up in any AP RO
# verificaton output.
ERR_MESSAGES = ['Could not find GBB area', 'WATCHDOG']
def initialize(self, host, cmdline_args, full_args={}):
"""Initialize servo"""
self.ran_test = False
super(firmware_GSCAPROV1Trigger,
self).initialize(host,
cmdline_args,
full_args,
restore_cr50_image=True)
if not self.gsc.ap_ro_version_is_supported(self.TEST_AP_RO_VER):
raise error.TestNAError('GSC does not support AP RO v%s' %
self.TEST_AP_RO_VER)
self._original_timeout = float(self.servo.get('cr50_uart_timeout'))
rw_ver = self.get_saved_cr50_original_version()[1]
_, major, minor = rw_ver.split('.')
if (int(major) < self.MIN_RELEASE_MAJOR or
int(minor) < self.MIN_RELEASE_MINOR):
raise error.TestNAError('Test does not support cr50 (%r). Update '
'to 0.%s.%s' % (rw_ver,
self.MIN_RELEASE_MAJOR,
self.MIN_RELEASE_MINOR))
dbg_ver = cr50_utils.InstallImage(self.host,
self.get_saved_dbg_image_path(),
'/tmp/cr50.bin')[1][1]
if cr50_utils.GetNewestVersion(dbg_ver,
self.MIN_DBG_VER) == self.MIN_DBG_VER:
raise error.TestNAError('Update DBG image to 6.100 or newer.')
self._start_gbb_flags = self.faft_client.bios.get_gbb_flags()
logging.info('GBB flags: %x', self._start_gbb_flags)
# Refuse to run with unsupported gbb flags, because this test sets the
# flags to 0 and it could prevent the device from booting if it's
# currently relying on the FORCE_DEV_MODE flag.
if self._start_gbb_flags not in self.SUPPORTED_FLAGS:
raise error.TestNaError('Unsupported DUT GBB flags 0x%x. Set the '
'flags to one of %r to run the test.' %
(self._start_gbb_flags,
self.SUPPORTED_FLAGS))
def run_ro_vpd_cmd(self, cmd):
"""Run RO_VPD command
@param cmd: the RO vpd command to run
"""
return self.host.run('vpd -i RO_VPD ' + cmd)
def delete_test_ro_vpd_key(self):
"""Remove the test key from the RO_VPD"""
self.run_ro_vpd_cmd('-d ' + self.TEST_RO_VPD_KEY)
def set_test_ro_vpd_key(self, val):
"""Remove the test key from the RO_VPD"""
self.run_ro_vpd_cmd('-s %s=%s' % (self.TEST_RO_VPD_KEY, val))
def restore_ro(self):
"""Restore the original test RO_VPD test key value."""
self.set_test_ro_vpd_key('original_val')
self._ro_desc = 'ok'
def modify_ro(self):
"""Change a RO_VPD value to modify RO."""
self.set_test_ro_vpd_key('modified_val')
self._ro_desc = 'modified'
def update_to_dbg_and_clear_hash(self):
"""Clear the Hash."""
# Make sure the AP is up before trying to update.
self.recover_dut()
self.clear_gbb_flags()
self._retry_gsc_update_with_ccd_and_ap(self._dbg_image_path, 3, False)
self.gsc.send_command('ap_ro_info erase')
time.sleep(3)
if self.gsc.get_ap_ro_info()['hash']:
raise error.TestError('Could not erase hash')
self._try_to_bring_dut_up()
def after_run_once(self):
"""Reboot cr50 to recover the dut."""
try:
self.recover_dut()
finally:
super(firmware_GSCAPROV1Trigger, self).after_run_once()
def set_hash(self, regions):
"""Set the Hash.
@param regions: a space separated string with the names of the regions
to include in the hash. ex "FMAP GBB"
"""
self.recover_dut()
result = self.host.run('ap_ro_hash.py -v True %s' % regions)
logging.info(result)
time.sleep(3)
ap_ro_info = self.gsc.get_ap_ro_info()
self._hash_desc = '%s flags' % self._flag_desc
ap_ro_info = self.gsc.get_ap_ro_info()
if ap_ro_info['supported'] and not ap_ro_info['hash']:
raise error.TestError('Could not set hash %r' % result)
def rollback_to_release_image(self):
"""Update to the release image."""
self._retry_gsc_update_with_ccd_and_ap(
self.get_saved_cr50_original_path(), 3, rollback=True)
self._try_to_bring_dut_up()
def set_gbb_flags(self, flags):
"""Set the GBB flags.
@params flags: integer value to set the gbb flags to.
"""
self.host.run('/usr/bin/futility gbb --set --flash --flags=0x%x' %
flags)
logging.info('Set GBB: %x', self.faft_client.bios.get_gbb_flags())
def set_factory_gbb_flags(self):
"""Set the GBB flags to one of the cr50 factory flags."""
self.set_gbb_flags(self.LAST_FACTORY_FLAG_VAL)
self._flag_desc = '%x' % self.LAST_FACTORY_FLAG_VAL
def clear_gbb_flags(self):
"""Set the GBB flags to 0."""
self.set_gbb_flags(0)
self._flag_desc = 'cleared'
def cleanup(self):
"""Clear the hash, remove the test ro vpd key, and restore the flags."""
try:
if not self.ran_test:
return
logging.info('Cleanup')
self.recover_dut()
self.clear_gbb_flags()
self.update_to_dbg_and_clear_hash()
self.rollback_to_release_image()
self.delete_test_ro_vpd_key()
self.faft_client.bios.set_gbb_flags(self._start_gbb_flags)
finally:
super(firmware_GSCAPROV1Trigger, self).cleanup()
def recover_dut(self):
"""Reboot gsc to recover the dut."""
logging.info('Recover DUT')
ap_ro_info = self.gsc.get_ap_ro_info()
logging.info(ap_ro_info)
if ap_ro_info['result'] != self.APRO_FAIL:
self._try_to_bring_dut_up()
return
time.sleep(3)
self.gsc.send_command('ccd testlab open')
time.sleep(3)
self.gsc.reboot()
time.sleep(self.faft_config.delay_reboot_to_ping)
self.gsc.get_ap_ro_info()
self._try_to_bring_dut_up()
self.gsc.send_command('ccd testlab open')
def verification_in_progress(self):
"""Returns True if AP RO verification is running."""
return self.gsc.get_ap_ro_info()['result'] == self.APRO_IN_PROGRESS
def get_apro_output(self, timeout):
"""Get the AP RO console output.
@param timeout: time in seconds to wait for AP RO verification to
finish.
"""
self.servo.set_nocheck('cr50_uart_timeout', timeout + self.START_DELAY)
start_time = time.time()
try:
# AP RO verification will start in the background. Wait for cr50 to
# finish verification collect all of the AP RO output.
cmd = 'noop_wait_apro ' + self._desc
rv = self.gsc.send_command_get_output(cmd, [self.APRO_OUTPUT_RE])
finally:
self.servo.set_nocheck('cr50_uart_timeout', self._original_timeout)
logging.info('AP RO verification ran in %ds', time.time() - start_time)
return rv[0][0]
def _start_apro_verify(self):
"""Start AP RO verification with a delay.
Delay starting AP RO verification, so the test can get the full
AP RO console output.
"""
if not self.host.ping_wait_up(self.faft_config.delay_reboot_to_ping):
raise error.TestError('AP is %s. Dut is not sshable. ' %
self.cr50.get_ccdstate('AP'))
apro_start_cmd = utils.sh_escape('sleep %d ; gsctool -aB start' %
self.START_DELAY)
full_ssh_cmd = '%s "%s"' % (self.host.ssh_command(options='-tt'),
apro_start_cmd)
# Start running the Cr50 Open process in the background.
self._apro_start = utils.BgJob(full_ssh_cmd,
nickname='apro_start',
stdout_tee=utils.TEE_TO_LOGS,
stderr_tee=utils.TEE_TO_LOGS)
def _close_apro_start(self):
"""Terminate the process and check the results."""
exit_status = utils.nuke_subprocess(self._apro_start.sp)
delattr(self, '_apro_start')
if exit_status:
logging.info('exit status: %d', exit_status)
def trigger_verification(self,
exp_result,
exp_calculations,
timeout,
exp_gbb,
exp_flags,
end_if_unsupported=True):
"""Trigger verification.
Trigger verification. Verify the AP RO behavior by checking the result
matches the expected result. Check that cr50 calculated the expected
number of hashes within timeout seconds. Check the gbb value from
ap_ro_info matches exp_gbb and all of the expected strings show up
in the output.
@param exp_result: expected value for the ap_ro_info result field after
verification runs.
@param exp_calculations: expected number of hashes cr50 will generate
during verification.
@param timeout: maximum time in seconds that the AP RO verification run
can take.
@param exp_gbb: string that should be found in the ap_ro_info gbb field.
@param exp_flags: None or the flag string if the gbbd is saved
@param end_if_unsupported: raise testNA if the device doesn't support
verification.
"""
self._desc = ('%s: current flags(%s) ro(%s) saved hash(%s) - '
'expected result(%d)' %
(self._prefix, self._flag_desc, self._ro_desc,
self._hash_desc, exp_result))
# CCD has to be open to trigger verification.
self.fast_ccd_open(True)
logging.info('Run: %s', self._desc)
self.recover_dut()
try:
self._start_apro_verify()
contents = self.get_apro_output(timeout)
finally:
self._close_apro_start()
logging.info('finished %r:%s', self._desc, contents)
try:
if self.verification_in_progress():
raise error.TestFail('%s: Verification did not finish in %ds' %
(self._desc, timeout))
for msg in self.ERR_MESSAGES:
if msg in contents:
raise error.TestFail('%s: %r showed up in contents %s' %
(self._desc, msg, contents))
finally:
ap_ro_info = self.gsc.get_ap_ro_info()
# Make sure to recover the dut.
self.recover_dut()
# cr50 only prints calculated and stored hashes after AP RO verificaiton
# fails. These sets will be empty if verification passed every time.
calculated = set(re.findall(self.CALCULATED_DIGEST_RE, contents))
stored = set(re.findall(self.STORED_DIGEST_RE, contents))
logging.info('Stored: %r', stored)
logging.info('Calculated (%d): %s', len(calculated),
pprint.pformat(calculated))
logging.info('Results: %s', pprint.pformat(ap_ro_info))
if not ap_ro_info['supported']:
reason = ap_ro_info['reason']
if 'not programmed' not in reason:
if end_if_unsupported:
raise error.TestNAError(reason)
raise error.TestFail('%s: verification unsupported: %s' %
(self._desc, reason))
if len(calculated) != exp_calculations:
raise error.TestFail('%s: Calculated %d digests instead of %d' %
(self._desc, len(calculated),
exp_calculations))
if exp_flags != ap_ro_info['flags']:
raise error.TestFail(
'%s: %r not found in flags %r -- stored: %r calc: %r' %
(self._desc, exp_flags, ap_ro_info['flags'], stored,
calculated))
if exp_gbb not in ap_ro_info['gbbd']:
raise error.TestFail(
'%s: %r not found in gbb %r -- stored: %r calc: %r' %
(self._desc, exp_gbb, ap_ro_info['gbbd'], stored,
calculated))
if exp_result != ap_ro_info['result']:
raise error.TestFail(
'%s: %r not found in status %r -- stored: %r calc: %r' %
(self._desc, exp_result, ap_ro_info['result'], stored,
calculated))
def run_once(self):
"""Save hash and trigger verification"""
self.ran_test = True
# The DBG image can set the hash when the board id is saved. The release
# image can't. Set the hash with the DBG image, so the test doesn't need
# to erase the board id. This test verifies triggering AP RO
# verification. It's not about saving the hash. Whenever the test
# updates the hash, it'll update to the dbg image, set the hash, and
# then rollback to the released image.
self.update_to_dbg_and_clear_hash()
# Generate a hash with the GBB flags set to 0.
# Set the GBB flags to 0.
self.clear_gbb_flags()
# The test creates a RO_VPD key and modifies it to modify RO. It's a
# bogus key. It doesn't change any device behavior. It's just a way
# to easily modify RO.
self.restore_ro()
self.set_hash('WP_RO')
self.rollback_to_release_image()
self.fast_ccd_open(True)
logging.info('Verifying standard behavior')
logging.info('the hash was generated with 0 gbb flags')
self._prefix = 'standard'
# Set the flags to a non-zero value. Make sure it fails almost
# immediately because the flags are wrong.
self.set_factory_gbb_flags()
# This is a pretty basic test. If the board says verification is
# unsupported, fail with TestNA.
self.trigger_verification(self.APRO_FAIL,
0,
self.TIMEOUT_FLAG_FAILURE,
self.GBBD_INVALID,
self.FLAGS_NONE,
end_if_unsupported=True)
# Modify RO. Cr50 should still fail immediately because the flags are
# wrong.
self.modify_ro()
self.trigger_verification(self.APRO_FAIL, 0,
self.TIMEOUT_FLAG_FAILURE,
self.GBBD_INVALID,
self.FLAGS_NONE)
self.clear_gbb_flags()
# Cr50 should try all factory flags and fail with a hash mismatch.
self.trigger_verification(self.APRO_FAIL, self.FACTORY_FLAG_COUNT,
self.TIMEOUT_ALL_FLAGS,
self.GBBD_INVALID,
self.FLAGS_NONE)
# Restore RO. Check AP RO verification starts passing.
self.restore_ro()
# Verification should pass during the first flag check since the hash
# was generated with gbb flags set to 0.
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_0,
self.FLAG_0)
# Trigger verification multiple times. Make sure it doesn't fail or
# change.
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_0,
self.FLAG_0)
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_0,
self.FLAG_0)
# With the saved flags AP RO verification should only try one flag. It
# should fail more quickly.
self.modify_ro()
self.trigger_verification(self.APRO_FAIL, 1,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_0,
self.FLAG_0)
# TODO(b/327795084): reenable after flakiness is investigated.
return
# Generate a hash with non-zero flags.
self.update_to_dbg_and_clear_hash()
self.restore_ro()
self.set_factory_gbb_flags()
self.set_hash('WP_RO')
# Clear the gbb flags, so the test can open ccd.
self.clear_gbb_flags()
self.rollback_to_release_image()
self.fast_ccd_open(True)
self.set_factory_gbb_flags()
logging.info('Verifying the gbb workaround')
logging.info('cr50 can handle hashes generated with non-zero flags')
self._prefix = 'non-zero factory flags'
# The gbb flags are non-zero. Cr50 should fail immediately since they're
# non-zero.
self.trigger_verification(self.APRO_FAIL, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_INVALID,
self.FLAGS_NONE)
# The flags are wrong and RO is wrong. Cr50 verifies the flags first, so
# it should still fail immediately and not generate any hashes.
self.modify_ro()
self.trigger_verification(self.APRO_FAIL, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_INVALID,
self.FLAGS_NONE)
# The flags are ok. RO is wrong. Cr50 should try all factory flags and
# fail with a hash mismatch.
self.clear_gbb_flags()
self.trigger_verification(self.APRO_FAIL, self.FACTORY_FLAG_COUNT,
self.TIMEOUT_ALL_FLAGS,
self.GBBD_INVALID,
self.FLAGS_NONE)
# The test saved the hash with the last factory flag. Cr50 should
# calculate hashes with all of the factory flags and pass on the last
# one.
self.restore_ro()
# Cr50 should match the gbb flag and then save it.
self.trigger_verification(self.APRO_PASS, self.FACTORY_FLAG_COUNT - 1,
self.TIMEOUT_ALL_FLAGS,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
# The next verification cr50 should load the matched flag and only try
# that.
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
# RO verificaiton will fail since the RO is changed, but cr50 should
# only try the flag it matched originally.
self.modify_ro()
self.trigger_verification(self.APRO_FAIL, 1,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
# RO and the flags are wrong, but cr50 shouldn't try to generate a hash.
# It should fail before that since the flags are wrong.
self.set_factory_gbb_flags()
# Cr50 checks the current flags, before it tries to use the saved gbbd.
# Cr50 will fail with non-zero flags before it loads the gbbd, so
# LOAD_42B9 shouldn't show up in the result. It should only have the
# invalid gbb flags messages.
self.trigger_verification(self.APRO_FAIL, 0,
self.TIMEOUT_FLAG_FAILURE,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
# After the state is restored to a good state, cr50 should pass again.
self.clear_gbb_flags()
self.restore_ro()
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
self.gsc.reboot()
self._try_to_bring_dut_up()
# Make sure it works the same after cr50 reboots.
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_LAST_FLAG,
self.FLAG_42B9)
# Set a hash with FMAP in the hash and the GBB outside of the hash.
# Cr50 should not care about the GBB flags in this case.
self.update_to_dbg_and_clear_hash()
self.restore_ro()
self.set_factory_gbb_flags()
self.set_hash('FMAP RO_VPD')
self.clear_gbb_flags()
self.rollback_to_release_image()
self.fast_ccd_open(True)
self.set_factory_gbb_flags()
logging.info('Verifying setup with GBB outside of hash')
self._prefix = 'gbb outside of hash'
# GBB flags are non-zero, but they're outside of the hash, so it
# shouldn't matter.
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_FLAGS_NOT_IN_HASH,
self.FLAG_NA)
# GBB flags are now zero, but they're outside of the hash, so it
# shouldn't matter.
self.clear_gbb_flags()
self.trigger_verification(self.APRO_PASS, 0,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_FLAGS_NOT_IN_HASH,
self.FLAG_NA)
# Modify the RO_VPD. It's in the hash, so verification should fail.
self.modify_ro()
self.trigger_verification(self.APRO_FAIL, 1,
self.TIMEOUT_SINGLE_RUN,
self.GBBD_SAVED_FLAGS_NOT_IN_HASH,
self.FLAG_NA)
# Cr50 will fail verification if the FMAP is outside of the hash since
# it can't trust the GBB location.
self.update_to_dbg_and_clear_hash()
self.restore_ro()
self.clear_gbb_flags()
self.set_hash('GBB')
self.rollback_to_release_image()
self.fast_ccd_open(True)
logging.info('Verifying setup with FMAP outside of hash')
self._prefix = 'fmap not in hash'
# There's no way to pass since FMAP isn't in the hash. The factory
# process is standard. FMAP should be in the hash.
self.trigger_verification(self.APRO_FAIL, 0,
self.TIMEOUT_FLAG_FAILURE,
self.GBBD_INVALID,
self.FLAGS_NONE)