1#!/bin/sh 2# 3# Copyright 2011 The ChromiumOS Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6# 7# Note: This file must be written in dash compatible way as scripts that use 8# this may run in the Chrome OS client enviornment. 9 10# shellcheck disable=SC2039,SC2059,SC2155 11 12# Determine script directory 13SCRIPT_DIR=$(dirname "$0") 14PROG=$(basename "$0") 15: "${GPT:=cgpt}" 16: "${FUTILITY:=futility}" 17 18# The tag when the rootfs is changed. 19TAG_NEEDS_TO_BE_SIGNED="/root/.need_to_be_signed" 20 21# List of Temporary files and mount points. 22TEMP_FILE_LIST=$(mktemp) 23TEMP_DIR_LIST=$(mktemp) 24 25# Finds and loads the 'shflags' library, or return as failed. 26load_shflags() { 27 # Load shflags 28 if [ -f /usr/share/misc/shflags ]; then 29 # shellcheck disable=SC1090,SC1091 30 . /usr/share/misc/shflags 31 elif [ -f "${SCRIPT_DIR}/lib/shflags/shflags" ]; then 32 # shellcheck disable=SC1090 33 . "${SCRIPT_DIR}/lib/shflags/shflags" 34 else 35 echo "ERROR: Cannot find the required shflags library." 36 return 1 37 fi 38 39 # Add debug option for debug output below 40 DEFINE_boolean debug $FLAGS_FALSE "Provide debug messages" "d" 41} 42 43# Functions for debug output 44# ---------------------------------------------------------------------------- 45 46# These helpers are for runtime systems. For scripts using common.sh, 47# they'll get better definitions that will clobber these ones. 48info() { 49 echo "${PROG}: INFO: $*" >&2 50} 51 52warn() { 53 echo "${PROG}: WARN: $*" >&2 54} 55 56error() { 57 echo "${PROG}: ERROR: $*" >&2 58} 59 60# Reports error message and exit(1) 61# Args: error message 62die() { 63 error "$@" 64 exit 1 65} 66 67# Returns true if we're running in debug mode. 68# 69# Note that if you don't set up shflags by calling load_shflags(), you 70# must set $FLAGS_debug and $FLAGS_TRUE yourself. The default 71# behavior is that debug will be off if you define neither $FLAGS_TRUE 72# nor $FLAGS_debug. 73is_debug_mode() { 74 [ "${FLAGS_debug:-not$FLAGS_TRUE}" = "$FLAGS_TRUE" ] 75} 76 77# Prints messages (in parameters) in debug mode 78# Args: debug message 79debug_msg() { 80 if is_debug_mode; then 81 echo "DEBUG: $*" 1>&2 82 fi 83} 84 85# Functions for temporary files and directories 86# ---------------------------------------------------------------------------- 87 88# Create a new temporary file and return its name. 89# File is automatically cleaned when cleanup_temps_and_mounts() is called. 90make_temp_file() { 91 local tempfile="$(mktemp)" 92 echo "$tempfile" >> "$TEMP_FILE_LIST" 93 echo "$tempfile" 94} 95 96# Create a new temporary directory and return its name. 97# Directory is automatically deleted and any filesystem mounted on it unmounted 98# when cleanup_temps_and_mounts() is called. 99make_temp_dir() { 100 local tempdir=$(mktemp -d) 101 echo "$tempdir" >> "$TEMP_DIR_LIST" 102 echo "$tempdir" 103} 104 105cleanup_temps_and_mounts() { 106 while read -r line; do 107 rm -f "$line" 108 done < "$TEMP_FILE_LIST" 109 110 set +e # umount may fail for unmounted directories 111 while read -r line; do 112 if [ -n "$line" ]; then 113 if has_needs_to_be_resigned_tag "$line"; then 114 echo "Warning: image may be modified. Please resign image." 115 fi 116 sudo umount "$line" 2>/dev/null 117 rm -rf "$line" 118 fi 119 done < "$TEMP_DIR_LIST" 120 set -e 121 rm -rf "$TEMP_DIR_LIST" "$TEMP_FILE_LIST" 122} 123 124trap "cleanup_temps_and_mounts" EXIT 125 126# Functions for partition management 127# ---------------------------------------------------------------------------- 128 129# Construct a partition device name from a whole disk block device and a 130# partition number. 131# This works for [/dev/sda, 3] (-> /dev/sda3) as well as [/dev/mmcblk0, 2] 132# (-> /dev/mmcblk0p2). 133make_partition_dev() { 134 local block="$1" 135 local num="$2" 136 # If the disk block device ends with a number, we add a 'p' before the 137 # partition number. 138 if [ "${block%[0-9]}" = "${block}" ]; then 139 echo "${block}${num}" 140 else 141 echo "${block}p${num}" 142 fi 143} 144 145# Find the block size of a device in bytes 146# Args: DEVICE (e.g. /dev/sda) 147# Return: block size in bytes 148blocksize() { 149 local output='' 150 local path="$1" 151 if [ -b "${path}" ]; then 152 local dev="${path##*/}" 153 local sys="/sys/block/${dev}/queue/logical_block_size" 154 output="$(cat "${sys}" 2>/dev/null)" 155 fi 156 echo "${output:-512}" 157} 158 159# Read GPT table to find the starting location of a specific partition. 160# Args: DEVICE PARTNUM 161# Returns: offset (in sectors) of partition PARTNUM 162partoffset() { 163 sudo "$GPT" show -b -i "$2" "$1" 164} 165 166# Read GPT table to find the size of a specific partition. 167# Args: DEVICE PARTNUM 168# Returns: size (in sectors) of partition PARTNUM 169partsize() { 170 sudo "$GPT" show -s -i "$2" "$1" 171} 172 173# Tags a file system as "needs to be resigned". 174# Args: MOUNTDIRECTORY 175tag_as_needs_to_be_resigned() { 176 local mount_dir="$1" 177 sudo touch "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" 178} 179 180# Determines if the target file system has the tag for resign 181# Args: MOUNTDIRECTORY 182# Returns: true if the tag is there otherwise false 183has_needs_to_be_resigned_tag() { 184 local mount_dir="$1" 185 [ -f "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" ] 186} 187 188# Determines if the target file system is a Chrome OS root fs 189# Args: MOUNTDIRECTORY 190# Returns: true if MOUNTDIRECTORY looks like root fs, otherwise false 191is_rootfs_partition() { 192 local mount_dir="$1" 193 [ -f "$mount_dir/$(dirname "$TAG_NEEDS_TO_BE_SIGNED")" ] 194} 195 196# If the kernel is buggy and is unable to loop+mount quickly, 197# retry the operation a few times. 198# Args: IMAGE PARTNUM MOUNTDIRECTORY [ro] 199# 200# This function does not check whether the partition is allowed to be mounted as 201# RW. Callers must ensure the partition can be mounted as RW before calling 202# this function without |ro| argument. 203_mount_image_partition_retry() { 204 local image=$1 205 local partnum=$2 206 local mount_dir=$3 207 local ro=$4 208 local bs="$(blocksize "${image}")" 209 local offset=$(( $(partoffset "${image}" "${partnum}") * bs )) 210 local out try 211 212 # shellcheck disable=SC2086 213 set -- sudo LC_ALL=C mount -o loop,offset=${offset},${ro} \ 214 "${image}" "${mount_dir}" 215 try=1 216 while [ ${try} -le 5 ]; do 217 if ! out=$("$@" 2>&1); then 218 if [ "${out}" = "mount: you must specify the filesystem type" ]; then 219 printf 'WARNING: mounting %s at %s failed (try %i); retrying\n' \ 220 "${image}" "${mount_dir}" "${try}" 221 # Try to "quiet" the disks and sleep a little to reduce contention. 222 sync 223 sleep $(( try * 5 )) 224 else 225 # Failed for a different reason; abort! 226 break 227 fi 228 else 229 # It worked! 230 return 0 231 fi 232 : $(( try += 1 )) 233 done 234 echo "ERROR: mounting ${image} at ${mount_dir} failed:" 235 echo "${out}" 236 # We don't preserve the exact exit code of `mount`, but since 237 # no one in this code base seems to check it, it's a moot point. 238 return 1 239} 240 241# If called without 'ro', make sure the partition is allowed to be mounted as 242# 'rw' before actually mounting it. 243# Args: IMAGE PARTNUM MOUNTDIRECTORY [ro] 244_mount_image_partition() { 245 local image=$1 246 local partnum=$2 247 local mount_dir=$3 248 local ro=$4 249 local bs="$(blocksize "${image}")" 250 local offset=$(( $(partoffset "${image}" "${partnum}") * bs )) 251 252 if [ "$ro" != "ro" ]; then 253 # Forcibly call enable_rw_mount. It should fail on unsupported 254 # filesystems and be idempotent on ext*. 255 enable_rw_mount "${image}" ${offset} 2> /dev/null 256 fi 257 258 _mount_image_partition_retry "$@" 259} 260 261# If called without 'ro', make sure the partition is allowed to be mounted as 262# 'rw' before actually mounting it. 263# Args: LOOPDEV PARTNUM MOUNTDIRECTORY [ro] 264_mount_loop_image_partition() { 265 local loopdev=$1 266 local partnum=$2 267 local mount_dir=$3 268 local ro=$4 269 local loop_rootfs="${loopdev}p${partnum}" 270 271 if [ "$ro" != "ro" ]; then 272 # Forcibly call enable_rw_mount. It should fail on unsupported 273 # filesystems and be idempotent on ext*. 274 enable_rw_mount "${loop_rootfs}" 2>/dev/null 275 fi 276 277 sudo mount -o "${ro}" "${loop_rootfs}" "${mount_dir}" 278} 279 280# Mount a partition read-only from an image into a local directory 281# Args: IMAGE PARTNUM MOUNTDIRECTORY 282mount_image_partition_ro() { 283 _mount_image_partition "$@" "ro" 284} 285 286# Mount a partition read-only from an image into a local directory 287# Args: LOOPDEV PARTNUM MOUNTDIRECTORY 288mount_loop_image_partition_ro() { 289 _mount_loop_image_partition "$@" "ro" 290} 291 292# Mount a partition from an image into a local directory 293# Args: IMAGE PARTNUM MOUNTDIRECTORY 294mount_image_partition() { 295 local mount_dir=$3 296 _mount_image_partition "$@" 297 if is_rootfs_partition "${mount_dir}"; then 298 tag_as_needs_to_be_resigned "${mount_dir}" 299 fi 300} 301 302# Mount a partition from an image into a local directory 303# Args: LOOPDEV PARTNUM MOUNTDIRECTORY 304mount_loop_image_partition() { 305 local mount_dir=$3 306 _mount_loop_image_partition "$@" 307 if is_rootfs_partition "${mount_dir}"; then 308 tag_as_needs_to_be_resigned "${mount_dir}" 309 fi 310} 311 312# Mount the image's ESP (EFI System Partition) on a newly created temporary 313# directory. 314# Prints out the newly created temporary directory path if succeeded. 315# If the image doens't have an ESP partition, returns 0 without print anything. 316# Args: LOOPDEV 317# Returns: 0 if succeeded, 1 otherwise. 318mount_image_esp() { 319 local loopdev="$1" 320 local ESP_PARTNUM=12 321 local loop_esp="${loopdev}p${ESP_PARTNUM}" 322 323 local esp_offset=$(( $(partoffset "${loopdev}" "${ESP_PARTNUM}") )) 324 # Check if the image has an ESP partition. 325 if [[ "${esp_offset}" == "0" ]]; then 326 return 0 327 fi 328 329 local esp_dir="$(make_temp_dir)" 330 if ! sudo mount -o "${ro}" "${loop_esp}" "${esp_dir}"; then 331 return 1 332 fi 333 334 echo "${esp_dir}" 335 return 0 336} 337 338# Extract a partition to a file 339# Args: IMAGE PARTNUM OUTPUTFILE 340extract_image_partition() { 341 local image=$1 342 local partnum=$2 343 local output_file=$3 344 local offset=$(partoffset "$image" "$partnum") 345 local size=$(partsize "$image" "$partnum") 346 347 # shellcheck disable=SC2086 348 dd if="$image" of="$output_file" bs=512 skip=$offset count=$size \ 349 conv=notrunc 2>/dev/null 350} 351 352# Replace a partition in an image from file 353# Args: IMAGE PARTNUM INPUTFILE 354replace_image_partition() { 355 local image=$1 356 local partnum=$2 357 local input_file=$3 358 local offset=$(partoffset "$image" "$partnum") 359 local size=$(partsize "$image" "$partnum") 360 361 # shellcheck disable=SC2086 362 dd if="$input_file" of="$image" bs=512 seek=$offset count=$size \ 363 conv=notrunc 2>/dev/null 364} 365 366# For details, see crosutils.git/common.sh 367enable_rw_mount() { 368 local rootfs="$1" 369 local offset="${2-0}" 370 371 # Make sure we're checking an ext2 image 372 # shellcheck disable=SC2086 373 if ! is_ext2 "$rootfs" $offset; then 374 echo "enable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2 375 return 1 376 fi 377 378 local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte 379 # Dash can't do echo -ne, but it can do printf "\NNN" 380 # We could use /dev/zero here, but this matches what would be 381 # needed for disable_rw_mount (printf '\377'). 382 printf '\000' | 383 sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \ 384 conv=notrunc count=1 bs=1 2>/dev/null 385} 386 387# For details, see crosutils.git/common.sh 388is_ext2() { 389 local rootfs="$1" 390 local offset="${2-0}" 391 392 # Make sure we're checking an ext2 image 393 local sb_magic_offset=$((0x438)) 394 local sb_value=$(sudo dd if="$rootfs" skip=$((offset + sb_magic_offset)) \ 395 count=2 bs=1 2>/dev/null) 396 local expected_sb_value=$(printf '\123\357') 397 if [ "$sb_value" = "$expected_sb_value" ]; then 398 return 0 399 fi 400 return 1 401} 402 403disable_rw_mount() { 404 local rootfs="$1" 405 local offset="${2-0}" 406 407 # Make sure we're checking an ext2 image 408 # shellcheck disable=SC2086 409 if ! is_ext2 "$rootfs" $offset; then 410 echo "disable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2 411 return 1 412 fi 413 414 local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte 415 # Dash can't do echo -ne, but it can do printf "\NNN" 416 # We could use /dev/zero here, but this matches what would be 417 # needed for disable_rw_mount (printf '\377'). 418 printf '\377' | 419 sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \ 420 conv=notrunc count=1 bs=1 2>/dev/null 421} 422 423rw_mount_disabled() { 424 local rootfs="$1" 425 local offset="${2-0}" 426 427 # Make sure we're checking an ext2 image 428 # shellcheck disable=SC2086 429 if ! is_ext2 "$rootfs" $offset; then 430 return 2 431 fi 432 433 local ro_compat_offset=$((0x464 + 3)) # Set 'highest' byte 434 local ro_value=$(sudo dd if="$rootfs" skip=$((offset + ro_compat_offset)) \ 435 count=1 bs=1 2>/dev/null) 436 local expected_ro_value=$(printf '\377') 437 if [ "$ro_value" = "$expected_ro_value" ]; then 438 return 0 439 fi 440 return 1 441} 442 443# Functions for CBFS management 444# ---------------------------------------------------------------------------- 445 446# Get the compression algorithm used for the given CBFS file. 447# Args: INPUT_CBFS_IMAGE CBFS_FILE_NAME 448get_cbfs_compression() { 449 cbfstool "$1" print -r "FW_MAIN_A" | awk -vname="$2" '$1 == name {print $5}' 450} 451 452# Store a file in CBFS. 453# Args: INPUT_CBFS_IMAGE INPUT_FILE CBFS_FILE_NAME 454store_file_in_cbfs() { 455 local image="$1" 456 local file="$2" 457 local name="$3" 458 local compression=$(get_cbfs_compression "$1" "${name}") 459 460 # Don't re-add a file to a section if it's unchanged. Otherwise this seems 461 # to break signature of existing contents. https://crbug.com/889716 462 if cbfstool "${image}" extract -r "FW_MAIN_A,FW_MAIN_B" \ 463 -f "${file}.orig" -n "${name}"; then 464 if cmp -s "${file}" "${file}.orig"; then 465 rm -f "${file}.orig" 466 return 467 fi 468 rm -f "${file}.orig" 469 fi 470 471 cbfstool "${image}" remove -r "FW_MAIN_A,FW_MAIN_B" -n "${name}" || return 472 # This add can fail if 473 # 1. Size of a signature after compression is larger 474 # 2. CBFS is full 475 # These conditions extremely unlikely become true at the same time. 476 cbfstool "${image}" add -r "FW_MAIN_A,FW_MAIN_B" -t "raw" \ 477 -c "${compression}" -f "${file}" -n "${name}" || return 478} 479 480# Misc functions 481# ---------------------------------------------------------------------------- 482 483# Parses the version file containing key=value lines 484# Args: key file 485# Returns: value 486get_version() { 487 local key="$1" 488 local file="$2" 489 awk -F= -vkey="${key}" '$1 == key { print $NF }' "${file}" 490} 491 492# Returns true if all files in parameters exist. 493# Args: List of files 494ensure_files_exist() { 495 local filename return_value=0 496 for filename in "$@"; do 497 if [ ! -f "$filename" ] && [ ! -b "$filename" ]; then 498 echo "ERROR: Cannot find required file: $filename" 499 return_value=1 500 fi 501 done 502 503 return $return_value 504} 505 506# Check if the 'chronos' user already has a password 507# Args: rootfs 508no_chronos_password() { 509 local rootfs=$1 510 # Make sure the chronos user actually exists. 511 if grep -qs '^chronos:' "${rootfs}/etc/passwd"; then 512 sudo grep -q '^chronos:\*:' "${rootfs}/etc/shadow" 513 fi 514} 515 516# Returns true if given ec.bin is signed or false if not. 517is_ec_rw_signed() { 518 ${FUTILITY} dump_fmap "$1" | grep -q KEY_RO 519} 520