| #!/bin/sh | |
| # | |
| # Copyright 2011 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # | |
| # Note: This file must be written in dash compatible way as scripts that use | |
| # this may run in the Chrome OS client enviornment. | |
| # shellcheck disable=SC2039,SC2059,SC2155 | |
| # Determine script directory | |
| SCRIPT_DIR=$(dirname "$0") | |
| PROG=$(basename "$0") | |
| : "${GPT:=cgpt}" | |
| : "${FUTILITY:=futility}" | |
| # The tag when the rootfs is changed. | |
| TAG_NEEDS_TO_BE_SIGNED="/root/.need_to_be_signed" | |
| # List of Temporary files and mount points. | |
| TEMP_FILE_LIST=$(mktemp) | |
| TEMP_DIR_LIST=$(mktemp) | |
| # Finds and loads the 'shflags' library, or return as failed. | |
| load_shflags() { | |
| # Load shflags | |
| if [ -f /usr/share/misc/shflags ]; then | |
| # shellcheck disable=SC1090,SC1091 | |
| . /usr/share/misc/shflags | |
| elif [ -f "${SCRIPT_DIR}/lib/shflags/shflags" ]; then | |
| # shellcheck disable=SC1090 | |
| . "${SCRIPT_DIR}/lib/shflags/shflags" | |
| else | |
| echo "ERROR: Cannot find the required shflags library." | |
| return 1 | |
| fi | |
| # Add debug option for debug output below | |
| DEFINE_boolean debug $FLAGS_FALSE "Provide debug messages" "d" | |
| } | |
| # Functions for debug output | |
| # ---------------------------------------------------------------------------- | |
| # These helpers are for runtime systems. For scripts using common.sh, | |
| # they'll get better definitions that will clobber these ones. | |
| info() { | |
| echo "${PROG}: INFO: $*" >&2 | |
| } | |
| warn() { | |
| echo "${PROG}: WARN: $*" >&2 | |
| } | |
| error() { | |
| echo "${PROG}: ERROR: $*" >&2 | |
| } | |
| # Reports error message and exit(1) | |
| # Args: error message | |
| die() { | |
| error "$@" | |
| exit 1 | |
| } | |
| # Returns true if we're running in debug mode. | |
| # | |
| # Note that if you don't set up shflags by calling load_shflags(), you | |
| # must set $FLAGS_debug and $FLAGS_TRUE yourself. The default | |
| # behavior is that debug will be off if you define neither $FLAGS_TRUE | |
| # nor $FLAGS_debug. | |
| is_debug_mode() { | |
| [ "${FLAGS_debug:-not$FLAGS_TRUE}" = "$FLAGS_TRUE" ] | |
| } | |
| # Prints messages (in parameters) in debug mode | |
| # Args: debug message | |
| debug_msg() { | |
| if is_debug_mode; then | |
| echo "DEBUG: $*" 1>&2 | |
| fi | |
| } | |
| # Functions for temporary files and directories | |
| # ---------------------------------------------------------------------------- | |
| # Create a new temporary file and return its name. | |
| # File is automatically cleaned when cleanup_temps_and_mounts() is called. | |
| make_temp_file() { | |
| local tempfile="$(mktemp)" | |
| echo "$tempfile" >> "$TEMP_FILE_LIST" | |
| echo "$tempfile" | |
| } | |
| # Create a new temporary directory and return its name. | |
| # Directory is automatically deleted and any filesystem mounted on it unmounted | |
| # when cleanup_temps_and_mounts() is called. | |
| make_temp_dir() { | |
| local tempdir=$(mktemp -d) | |
| echo "$tempdir" >> "$TEMP_DIR_LIST" | |
| echo "$tempdir" | |
| } | |
| cleanup_temps_and_mounts() { | |
| while read -r line; do | |
| rm -f "$line" | |
| done < "$TEMP_FILE_LIST" | |
| set +e # umount may fail for unmounted directories | |
| while read -r line; do | |
| if [ -n "$line" ]; then | |
| if has_needs_to_be_resigned_tag "$line"; then | |
| echo "Warning: image may be modified. Please resign image." | |
| fi | |
| sudo umount "$line" 2>/dev/null | |
| rm -rf "$line" | |
| fi | |
| done < "$TEMP_DIR_LIST" | |
| set -e | |
| rm -rf "$TEMP_DIR_LIST" "$TEMP_FILE_LIST" | |
| } | |
| trap "cleanup_temps_and_mounts" EXIT | |
| # Functions for partition management | |
| # ---------------------------------------------------------------------------- | |
| # Construct a partition device name from a whole disk block device and a | |
| # partition number. | |
| # This works for [/dev/sda, 3] (-> /dev/sda3) as well as [/dev/mmcblk0, 2] | |
| # (-> /dev/mmcblk0p2). | |
| make_partition_dev() { | |
| local block="$1" | |
| local num="$2" | |
| # If the disk block device ends with a number, we add a 'p' before the | |
| # partition number. | |
| if [ "${block%[0-9]}" = "${block}" ]; then | |
| echo "${block}${num}" | |
| else | |
| echo "${block}p${num}" | |
| fi | |
| } | |
| # Find the block size of a device in bytes | |
| # Args: DEVICE (e.g. /dev/sda) | |
| # Return: block size in bytes | |
| blocksize() { | |
| local output='' | |
| local path="$1" | |
| if [ -b "${path}" ]; then | |
| local dev="${path##*/}" | |
| local sys="/sys/block/${dev}/queue/logical_block_size" | |
| output="$(cat "${sys}" 2>/dev/null)" | |
| fi | |
| echo "${output:-512}" | |
| } | |
| # Read GPT table to find the starting location of a specific partition. | |
| # Args: DEVICE PARTNUM | |
| # Returns: offset (in sectors) of partition PARTNUM | |
| partoffset() { | |
| sudo "$GPT" show -b -i "$2" "$1" | |
| } | |
| # Read GPT table to find the size of a specific partition. | |
| # Args: DEVICE PARTNUM | |
| # Returns: size (in sectors) of partition PARTNUM | |
| partsize() { | |
| sudo "$GPT" show -s -i "$2" "$1" | |
| } | |
| # Tags a file system as "needs to be resigned". | |
| # Args: MOUNTDIRECTORY | |
| tag_as_needs_to_be_resigned() { | |
| local mount_dir="$1" | |
| sudo touch "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" | |
| } | |
| # Determines if the target file system has the tag for resign | |
| # Args: MOUNTDIRECTORY | |
| # Returns: true if the tag is there otherwise false | |
| has_needs_to_be_resigned_tag() { | |
| local mount_dir="$1" | |
| [ -f "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" ] | |
| } | |
| # Determines if the target file system is a Chrome OS root fs | |
| # Args: MOUNTDIRECTORY | |
| # Returns: true if MOUNTDIRECTORY looks like root fs, otherwise false | |
| is_rootfs_partition() { | |
| local mount_dir="$1" | |
| [ -f "$mount_dir/$(dirname "$TAG_NEEDS_TO_BE_SIGNED")" ] | |
| } | |
| # If the kernel is buggy and is unable to loop+mount quickly, | |
| # retry the operation a few times. | |
| # Args: IMAGE PARTNUM MOUNTDIRECTORY [ro] | |
| # | |
| # This function does not check whether the partition is allowed to be mounted as | |
| # RW. Callers must ensure the partition can be mounted as RW before calling | |
| # this function without |ro| argument. | |
| _mount_image_partition_retry() { | |
| local image=$1 | |
| local partnum=$2 | |
| local mount_dir=$3 | |
| local ro=$4 | |
| local bs="$(blocksize "${image}")" | |
| local offset=$(( $(partoffset "${image}" "${partnum}") * bs )) | |
| local out try | |
| # shellcheck disable=SC2086 | |
| set -- sudo LC_ALL=C mount -o loop,offset=${offset},${ro} \ | |
| "${image}" "${mount_dir}" | |
| try=1 | |
| while [ ${try} -le 5 ]; do | |
| if ! out=$("$@" 2>&1); then | |
| if [ "${out}" = "mount: you must specify the filesystem type" ]; then | |
| printf 'WARNING: mounting %s at %s failed (try %i); retrying\n' \ | |
| "${image}" "${mount_dir}" "${try}" | |
| # Try to "quiet" the disks and sleep a little to reduce contention. | |
| sync | |
| sleep $(( try * 5 )) | |
| else | |
| # Failed for a different reason; abort! | |
| break | |
| fi | |
| else | |
| # It worked! | |
| return 0 | |
| fi | |
| : $(( try += 1 )) | |
| done | |
| echo "ERROR: mounting ${image} at ${mount_dir} failed:" | |
| echo "${out}" | |
| # We don't preserve the exact exit code of `mount`, but since | |
| # no one in this code base seems to check it, it's a moot point. | |
| return 1 | |
| } | |
| # If called without 'ro', make sure the partition is allowed to be mounted as | |
| # 'rw' before actually mounting it. | |
| # Args: IMAGE PARTNUM MOUNTDIRECTORY [ro] | |
| _mount_image_partition() { | |
| local image=$1 | |
| local partnum=$2 | |
| local mount_dir=$3 | |
| local ro=$4 | |
| local bs="$(blocksize "${image}")" | |
| local offset=$(( $(partoffset "${image}" "${partnum}") * bs )) | |
| if [ "$ro" != "ro" ]; then | |
| # Forcibly call enable_rw_mount. It should fail on unsupported | |
| # filesystems and be idempotent on ext*. | |
| enable_rw_mount "${image}" ${offset} 2> /dev/null | |
| fi | |
| _mount_image_partition_retry "$@" | |
| } | |
| # If called without 'ro', make sure the partition is allowed to be mounted as | |
| # 'rw' before actually mounting it. | |
| # Args: LOOPDEV PARTNUM MOUNTDIRECTORY [ro] | |
| _mount_loop_image_partition() { | |
| local loopdev=$1 | |
| local partnum=$2 | |
| local mount_dir=$3 | |
| local ro=$4 | |
| local loop_rootfs="${loopdev}p${partnum}" | |
| if [ "$ro" != "ro" ]; then | |
| # Forcibly call enable_rw_mount. It should fail on unsupported | |
| # filesystems and be idempotent on ext*. | |
| enable_rw_mount "${loop_rootfs}" 2>/dev/null | |
| fi | |
| sudo mount -o "${ro}" "${loop_rootfs}" "${mount_dir}" | |
| } | |
| # Mount a partition read-only from an image into a local directory | |
| # Args: IMAGE PARTNUM MOUNTDIRECTORY | |
| mount_image_partition_ro() { | |
| _mount_image_partition "$@" "ro" | |
| } | |
| # Mount a partition read-only from an image into a local directory | |
| # Args: LOOPDEV PARTNUM MOUNTDIRECTORY | |
| mount_loop_image_partition_ro() { | |
| _mount_loop_image_partition "$@" "ro" | |
| } | |
| # Mount a partition from an image into a local directory | |
| # Args: IMAGE PARTNUM MOUNTDIRECTORY | |
| mount_image_partition() { | |
| local mount_dir=$3 | |
| _mount_image_partition "$@" | |
| if is_rootfs_partition "${mount_dir}"; then | |
| tag_as_needs_to_be_resigned "${mount_dir}" | |
| fi | |
| } | |
| # Mount a partition from an image into a local directory | |
| # Args: LOOPDEV PARTNUM MOUNTDIRECTORY | |
| mount_loop_image_partition() { | |
| local mount_dir=$3 | |
| _mount_loop_image_partition "$@" | |
| if is_rootfs_partition "${mount_dir}"; then | |
| tag_as_needs_to_be_resigned "${mount_dir}" | |
| fi | |
| } | |
| # Mount the image's ESP (EFI System Partition) on a newly created temporary | |
| # directory. | |
| # Prints out the newly created temporary directory path if succeeded. | |
| # If the image doens't have an ESP partition, returns 0 without print anything. | |
| # Args: LOOPDEV | |
| # Returns: 0 if succeeded, 1 otherwise. | |
| mount_image_esp() { | |
| local loopdev="$1" | |
| local ESP_PARTNUM=12 | |
| local loop_esp="${loopdev}p${ESP_PARTNUM}" | |
| local esp_offset=$(( $(partoffset "${loopdev}" "${ESP_PARTNUM}") )) | |
| # Check if the image has an ESP partition. | |
| if [[ "${esp_offset}" == "0" ]]; then | |
| return 0 | |
| fi | |
| local esp_dir="$(make_temp_dir)" | |
| if ! sudo mount "${loop_esp}" "${esp_dir}"; then | |
| return 1 | |
| fi | |
| echo "${esp_dir}" | |
| return 0 | |
| } | |
| # Extract a partition to a file | |
| # Args: IMAGE PARTNUM OUTPUTFILE | |
| extract_image_partition() { | |
| local image=$1 | |
| local partnum=$2 | |
| local output_file=$3 | |
| local offset=$(partoffset "$image" "$partnum") | |
| local size=$(partsize "$image" "$partnum") | |
| # shellcheck disable=SC2086 | |
| dd if="$image" of="$output_file" bs=512 skip=$offset count=$size \ | |
| conv=notrunc 2>/dev/null | |
| } | |
| # Replace a partition in an image from file | |
| # Args: IMAGE PARTNUM INPUTFILE | |
| replace_image_partition() { | |
| local image=$1 | |
| local partnum=$2 | |
| local input_file=$3 | |
| local offset=$(partoffset "$image" "$partnum") | |
| local size=$(partsize "$image" "$partnum") | |
| # shellcheck disable=SC2086 | |
| dd if="$input_file" of="$image" bs=512 seek=$offset count=$size \ | |
| conv=notrunc 2>/dev/null | |
| } | |
| # For details, see crosutils.git/common.sh | |
| enable_rw_mount() { | |
| local rootfs="$1" | |
| local offset="${2-0}" | |
| # Make sure we're checking an ext2 image | |
| # shellcheck disable=SC2086 | |
| if ! is_ext2 "$rootfs" $offset; then | |
| echo "enable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2 | |
| return 1 | |
| fi | |
| local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte | |
| # Dash can't do echo -ne, but it can do printf "\NNN" | |
| # We could use /dev/zero here, but this matches what would be | |
| # needed for disable_rw_mount (printf '\377'). | |
| printf '\000' | | |
| sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \ | |
| conv=notrunc count=1 bs=1 2>/dev/null | |
| } | |
| # For details, see crosutils.git/common.sh | |
| is_ext2() { | |
| local rootfs="$1" | |
| local offset="${2-0}" | |
| # Make sure we're checking an ext2 image | |
| local sb_magic_offset=$((0x438)) | |
| local sb_value=$(sudo dd if="$rootfs" skip=$((offset + sb_magic_offset)) \ | |
| count=2 bs=1 2>/dev/null) | |
| local expected_sb_value=$(printf '\123\357') | |
| if [ "$sb_value" = "$expected_sb_value" ]; then | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| disable_rw_mount() { | |
| local rootfs="$1" | |
| local offset="${2-0}" | |
| # Make sure we're checking an ext2 image | |
| # shellcheck disable=SC2086 | |
| if ! is_ext2 "$rootfs" $offset; then | |
| echo "disable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2 | |
| return 1 | |
| fi | |
| local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte | |
| # Dash can't do echo -ne, but it can do printf "\NNN" | |
| # We could use /dev/zero here, but this matches what would be | |
| # needed for disable_rw_mount (printf '\377'). | |
| printf '\377' | | |
| sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \ | |
| conv=notrunc count=1 bs=1 2>/dev/null | |
| } | |
| rw_mount_disabled() { | |
| local rootfs="$1" | |
| local offset="${2-0}" | |
| # Make sure we're checking an ext2 image | |
| # shellcheck disable=SC2086 | |
| if ! is_ext2 "$rootfs" $offset; then | |
| return 2 | |
| fi | |
| local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte | |
| local ro_value=$(sudo dd if="$rootfs" skip=$((offset + ro_compat_offset)) \ | |
| count=1 bs=1 2>/dev/null) | |
| local expected_ro_value=$(printf '\377') | |
| if [ "$ro_value" = "$expected_ro_value" ]; then | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # Functions for CBFS management | |
| # ---------------------------------------------------------------------------- | |
| # Get the compression algorithm used for the given CBFS file. | |
| # Args: INPUT_CBFS_IMAGE CBFS_FILE_NAME | |
| get_cbfs_compression() { | |
| cbfstool "$1" print -r "FW_MAIN_A" | awk -vname="$2" '$1 == name {print $5}' | |
| } | |
| # Store a file in CBFS. | |
| # Args: INPUT_CBFS_IMAGE INPUT_FILE CBFS_FILE_NAME | |
| store_file_in_cbfs() { | |
| local image="$1" | |
| local file="$2" | |
| local name="$3" | |
| local compression=$(get_cbfs_compression "$1" "${name}") | |
| # Don't re-add a file to a section if it's unchanged. Otherwise this seems | |
| # to break signature of existing contents. https://crbug.com/889716 | |
| if cbfstool "${image}" extract -r "FW_MAIN_A,FW_MAIN_B" \ | |
| -f "${file}.orig" -n "${name}"; then | |
| if cmp -s "${file}" "${file}.orig"; then | |
| rm -f "${file}.orig" | |
| return | |
| fi | |
| rm -f "${file}.orig" | |
| fi | |
| cbfstool "${image}" remove -r "FW_MAIN_A,FW_MAIN_B" -n "${name}" || return | |
| # This add can fail if | |
| # 1. Size of a signature after compression is larger | |
| # 2. CBFS is full | |
| # These conditions extremely unlikely become true at the same time. | |
| cbfstool "${image}" add -r "FW_MAIN_A,FW_MAIN_B" -t "raw" \ | |
| -c "${compression}" -f "${file}" -n "${name}" || return | |
| } | |
| # Misc functions | |
| # ---------------------------------------------------------------------------- | |
| # Parses the version file containing key=value lines | |
| # Args: key file | |
| # Returns: value | |
| get_version() { | |
| local key="$1" | |
| local file="$2" | |
| awk -F= -vkey="${key}" '$1 == key { print $NF }' "${file}" | |
| } | |
| # Returns true if all files in parameters exist. | |
| # Args: List of files | |
| ensure_files_exist() { | |
| local filename return_value=0 | |
| for filename in "$@"; do | |
| if [ ! -f "$filename" ] && [ ! -b "$filename" ]; then | |
| echo "ERROR: Cannot find required file: $filename" | |
| return_value=1 | |
| fi | |
| done | |
| return $return_value | |
| } | |
| # Check if the 'chronos' user already has a password | |
| # Args: rootfs | |
| no_chronos_password() { | |
| local rootfs=$1 | |
| # Make sure the chronos user actually exists. | |
| if grep -qs '^chronos:' "${rootfs}/etc/passwd"; then | |
| sudo grep -q '^chronos:\*:' "${rootfs}/etc/shadow" | |
| fi | |
| } | |
| # Returns true if given ec.bin is signed or false if not. | |
| is_ec_rw_signed() { | |
| ${FUTILITY} dump_fmap "$1" | grep -q KEY_RO | |
| } |