blob: 1b8bd4fe42517f966a60cc334c4f842996ec2480 [file] [log] [blame]
#!/bin/sh -x
# 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.
#
# /init script for use in factory install shim. Requires busybox in
# /bin/busybox, and a symlink from /bin/sh -> busybox.
# USB card partition and mount point.
USB_DEVS="sdb3 sdc3 mmcblk1p3"
USB_MNT=/usb
REAL_USB_DEV=
DM_NAME=
DST=
STATEFUL_MNT=/stateful
STATE_DEV=
LOG_DEV=
LOG_FILE="/log/recovery.log"
TPM_B_LOCKED=
TPM_PP_LOCKED=
KERN_B_VBLOCK="$STATEFUL_MNT/vmlinuz_hd.vblock"
REAL_KERN_B_HASH=
MOVE_MOUNTS="/sys /proc /dev"
# To be updated to keep logging after move_mounts.
TTY_PATH="/dev/tty"
TAIL_PID=
# Used to ensure the factory check only occurs with
# a properly matched root and kernel.
UNOFFICIAL_ROOT=0
# Size of the root ramdisk.
TMPFS_SIZE=300M
on_error() {
# Exit, thus causing a kernel panic. We don't want to do anything else (like
# start a shell) because it would be trivially easy to get here (just unplug
# the USB drive after the kernel starts but before the USB drives are probed
# by the kernel) and starting a shell here would be a BIG security hole.
log
log
log "An unrecoverable error occurred during recovery!"
log
log "Please try again or try a newer recovery image."
save_log_file
sleep 1d
exit 1
}
initial_mounts() {
mkdir -p /var/lock
mount -t proc -o nodev,noexec,nosuid none /proc
mount -t sysfs -o nodev,noexec,nosuid none /sys
if ! mount -t devtmpfs -o mode=0755 none /dev; then
mount -t tmpfs -o mode=0755 none /dev
mknod -m 0600 /dev/console c 5 1
mknod -m 0601 /dev/tty1 c 4 1
mknod -m 0601 /dev/tty2 c 4 2
mknod -m 0601 /dev/tty3 c 4 3
mknod -m 0600 /dev/tpm0 c 10 224
mknod /dev/null c 1 3
fi
mkdir -p /dev/pts
mount -t devpts -o noexec,nosuid none /dev/pts || true
}
# Look for a device with our GPT ID.
wait_for_gpt_root() {
[ -z "$KERN_ARG_KERN_GUID" ] && return 1
dlog -n "Looking for rootfs using kern_guid..."
for try in $(seq 20); do
plog " ."
kern=$(cgpt find -1 -u $KERN_ARG_KERN_GUID)
# We always try ROOT-A in recovery.
newroot="${kern%[0-9]*}3"
if [ -b "$newroot" ]; then
USB_DEV="$newroot"
dlog "Found $USB_DEV"
return 0
fi
sleep 1
done
dlog "Failed waiting for kern_guid"
return 1
}
# Look for any USB device.
wait_for_root() {
dlog -n "Waiting for $USB_DEVS to appear"
for try in $(seq 20); do
plog " ."
for dev in $USB_DEVS; do
if [ -b "/dev/${dev}" ]; then
USB_DEV="/dev/${dev}"
dlog "Found $USB_DEV"
return 0
fi
done
sleep 1
done
dlog "Failed waiting for root!"
return 1
}
# Wait for dm-0 to come up.
wait_for_dm_control() {
MAPPER_CONTROL=/dev/mapper/control
dlog -n "Waiting for $MAPPER_CONTROL to appear"
for try in $(seq 20); do
plog " ."
if [ -c "$MAPPER_CONTROL" ]; then
return 0
fi
sleep 1
done
dlog "Failed waiting for $MAPPER_CONTROL!"
return 1
}
check_if_dm_root() {
[ "$KERN_ARG_ROOT" = "/dev/dm-0" ]
}
# Attempt to find the root defined in the signed recovery
# kernel we're booted into to. Exports REAL_USB_DEV if there
# is a root partition that may be used - on succes or failure.
find_official_root() {
plog "Checking for an official recovery image . . ."
# Check for a kernel selected root device or one in a well known location.
wait_for_gpt_root || wait_for_root || return 1
# Now see if it has a Chrome OS rootfs partition.
cgpt find -t rootfs "$(strip_partition "$USB_DEV")" || return 1
REAL_USB_DEV="$USB_DEV"
LOG_DEV="$(strip_partition "$USB_DEV")"1 # Default to stateful.
# Now see if the root should be integrity checked.
if check_if_dm_root; then
setup_dm_root || return 1
fi
mount_usb || return 1
return 0
}
find_developer_root() {
is_developer_mode || return 1
# Lock the TPM prior to using an untrusted root.
lock_tpm || return 1
plog "\nSearching for developer root . . ."
# If an official root could not be mounted, free up the underlying device
# if it is claimed by verity.
dmsetup remove "$DM_NAME"
# If we found a valid rootfs earlier, then we're done.
USB_DEV="$REAL_USB_DEV"
[ -z "$USB_DEV" ] && return 1
set_unofficial_root || return 1
mount_usb || return 1
return 0
}
# If we have a verified recovery root, ensure all blocks are valid before
# handing it off to the installer.
validate_recovery_root() {
# Allow test recovery roots that are unverified.
[ "$USB_DEV" = "/dev/dm-0" ] || return 0
is_unofficial_root && return 0
plog "Validating official recovery image . . . "
# Ensure the verified rootfs is fully intact or fail with no USB_DEV.
# REAL_USB_DEV is left intact.
# Correctness wins over speed for this.
if ! dd if="$USB_DEV" of=/dev/null bs=$((16 * 1024 * 1024)); then
dlog "Included root filesystem could not be verified."
log " failed!"
dmsetup remove "$DM_NAME" # Free up the real root for use.
USB_DEV=
return 1
fi
log " completed."
return 0
}
setup_dm_root() {
dlog -n "Extracting the device mapper configuration..."
# export_args can't handle dm="..." at present.
DMARG=$(cat /proc/cmdline | sed -e 's/.*dm="\([^"\]*\)".*/\1/g')
DM_NAME=$(echo "$DMARG" | cut -f1 -d' ')
# We override the reboot-to-recovery error behavior so that we can fail
# gracefully on invalid rootfs.
DM_TABLE="$(echo "$DMARG" | cut -f2 -d,) eio"
# Don't attempt to call dmsetup if the root device isn't one that was
# discovered as the creation process will hang.
# TODO(wad) extract the UUID and use it with cgpt find instead.
if [ -n "$KERN_ARG_KERN_GUID" ]; then
[ "${DM_TABLE%$KERN_ARG_KERN_GUID*}" = "${DM_TABLE}" ] && return 1
elif [ -n "${USB_DEV}" ]; then
[ "${DM_TABLE%$USB_DEV*}" = "${DM_TABLE}" ] && return 1
fi
if ! dmsetup create -r "$DM_NAME" --major 254 --minor 0 --table "$DM_TABLE"
then
dlog "Failed to configure device mapper root"
return 1
fi
USB_DEV="/dev/dm-0"
if [ ! -b "$USB_DEV" ]; then
mknod -m 0600 "$USB_DEV" b 254 0
fi
dlog "Created device mapper root $DM_NAME."
return 0
}
mount_usb() {
dlog -n "Mounting usb"
for try in $(seq 20); do
plog " ."
if mount -n -o ro "$USB_DEV" "$USB_MNT"; then
dlog "ok"
return 0
fi
sleep 1
done
dlog "Failed to mount usb!"
return 1
}
check_if_factory_install() {
if is_unofficial_root; then
dlog "Skipping factory install check."
return 1
fi
if [ -e "${USB_MNT}/root/.factory_installer" ]; then
log "Detected factory install."
return 0
fi
return 1
}
get_stateful_dev() {
STATE_DEV=${REAL_USB_DEV%[0-9]*}1
if [ ! -b "$STATE_DEV" ]; then
dlog "Failed to determine stateful device"
return 1
fi
return 0
}
mount_tmpfs() {
dlog "Mounting tmpfs..."
mount -n -t tmpfs tmpfs "$NEWROOT_MNT" -o "size=$TMPFS_SIZE"
return $?
}
copy_contents() {
log "Copying usb->tmpfs..."
(cd "${USB_MNT}" ; tar cf - . | (cd "${NEWROOT_MNT}" && tar xf -))
RES=$?
log "Copy returned with result $RES"
return $RES
}
copy_lsb() {
STATEFUL_LSB="dev_image/etc/lsb-factory"
mkdir -p "${NEWROOT_MNT}/mnt/stateful_partition/dev_image/etc"
# Mounting ext3 as ext2 since the journal is unneeded in ro.
if ! mount -n "${STATE_DEV}" "${STATEFUL_MNT}"; then
log "Failed to mount ${STATE_DEV}!! Failing."
return 1
fi
if [ -f "${STATEFUL_MNT}/${STATEFUL_LSB}" ]; then
log "Found ${STATEFUL_MNT}/${STATEFUL_LSB}"
cp -a "${STATEFUL_MNT}/${STATEFUL_LSB}" \
"${NEWROOT_MNT}/mnt/stateful_partition/${STATEFUL_LSB}"
else
log "Failed to find ${STATEFUL_MNT}/${STATEFUL_LSB}!! Failing."
umount "$STATEFUL_MNT"
return 1
fi
umount "$STATEFUL_MNT"
rmdir "$STATEFUL_MNT"
}
move_mounts() {
dlog "Moving sys. proc, dev to $NEWROOT_MNT"
for mnt in $MOVE_MOUNTS; do
mkdir -p "$NEWROOT_MNT$mnt"
mount -n -o move "$mnt" "$NEWROOT_MNT$mnt"
done
TTY_PATH="$NEWROOT_MNT/dev/tty"
dlog "Done."
return 0
}
unmove_mounts() {
dlog "Moving sys. proc, dev to $NEWROOT_MNT"
for mnt in $MOVE_MOUNTS; do
mount -n -o move "$NEWROOT_MNT$mnt" "$mnt"
done
TTY_PATH="/dev/tty"
dlog "Done."
return 0
}
unmount_usb() {
dlog "Unmounting $USB_MNT"
umount "$USB_MNT"
# Make sure we clean up a device-mapper root.
if [ "$USB_DEV" = "/dev/dm-0" ]; then
dlog "Removing dm-verity target"
dmsetup remove "$DM_NAME"
fi
dlog
dlog "$REAL_USB_DEV can now be safely removed"
dlog
return 0
}
strip_partition() {
local dev="${1%[0-9]*}"
# handle mmcblk0p case as well
echo "${dev%p*}"
}
# Accomodate sd* or mmcblk* devices
get_dst() {
DST=$(echo ${REAL_USB_DEV%[0-9]*} | \
tr -s '[0-9]' '0' | \
sed -e 's/sd[b-z]/sda/g')
}
dev_wait_or_error() {
is_developer_mode || on_error # terminal if we get here in regular mode.
log ""
log "A developer key change is required to proceed."
plog "Please wait 300 seconds . . ."
# TODO(wad) Divvy the total up into a few different prompts.
make_user_wait 300
log ""
}
recovery_wait() {
log ""
log "Preparing to recover your system image."
plog "If you do not wish to proceed, please reboot in the next 10 seconds ."
make_user_wait 10
log ""
log "System recovery is beginning."
log "Please do not disconnect from a power source or power down."
log ""
}
make_user_wait() {
local delay_in_sec="${1-300}"
while [ $delay_in_sec -gt 0 ]; do
plog " ."
sleep 1
delay_in_sec=$((delay_in_sec - 1))
done
log ""
}
check_key_or_wait() {
plog "Searching the system disk for a matching kernel key . . ."
if ! cgpt find -t kernel -M "$1" "$DST"; then
log " failed."
dev_wait_or_error
return 0
fi
log " found."
plog "Validating matching signature(s) . . ."
# If we found a keyblock, at the right offset, make sure it actually signed
# the subsequent payload.
local kdev=
for kdev in $(cgpt find -t kernel -M "$1" "$DST"); do
plog " ."
verify_kernel_signature "$kdev" "/tmp/kern.keyblock" || continue
log " done."
return 0
done
log " failed."
dev_wait_or_error
return 0
}
get_kern_b_device() {
# TODO(wad) By changing boot priority, could we end up
# checking the recovery image or the recovery image could not
# be in slot A. In that case, it should fail in normal mode.
KERN_B_DEV=${REAL_USB_DEV%[0-9]*}4
if [ ! -b "${KERN_B_DEV}" ]; then
return 1
fi
return 0
}
get_real_kern_b_hash() {
REAL_KERN_B_HASH=$(dd if="${KERN_B_DEV}" | \
sha1sum | \
cut -f1 -d' ')
[ -n "$REAL_KERN_B_HASH" ]
}
verify_kernel_signature() {
local kern_dev="$1"
local keyblock="$2"
if ! dd if="$kern_dev" of="/tmp/kern.bin"; then
return 1
fi
# Validates the signature and outputs a keyblock.
if ! vbutil_kernel --verify "/tmp/kern.bin" \
--keyblock "$keyblock"; then
return 1
fi
return 0
}
verify_install_kernel() {
get_kern_b_device || return 1
get_real_kern_b_hash || return 1
# TODO(wad) check signatures from stateful on kern b using the
# root of trust instead of using a baked in cmdline.
if [ "$REAL_KERN_B_HASH" != "$KERN_ARG_KERN_B_HASH" ]; then
if ! is_developer_mode; then
log "The recovery image cannot be verified."
return 1
fi
# Extract the kernel so that vbutil_kernel will happily consume it.
log "Checking the install kernel for a valid developer signature . . ."
verify_kernel_signature "$KERN_B_DEV" "/tmp/kern_b.keyblock" || return 1
check_key_or_wait /tmp/kern_b.keyblock
fi
return 0
}
touch_developer_mode_file() {
is_developer_mode || return 1
mount -n -o rw -t ext3 "$DST"1 "$STATEFUL_MNT" || return 1
touch "$STATEFUL_MNT/.developer_mode" || return 1
umount "$STATEFUL_MNT" || return 1
return 0
}
call_image_recovery_script() {
mount -t tmpfs -o mode=1777 none "$USB_MNT/tmp" || return 1
move_mounts || return 1
# Start the copy.
log ""
log "Beginning system image copy. This will take some time . . ."
log ""
# Until images are built with the installer keyblock in KERN-B by
# default, we keep copying over the installer vblock from the
# stateful partition.
# TODO(wad) http://crosbug/8378
export IS_RECOVERY_INSTALL=1
chroot "$USB_MNT" \
/usr/sbin/chromeos-install --run_as_root --yes \
--payload_image="$1" \
--use_payload_kern_b
if [ $? -eq 0 ]; then
log "System copy completed."
else
log "Error performing system recovery!"
fi
# Clean up doesn't need to be successful.
umount "$USB_MNT/tmp"
unmove_mounts
# If we're in developer mode, touch the .developer_mode file on stateful
# to avoid a bonus wait period on reboot.
# Failure here is non-terminal and it may not succeed just because the
# partition table of the destination has not been synchronized.
dlog "Prepping destination stateful to avoid a secondary delay."
touch_developer_mode_file && log "Prepped image for developer use."
return 0
}
clear_tpm() {
plog "Resetting security device . . ."
# TODO(wad) should we fail on this?
tpmc ppon || dlog "tpmc ppon error: $?"
tpmc clear || dlog "tpmc clear error: $?"
tpmc enable || dlog "tpmc enable error: $?"
tpmc activate || dlog "tpmc activate error: $?"
tpmc pplock || dlog "tpmc pplock error: $?"
log " done."
return 0
}
save_log_file() {
local log_dev="${1:-$LOG_DEV}"
[ -z "$log_dev" ] && return 0
# The recovery stateful is usually too small for ext3.
# TODO(wad) We could also just write the data raw if needed.
# Should this also try to save
dlog "Attempting to save log file: $LOG_FILE -> $log_dev"
mount -n -o sync,rw -t ext2 "$log_dev" /tmp
cp "$LOG_FILE" /tmp
umount -n /tmp
}
stop_log_file() {
# Drop logging
exec &> "$TTY_PATH"3
[ -n "$TAIL_PID" ] && kill $TAIL_PID
}
is_unofficial_root() {
[ $UNOFFICIAL_ROOT -eq 1 ]
}
set_unofficial_root() {
UNOFFICIAL_ROOT=1
return 0
}
recover_system() {
local source=$(strip_partition "$REAL_USB_DEV")
dlog "Beginning system recovery from $source"
recovery_wait
# If we're not running a developer script then we're either
# installing a developer image or an official one. If we're
# in normal recovery mode, then we require that the KERN-B
# on the recovery image matches the hash on the command line.
# In developer mode, we will just check the keys.
verify_install_kernel || return 1
# Only clear on full installs. Shim scripts can call tpmc if they
# like. Only bGlobalLock will be in place in advance.
clear_tpm || return 1
call_image_recovery_script "$source" || return 1
return 0
}
use_new_root() {
move_mounts || on_error
# Chroot into newroot, erase the contents of the old /, and exec real init.
log "About to switch root"
stop_log_file
exec switch_root -c /dev/console "$NEWROOT_MNT" /sbin/init
# This should not really happen.
log "Failed to switch root."
save_log_file
return 1
}
is_nonchrome() {
crossystem mainfw_type?nonchrome
}
is_developer_mode() {
# Legacy/unsupported systems are mapped to developer mode.
is_nonchrome && return 0
# Otherwise the exit status will be accurate.
crossystem devsw_boot?1
}
lock_tpm() {
if [ -z "$TPM_B_LOCKED" ]; then
# Depending on the system, the tpm may need to be started.
# Don't fail if it doesn't work though.
tpmc startup
tpmc ctest
if ! tpmc block; then
if is_nonchrome; then
log "No security chip appears to exist in this non-Chrome device."
log "The security of your experience will suffer."
# Forge onward.
else
log "An unrecoverable error occurred with your security device"
log "Please power down and try again."
dlog "Failed to lock bGlobalLock."
on_error
return 1 # Never reached.
fi
fi
TPM_B_LOCKED=y
fi
if [ -z "$TPM_PP_LOCKED" ]; then
# TODO: tpmc pplock if appropriate
TPM_PP_LOCKED=y
fi
return 0
}
# Extract and export kernel arguments
export_args() {
# We trust out kernel command line explicitly.
local arg=
local key=
local val=
local acceptable_set='[A-Za-z0-9]_'
for arg in "$@"; do
key=$(echo "${arg%=*}" | tr 'a-z' 'A-Z' | \
tr -dc "$acceptable_set" '_')
val="${arg#*=}"
export "KERN_ARG_$key"="$val"
dlog "Exporting kernel argument $key as KERN_ARG_$key"
done
}
dlog() {
echo "$@" | tee -a "$TTY_PATH"2 "$TTY_PATH"3
}
# User visible
log() {
echo "$@" | tee -a "$TTY_PATH"1 "$TTY_PATH"2
}
plog() {
# plog doesn't go to /dev/tty3 or log file.
printf "$@" | tee "$TTY_PATH"1 "$TTY_PATH"2
}
main() {
exec &> "$LOG_FILE"
# Set up basic mounts, console.
initial_mounts
# Send all verbose output to tty3
(tail -f "$LOG_FILE" > "$TTY_PATH"3) &
TAIL_PID=$!
log "Recovery image booting . . ."
log ""
log "Press Ctrl + Alt + F1 - F3 for more detailed information."
log ""
# Export the kernel command line as a parsed blob prepending KERN_ARG_ to each
# argument.
export_args $(cat /proc/cmdline | sed -e 's/"[^"]*"/DROPPED/g')
if is_developer_mode; then
log "! Your computer's developer mode switch is in the ENABLED position."
log "!"
log "! If this is unintentional, you should power off and toggle it back "
log "! after recovery is completed."
log ""
fi
if find_official_root || find_developer_root; then
log " found."
else
log " not found."
on_error
fi
# Extract the real boot source which may be masked by dm-verity.
get_stateful_dev || on_error
# Check if we want to run from RAM, in the factory.
if check_if_factory_install; then
is_developer_mode || on_error # factory install requires it.
# Copy rootfs contents to tmpfs, then unmount USB device.
NEWROOT_MNT=/newroot
mount_tmpfs || on_error
copy_contents || on_error
copy_lsb || on_error
# USB device is unmounted, we can remove it now.
unmount_usb || on_error
# Switch to the new root
use_new_root || on_error
on_error # !! Never reached. !!
fi
# If not, we must be a recovery kernel.
NEWROOT_MNT="$USB_MNT"
if is_nonchrome; then
log " "
log "Your computer does not appear to a Chrome computer!"
log " "
log "Your experience with Chromium OS will be suboptimal."
log " "
fi
# Always lock the TPM. If a NVRAM reset is ever needed, we can change it.
lock_tpm || on_error
# Perform a full device mapper root validation to avoid any unexpected
# failures during postinst. It also allows us to detect if the root is
# intentionally mismatched - such as during Chromium OS recovery with a
# Chrome OS recovery kernel.
if ! validate_recovery_root; then
is_developer_mode || on_error
find_developer_root || on_error
log " found."
# This logic is duplicated to avoid double validating factory media. It
# will only be hit if a verified root can be mounted but is actually not
# intact.
get_stateful_dev || on_error
fi
get_dst || on_error
recover_system || on_error
log "System recovery is complete!"
log "Please remove the recovery device and reboot."
stop_log_file
# Save the recovery log to the target on success and the USB.
save_log_file "$DST"1
save_log_file
unmount_usb
log ""
log ""
plog "The system will automatically reboot in 2 minutes"
make_user_wait 120
reboot -f
exit 0
}
# Make this source-able for testing.
if [ "$0" = "/init" ]; then
main "$@"
# Should never reach here.
exit 1
fi