dd24c971a3
Bug: 79951743
Test: Run validate_target_files.py on target_files.zip that has
non-monotonic ranges listed in IMAGES/system.map.
Change-Id: I28d3ca6972d361362589cd51e60731af9994a551
Merged-In: I28d3ca6972d361362589cd51e60731af9994a551
(cherry picked from commit d32936d657
)
228 lines
7.9 KiB
Python
Executable file
228 lines
7.9 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
# Copyright (C) 2017 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Validate a given (signed) target_files.zip.
|
|
|
|
It performs checks to ensure the integrity of the input zip.
|
|
- It verifies the file consistency between the ones in IMAGES/system.img (read
|
|
via IMAGES/system.map) and the ones under unpacked folder of SYSTEM/. The
|
|
same check also applies to the vendor image if present.
|
|
"""
|
|
|
|
import logging
|
|
import os.path
|
|
import re
|
|
import sys
|
|
import zipfile
|
|
|
|
import common
|
|
|
|
|
|
def _ReadFile(file_name, unpacked_name, round_up=False):
|
|
"""Constructs and returns a File object. Rounds up its size if needed."""
|
|
|
|
assert os.path.exists(unpacked_name)
|
|
with open(unpacked_name, 'r') as f:
|
|
file_data = f.read()
|
|
file_size = len(file_data)
|
|
if round_up:
|
|
file_size_rounded_up = common.RoundUpTo4K(file_size)
|
|
file_data += '\0' * (file_size_rounded_up - file_size)
|
|
return common.File(file_name, file_data)
|
|
|
|
|
|
def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
|
|
"""Check if the file has the expected SHA-1."""
|
|
|
|
logging.info('Validating the SHA-1 of %s', file_name)
|
|
unpacked_name = os.path.join(input_tmp, file_path)
|
|
assert os.path.exists(unpacked_name)
|
|
actual_sha1 = _ReadFile(file_name, unpacked_name, False).sha1
|
|
assert actual_sha1 == expected_sha1, \
|
|
'SHA-1 mismatches for {}. actual {}, expected {}'.format(
|
|
file_name, actual_sha1, expected_sha1)
|
|
|
|
|
|
def ValidateFileConsistency(input_zip, input_tmp, info_dict):
|
|
"""Compare the files from image files and unpacked folders."""
|
|
|
|
def CheckAllFiles(which):
|
|
logging.info('Checking %s image.', which)
|
|
# Allow having shared blocks when loading the sparse image, because allowing
|
|
# that doesn't affect the checks below (we will have all the blocks on file,
|
|
# unless it's skipped due to the holes).
|
|
image = common.GetSparseImage(which, input_tmp, input_zip, True)
|
|
prefix = '/' + which
|
|
for entry in image.file_map:
|
|
# Skip entries like '__NONZERO-0'.
|
|
if not entry.startswith(prefix):
|
|
continue
|
|
|
|
# Read the blocks that the file resides. Note that it will contain the
|
|
# bytes past the file length, which is expected to be padded with '\0's.
|
|
ranges = image.file_map[entry]
|
|
|
|
incomplete = ranges.extra.get('incomplete', False)
|
|
if incomplete:
|
|
logging.warning('Skipping %s that has incomplete block list', entry)
|
|
continue
|
|
|
|
# TODO(b/79951650): Handle files with non-monotonic ranges.
|
|
if not ranges.monotonic:
|
|
logging.warning(
|
|
'Skipping %s that has non-monotonic ranges: %s', entry, ranges)
|
|
continue
|
|
|
|
blocks_sha1 = image.RangeSha1(ranges)
|
|
|
|
# The filename under unpacked directory, such as SYSTEM/bin/sh.
|
|
unpacked_name = os.path.join(
|
|
input_tmp, which.upper(), entry[(len(prefix) + 1):])
|
|
unpacked_file = _ReadFile(entry, unpacked_name, True)
|
|
file_sha1 = unpacked_file.sha1
|
|
assert blocks_sha1 == file_sha1, \
|
|
'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
|
|
entry, ranges, blocks_sha1, file_sha1)
|
|
|
|
logging.info('Validating file consistency.')
|
|
|
|
# TODO(b/79617342): Validate non-sparse images.
|
|
if info_dict.get('extfs_sparse_flag') != '-s':
|
|
logging.warning('Skipped due to target using non-sparse images')
|
|
return
|
|
|
|
# Verify IMAGES/system.img.
|
|
CheckAllFiles('system')
|
|
|
|
# Verify IMAGES/vendor.img if applicable.
|
|
if 'VENDOR/' in input_zip.namelist():
|
|
CheckAllFiles('vendor')
|
|
|
|
# Not checking IMAGES/system_other.img since it doesn't have the map file.
|
|
|
|
|
|
def ValidateInstallRecoveryScript(input_tmp, info_dict):
|
|
"""Validate the SHA-1 embedded in install-recovery.sh.
|
|
|
|
install-recovery.sh is written in common.py and has the following format:
|
|
|
|
1. full recovery:
|
|
...
|
|
if ! applypatch -c type:device:size:SHA-1; then
|
|
applypatch /system/etc/recovery.img type:device sha1 size && ...
|
|
...
|
|
|
|
2. recovery from boot:
|
|
...
|
|
applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
|
|
recovery_size patch_info && ...
|
|
...
|
|
|
|
For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
|
|
and compare it against the one embedded in the script. While for recovery
|
|
from boot, we want to check the SHA-1 for both recovery.img and boot.img
|
|
under IMAGES/.
|
|
"""
|
|
|
|
script_path = 'SYSTEM/bin/install-recovery.sh'
|
|
if not os.path.exists(os.path.join(input_tmp, script_path)):
|
|
logging.info('%s does not exist in input_tmp', script_path)
|
|
return
|
|
|
|
logging.info('Checking %s', script_path)
|
|
with open(os.path.join(input_tmp, script_path), 'r') as script:
|
|
lines = script.read().strip().split('\n')
|
|
assert len(lines) >= 6
|
|
check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
|
|
lines[1].strip())
|
|
expected_recovery_check_sha1 = check_cmd.group(1)
|
|
patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
|
|
applypatch_argv = patch_cmd.group(1).strip().split()
|
|
|
|
full_recovery_image = info_dict.get("full_recovery_image") == "true"
|
|
if full_recovery_image:
|
|
assert len(applypatch_argv) == 5
|
|
# Check we have the same expected SHA-1 of recovery.img in both check mode
|
|
# and patch mode.
|
|
expected_recovery_sha1 = applypatch_argv[3].strip()
|
|
assert expected_recovery_check_sha1 == expected_recovery_sha1
|
|
ValidateFileAgainstSha1(input_tmp, 'recovery.img',
|
|
'SYSTEM/etc/recovery.img', expected_recovery_sha1)
|
|
else:
|
|
# We're patching boot.img to get recovery.img where bonus_args is optional
|
|
if applypatch_argv[1] == "-b":
|
|
assert len(applypatch_argv) == 8
|
|
boot_info_index = 3
|
|
else:
|
|
assert len(applypatch_argv) == 6
|
|
boot_info_index = 1
|
|
|
|
# boot_info: boot_type:boot_device:boot_size:boot_sha1
|
|
boot_info = applypatch_argv[boot_info_index].strip().split(':')
|
|
assert len(boot_info) == 4
|
|
ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
|
|
file_path='IMAGES/boot.img',
|
|
expected_sha1=boot_info[3])
|
|
|
|
recovery_sha1_index = boot_info_index + 2
|
|
expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
|
|
assert expected_recovery_check_sha1 == expected_recovery_sha1
|
|
ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
|
|
file_path='IMAGES/recovery.img',
|
|
expected_sha1=expected_recovery_sha1)
|
|
|
|
logging.info('Done checking %s', script_path)
|
|
|
|
|
|
def main(argv):
|
|
def option_handler():
|
|
return True
|
|
|
|
args = common.ParseOptions(
|
|
argv, __doc__, extra_opts="",
|
|
extra_long_opts=[],
|
|
extra_option_handler=option_handler)
|
|
|
|
if len(args) != 1:
|
|
common.Usage(__doc__)
|
|
sys.exit(1)
|
|
|
|
logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
|
|
date_format = '%Y/%m/%d %H:%M:%S'
|
|
logging.basicConfig(level=logging.INFO, format=logging_format,
|
|
datefmt=date_format)
|
|
|
|
logging.info("Unzipping the input target_files.zip: %s", args[0])
|
|
input_tmp = common.UnzipTemp(args[0])
|
|
|
|
info_dict = common.LoadInfoDict(input_tmp)
|
|
with zipfile.ZipFile(args[0], 'r') as input_zip:
|
|
ValidateFileConsistency(input_zip, input_tmp, info_dict)
|
|
|
|
ValidateInstallRecoveryScript(input_tmp, info_dict)
|
|
|
|
# TODO: Check if the OTA keys have been properly updated (the ones on /system,
|
|
# in recovery image).
|
|
|
|
logging.info("Done.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main(sys.argv[1:])
|
|
finally:
|
|
common.Cleanup()
|