blob: a6cd9e205af9908007517d119f558f18806cee5c [file] [log] [blame]
#!/bin/bash
#
# Copyright 2010 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Script which ensures that a given image has an up-to-date
# kernel partition, rootfs integrity hashes, and legacy bootloader configs.
# --- BEGIN COMMON.SH BOILERPLATE ---
# Load common CrOS utilities. Inside the chroot this file is installed in
# /usr/lib/crosutils. Outside the chroot we find it relative to the script's
# location.
find_common_sh() {
local common_paths=("$(dirname "$(readlink -f "$0")")/.." /usr/lib/crosutils)
local path
SCRIPT_ROOT="${common_paths[0]}"
for path in "${common_paths[@]}"; do
if [ -r "${path}/common.sh" ]; then
SCRIPT_ROOT="${path}"
break
fi
done
}
find_common_sh
# shellcheck source=../common.sh
. "${SCRIPT_ROOT}/common.sh" || exit 1
# --- END COMMON.SH BOILERPLATE ---
# Need to be inside the chroot to load chromeos-common.sh
assert_inside_chroot
# Load functions and constants for chromeos-install
# shellcheck source=../../platform2/chromeos-common-script/share/chromeos-common.sh
. /usr/share/misc/chromeos-common.sh || exit 1
# shellcheck source=../build_library/build_image_util.sh
. "${BUILD_LIBRARY_DIR}/build_image_util.sh" || exit 1
# shellcheck source=../build_library/disk_layout_util.sh
. "${BUILD_LIBRARY_DIR}/disk_layout_util.sh" || exit 1
# shellcheck source=../build_library/mount_gpt_util.sh
. "${BUILD_LIBRARY_DIR}/mount_gpt_util.sh" || exit 1
# shellcheck source=../build_library/ext2_sb_util.sh
. "${BUILD_LIBRARY_DIR}/ext2_sb_util.sh" || exit 1
switch_to_strict_mode
if [ $# -lt 2 ]; then
echo "Usage: ${0} /PATH/TO/IMAGE IMAGE.BIN [shflags overrides]"
exit 1
fi
IMAGE_DIR="$(readlink -f "${1}")"
IMAGE="${IMAGE_DIR}/${2}"
shift
shift
FLAG_OVERRIDES=( "$@" )
if get_boot_desc "${IMAGE_DIR}/boot.desc"; then
info "Boot-time configuration for ${IMAGE_DIR}:"
for flag in "${boot_desc_flags[@]}"; do
info " ${flag}"
done
else
warn "Falling back to command line parsing."
fi
if [ ! -r "${IMAGE}" ]; then
die "${IMAGE} cannot be read!"
fi
locate_gpt
set +e
# Now parse the build settings from ${OUTPUT_DIR}/boot.desc
DEFINE_string adjust_part "" \
"Adjustments to apply to the partition table"
DEFINE_string board "${DEFAULT_BOARD}" \
"Board we're building for."
DEFINE_string image_type "base" \
"Type of image we're building for (base/factory_install)."
DEFINE_string output_dir "/tmp" \
"Directory to place output in."
DEFINE_string arch "x86" \
"Architecture to make bootable for: arm, mips, x86, or amd64"
DEFINE_boolean cleanup_dirs "${FLAGS_TRUE}" \
"Whether the mount dirs should be removed on completion."
DEFINE_string boot_args "noinitrd" \
"Additional boot arguments to pass to the commandline"
DEFINE_string rootfs_hash "/tmp/rootfs.hash" \
"Path where the rootfs hash should be stored."
DEFINE_boolean enable_rootfs_verification "${FLAGS_FALSE}" \
"Default all bootloaders to NOT use kernel-based root fs integrity checking."
DEFINE_integer verity_error_behavior 3 \
"Kernel verified boot error behavior (0: I/O errors, 1: reboot, 2: nothing)"
DEFINE_integer verity_max_ios -1 \
"Number of outstanding I/O operations dm-verity caps at."
DEFINE_string verity_algorithm "sha256" \
"Cryptographic hash algorithm used for kernel vboot."
DEFINE_string verity_salt "" \
"Salt for rootfs hash tree."
DEFINE_string keys_dir "${VBOOT_DEVKEYS_DIR}" \
"Directory containing the signing keys."
DEFINE_string rootfs_mountpoint "/tmp/rootfs" \
"Path where the rootfs can be safely mounted"
DEFINE_string statefulfs_mountpoint "/tmp/statefulfs" \
"Path where the statefulfs can be safely mounted"
DEFINE_string espfs_mountpoint "/tmp/espfs" \
"Path where the espfs can be safely mounted"
DEFINE_boolean use_dev_keys "${FLAGS_FALSE}" \
"Use developer keys for signing. (Default: false)"
DEFINE_boolean fsck_rootfs "${FLAGS_FALSE}" \
"Check integrity of the rootfs on the modified image."
DEFINE_boolean force_developer_mode "${FLAGS_FALSE}" \
"Add cros_debug to boot args."
DEFINE_string enable_serial "" \
"Enable serial port for printks. Example values: ttyS0"
DEFINE_integer loglevel 7 \
"The loglevel to add to the kernel command line."
# Parse the boot.desc and any overrides
set -- "${boot_desc_flags[@]}" "${FLAG_OVERRIDES[@]}"
FLAGS "${@}" || exit 1
[ -z "${FLAGS_verity_salt}" ] && FLAGS_verity_salt=$(make_salt)
# board_options.sh relies on ${SRC_IMAGE} environment variable.
SRC_IMAGE="${IMAGE}"
# shellcheck source=../build_library/board_options.sh
. "${BUILD_LIBRARY_DIR}/board_options.sh" || exit 1
load_board_specific_script "board_specific_setup.sh"
# Only now can we die on error. shflags functions leak non-zero error codes,
# so will die prematurely if 'switch_to_strict_mode' is specified before now.
switch_to_strict_mode -u
append_boot_flag() {
local file="$1"
local pattern="$2"
local base_pattern="$3"
[[ -f "${file}" ]] || return "${FLAGS_TRUE}"
sudo grep -wq "${pattern}" "${file}" && return "${FLAGS_TRUE}"
sudo sed -i "s/\b${base_pattern}\b/& ${pattern}/g" "${file}"
}
check_kernel_size() {
local kernel_image_size="$1"
local kernel_part="$2"
local kernel_slot="$3"
local kernel_partition_size
kernel_partition_size=$(get_partition_size "${FLAGS_image_type}" \
"${kernel_part}")
local kernel_partition_size_90
kernel_partition_size_90=$(( kernel_partition_size * 90 / 100 ))
info "Kernel partition ${kernel_slot} size is ${kernel_partition_size} bytes."
if [[ ${kernel_image_size} -gt ${kernel_partition_size} ]]; then
die "Kernel image won't fit in partition ${kernel_slot}!"
elif [[ ${kernel_image_size} -gt ${kernel_partition_size_90} ]]; then
warn "Kernel partition ${kernel_slot} is more than 90% full!"
fi
}
build_img() {
local image_name="$1"
local root_dev="$2"
local root_dev_size="$3"
local keyblock="$4"
local private="$5"
local public="$6"
local vblock=${7:-""}
local extra_arguments=()
if [[ "${FLAGS_enable_rootfs_verification}" -eq "${FLAGS_TRUE}" ]]; then
extra_arguments+=(--enable_rootfs_verification)
else
# Default to non-verified
extra_arguments+=(--noenable_rootfs_verification)
fi
if [[ -n "${vblock}" ]]; then
extra_arguments+=(--hd_vblock="${FLAGS_output_dir}/${vblock}")
fi
"${SCRIPTS_DIR}"/build_kernel_image.sh \
--board="${FLAGS_board}" \
--arch="${FLAGS_arch}" \
--to="${FLAGS_output_dir}/${image_name}" \
--vmlinuz="${VMLINUZ}" \
--working_dir="${FLAGS_output_dir}" \
--boot_args="${FLAGS_boot_args}" \
--keep_work \
--rootfs_image="${root_dev}" \
--rootfs_image_size="${root_dev_size}" \
--rootfs_hash="${FLAGS_rootfs_hash}" \
--verity_hash_alg="${FLAGS_verity_algorithm}" \
--verity_max_ios="${FLAGS_verity_max_ios}" \
--verity_error_behavior="${FLAGS_verity_error_behavior}" \
--verity_salt="${FLAGS_verity_salt}" \
--keys_dir="${FLAGS_keys_dir}" \
--keyblock="${keyblock}" \
--private="${private}" \
--public="${public}" \
--enable_serial="${FLAGS_enable_serial}" \
"${extra_arguments[@]}"
}
make_image_bootable() {
local image="$1"
# Update legacy boot config templates (in rootfs) before rootfs is locked.
# This is required because postinst will copy new legacy boot configurations
# from rootfs partition instead of modifying existing entries in EFI
# partition.
if [[ "${FLAGS_force_developer_mode}" -eq "${FLAGS_TRUE}" ]]; then
trap "unmount_image ; die 'cros_make_image_bootable failed.'" EXIT
mount_image "${image}" "${FLAGS_rootfs_mountpoint}" \
"${FLAGS_statefulfs_mountpoint}"
append_boot_flag "${FLAGS_rootfs_mountpoint}/boot/syslinux/root.A.cfg" \
"cros_debug" "cros_legacy"
append_boot_flag "${FLAGS_rootfs_mountpoint}/boot/syslinux/root.B.cfg" \
"cros_debug" "cros_legacy"
append_boot_flag "${FLAGS_rootfs_mountpoint}/boot/syslinux/usb.A.cfg" \
"cros_debug" "cros_legacy"
append_boot_flag "${FLAGS_rootfs_mountpoint}/boot/efi/boot/grub.cfg" \
"cros_debug" "cros_efi"
unmount_image
trap - EXIT
fi
# Make the filesystem un-mountable as read-write.
# mount_gpt_image.sh will undo this as needed.
# TODO(wad) make sure there is parity in the signing scripts.
if [[ "${FLAGS_enable_rootfs_verification}" -eq "${FLAGS_TRUE}" ]]; then
# TODO(wad) this would be a good place to reset any other ext2 metadata.
info "Disabling r/w mount of the root filesystem"
local rootfs_offset
rootfs_offset="$(partoffset "${image}" 3)"
disable_rw_mount "${image}" "$(( rootfs_offset * 512 ))"
# For factory_install images, override FLAGS_enable_rootfs_verification
# here, so the following build_img calls won't make kernel set up the
# device mapper on initialization.
if [[ "${FLAGS_image_type}" == "factory_install" ]]; then
FLAGS_enable_rootfs_verification="${FLAGS_FALSE}"
fi
fi
trap "unmount_image ; die 'cros_make_image_bootable failed.'" EXIT
mount_image "${image}" "${FLAGS_rootfs_mountpoint}" \
"${FLAGS_statefulfs_mountpoint}" "" "--safe"
# Newer `mount` will decode the filename backing the loop device,
# so we need to dig deeper and find the answer ourselves.
root_dev=$(awk -v mnt="${FLAGS_rootfs_mountpoint}" \
'$2 == mnt { print $1 }' /proc/mounts)
if [[ -z "${root_dev}" ]]; then
# If the read-only rootfs is not mounted via the kernel using a real block
# device and we need to create one here. So far, all the filesystems we
# use in the rootfs can be mounted read-only by the kernel (including
# squashfs) so we just exit if that's the case.
die "Didn't find the rootfs block device device after mounting it."
fi
# We sign the image with the recovery_key, because this is what goes onto the
# USB key. We can only boot from the USB drive in recovery mode.
# For dev install shim, we need to use the installer keyblock instead of
# the recovery keyblock because of the difference in flags.
local keyblock
if [[ "${FLAGS_use_dev_keys}" -eq "${FLAGS_TRUE}" ]]; then
keyblock=installer_kernel.keyblock
info "DEBUG: use dev install keyblock"
else
keyblock=recovery_kernel.keyblock
info "DEBUG: use recovery keyblock"
fi
if [[ "${FLAGS_force_developer_mode}" -eq "${FLAGS_TRUE}" ]]; then
FLAGS_boot_args="${FLAGS_boot_args} cros_debug"
fi
# Builds the kernel partition image.
local partition_num_root_a
partition_num_root_a="$(get_layout_partition_number \
"${FLAGS_image_type}" ROOT-A)"
local rootfs_fs_size
rootfs_fs_size=$(get_filesystem_size "${FLAGS_image_type}" \
"${partition_num_root_a}")
# Usually we need to ensure that there will always be a regular kernel on
# recovery and non-recovery images. But for factory shim, we need to
# support 2 kernels signed with different recovery key. b/269192903
local kern_a_image="vmlinuz.image"
build_img "${kern_a_image}" "${root_dev}" "${rootfs_fs_size}" \
"${keyblock}" "recovery_kernel_data_key.vbprivk" "recovery_key.vbpubk"
local kern_b_image="vmlinuz_b.image"
if [[ "${FLAGS_image_type}" == "factory_install" ]]; then
kern_b_image="vmlinuz.image"
else
build_img "${kern_b_image}" "${root_dev}" "${rootfs_fs_size}" \
"kernel.keyblock" "kernel_data_key.vbprivk" \
"kernel_subkey.vbpubk" "vmlinuz_hd.vblock"
fi
# Check the size of kernel image and issue warning when image size is
# near the limit.
local kernel_image_size_A
kernel_image_size_A=$(stat -c '%s' "${FLAGS_output_dir}/${kern_a_image}")
info "Kernel image A size is ${kernel_image_size_A} bytes."
local kernel_image_size_B
kernel_image_size_B=$(stat -c '%s' "${FLAGS_output_dir}/${kern_b_image}")
info "Kernel image B size is ${kernel_image_size_B} bytes."
local partition_num_kern_a
partition_num_kern_a="$(get_layout_partition_number \
"${FLAGS_image_type}" KERN-A)"
check_kernel_size "${kernel_image_size_A}" "${partition_num_kern_a}" A
local partition_num_kern_b
partition_num_kern_b="$(get_layout_partition_number \
"${FLAGS_image_type}" KERN-B)"
# Since the kernel-b of factory shim is optional, ignore kernel-b if the size
# of kernel-b is less than or equal to the default value 1MiB.
local need_kern_b="${FLAGS_TRUE}"
if [[ "${FLAGS_image_type}" == "factory_install" ]]; then
local kernel_partition_size
kernel_partition_size=$(get_partition_size "${FLAGS_image_type}" \
"${partition_num_kern_b}")
local block_size
block_size="$(get_block_size)"
if [[ "${kernel_partition_size}" -le "${block_size}" ]]; then
need_kern_b="${FLAGS_FALSE}"
warn "Kernel partition B is skipped!"
fi
fi
if [[ ${need_kern_b} -eq ${FLAGS_TRUE} ]]; then
check_kernel_size "${kernel_image_size_B}" "${partition_num_kern_b}" B
fi
local rootfs_hash_size
rootfs_hash_size=$(stat -c '%s' "${FLAGS_rootfs_hash}")
local rootfs_partition_size
rootfs_partition_size=$(get_partition_size "${FLAGS_image_type}" \
"${partition_num_root_a}")
local rootfs_hash_pad
rootfs_hash_pad=$(( rootfs_partition_size - rootfs_fs_size ))
info "Appending rootfs.hash (${rootfs_hash_size} bytes) to the root fs"
if [[ ${rootfs_hash_size} -gt ${rootfs_hash_pad} ]]; then
die "rootfs_partition_size - rootfs_fs_size is less than the needed " \
"rootfs_hash_size (${rootfs_hash_size}), update your disk layout " \
"configuration"
fi
# Unfortunately, mount_gpt_image uses mount and not losetup to create the
# loop devices. This means that they are not the correct size. We have to
# write directly to the image to append the hash tree data.
local hash_offset
hash_offset="$(partoffset "${image}" "${partition_num_root_a}")"
hash_offset=$((hash_offset + ("${rootfs_fs_size}" / 512)))
sudo dd bs=512 \
seek="${hash_offset}" \
if="${FLAGS_rootfs_hash}" \
of="${image}" \
conv=notrunc \
status=none
# Move the verification block needed for the hard disk install to the
# stateful partition. Mount stateful fs, copy file, and umount fs.
# In original CL: http://codereview.chromium.org/2868044, this was done in
# create_base_image(). However, it could break the build if it is a clean
# build because vmlinuz_hd.vblock hasn't been created by build_kernel_image.sh
# In some builds that don't use vboot to verify the kernel, this file might
# not get created as part of the build, so only copy them if they were.
if [ -f "${FLAGS_output_dir}/vmlinuz_hd.vblock" ]; then
sudo cp "${FLAGS_output_dir}/vmlinuz_hd.vblock" \
"${FLAGS_statefulfs_mountpoint}"
fi
# Install the kernel to both slots A and B.
local koffset
koffset="$(partoffset "${image}" "${partition_num_kern_a}")"
sudo dd if="${FLAGS_output_dir}/${kern_a_image}" of="${image}" \
conv=notrunc bs=512 seek="${koffset}" status=none
if [[ ${need_kern_b} -eq ${FLAGS_TRUE} ]]; then
koffset="$(partoffset "${image}" "${partition_num_kern_b}")"
sudo dd if="${FLAGS_output_dir}/${kern_b_image}" of="${image}" \
conv=notrunc bs=512 seek="${koffset}" status=none
fi
# Update the bootloaders. The EFI system partition will be updated.
local kernel_part=
# We should update the esp in place in the image.
local partition_num_efi_system
partition_num_efi_system="$(get_layout_partition_number \
"${FLAGS_image_type}" EFI-SYSTEM)"
local image_dev
image_dev=$(loopback_partscan "${image}")
loopback_detach_and_die() {
loopback_detach "${image_dev}"
unmount_image
die "cros_make_image_bootable failed."
}
trap loopback_detach_and_die EXIT
local bootloader_to="${image_dev}"
# Make this writable by the user, it will get deleted by losetup -d.
sudo chown "$(id -u)" "${bootloader_to}" "${bootloader_to}"p*
local esp_size
esp_size="$(partsize "${image}" "${partition_num_efi_system}")"
esp_size=$((esp_size * 512)) # sectors to bytes
if [[ "${FLAGS_arch}" == "x86" || "${FLAGS_arch}" == "amd64" ]]; then
# Use the kernel partition to acquire configuration flags.
kernel_part=("--kernel_partition='${FLAGS_output_dir}/${kern_a_image}'")
# Install syslinux on the EFI System Partition.
kernel_part+=(--install_syslinux)
elif [[ "${FLAGS_arch}" == "arm64" ]]; then
# Use the kernel partition to acquire configuration flags.
kernel_part=("--kernel_partition='${FLAGS_output_dir}/${kern_a_image}'")
elif [[ "${FLAGS_arch}" == "arm" || "${FLAGS_arch}" == "mips" ]]; then
# These flags are not used for ARM / MIPS update_bootloaders.sh
kernel_part=()
fi
# Force all of the file writes to complete, in case it's necessary for
# crbug.com/938958
sync
if [[ ${esp_size} -gt 0 ]]; then
# Update EFI partition
"${SCRIPTS_DIR}"/update_bootloaders.sh \
--arch="${FLAGS_arch}" \
--board="${BOARD}" \
--image_type="${FLAGS_image_type}" \
--to="${bootloader_to}" \
--to_partition="${partition_num_efi_system}" \
--from="${FLAGS_rootfs_mountpoint}"/boot \
--vmlinuz="${VMLINUZ}" \
--zimage="${ZIMAGE}" \
"${kernel_part[@]}"
fi
# We don't need to keep these files around anymore.
sudo rm -f "${FLAGS_rootfs_hash}" "${FLAGS_output_dir}/${kern_a_image}" \
"${FLAGS_output_dir}/${kern_b_image}" \
"${FLAGS_output_dir}/vmlinuz_hd.vblock"
sudo losetup -d "${image_dev}"
unmount_image
# Since the kern-b will be signed with another recovery key, need to make
# kern-b bootable.
# TODO(b/270262345) Remove it after updating GPT flags.
if [[ "${FLAGS_image_type}" == "factory_install" \
&& ${need_kern_b} -eq ${FLAGS_TRUE} ]]
then
sudo cgpt add -i "${partition_num_kern_b}" -P 15 -S 1 -T 15 "${IMAGE}"
fi
trap - EXIT
}
verify_image_rootfs() {
local image=$1
local partition_num_root_a
partition_num_root_a="$(get_layout_partition_number \
"${FLAGS_image_type}" ROOT-A)"
local rootfs_offset
rootfs_offset="$(partoffset "${image}" "${partition_num_root_a}")"
local rootfs_tmp_file
rootfs_tmp_file=$(mktemp)
# Immediately resolve the local variable for the trap.
# shellcheck disable=SC2064
trap "rm '${rootfs_tmp_file}'" EXIT
sudo dd if="${image}" of="${rootfs_tmp_file}" bs=512 skip="${rootfs_offset}" \
status=none
# This flips the read-only compatibility flag, so that
# e2fsck does not complain about unknown file system capabilities.
enable_rw_mount "${rootfs_tmp_file}"
info "Running e2fsck to check root file system for errors"
sudo e2fsck -fn "${rootfs_tmp_file}" ||
die "Root file system has errors, please ensure boot.desc and/or \
command line parameters are correct"
}
# Store output and temporary files next to image.
FLAGS_output_dir="${IMAGE_DIR}"
FLAGS_rootfs_hash="${IMAGE_DIR}/rootfs.hash"
FLAGS_rootfs_mountpoint="${IMAGE_DIR}/rootfs_dir"
FLAGS_statefulfs_mountpoint="${IMAGE_DIR}/stateful_dir"
FLAGS_espfs_mountpoint="${IMAGE_DIR}/esp"
# Create the directories if they don't exist.
mkdir -p "${FLAGS_rootfs_mountpoint}"
mkdir -p "${FLAGS_statefulfs_mountpoint}"
mkdir -p "${FLAGS_espfs_mountpoint}"
# base_image_utils.sh always places the kernel images in
# ${IMAGE_DIR}/boot_images.
declare -r VMLINUZ="${IMAGE_DIR}/boot_images/vmlinuz"
declare -r ZIMAGE="${IMAGE_DIR}/boot_images/zimage"
make_image_bootable "${IMAGE}"
if type -p board_make_image_bootable; then
board_make_image_bootable "${IMAGE}"
fi
if [[ "${FLAGS_fsck_rootfs}" -eq "${FLAGS_TRUE}" ]]; then
verify_image_rootfs "${IMAGE}"
fi
if [[ "${FLAGS_cleanup_dirs}" -eq "${FLAGS_TRUE}" ]]; then
# These paths are already cleaned up by make_image_bootable when unmounting
# the image. This is a fallback in case there are errors in that script.
for path in ${FLAGS_rootfs_mountpoint} ${FLAGS_statefulfs_mountpoint} \
${FLAGS_espfs_mountpoint}; do
if [[ -d "${path}" ]]; then
rmdir "${path}"
fi
done
fi