#!/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 -o "${ro}" "${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 }