• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 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
5from __future__ import print_function
6
7import logging
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib.cros import cr50_utils
11from autotest_lib.server.cros import filesystem_util
12from autotest_lib.server.cros.faft.cr50_test import Cr50Test
13
14
15class firmware_Cr50BID(Cr50Test):
16    """Verify cr50 board id behavior on a board id locked image.
17
18    Check that cr50 will not accept mismatched board ids when it is running a
19    board id locked image.
20
21    Set the board id on a non board id locked image and verify cr50 will
22    rollback when it is updated to a mismatched board id image.
23
24    @param cr50_dbg_image_path: path to the node locked dev image.
25    """
26    version = 1
27
28    MAX_BID = 0xffffffff
29
30    # The universal image can be run on any system no matter the board id.
31    UNIVERSAL = 'universal'
32    # The board id locked can only run on devices with the right chip board id.
33    BID_LOCKED = 'board_id_locked'
34    # Full support required for this test was added in different MP releases.
35    # - BID support was added in 0.0.21.
36    # - Keeping the rollback state after AP boot was added in 0.3.4.
37    # - Complete support for SPI PLT_RST straps was added in 0.3.18
38    # - 4us INT_AP_L pulse was added in 0.3.25
39    # - EC-EFS2 support was added in 0.5.4
40    # - 100us INT_AP_L pulse was added in 0.5.5 (Planned)
41    # TODO: use 5.5, so boards that require a 100us pulse can boot.
42    BID_SUPPORT = '0.5.4'
43
44    BID_MISMATCH = ['Board ID mismatched, but can not reboot.']
45    BID_ERROR = 5
46    SUCCESS = 0
47
48    # BID_BASE_TESTS is a list with the the board id and flags to test for each
49    # run. Each item in the list is a list of [board_id, flags, exit status].
50    # exit_status should be BID_ERROR if the board id and flags should not be
51    # compatible with the board id locked image.
52    #
53    # A image without board id will be able to run on a device with all of the
54    # board id and flag combinations.
55    #
56    # When using a non-symbolic board id, make sure the length of the string is
57    # greater than 4. If the string length is less than 4, usb_updater will
58    # treat it as a symbolic string
59    # ex: bid of 0 needs to be given as '0x0000'. If it were given as '0', the
60    # board id value would be interpreted as ord('0')
61    #
62    # These base tests are be true no matter the board id, mask, or flags. If a
63    # value is None, then it will be replaced with the test board id or flags
64    # while running the test.
65    BID_BASE_TESTS = [
66        [None, None, SUCCESS],
67
68        # Cr50 images are board id locked with flags. If we use 0 for the BID
69        # flags, there should be an error.
70        [None, 0, BID_ERROR],
71
72        # All 1s in the board id flags should be acceptable no matter the
73        # actual image flags
74        [None, MAX_BID, SUCCESS],
75    ]
76
77    # Settings to test all of the cr50 BID responses. The dictionary conatins
78    # the name of the BID verification as the key and a list as a value.
79    #
80    # The value of the list is the image to start running the test with then
81    # the method to update to the board id locked image as the value.
82    #
83    # If the start image is 'board_id_locked', we won't try to update to the
84    # board id locked image.
85    BID_TEST_TYPE = [
86        # Verify that the board id locked image rejects invalid board ids
87        ['get/set', BID_LOCKED],
88
89        # Verify the cr50 response when doing a normal update to a board id
90        # locked image. If there is a board id mismatch, cr50 should rollback
91        # to the image that was already running.
92        ['rollback', UNIVERSAL],
93
94        # TODO (mruthven): add support for verifying recovery
95        # Certain devices are not able to successfully jump to the recovery
96        # image when the TPM is locked down. We need to find a way to verify the
97        # DUT is in recovery without being able to ssh into the DUT.
98    ]
99
100    def initialize(self, host, cmdline_args, basic=False, full_args={}):
101        # Restore the original image and board id during cleanup.
102        super(firmware_Cr50BID, self).initialize(host, cmdline_args, full_args,
103                                                 restore_cr50_image=True,
104                                                 restore_cr50_board_id=True)
105        if self.servo.main_device_is_ccd():
106            raise error.TestNAError('Use a flex cable instead of CCD cable.')
107
108        if not self.cr50.has_command('bid'):
109            raise error.TestNAError('Cr50 image does not support board id')
110
111        self.image_versions = {}
112
113        self.save_board_id_locked_image()
114        self.save_universal_image()
115
116        # Add tests to the test list based on the running board id infomation
117        self.build_tests(basic)
118
119
120    def add_test(self, board_id, flags, expected_result):
121        """Add a test case to the list of tests
122
123        The test will see if the board id locked image behaves as expected with
124        the given board_id and flags.
125
126        Args:
127            board_id: A symbolic string or hex str representing the board id.
128            flags: a int value for the flags
129            expected_result: SUCCESS if the board id and flags should be
130                accepted by the board id locked image. BID_ERROR if it should be
131                rejected.
132        """
133        logging.info('Test Case: image board id %s with chip board id %s:%x '
134                     'should %s', self.test_bid_str, board_id, flags,
135                     'fail' if expected_result else 'succeed')
136        self.tests.append([board_id, flags, expected_result])
137
138
139    def add_board_id_tests(self):
140        """Create a list of tests based on the board id and mask.
141
142        For each bit set to 1 in the board id image mask, Cr50 checks that the
143        bit in the board id infomask matches the image board id. Create a
144        couple of test cases based on the test mask and board id to verify this
145        behavior.
146        """
147        mask_str = bin(self.test_mask).split('b')[1]
148        mask_str = '0' + mask_str if len(mask_str) < 32 else mask_str
149        mask_str = mask_str[::-1]
150        zero_index = mask_str.find('0')
151        one_index = mask_str.find('1')
152
153        # The hex version of the board id should be accepted.
154        self.add_test(hex(self.test_bid_int), self.test_flags, self.SUCCESS)
155
156        # Flip a bit we don't care about to make sure it is accepted
157        if zero_index != -1:
158            test_bid = self.test_bid_int ^ (1 << zero_index)
159            self.add_test(hex(test_bid), self.test_flags, self.SUCCESS)
160
161
162        if one_index != -1:
163            # Flip a bit we care about to make sure it is rejected
164            test_bid = self.test_bid_int ^ (1 << one_index)
165            self.add_test(hex(test_bid), self.test_flags, self.BID_ERROR)
166        else:
167            # If there is not a 1 in the board id mask, then we don't care about
168            # the board id at all. Flip all the bits and make sure setting the
169            # board id still succeeds.
170            test_bid = self.test_bid_int ^ self.MAX_BID
171            self.add_test(hex(test_bid), self.test_flags, self.SUCCESS)
172
173
174    def add_flag_tests(self):
175        """Create a list of tests based on the test flags.
176
177        When comparing the flag field, cr50 makes sure all 1s set in the image
178        flags are also set as 1 in the infomask. Create a couple of test cases
179        to verify cr50 responds appropriately to different flags.
180        """
181        flag_str = bin(self.test_flags).split('b')[1]
182        flag_str_pad = '0' + flag_str if len(flag_str) < 32 else flag_str
183        flag_str_pad_rev = flag_str_pad[::-1]
184        zero_index = flag_str_pad_rev.find('0')
185        one_index = flag_str_pad_rev.find('1')
186
187        # If we care about any flag bits, setting the flags to 0 should cause
188        # a rejection
189        if self.test_flags:
190            self.add_test(self.test_bid_sym, 0, self.BID_ERROR)
191
192        # Flip a 0 to 1 to make sure it is accepted.
193        if zero_index != -1:
194            test_flags = self.test_flags | (1 << zero_index)
195            self.add_test(self.test_bid_sym, test_flags, self.SUCCESS)
196
197        # Flip a 1 to 0 to make sure it is rejected.
198        if one_index != -1:
199            test_flags = self.test_flags ^ (1 << one_index)
200            self.add_test(self.test_bid_sym, test_flags, self.BID_ERROR)
201
202
203    def build_tests(self, basic):
204        """Add more test cases based on the image board id, flags, and mask"""
205        self.tests = self.BID_BASE_TESTS
206        if not basic:
207            self.add_flag_tests()
208            self.add_board_id_tests()
209        logging.info('Running tests %r', self.tests)
210
211
212    def save_universal_image(self, rw_ver=BID_SUPPORT):
213        """Get the non board id locked image
214
215        Save the universal image. Use the current cr50 image if it is not board
216        id locked. If the original image is board id locked, download a release
217        image from google storage.
218
219        Args:
220            rw_ver: The rw release version to use for the universal image.
221        """
222        release_info = self.download_cr50_release_image(rw_ver)
223        self.universal_path, universal_ver = release_info
224
225        logging.info('Running test with universal image %s', universal_ver)
226
227        self.replace_image_if_newer(universal_ver[1], cr50_utils.CR50_PROD)
228        self.replace_image_if_newer(universal_ver[1], cr50_utils.CR50_PREPVT)
229
230        self.image_versions[self.UNIVERSAL] = universal_ver
231
232
233    def replace_image_if_newer(self, universal_rw_ver, path):
234        """Replace the image at path if it is newer than the universal image
235
236        Copy the universal image to path, if the universal image is older than
237        the image at path.
238
239        Args:
240            universal_rw_ver: The rw version string of the universal image
241            path: The path of the image that may need to be replaced.
242        """
243        if self.host.path_exists(path):
244            dut_ver = cr50_utils.GetBinVersion(self.host, path)[1]
245            # If the universal version is lower than the DUT image, install the
246            # universal image. It has the lowest version of any image in the
247            # test, so cr50-update won't try to update cr50 at any point during
248            # the test.
249            install_image = (cr50_utils.GetNewestVersion(dut_ver,
250                    universal_rw_ver) == dut_ver)
251        else:
252            # If the DUT doesn't have a file at path, install the image.
253            install_image = True
254
255        if install_image:
256            # Disable rootfs verification so we can copy the image to the DUT
257            filesystem_util.make_rootfs_writable(self.host)
258            # Copy the universal image onto the DUT.
259            dest, ver = cr50_utils.InstallImage(self.host, self.universal_path,
260                    path)
261            logging.info('Copied %s to %s', ver, dest)
262
263
264    def save_board_id_locked_image(self):
265        """Save the running image and get the board id information.
266
267        Save the board id locked image. If the running image isn't board id
268        locked, the test will be skipped.
269
270        Raises:
271            TestNAError if the running cr50 image is not board id locked.
272        """
273        version = self.get_saved_cr50_original_version()
274        if not version[2]:
275            raise error.TestNAError('The cr50 image is not board id locked')
276
277        self.board_id_locked_path = self.get_saved_cr50_original_path()
278
279        image_bid_info = cr50_utils.GetBoardIdInfoTuple(version[2])
280        self.test_bid_int, self.test_mask, self.test_flags = image_bid_info
281        self.test_bid_sym = cr50_utils.GetSymbolicBoardId(self.test_bid_int)
282        self.test_bid_str = cr50_utils.GetBoardIdInfoString(version[2])
283
284        if not self.test_flags:
285            raise error.TestNAError('Image needs to have non-zero flags to run '
286                                    'test')
287        logging.info('Running test with bid locked image %s', version)
288        self.image_versions[self.BID_LOCKED] = version
289
290
291    def is_running_version(self, rw_ver, bid_str):
292        """Returns True if the running image has the same rw ver and bid
293
294        Args:
295            rw_ver: rw version string
296            bid_str: A symbolic or non-smybolic board id
297
298        Returns:
299            True if cr50 is running an image with the given rw version and
300            board id.
301        """
302        running_rw = self.cr50.get_version()
303        running_bid = self.cr50.get_active_board_id_str()
304        # Convert the image board id to a non symbolic board id
305        bid_str = cr50_utils.GetBoardIdInfoString(bid_str, symbolic=False)
306        return running_rw == rw_ver and bid_str == running_bid
307
308
309    def reset_state(self, image_type):
310        """Update to the image and erase the board id.
311
312        We can't erase the board id unless we are running a debug image. Update
313        to the debug image so we can erase the board id and then rollback to the
314        right image.
315
316        Args:
317            image_type: the name of the image we want to be running at the end
318                        of reset_state: 'universal' or 'board_id_locked'. This
319                        image name needs to correspond with some test attribute
320                        ${image_type}_path
321
322        Raises:
323            TestFail if the board id was not erased
324        """
325        _, rw_ver, bid = self.image_versions[image_type]
326        chip_bid = cr50_utils.GetChipBoardId(self.host)
327        if self.is_running_version(rw_ver, bid) and (chip_bid ==
328            cr50_utils.ERASED_CHIP_BID):
329            logging.info('Skipping reset. Already running %s image with erased '
330                'chip board id', image_type)
331            return
332        logging.info('Updating to %s image and erasing chip bid', image_type)
333
334        self.eraseflashinfo_and_restore_image(self.get_saved_dbg_image_path())
335
336        self.cr50_update(getattr(self, image_type + '_path'), rollback=True)
337
338        # Verify the board id was erased
339        if cr50_utils.GetChipBoardId(self.host) != cr50_utils.ERASED_CHIP_BID:
340            raise error.TestFail('Could not erase bid')
341
342
343    def updater_set_bid(self, bid, flags, exit_code):
344        """Set the flags using usb_updater and verify the result
345
346        Args:
347            board_id: board id string
348            flags: An int with the flag value
349            exit_code: the expected error code. 0 if it should succeed
350
351        Raises:
352            TestFail if usb_updater had an unexpected exit status or setting the
353            board id failed
354        """
355
356        original_bid, _, original_flags = cr50_utils.GetChipBoardId(self.host)
357
358        if exit_code:
359            exit_code = 'Error %d while setting board id' % exit_code
360
361        try:
362            cr50_utils.SetChipBoardId(self.host, bid, flags)
363            result = self.SUCCESS
364        except error.AutoservRunError as e:
365            result = e.result_obj.stderr.strip()
366
367        if result != exit_code:
368            raise error.TestFail("Unexpected result setting %s:%x expected "
369                                 "'%s' got '%s'" %
370                                 (bid, flags, exit_code, result))
371
372        # Verify cr50 is still running with the same board id and flags
373        if exit_code:
374            cr50_utils.CheckChipBoardId(self.host, original_bid, original_flags)
375
376
377    def run_bid_test(self, image_name, bid, flags, bid_error):
378        """Set the bid and flags. Verify a board id locked image response
379
380        Update to the right image type and try to set the board id. Only the
381        board id locked image should reject the given board id and flags.
382
383        If we are setting the board id on a non-board id locked image, try to
384        update to the board id locked image afterwards to verify that cr50 does
385        or doesn't rollback. If there is a bid error, cr50 should fail to update
386        to the board id locked image.
387
388
389        Args:
390            image_name: The image name 'universal', 'dev', or 'board_id_locked'
391            bid: A string representing the board id. Either the hex or symbolic
392                 value
393            flags: A int value for the flags to set
394            bid_error: The expected usb_update error code. 0 for success 5 for
395                       failure
396        """
397        is_bid_locked_image = image_name == self.BID_LOCKED
398
399        # If the image is not board id locked, it should accept any board id and
400        # flags
401        exit_code = bid_error if is_bid_locked_image else self.SUCCESS
402
403        response = 'error %d' % exit_code if exit_code else 'success'
404        logging.info('EXPECT %s setting bid to %s:%x with %s image',
405                     response, bid, flags, image_name)
406
407        # Erase the chip board id and update to the correct image
408        self.reset_state(image_name)
409
410        # Try to set the board id and flags
411        self.updater_set_bid(bid, flags, exit_code)
412
413        # If it failed before, it should fail with the same error. If we already
414        # set the board id, it should fail because the board id is already set.
415        self.updater_set_bid(bid, flags, exit_code if exit_code else 7)
416
417        # After setting the board id with a non boardid locked image, try to
418        # update to the board id locked image. Verify that cr50 does/doesn't run
419        # it. If there is a mismatch, the update should fail and Cr50 should
420        # rollback to the universal image.
421        if not is_bid_locked_image:
422            self.cr50_update(self.board_id_locked_path,
423                             expect_rollback=(not not bid_error))
424
425
426    def run_once(self):
427        """Verify the Cr50 BID response of each test bid."""
428        errors = []
429        for test_type, image_name in self.BID_TEST_TYPE:
430            logging.info('VERIFY: BID %s', test_type)
431            for i, args in enumerate(self.tests):
432                bid, flags, bid_error = args
433                # Replace place holder values with the test values
434                bid = bid if bid != None else self.test_bid_sym
435                flags = flags if flags != None else self.test_flags
436                message = '%s %d %s:%x %s' % (test_type, i, bid, flags,
437                    bid_error)
438
439                # Run the test with the given bid, flags, and result
440                try:
441                    self.run_bid_test(image_name, bid, flags, bid_error)
442                    logging.info('Verified %s', message)
443                except (error.TestFail, error.TestError) as e:
444                    logging.info('FAILED %s with "%s"', message, e)
445                    errors.append('%s with "%s"' % (message, e))
446        if len(errors):
447            raise error.TestFail('failed tests: %s' % errors)
448