common_minimal.sh 15 KB

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