From c8810406da252e2a3752bd6db7c10d706fbdcd9b Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 30 Jun 2021 14:20:58 +0200 Subject: [PATCH] tici: flash bootloader partitions from manifest (#21399) * move swapping to python * only create downloader if needed * typo * number * add sanity check * boot full check to test * manifest is required argument * implement full hash check * off by one * new manifest * only write tag for system * bump splash * review comments part 1 * trigger update Co-authored-by: Robbe Derks old-commit-hash: 7c6bf89e043fe559b3bc4d19391c9c9af095a998 --- launch_chffrplus.sh | 57 +------------ selfdrive/hardware/tici/agnos.json | 63 ++++++++++++-- selfdrive/hardware/tici/agnos.py | 128 +++++++++++++++++++++++------ selfdrive/updated.py | 5 +- 4 files changed, 162 insertions(+), 91 deletions(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 4f7c9ad88e..29d5d8c188 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -104,62 +104,11 @@ function tici_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - # Get number of slot to switch to - CUR_SLOT=$(abctl --boot_slot) - if [[ "$CUR_SLOT" == "_a" ]]; then - OTHER_SLOT="_b" - OTHER_SLOT_NUMBER="1" - else - OTHER_SLOT="_a" - OTHER_SLOT_NUMBER="0" - fi - echo "Cur slot $CUR_SLOT, target $OTHER_SLOT" - - # Get expected hashes from manifest MANIFEST="$DIR/selfdrive/hardware/tici/agnos.json" - SYSTEM_HASH_EXPECTED=$(jq -r ".[] | select(.name == \"system\") | .hash_raw" $MANIFEST) - SYSTEM_SIZE=$(jq -r ".[] | select(.name == \"system\") | .size" $MANIFEST) - BOOT_HASH_EXPECTED=$(jq -r ".[] | select(.name == \"boot\") | .hash_raw" $MANIFEST) - BOOT_SIZE=$(jq -r ".[] | select(.name == \"boot\") | .size" $MANIFEST) - echo "Expected hashes:" - echo "System: $SYSTEM_HASH_EXPECTED" - echo "Boot: $BOOT_HASH_EXPECTED" - - # Read hashes from alternate partitions, should already be flashed by updated - SYSTEM_HASH=$(dd if="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 skip="$SYSTEM_SIZE" count=64 2>/dev/null) - BOOT_HASH=$(dd if="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 skip="$BOOT_SIZE" count=64 2>/dev/null) - echo "Found hashes:" - echo "System: $SYSTEM_HASH" - echo "Boot: $BOOT_HASH" - - if [[ "$SYSTEM_HASH" == "$SYSTEM_HASH_EXPECTED" && "$BOOT_HASH" == "$BOOT_HASH_EXPECTED" ]]; then - echo "Swapping active slot to $OTHER_SLOT_NUMBER" - - # Clean hashes before swapping to prevent looping - dd if=/dev/zero of="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 seek="$SYSTEM_SIZE" count=64 - dd if=/dev/zero of="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 seek="$BOOT_SIZE" count=64 - sync - - abctl --set_active "$OTHER_SLOT_NUMBER" - - sleep 1 - sudo reboot - else - echo "Hash mismatch, downloading agnos" - if $DIR/selfdrive/hardware/tici/agnos.py $MANIFEST; then - echo "Download done, swapping active slot to $OTHER_SLOT_NUMBER" + $DIR/selfdrive/hardware/tici/agnos.py --swap $MANIFEST - # Clean hashes before swapping to prevent looping - dd if=/dev/zero of="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 seek="$SYSTEM_SIZE" count=64 - dd if=/dev/zero of="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 seek="$BOOT_SIZE" count=64 - sync - - abctl --set_active "$OTHER_SLOT_NUMBER" - fi - - sleep 1 - sudo reboot - fi + sleep 1 + sudo reboot fi } diff --git a/selfdrive/hardware/tici/agnos.json b/selfdrive/hardware/tici/agnos.json index 3e0e0f2153..b284f563ef 100644 --- a/selfdrive/hardware/tici/agnos.json +++ b/selfdrive/hardware/tici/agnos.json @@ -1,18 +1,63 @@ [ - { - "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467.img.xz", - "hash": "13842404eaceb5466a028a2eb91db938a20c8ff5bc541599c6b9729a4bb608f5", - "hash_raw": "96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467", - "size": 10737418240, - "sparse": true - }, { "name": "boot", "url": "https://commadist.azureedge.net/agnosupdate/boot-1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c.img.xz", "hash": "1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c", "hash_raw": "1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c", "size": 14772224, - "sparse": false + "sparse": false, + "full_check": true, + "has_ab": true + }, + { + "name": "abl", + "url": "https://commadist.azureedge.net/agnosupdate/abl-f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c.img.xz", + "hash": "f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c", + "hash_raw": "f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c", + "size": 274432, + "sparse": false, + "full_check": true, + "has_ab": true + }, + { + "name": "xbl", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35.img.xz", + "hash": "2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35", + "hash_raw": "2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35", + "size": 3670016, + "sparse": false, + "full_check": true, + "has_ab": true + }, + { + "name": "xbl_config", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad.img.xz", + "hash": "3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad", + "hash_raw": "3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad", + "size": 364544, + "sparse": false, + "full_check": true, + "has_ab": true + }, + { + "name": "splash", + "url": "https://commadist.azureedge.net/agnosupdate/splash-5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08.img.xz", + "hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08", + "hash_raw": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08", + "size": 34226176, + "sparse": false, + "full_check": true, + "has_ab": false + }, + { + "name": "system", + "url": "https://commadist.azureedge.net/agnosupdate/system-96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467.img.xz", + "hash": "13842404eaceb5466a028a2eb91db938a20c8ff5bc541599c6b9729a4bb608f5", + "hash_raw": "96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467", + "size": 10737418240, + "sparse": true, + "full_check": false, + "has_ab": true } ] + diff --git a/selfdrive/hardware/tici/agnos.py b/selfdrive/hardware/tici/agnos.py index b9ec022cba..3eeb03e094 100755 --- a/selfdrive/hardware/tici/agnos.py +++ b/selfdrive/hardware/tici/agnos.py @@ -10,6 +10,8 @@ from typing import Generator from common.spinner import Spinner +SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') + class StreamingDecompressor: def __init__(self, url: str) -> None: @@ -39,7 +41,7 @@ class StreamingDecompressor: self.sha256.update(result) return result -SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') + def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]: # https://source.android.com/devices/bootloader/images#sparse-format magic = struct.unpack("I", f.read(4))[0] @@ -74,25 +76,75 @@ def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]: raise Exception("Unhandled sparse chunk type") -def flash_partition(cloudlog, spinner, target_slot, partition): - cloudlog.info(f"Downloading and writing {partition['name']}") +def get_target_slot_number(): + current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip() + return 1 if current_slot == "_a" else 0 - downloader = StreamingDecompressor(partition['url']) - with open(f"/dev/disk/by-partlabel/{partition['name']}{target_slot}", 'wb+') as out: - partition_size = partition['size'] - # Check if partition is already flashed - out.seek(partition_size) - if out.read(64) == partition['hash_raw'].lower().encode(): - cloudlog.info(f"Already flashed {partition['name']}") - return +def slot_number_to_suffix(slot_number): + assert slot_number in (0, 1) + return '_a' if slot_number == 0 else '_b' + + +def get_partition_path(target_slot_number, partition): + path = f"/dev/disk/by-partlabel/{partition['name']}" + + if partition.get('has_ab', True): + path += slot_number_to_suffix(target_slot_number) + + return path + + +def verify_partition(target_slot_number, partition): + full_check = partition['full_check'] + path = get_partition_path(target_slot_number, partition) + partition_size = partition['size'] + + with open(path, 'rb+') as out: + if full_check: + raw_hash = hashlib.sha256() + + pos = 0 + chunk_size = 1024 * 1024 + while pos < partition_size: + n = min(chunk_size, partition_size - pos) + raw_hash.update(out.read(n)) + pos += n + + return raw_hash.hexdigest().lower() == partition['hash_raw'].lower() + else: + out.seek(partition_size) + return out.read(64) == partition['hash_raw'].lower().encode() + + +def clear_partition_hash(target_slot_number, partition): + path = get_partition_path(target_slot_number, partition) + with open(path, 'wb+') as out: + partition_size = partition['size'] - # Clear hash before flashing out.seek(partition_size) out.write(b"\x00" * 64) - out.seek(0) os.sync() + +def flash_partition(target_slot_number, partition, cloudlog, spinner=None): + cloudlog.info(f"Downloading and writing {partition['name']}") + + if verify_partition(target_slot_number, partition): + cloudlog.info(f"Already flashed {partition['name']}") + return + + downloader = StreamingDecompressor(partition['url']) + + # Clear hash before flashing in case we get interrupted + full_check = partition['full_check'] + if not full_check: + clear_partition_hash(target_slot_number, partition) + + path = get_partition_path(target_slot_number, partition) + with open(path, 'wb+') as out: + partition_size = partition['size'] + # Flash partition if partition['sparse']: raw_hash = hashlib.sha256() @@ -120,17 +172,23 @@ def flash_partition(cloudlog, spinner, target_slot, partition): # Write hash after successfull flash os.sync() - out.write(partition['hash_raw'].lower().encode()) + if not full_check: + out.write(partition['hash_raw'].lower().encode()) -def flash_agnos_update(manifest_path, cloudlog, spinner=None): +def swap(manifest_path, target_slot_number): update = json.load(open(manifest_path)) + for partition in update: + if not partition.get('full_check', False): + clear_partition_hash(target_slot_number, partition) + + subprocess.check_output(f"abctl --set_active {target_slot_number}", shell=True) - current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip() - target_slot = "_b" if current_slot == "_a" else "_a" - target_slot_number = "0" if target_slot == "_a" else "1" - cloudlog.info(f"Current slot {current_slot}, target slot {target_slot}") +def flash_agnos_update(manifest_path, target_slot_number, cloudlog, spinner=None): + update = json.load(open(manifest_path)) + + cloudlog.info(f"Target slot {target_slot_number}") # set target slot as unbootable os.system(f"abctl --set_unbootable {target_slot_number}") @@ -140,7 +198,7 @@ def flash_agnos_update(manifest_path, cloudlog, spinner=None): for retries in range(10): try: - flash_partition(cloudlog, spinner, target_slot, partition) + flash_partition(target_slot_number, partition, cloudlog, spinner) success = True break @@ -154,21 +212,39 @@ def flash_agnos_update(manifest_path, cloudlog, spinner=None): cloudlog.info(f"Failed to flash {partition['name']}, aborting") raise Exception("Maximum retries exceeded") - cloudlog.info(f"AGNOS ready on slot {target_slot}") + cloudlog.info(f"AGNOS ready on slot {target_slot_number}") + + +def verify_agnos_update(manifest_path, target_slot_number): + update = json.load(open(manifest_path)) + return all(verify_partition(target_slot_number, partition) for partition in update) if __name__ == "__main__": import logging import time - import sys + import argparse + + parser = argparse.ArgumentParser(description="Flash and verify AGNOS update", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) - if len(sys.argv) != 2: - print("Usage: ./agnos.py ") - exit(1) + parser.add_argument("--swap", action="store_true", help="Verify and perform swap, downloads if necessary") + parser.add_argument("manifest", help="Manifest json") + args = parser.parse_args() spinner = Spinner() spinner.update("Updating AGNOS") time.sleep(5) logging.basicConfig(level=logging.INFO) - flash_agnos_update(sys.argv[1], logging, spinner) + + target_slot_number = get_target_slot_number() + if args.swap: + while not verify_agnos_update(args.manifest, target_slot_number): + logging.error("Verification failed. Flashing AGNOS") + flash_agnos_update(args.manifest, target_slot_number, logging, spinner) + + logging.warning(f"Verification succeeded. Swapping to slot {target_slot_number}") + swap(args.manifest, target_slot_number) + else: + flash_agnos_update(args.manifest, target_slot_number, logging, spinner) diff --git a/selfdrive/updated.py b/selfdrive/updated.py index d50d6d6fe0..c5b13594b6 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -219,7 +219,7 @@ def finalize_update() -> None: def handle_agnos_update(wait_helper): - from selfdrive.hardware.tici.agnos import flash_agnos_update + from selfdrive.hardware.tici.agnos import flash_agnos_update, get_target_slot_number cur_version = HARDWARE.get_os_version() updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \ @@ -236,7 +236,8 @@ def handle_agnos_update(wait_helper): set_offroad_alert("Offroad_NeosUpdate", True) manifest_path = os.path.join(OVERLAY_MERGED, "selfdrive/hardware/tici/agnos.json") - flash_agnos_update(manifest_path, cloudlog) + target_slot_number = get_target_slot_number() + flash_agnos_update(manifest_path, target_slot_number, cloudlog) set_offroad_alert("Offroad_NeosUpdate", False)