blob: 3cf914242322d773b43f0c2fb631d357bbf7a9d1 [file] [log] [blame]
#!/bin/sh -u
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# A script to display or change the preferred image
# Load functions and constants for chromeos-install.
. "$(dirname "$0")/chromeos-common.sh" || exit 1
if [ -e /usr/lib/shflags ]; then
. /usr/lib/shflags || exit 1
else
# In case we're being called from a mounted image.
if [ -e "$(dirname "$0")"/../../usr/lib/shflags ]; then
. "$(dirname "$0")"/../../usr/lib/shflags || exit 1
else
# Try to find it relative to src/scripts
. ../third_party/shflags/files/src/shflags || exit 1
fi
fi
DEFINE_string dst "" "Boot device to update (Default: current root device)"
# Any time the verified boot flags change, this is needed
# as well as a new grub.cfg.
DEFINE_string update_syslinux_cfg "" \
"If specified, the path to a syslinux root.[A|B].cfg."
DEFINE_string update_grub_cfg "" \
"If specified, the path to a new grub.cfg."
DEFINE_string update_vmlinuz "" \
"If specified, the path to a vmlinuz to use as syslinux's vmlinuz.[A|B]"
DEFINE_boolean run_as_root ${FLAGS_FALSE} \
"Allow root to run this script (Careful, it won't prompt for a password!)"
DEFINE_boolean enable_rootfs_verification ${FLAGS_TRUE} \
"With override_kernel_options, will toggle verified rootfs for legacy \
bootloaders"
DEFINE_boolean override_kernel_options ${FLAGS_FALSE} \
"If given, setimage will ignore the configuration in the new kernel for vboot"
DEFINE_string esp_mounted_at "" \
"Specify a location if the esp has already been mounted"
DEFINE_string kernel_image "" \
"Specifies a kernel image/partition to extract the configuration from. \
By default, it uses the kernel partition for A or B as requested."
DEFINE_string rootfs_image "" \
"Specifies a rootfs image/partition to hash and update for vboot.
By default, it uses the current root device."
DEFINE_string install_root "/" \
"Specifies the base location to find utilities in the new root"
DEFINE_boolean ignore_bad_verification ${FLAGS_FALSE} \
"With override_kernel_options, will ignore rootfs integrity errors"
DEFINE_boolean debug ${FLAGS_FALSE} "Show debug output. Default: false"
# Parse command line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"
set -e
if [ "$FLAGS_debug" -eq "${FLAGS_TRUE}" ]; then
set -x
fi
EXIT_CODE=0 # To be made non-zero in case of a non-terminal error case
INSTALL_ROOT="${FLAGS_install_root}"
# This export is so that locate_gpt will look within this install
# partition to find that copy of the cgpt program.
# TODO(wad,adlr) This duplicates the hack from postinst and should be fixed.
export DEFAULT_CHROOT_DIR="${INSTALL_ROOT}"
locate_gpt # sets $GPT to cgpt executable
if [ "$PATH" != "/" ]; then
ir="${FLAGS_install_root}"
PATH="$ir/bin:$ir/sbin:$ir/usr/bin:$ir/usr/sbin:${PATH}"
unset ir
fi
# Hack to run the code to setup the default dst only if not provided since if
# this is run from a mounted image on a different machine, rootdev will fail.
if [ -z "${FLAGS_dst}" ]; then
# The default device is the one we booted from.
ROOTDEV=$(rootdev -s)
# From root partition to root block device.
DEFAULT_DST=$(get_block_dev_from_partition_dev ${ROOTDEV})
FLAGS_dst="${DEFAULT_DST}"
fi
# Validate args.
if [ -n "${@:-}" ]; then
if [ "$1" = "0" ] || [ "$1" = "A" ] || [ "$1" = "a" ]; then
newimg=0
newimg_letter=A
elif [ "$1" = "1" ] || [ "$1" = "B" ] || [ "$1" = "b" ]; then
newimg=1
newimg_letter=B
else
echo "Usage: $0 [A|B]" 1>&2
exit 1
fi
fi
# Don't run this as root
dont_run_as_root
# Check out the dst device.
if [ ! -b "$FLAGS_dst" ]
then
echo "Error: Unable to find block device: $FLAGS_dst"
exit 1
fi
# Setup loop devices so that we don't get burned by the kernel not knowing
# about updated partition changes.
ESP_LOOP=
ROOT_LOOP=
KERN_LOOP=
# Cleans up a device and unmount if required.
cleanup_loop() {
local dev="$1"
local need_umount="$2"
if [ -z "${dev}" ]; then
return 0
fi
if [ ${need_umount} -eq ${FLAGS_TRUE} ]; then
sudo umount -d "${dev}" || true
elif [ ${need_umount} -eq ${FLAGS_FALSE} ]; then
sudo losetup -d "${dev}" || true
else
echo "INTERNAL ERROR: unknown parameter for cleanup_loop: ${need_umount}" >&2
fi
}
cleanup() {
# Currently only ESP may be mounted. KERN and ROOT are never mounted.
cleanup_loop "${ESP_LOOP}" "${FLAGS_TRUE}"
ESP_LOOP=
cleanup_loop "${KERN_LOOP}" "${FLAGS_FALSE}"
KERN_LOOP=
cleanup_loop "${ROOT_LOOP}" "${FLAGS_FALSE}"
ROOT_LOOP=
# Failing to clean this up isn't the worst.
if [ -n "${ESP_TMP_DIR}" ]; then
rmdir "${ESP_TMP_DIR}" || true
fi
rm -f ${tempfile} || true
}
setup_dst_loop() {
local part="$1"
local dev=$(sudo losetup -f)
if [ -z "$dev" ]; then
echo "No free loop devices." 1>&2
return 1
fi
local offset=$(partoffset ${FLAGS_dst} $part)
local size=$(partsize ${FLAGS_dst} $part)
sudo losetup -o $((512 * offset)) \
--sizelimit $((512 * size)) \
${dev} \
${FLAGS_dst}
echo $dev
}
# For details, see crosutils.git/common.sh
enable_rw_mount() {
local rootfs="$1"
local offset="${2-0}"
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
}
ESP_TMP_DIR=
tempfile=$(mktemp /tmp/grubcfg_XXXXXXXXX)
trap cleanup EXIT
# Mount the EFI System Partition
mountpoint="${FLAGS_esp_mounted_at}"
if [ -z "${mountpoint}" ]; then
mountpoint=$(mktemp -d /tmp/mountesp_XXXXXXXXX)
ESP_TMP_DIR=${mountpoint}
ESP_LOOP=$(setup_dst_loop 12)
sudo mount ${ESP_LOOP} ${mountpoint}
fi
if [ -n "$FLAGS_update_syslinux_cfg" -a ! -r "$FLAGS_update_syslinux_cfg" ];
then
echo "Warning: Specified syslinux root cfg file not found. Skipping"
fi
# Re-hash the root filesystem and use the table for dm-verity.
# We extract the parameters for verification from the kernel
# partition, but we regenerate and reappend the hash tree to
# keep the updater from needing to manage them explicitly.
# Instead, rootfs integrity will be validated on next boot through
# the verified kernel configuration.
selected_kernel=$(( (newimg + 1) * 2)) # part 2 or 4
kernel_image="${FLAGS_kernel_image}"
if [ -z "${FLAGS_kernel_image}" ]; then
KERN_LOOP=$(setup_dst_loop ${selected_kernel})
kernel_image="${KERN_LOOP}"
fi
# Grab the contents of dm="" and trim off the preamble.
# TODO(kwaters): dump_kernel_config segfaults on ARM. We use this to know that
# we need to scrape the u-boot header. This needs to be cleaned up once
# verified root works on the ARM kernel.
DUMP_KERNEL_CONFIG="${INSTALL_ROOT}"/usr/bin/dump_kernel_config
table=
if ! KERNEL_CONFIG=$(sudo "${DUMP_KERNEL_CONFIG}" "${kernel_image}"); then
# FIXME(kwaters): Terrible hack to extract the kernel config from the uboot
# header. There is a 64 byte u-boot header, followed by the size of the
# script as a uint32_t, followed by (uint32_t)0. So, the script itself
# starts 72 bytes into the image.
dd if="${kernel_image}" of="${tempfile}" bs=1 count=512 skip=72
KERNEL_CONFIG=$(sed -n '/setenv bootargs/{s/setenv bootargs *//;p}' \
"${tempfile}")
fi
# Use the root to determine if verification should be happening.
kernel_root="$(echo "${KERNEL_CONFIG}" | sed -e 's/.*root=\([^ ]*\).*/\1/g')"
# Unless specified, the kernel root should dictate whether we are verified
# or not.
if [ ${FLAGS_override_kernel_options} -eq ${FLAGS_FALSE} ]; then
if [ "${kernel_root}" != "/dev/dm-0" ]; then
FLAGS_enable_rootfs_verification=${FLAGS_FALSE}
FLAGS_ignore_bad_verification=${FLAGS_TRUE}
else
FLAGS_enable_rootfs_verification=${FLAGS_TRUE}
FLAGS_ignore_bad_verification=${FLAGS_FALSE}
fi
fi
# Extract the dm-verity target options.
kernel_cfg="$(echo "${KERNEL_CONFIG}" | sed -e 's/.*dm="\([^"]*\)".*/\1/g' |
cut -f2- -d,)"
verity_preamble="$(echo "${KERNEL_CONFIG}" |
sed -e 's/.*dm="\([^"]*\)".*/\1/g' |
cut -f1 -d,)"
rootfs_sectors=$(echo ${kernel_cfg} | cut -f2 -d' ')
verity_depth=$(echo ${kernel_cfg} | cut -f7 -d' ')
verity_algorithm=$(echo ${kernel_cfg} | cut -f8 -d' ')
rootfs_image="${FLAGS_rootfs_image}"
selected_root=$((selected_kernel + 1))
if [ -z "${rootfs_image}" ]; then
ROOT_LOOP=$(setup_dst_loop ${selected_root})
rootfs_image="${ROOT_LOOP}"
fi
# Update the root filesystem to ensure it can be mounted if rw access is
# expected. We never disable it going back to verification here since
# we don't want to change a pre-created image at all if it is verified.
if [ ${FLAGS_enable_rootfs_verification} -eq ${FLAGS_FALSE} ]; then
enable_rw_mount "${rootfs_image}"
fi
# Compute the rootfs hash tree and dump it in the tempfile.
# TODO(wad) take preamble from dump_kernel_config.
VERITY="${INSTALL_ROOT}"/bin/verity
table="${verity_preamble},"$(sudo "${VERITY}" create \
${verity_depth} \
"${verity_algorithm}" \
"${rootfs_image}" \
$((rootfs_sectors / 8)) \
${tempfile})
expected_hash=$(echo ${kernel_cfg} | cut -f9 -d' ')
generated_hash=$(echo ${table} | cut -f2- -d, | cut -f9 -d' ')
if [ ${FLAGS_ignore_bad_verification} -eq ${FLAGS_FALSE} ]; then
if [ "${expected_hash}" != "${generated_hash}" ]; then
echo "Root filesystem has been modified unexpectedly!" 1>&2
echo "Expected ${expected_hash} != ${generated_hash}" 1>&2
# It doesn't make sense to exit here since the root filesystem
# is already in place. Might as well get everything in sync.
EXIT_CODE=1
fi
fi
if [ -n "${newimg:-}" ]; then
# This will usually be ${rootfs_image} except for cases where
# FLAGS_dst and FLAGS_rootfs_image diverge.
dst_part=$(make_partition_dev ${FLAGS_dst} ${selected_root})
# Replace the placeholders from the verity tool.
table="$(echo "$table" |
sed -s "s|ROOT_DEV|${dst_part}|g;s|HASH_DEV|${dst_part}|g")"
fi
# Overwrite the appended hashes with the new ones.
sudo dd if=${tempfile} of="${rootfs_image}" \
bs=512 seek=${rootfs_sectors} conv=notrunc
# Make the change
if [ -z "$FLAGS_update_grub_cfg" ]; then
FLAGS_update_grub_cfg=${mountpoint}/efi/boot/grub.cfg
fi
if [ -n "${newimg:-}" -a -s "$FLAGS_update_grub_cfg" ]; then
grubid=${newimg}
test ${FLAGS_enable_rootfs_verification} -eq ${FLAGS_TRUE} && \
grubid=$((newimg+2))
echo "Updating the verified boot config and grub boot default: $grubid"
sed -e "s/^set default=.*/set default=${grubid}/" \
-e "s|DMTABLE${newimg_letter}|${table}|g" \
$FLAGS_update_grub_cfg > ${tempfile}
# TODO(wad) Break grub.cfg into multiple files if possible.
# Because grub.cfg is monolithic when we update from a templated
# copy, the inactive image will be unpopulated since we do this
# once per call to setimage.
sudo mkdir -p "${mountpoint}/efi/boot"
sudo cp ${tempfile} ${mountpoint}/efi/boot/grub.cfg
fi
# Rinse and repeat for the Legacy BIOS bootloader (syslinux)
syslinux_root_cfg=${mountpoint}/syslinux/root.${newimg_letter}.cfg
if [ -z "$FLAGS_update_syslinux_cfg" ]; then
# Update the root.X.cfg in place if none given.
# Note that the replacement variables may not be intact.
FLAGS_update_syslinux_cfg=$syslinux_root_cfg
fi
if [ -n "${newimg:-}" -a -s "$FLAGS_update_syslinux_cfg" ]; then
echo "Updating default syslinux target to $newimg"
target_hd=chromeos-hd
test ${FLAGS_enable_rootfs_verification} -eq ${FLAGS_TRUE} && \
target_hd=chromeos-vhd
echo "DEFAULT ${target_hd}.${newimg_letter}" > ${tempfile}
sudo cp ${tempfile} ${mountpoint}/syslinux/default.cfg
vmlinuz_tgt=${mountpoint}/syslinux/vmlinuz.${newimg_letter}
# 3 and 5 are the current rootfs a and b partitions
DST_PART_A=$(make_partition_dev ${FLAGS_dst} 3)
DST_PART_B=$(make_partition_dev ${FLAGS_dst} 5)
sed -e "s|HDROOTA|${DST_PART_A}|g;s|HDROOTB|${DST_PART_B}|g" \
-e "s|DMTABLEA|${table}|g;s|DMTABLEB|${table}|g" \
$FLAGS_update_syslinux_cfg > ${tempfile}
# Even though we replace both A and B, we're only updating the file
# that is relevant to the setimage request so we won't put an invalid
# value for the old root nor will the old root.cfg be broken.
sudo cp ${tempfile} $syslinux_root_cfg
# We only need to do this for syslinux since grub can read the rootfs.
if [ -n "$FLAGS_update_vmlinuz" ]; then
echo "Updating vmlinuz..."
sudo cp -fL "$FLAGS_update_vmlinuz" "$vmlinuz_tgt"
fi
fi
# Lather, rinse and repeat for Das U-Boot
if [ -s "${INSTALL_ROOT}/boot/boot-A.scr.uimg" ]; then
cp "${INSTALL_ROOT}/boot/boot-${newimg_letter}.scr.uimg" \
"${mountpoint}/u-boot/boot.scr.uimg"
fi
set +e # just grep oneliners and cleanup from here on out.
echo "Current EFI boot default is:"
# Print the [new] default choice
grep -qs '^set default=0' ${mountpoint}/efi/boot/grub.cfg && echo "A"
grep -qs '^set default=1' ${mountpoint}/efi/boot/grub.cfg && echo "B"
grep -qs '^set default=2' ${mountpoint}/efi/boot/grub.cfg && echo "A (verified)"
grep -qs '^set default=3' ${mountpoint}/efi/boot/grub.cfg && echo "B (verified)"
echo "Current legacy boot default is:"
# Print the [new] default choice
grep -qs 's-hd\.A$' ${mountpoint}/syslinux/default.cfg && echo "A"
grep -qs 's-hd\.B$' ${mountpoint}/syslinux/default.cfg && echo "B"
grep -qs 'vhd\.A$' ${mountpoint}/syslinux/default.cfg && echo "A (verified)"
grep -qs 'vhd\.B$' ${mountpoint}/syslinux/default.cfg && echo "B (verified)"
exit $EXIT_CODE