• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import argparse
11import logging
12import re
13import six
14from six.moves import range
15
16from autotest_lib.client.common_lib import error
17
18
19RO = 'ro'
20RW = 'rw'
21BID = 'bid'
22CR50_PROD = '/opt/google/cr50/firmware/cr50.bin.prod'
23CR50_PREPVT = '/opt/google/cr50/firmware/cr50.bin.prepvt'
24CR50_STATE = '/var/cache/cr50*'
25CR50_VERSION = '/var/cache/cr50-version'
26GET_CR50_VERSION = 'cat %s' % CR50_VERSION
27GET_CR50_MESSAGES ='grep "cr50-.*\[" /var/log/messages'
28UPDATE_FAILURE = 'unexpected cr50-update exit code'
29STUB_VER = '-1.-1.-1'
30# This dictionary is used to search the gsctool output for the version strings.
31# There are two gsctool commands that will return versions: 'fwver' and
32# 'binvers'.
33#
34# 'fwver'   is used to get the running RO and RW versions from cr50
35# 'binvers'  gets the version strings for each RO and RW region in the given
36#            file
37#
38# The value in the dictionary is the regular expression that can be used to
39# find the version strings for each region.
40#
41# --fwver
42#   example output:
43#           open_device 18d1:5014
44#           found interface 3 endpoint 4, chunk_len 64
45#           READY
46#           -------
47#           start
48#           target running protocol version 6
49#           keyids: RO 0xaa66150f, RW 0xde88588d
50#           offsets: backup RO at 0x40000, backup RW at 0x44000
51#           Current versions:
52#           RO 0.0.10
53#           RW 0.0.21
54#   match groupdict:
55#           {
56#               'ro': '0.0.10',
57#               'rw': '0.0.21'
58#           }
59#
60# --binvers
61#   example output:
62#           read 524288(0x80000) bytes from /tmp/cr50.bin
63#           RO_A:0.0.10 RW_A:0.0.21[00000000:00000000:00000000]
64#           RO_B:0.0.10 RW_B:0.0.21[00000000:00000000:00000000]
65#   match groupdict:
66#           {
67#               'rw_b': '0.0.21',
68#               'rw_a': '0.0.21',
69#               'ro_b': '0.0.10',
70#               'ro_a': '0.0.10',
71#               'bid_a': '00000000:00000000:00000000',
72#               'bid_b': '00000000:00000000:00000000'
73#           }
74VERSION_RE = {
75    '--fwver' : '\nRO (?P<ro>\S+).*\nRW (?P<rw>\S+)',
76    '--binvers' : 'RO_A:(?P<ro_a>[\d\.]+).*' \
77           'RW_A:(?P<rw_a>[\d\.]+)(\[(?P<bid_a>[\d\:A-z]+)\])?.*' \
78           'RO_B:(?P<ro_b>\S+).*' \
79           'RW_B:(?P<rw_b>[\d\.]+)(\[(?P<bid_b>[\d\:A-z]+)\])?.*',
80}
81UPDATE_TIMEOUT = 60
82UPDATE_OK = 1
83
84MP_BID_FLAGS = 0x7f80
85ERASED_BID_INT = 0xffffffff
86ERASED_BID_STR = hex(ERASED_BID_INT)
87# With an erased bid, the flags and board id will both be erased
88ERASED_CHIP_BID = (ERASED_BID_INT, ERASED_BID_INT, ERASED_BID_INT)
89# Any image with this board id will run on any device
90EMPTY_IMAGE_BID = '00000000:00000000:00000000'
91EMPTY_IMAGE_BID_CHARACTERS = set(EMPTY_IMAGE_BID)
92SYMBOLIC_BID_LENGTH = 4
93
94gsctool = argparse.ArgumentParser()
95gsctool.add_argument('-a', '--any', dest='universal', action='store_true')
96# use /dev/tpm0 to send the command
97gsctool.add_argument('-s', '--systemdev', dest='systemdev', action='store_true')
98gsctool.add_argument('-o', '--ccd_open', dest='ccd_open', action='store_true')
99# Any command used for something other than updating. These commands should
100# never timeout because they do not force cr50 to reboot. They should all just
101# return information about cr50 and should only have a nonzero exit status if
102# something went wrong.
103gsctool.add_argument('-b',
104                     '--binvers',
105                     '-f',
106                     '--fwver',
107                     '-g',
108                     '--getbootmode',
109                     '-i',
110                     '--board_id',
111                     '-r',
112                     '--rma_auth',
113                     '-F',
114                     '--factory',
115                     '-m',
116                     '--tpm_mode',
117                     '-L',
118                     '--flog',
119                     '-A',
120                     '--get_apro_hash',
121                     '-H',
122                     '--erase_ap_ro_hash',
123                     dest='info_cmd',
124                     action='store_true')
125# upstart and post_reset will post resets instead of rebooting immediately
126gsctool.add_argument('-u', '--upstart', '-p', '--post_reset', dest='post_reset',
127                     action='store_true')
128gsctool.add_argument('extras', nargs=argparse.REMAINDER)
129
130
131def AssertVersionsAreEqual(name_a, ver_a, name_b, ver_b):
132    """Raise an error ver_a isn't the same as ver_b
133
134    Args:
135        name_a: the name of section a
136        ver_a: the version string for section a
137        name_b: the name of section b
138        ver_b: the version string for section b
139
140    Raises:
141        AssertionError if ver_a is not equal to ver_b
142    """
143    assert ver_a == ver_b, ('Versions do not match: %s %s %s %s' %
144                            (name_a, ver_a, name_b, ver_b))
145
146
147def GetNewestVersion(ver_a, ver_b):
148    """Compare the versions. Return the newest one. If they are the same return
149    None."""
150    a = [int(x) for x in ver_a.split('.')]
151    b = [int(x) for x in ver_b.split('.')]
152
153    if a > b:
154        return ver_a
155    if b > a:
156        return ver_b
157    return None
158
159
160def GetVersion(versions, name):
161    """Return the version string from the dictionary.
162
163    Get the version for each key in the versions dictionary that contains the
164    substring name. Make sure all of the versions match and return the version
165    string. Raise an error if the versions don't match.
166
167    Args:
168        version: dictionary with the partition names as keys and the
169                 partition version strings as values.
170        name: the string used to find the relevant items in versions.
171
172    Returns:
173        the version from versions or "-1.-1.-1" if an invalid RO was detected.
174    """
175    ver = None
176    key = None
177    for k, v in six.iteritems(versions):
178        if name in k:
179            if v == STUB_VER:
180                logging.info('Detected invalid %s %s', name, v)
181                return v
182            elif ver:
183                AssertVersionsAreEqual(key, ver, k, v)
184            else:
185                ver = v
186                key = k
187    return ver
188
189
190def FindVersion(output, arg):
191    """Find the ro and rw versions.
192
193    Args:
194        output: The string to search
195        arg: string representing the gsctool option, either '--binvers' or
196             '--fwver'
197
198    Returns:
199        a tuple of the ro and rw versions
200    """
201    versions = re.search(VERSION_RE[arg], output)
202    if not versions:
203        raise Exception('Unable to determine version from: %s' % output)
204
205    versions = versions.groupdict()
206    ro = GetVersion(versions, RO)
207    rw = GetVersion(versions, RW)
208    # --binver is the only gsctool command that may have bid keys in its
209    # versions dictionary. If no bid keys exist, bid will be None.
210    bid = GetVersion(versions, BID)
211    # Use GetBoardIdInfoString to standardize all board ids to the non
212    # symbolic form.
213    return ro, rw, GetBoardIdInfoString(bid, symbolic=False)
214
215
216def GetSavedVersion(client):
217    """Return the saved version from /var/cache/cr50-version
218
219    Some boards dont have cr50.bin.prepvt. They may still have prepvt flags.
220    It is possible that cr50-update wont successfully run in this case.
221    Return None if the file doesn't exist.
222
223    Returns:
224        the version saved in cr50-version or None if cr50-version doesn't exist
225    """
226    if not client.path_exists(CR50_VERSION):
227        return None
228
229    result = client.run(GET_CR50_VERSION).stdout.strip()
230    return FindVersion(result, '--fwver')
231
232
233def StopTrunksd(client):
234    """Stop trunksd on the client"""
235    if 'running' in client.run('status trunksd').stdout:
236        client.run('stop trunksd')
237
238
239def GSCTool(client, args, ignore_status=False, expect_reboot=False):
240    """Run gsctool with the given args.
241
242    Args:
243        client: the object to run commands on
244        args: a list of strings that contiain the gsctool args
245        ignore_status: Ignore the exit status
246        expect_reboot: Expect a reboot
247
248    Returns:
249        the result of gsctool
250    """
251    options = gsctool.parse_args(args)
252
253    if options.systemdev:
254        StopTrunksd(client)
255
256    # If we are updating the cr50 image, gsctool will return a non-zero exit
257    # status so we should ignore it.
258    ignore_status = not options.info_cmd or ignore_status
259    # immediate reboots are only honored if the command is sent using /dev/tpm0
260    expect_reboot = expect_reboot or ((options.systemdev or options.universal)
261                                      and not options.post_reset
262                                      and not options.info_cmd)
263
264    result = client.run('gsctool %s' % ' '.join(args),
265                        ignore_status=ignore_status,
266                        ignore_timeout=expect_reboot,
267                        timeout=UPDATE_TIMEOUT)
268
269    # After a posted reboot, the gsctool exit code should equal 1.
270    if (result and result.exit_status and result.exit_status != UPDATE_OK and
271        not ignore_status):
272        logging.debug(result)
273        raise error.TestFail('Unexpected gsctool exit code after %s %d' %
274                             (' '.join(args), result.exit_status))
275    return result
276
277
278def GetVersionFromUpdater(client, args):
279    """Return the version from gsctool"""
280    result = GSCTool(client, args).stdout.strip()
281    return FindVersion(result, args[0])
282
283
284def GetFwVersion(client):
285    """Get the running version using 'gsctool --fwver'"""
286    return GetVersionFromUpdater(client, ['--fwver', '-a'])
287
288
289def GetBinVersion(client, image=CR50_PROD):
290    """Get the image version using 'gsctool --binvers image'"""
291    return GetVersionFromUpdater(client, ['--binvers', image])
292
293
294def GetVersionString(ver):
295    """Combine the RO and RW tuple into a understandable string"""
296    return 'RO %s RW %s%s' % (ver[0], ver[1],
297           ' BID %s' % ver[2] if ver[2] else '')
298
299
300def GetRunningVersion(client):
301    """Get the running Cr50 version.
302
303    The version from gsctool and /var/cache/cr50-version should be the
304    same. Get both versions and make sure they match.
305
306    Args:
307        client: the object to run commands on
308
309    Returns:
310        running_ver: a tuple with the ro and rw version strings
311
312    Raises:
313        TestFail
314        - If the version in /var/cache/cr50-version is not the same as the
315          version from 'gsctool --fwver'
316    """
317    running_ver = GetFwVersion(client)
318    saved_ver = GetSavedVersion(client)
319
320    if saved_ver:
321        AssertVersionsAreEqual('Running', GetVersionString(running_ver),
322                               'Saved', GetVersionString(saved_ver))
323    return running_ver
324
325
326def GetActiveCr50ImagePath(client):
327    """Get the path the device uses to update cr50
328
329    Extract the active cr50 path from the cr50-update messages. This path is
330    determined by cr50-get-name based on the board id flag value.
331
332    Args:
333        client: the object to run commands on
334
335    Raises:
336        TestFail
337            - If cr50-update uses more than one path or if the path we find
338              is not a known cr50 update path.
339    """
340    ClearUpdateStateAndReboot(client)
341    messages = client.run(GET_CR50_MESSAGES).stdout.strip()
342    paths = set(re.findall('/opt/google/cr50/firmware/cr50.bin[\S]+', messages))
343    if not paths:
344        raise error.TestFail('Could not determine cr50-update path')
345    path = paths.pop()
346    if len(paths) > 1 or (path != CR50_PROD and path != CR50_PREPVT):
347        raise error.TestFail('cannot determine cr50 path')
348    return path
349
350
351def CheckForFailures(client, last_message):
352    """Check for any unexpected cr50-update exit codes.
353
354    This only checks the cr50 update messages that have happened since
355    last_message. If a unexpected exit code is detected it will raise an error>
356
357    Args:
358        client: the object to run commands on
359        last_message: the last cr50 message from the last update run
360
361    Returns:
362        the last cr50 message in /var/log/messages
363
364    Raises:
365        TestFail
366            - If there is a unexpected cr50-update exit code after last_message
367              in /var/log/messages
368    """
369    messages = client.run(GET_CR50_MESSAGES).stdout.strip()
370    if last_message:
371        messages = messages.rsplit(last_message, 1)[-1].split('\n')
372        failures = []
373        for message in messages:
374            if UPDATE_FAILURE in message:
375                failures.append(message)
376        if len(failures):
377            logging.info(messages)
378            raise error.TestFail('Detected unexpected exit code during update: '
379                                 '%s' % failures)
380    return messages[-1]
381
382
383def VerifyUpdate(client, ver='', last_message=''):
384    """Verify that the saved update state is correct and there were no
385    unexpected cr50-update exit codes since the last update.
386
387    Args:
388        client: the object to run commands on
389        ver: the expected version tuple (ro ver, rw ver)
390        last_message: the last cr50 message from the last update run
391
392    Returns:
393        new_ver: a tuple containing the running ro and rw versions
394        last_message: The last cr50 update message in /var/log/messages
395    """
396    # Check that there were no unexpected reboots from cr50-result
397    last_message = CheckForFailures(client, last_message)
398    logging.debug('last cr50 message %s', last_message)
399
400    new_ver = GetRunningVersion(client)
401    if ver != '':
402        if STUB_VER != ver[0]:
403            AssertVersionsAreEqual('Old RO', ver[0], 'Updated RO', new_ver[0])
404        AssertVersionsAreEqual('Old RW', ver[1], 'Updated RW', new_ver[1])
405    return new_ver, last_message
406
407
408def GetDevicePath(ext):
409    """Return the device path for the .prod or .prepvt image."""
410    if ext == 'prod':
411        return CR50_PROD
412    elif ext == 'prepvt':
413        return CR50_PREPVT
414    raise error.TestError('Unsupported cr50 image type %r' % ext)
415
416
417def ClearUpdateStateAndReboot(client):
418    """Removes the cr50 status files in /var/cache and reboots the AP"""
419    # If any /var/cache/cr50* files exist, remove them.
420    result = client.run('ls %s' % CR50_STATE, ignore_status=True)
421    if not result.exit_status:
422        client.run('rm %s' % ' '.join(result.stdout.split()))
423    elif result.exit_status != 2:
424        # Exit status 2 means the file didn't exist. If the command fails for
425        # some other reason, raise an error.
426        logging.debug(result)
427        raise error.TestFail(result.stderr)
428    client.reboot()
429
430
431def InstallImage(client, src, dest=CR50_PROD):
432    """Copy the image at src to dest on the dut
433
434    Args:
435        client: the object to run commands on
436        src: the image location of the server
437        dest: the desired location on the dut
438
439    Returns:
440        The filename where the image was copied to on the dut, a tuple
441        containing the RO and RW version of the file
442    """
443    # Send the file to the DUT
444    client.send_file(src, dest)
445
446    ver = GetBinVersion(client, dest)
447    client.run('sync')
448    return dest, ver
449
450
451def GetBoardIdInfoTuple(board_id_str):
452    """Convert the string into board id args.
453
454    Split the board id string board_id:(mask|board_id_inv):flags to a tuple of
455    its parts. Each element will be converted to an integer.
456
457    Returns:
458        board id int, mask|board_id_inv, and flags or None if its a universal
459        image.
460    """
461    # In tests None is used for universal board ids. Some old images don't
462    # support getting board id, so we use None. Convert 0:0:0 to None.
463    if not board_id_str or set(board_id_str) == EMPTY_IMAGE_BID_CHARACTERS:
464        return None
465
466    board_id, param2, flags = board_id_str.split(':')
467    return GetIntBoardId(board_id), int(param2, 16), int(flags, 16)
468
469
470def GetBoardIdInfoString(board_id_info, symbolic=False):
471    """Convert the board id list or str into a symbolic or non symbolic str.
472
473    This can be used to convert the board id info list into a symbolic or non
474    symbolic board id string. It can also be used to convert a the board id
475    string into a board id string with a symbolic or non symbolic board id
476
477    Args:
478        board_id_info: A string of the form board_id:(mask|board_id_inv):flags
479                       or a list with the board_id, (mask|board_id_inv), flags
480
481    Returns:
482        (board_id|symbolic_board_id):(mask|board_id_inv):flags. Will return
483        None if if the given board id info is empty or is not valid
484    """
485    # Convert board_id_info to a tuple if it's a string.
486    if isinstance(board_id_info, six.string_types):
487        board_id_info = GetBoardIdInfoTuple(board_id_info)
488
489    if not board_id_info:
490        return None
491
492    board_id, param2, flags = board_id_info
493    # Get the hex string for board id
494    board_id = '%08x' % GetIntBoardId(board_id)
495
496    # Convert the board id hex to a symbolic board id
497    if symbolic:
498        board_id = GetSymbolicBoardId(board_id)
499
500    # Return the board_id_str:8_digit_hex_mask: 8_digit_hex_flags
501    return '%s:%08x:%08x' % (board_id, param2, flags)
502
503
504def GetSymbolicBoardId(board_id):
505    """Convert an integer board id to a symbolic string
506
507    Args:
508        board_id: the board id to convert to the symbolic board id
509
510    Returns:
511        the 4 character symbolic board id
512    """
513    symbolic_board_id = ''
514    board_id = GetIntBoardId(board_id)
515
516    # Convert the int to a symbolic board id
517    for i in range(SYMBOLIC_BID_LENGTH):
518        symbolic_board_id += chr((board_id >> (i * 8)) & 0xff)
519    symbolic_board_id = symbolic_board_id[::-1]
520
521    # Verify the created board id is 4 characters
522    if len(symbolic_board_id) != SYMBOLIC_BID_LENGTH:
523        raise error.TestFail('Created invalid symbolic board id %s' %
524                             symbolic_board_id)
525    return symbolic_board_id
526
527
528def ConvertSymbolicBoardId(symbolic_board_id):
529    """Convert the symbolic board id str to an int
530
531    Args:
532        symbolic_board_id: a ASCII string. It can be up to 4 characters
533
534    Returns:
535        the symbolic board id string converted to an int
536    """
537    board_id = 0
538    for c in symbolic_board_id:
539        board_id = ord(c) | (board_id << 8)
540    return board_id
541
542
543def GetIntBoardId(board_id):
544    """"Return the gsctool interpretation of board_id
545
546    Args:
547        board_id: a int or string value of the board id
548
549    Returns:
550        a int representation of the board id
551    """
552    if type(board_id) == int:
553        return board_id
554
555    if len(board_id) <= SYMBOLIC_BID_LENGTH:
556        return ConvertSymbolicBoardId(board_id)
557
558    return int(board_id, 16)
559
560
561def GetExpectedFlags(flags):
562    """If flags are not specified, gsctool will set them to 0xff00
563
564    Args:
565        flags: The int value or None
566
567    Returns:
568        the original flags or 0xff00 if flags is None
569    """
570    return flags if flags != None else 0xff00
571
572
573def RMAOpen(client, cmd='', ignore_status=False):
574    """Run gsctool RMA commands"""
575    return GSCTool(client, ['-a', '-r', cmd], ignore_status)
576
577
578def GetChipBoardId(client):
579    """Return the board id and flags
580
581    Args:
582        client: the object to run commands on
583
584    Returns:
585        a tuple with the int values of board id, board id inv, flags
586
587    Raises:
588        TestFail if the second board id response field is not ~board_id
589    """
590    result = GSCTool(client, ['-a', '-i']).stdout.strip()
591    board_id_info = result.split('Board ID space: ')[-1].strip().split(':')
592    board_id, board_id_inv, flags = [int(val, 16) for val in board_id_info]
593    logging.info('BOARD_ID: %x:%x:%x', board_id, board_id_inv, flags)
594
595    if board_id == board_id_inv == ERASED_BID_INT:
596        if flags == ERASED_BID_INT:
597            logging.info('board id is erased')
598        else:
599            logging.info('board id type is erased')
600    elif board_id & board_id_inv:
601        raise error.TestFail('board_id_inv should be ~board_id got %x %x' %
602                             (board_id, board_id_inv))
603    return board_id, board_id_inv, flags
604
605
606def GetChipBIDFromImageBID(image_bid, brand):
607    """Calculate a chip bid that will work with the image bid.
608
609    Returns:
610        A tuple of integers (bid type, ~bid type, bid flags)
611    """
612    image_bid_tuple = GetBoardIdInfoTuple(image_bid)
613    # GetBoardIdInfoTuple returns None if the image isn't board id locked.
614    # Generate a Tuple of all 0s the rest of the function can use.
615    if not image_bid_tuple:
616        image_bid_tuple = (0, 0, 0)
617
618    image_bid, image_mask, image_flags = image_bid_tuple
619    if image_mask:
620        new_brand = GetSymbolicBoardId(image_bid)
621    else:
622        new_brand = brand
623    new_flags = image_flags or MP_BID_FLAGS
624    bid_type = GetIntBoardId(new_brand)
625    # If the board id type is erased, type_inv should also be unset.
626    if bid_type == ERASED_BID_INT:
627        return (ERASED_BID_INT, ERASED_BID_INT, new_flags)
628    return bid_type, 0xffffffff & ~bid_type, new_flags
629
630
631def CheckChipBoardId(client, board_id, flags, board_id_inv=None):
632    """Compare the given board_id and flags to the running board_id and flags
633
634    Interpret board_id and flags how gsctool would interpret them, then compare
635    those interpreted values to the running board_id and flags.
636
637    Args:
638        client: the object to run commands on
639        board_id: a hex str, symbolic str, or int value for board_id
640        board_id_inv: a hex str or int value of board_id_inv. Ignore
641                      board_id_inv if None. board_id_inv is ~board_id unless
642                      the board id is erased. In case both should be 0xffffffff.
643        flags: the int value of flags or None
644
645    Raises:
646        TestFail if the new board id info does not match
647    """
648    # Read back the board id and flags
649    new_board_id, new_board_id_inv, new_flags = GetChipBoardId(client)
650
651    expected_board_id = GetIntBoardId(board_id)
652    expected_flags = GetExpectedFlags(flags)
653
654    if board_id_inv == None:
655        new_board_id_inv_str = ''
656        expected_board_id_inv_str = ''
657    else:
658        new_board_id_inv_str = '%08x:' % new_board_id_inv
659        expected_board_id_inv = GetIntBoardId(board_id_inv)
660        expected_board_id_inv_str = '%08x:' % expected_board_id_inv
661
662    expected_str = '%08x:%s%08x' % (expected_board_id,
663                                    expected_board_id_inv_str,
664                                    expected_flags)
665    new_str = '%08x:%s%08x' % (new_board_id, new_board_id_inv_str, new_flags)
666
667    if new_str != expected_str:
668        raise error.TestFail('Failed to set board id: expected %r got %r' %
669                             (expected_str, new_str))
670
671
672def SetChipBoardId(client, board_id, flags=None, pad=True):
673    """Sets the board id and flags
674
675    Args:
676        client: the object to run commands on
677        board_id: a string of the symbolic board id or board id hex value. If
678                  the string is less than 4 characters long it will be
679                  considered a symbolic value
680        flags: a int flag value. If board_id is a symbolic value, then this will
681               be ignored.
682        pad: pad any int board id, so the string is not 4 characters long.
683
684    Raises:
685        TestFail if we were unable to set the flags to the correct value
686    """
687    if isinstance(board_id, int):
688        # gsctool will interpret any 4 character string as a RLZ code. If pad is
689        # true, pad the board id with 0s to make sure the board id isn't 4
690        # characters long.
691        board_id_arg = ('0x%08x' % board_id) if pad else hex(board_id)
692    else:
693        board_id_arg = board_id
694    if flags != None:
695        board_id_arg += ':' + hex(flags)
696    # Set the board id using the given board id and flags
697    result = GSCTool(client, ['-a', '-i', board_id_arg]).stdout.strip()
698
699    CheckChipBoardId(client, board_id, flags)
700
701def DumpFlog(client):
702    """Retrieve contents of the flash log"""
703    return GSCTool(client, ['-a', '-L']).stdout.strip()
704