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