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