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