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
10# Determine script directory
11SCRIPT_DIR=$(dirname $0)
12PROG=$(basename $0)
13GPT=${GPT:-"cgpt"}
14
15# The tag when the rootfs is changed.
16TAG_NEEDS_TO_BE_SIGNED="/root/.need_to_be_signed"
17
18# List of Temporary files and mount points.
19TEMP_FILE_LIST=$(mktemp)
20TEMP_DIR_LIST=$(mktemp)
21
22# Finds and loads the 'shflags' library, or return as failed.
23load_shflags() {
24  # Load shflags
25  if [ -f /usr/share/misc/shflags ]; then
26    .  /usr/share/misc/shflags
27  elif [ -f "${SCRIPT_DIR}/lib/shflags/shflags" ]; then
28    . "${SCRIPT_DIR}/lib/shflags/shflags"
29  else
30    echo "ERROR: Cannot find the required shflags library."
31    return 1
32  fi
33
34  # Add debug option for debug output below
35  DEFINE_boolean debug $FLAGS_FALSE "Provide debug messages" "d"
36}
37
38# Functions for debug output
39# ----------------------------------------------------------------------------
40
41# Reports error message and exit(1)
42# Args: error message
43err_die() {
44  echo "ERROR: $*" 1>&2
45  exit 1
46}
47
48# Returns true if we're running in debug mode.
49#
50# Note that if you don't set up shflags by calling load_shflags(), you
51# must set $FLAGS_debug and $FLAGS_TRUE yourself.  The default
52# behavior is that debug will be off if you define neither $FLAGS_TRUE
53# nor $FLAGS_debug.
54is_debug_mode() {
55  [ "${FLAGS_debug:-not$FLAGS_TRUE}" = "$FLAGS_TRUE" ]
56}
57
58# Prints messages (in parameters) in debug mode
59# Args: debug message
60debug_msg() {
61  if is_debug_mode; then
62    echo "DEBUG: $*" 1>&2
63  fi
64}
65
66# Functions for temporary files and directories
67# ----------------------------------------------------------------------------
68
69# Create a new temporary file and return its name.
70# File is automatically cleaned when cleanup_temps_and_mounts() is called.
71make_temp_file() {
72  local tempfile=$(mktemp)
73  echo "$tempfile" >> $TEMP_FILE_LIST
74  echo $tempfile
75}
76
77# Create a new temporary directory and return its name.
78# Directory is automatically deleted and any filesystem mounted on it unmounted
79# when cleanup_temps_and_mounts() is called.
80make_temp_dir() {
81  local tempdir=$(mktemp -d)
82  echo "$tempdir" >> $TEMP_DIR_LIST
83  echo $tempdir
84}
85
86cleanup_temps_and_mounts() {
87  for i in $(cat $TEMP_FILE_LIST); do
88    rm -f $i
89  done
90  set +e  # umount may fail for unmounted directories
91  for i in $(cat $TEMP_DIR_LIST); do
92    if [ -n "$i" ]; then
93      if has_needs_to_be_resigned_tag "$i"; then
94        echo "Warning: image may be modified. Please resign image."
95      fi
96      sudo umount $i 2>/dev/null
97      rm -rf $i
98    fi
99  done
100  set -e
101  rm -rf $TEMP_DIR_LIST $TEMP_FILE_LIST
102}
103
104trap "cleanup_temps_and_mounts" EXIT
105
106# Functions for partition management
107# ----------------------------------------------------------------------------
108
109# Construct a partition device name from a whole disk block device and a
110# partition number.
111# This works for [/dev/sda, 3] (-> /dev/sda3) as well as [/dev/mmcblk0, 2]
112# (-> /dev/mmcblk0p2).
113make_partition_dev() {
114  local block="$1"
115  local num="$2"
116  # If the disk block device ends with a number, we add a 'p' before the
117  # partition number.
118  if [ "${block%[0-9]}" = "${block}" ]; then
119    echo "${block}${num}"
120  else
121    echo "${block}p${num}"
122  fi
123}
124
125# Read GPT table to find the starting location of a specific partition.
126# Args: DEVICE PARTNUM
127# Returns: offset (in sectors) of partition PARTNUM
128partoffset() {
129  sudo $GPT show -b -i $2 $1
130}
131
132# Read GPT table to find the size of a specific partition.
133# Args: DEVICE PARTNUM
134# Returns: size (in sectors) of partition PARTNUM
135partsize() {
136  sudo $GPT show -s -i $2 $1
137}
138
139# Tags a file system as "needs to be resigned".
140# Args: MOUNTDIRECTORY
141tag_as_needs_to_be_resigned() {
142  local mount_dir="$1"
143  sudo touch "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED"
144}
145
146# Determines if the target file system has the tag for resign
147# Args: MOUNTDIRECTORY
148# Returns: true if the tag is there otherwise false
149has_needs_to_be_resigned_tag() {
150  local mount_dir="$1"
151  [ -f "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" ]
152}
153
154# Determines if the target file system is a Chrome OS root fs
155# Args: MOUNTDIRECTORY
156# Returns: true if MOUNTDIRECTORY looks like root fs, otherwise false
157is_rootfs_partition() {
158  local mount_dir="$1"
159  [ -f "$mount_dir/$(dirname "$TAG_NEEDS_TO_BE_SIGNED")" ]
160}
161
162# If the kernel is buggy and is unable to loop+mount quickly,
163# retry the operation a few times.
164# Args: IMAGE PARTNUM MOUNTDIRECTORY [ro]
165_mount_image_partition_retry() {
166  local image=$1
167  local partnum=$2
168  local mount_dir=$3
169  local ro=$4
170  local offset=$(( $(partoffset "$image" "$partnum") * 512 ))
171  local out try
172
173  if [ "$ro" != "ro" ]; then
174    # Forcibly call enable_rw_mount.  It should fail on unsupported
175    # filesystems and be idempotent on ext*.
176    enable_rw_mount "$image" ${offset} 2> /dev/null
177  fi
178
179  set -- sudo LC_ALL=C mount -o loop,offset=${offset},${ro} \
180    "${image}" "${mount_dir}"
181  try=1
182  while [ ${try} -le 5 ]; do
183    if ! out=$("$@" 2>&1); then
184      if [ "${out}" = "mount: you must specify the filesystem type" ]; then
185        printf 'WARNING: mounting %s at %s failed (try %i); retrying\n' \
186               "${image}" "${mount_dir}" "${try}"
187        # Try to "quiet" the disks and sleep a little to reduce contention.
188        sync
189        sleep $(( try * 5 ))
190      else
191        # Failed for a different reason; abort!
192        break
193      fi
194    else
195      # It worked!
196      return 0
197    fi
198    : $(( try += 1 ))
199  done
200  echo "ERROR: mounting ${image} at ${mount_dir} failed:"
201  echo "${out}"
202  # We don't preserve the exact exit code of `mount`, but since
203  # no one in this code base seems to check it, it's a moot point.
204  return 1
205}
206
207# Mount a partition read-only from an image into a local directory
208# Args: IMAGE PARTNUM MOUNTDIRECTORY
209mount_image_partition_ro() {
210  _mount_image_partition_retry "$@" "ro"
211}
212
213# Mount a partition from an image into a local directory
214# Args: IMAGE PARTNUM MOUNTDIRECTORY
215mount_image_partition() {
216  local mount_dir=$3
217  _mount_image_partition_retry "$@"
218  if is_rootfs_partition "$mount_dir"; then
219    tag_as_needs_to_be_resigned "$mount_dir"
220  fi
221}
222
223# Extract a partition to a file
224# Args: IMAGE PARTNUM OUTPUTFILE
225extract_image_partition() {
226  local image=$1
227  local partnum=$2
228  local output_file=$3
229  local offset=$(partoffset "$image" "$partnum")
230  local size=$(partsize "$image" "$partnum")
231  dd if=$image of=$output_file bs=512 skip=$offset count=$size \
232    conv=notrunc 2>/dev/null
233}
234
235# Replace a partition in an image from file
236# Args: IMAGE PARTNUM INPUTFILE
237replace_image_partition() {
238  local image=$1
239  local partnum=$2
240  local input_file=$3
241  local offset=$(partoffset "$image" "$partnum")
242  local size=$(partsize "$image" "$partnum")
243  dd if=$input_file of=$image bs=512 seek=$offset count=$size \
244    conv=notrunc 2>/dev/null
245}
246
247# For details, see crosutils.git/common.sh
248enable_rw_mount() {
249  local rootfs="$1"
250  local offset="${2-0}"
251
252  # Make sure we're checking an ext2 image
253  if ! is_ext2 "$rootfs" $offset; then
254    echo "enable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2
255    return 1
256  fi
257
258  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
259  # Dash can't do echo -ne, but it can do printf "\NNN"
260  # We could use /dev/zero here, but this matches what would be
261  # needed for disable_rw_mount (printf '\377').
262  printf '\000' |
263    sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \
264            conv=notrunc count=1 bs=1 2>/dev/null
265}
266
267# For details, see crosutils.git/common.sh
268is_ext2() {
269  local rootfs="$1"
270  local offset="${2-0}"
271
272  # Make sure we're checking an ext2 image
273  local sb_magic_offset=$((0x438))
274  local sb_value=$(sudo dd if="$rootfs" skip=$((offset + sb_magic_offset)) \
275                   count=2 bs=1 2>/dev/null)
276  local expected_sb_value=$(printf '\123\357')
277  if [ "$sb_value" = "$expected_sb_value" ]; then
278    return 0
279  fi
280  return 1
281}
282
283disable_rw_mount() {
284  local rootfs="$1"
285  local offset="${2-0}"
286
287  # Make sure we're checking an ext2 image
288  if ! is_ext2 "$rootfs" $offset; then
289    echo "disable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2
290    return 1
291  fi
292
293  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
294  # Dash can't do echo -ne, but it can do printf "\NNN"
295  # We could use /dev/zero here, but this matches what would be
296  # needed for disable_rw_mount (printf '\377').
297  printf '\377' |
298    sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \
299            conv=notrunc count=1 bs=1 2>/dev/null
300}
301
302rw_mount_disabled() {
303  local rootfs="$1"
304  local offset="${2-0}"
305
306  # Make sure we're checking an ext2 image
307  if ! is_ext2 "$rootfs" $offset; then
308    return 2
309  fi
310
311  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
312  local ro_value=$(sudo dd if="$rootfs" skip=$((offset + ro_compat_offset)) \
313                   count=1 bs=1 2>/dev/null)
314  local expected_ro_value=$(printf '\377')
315  if [ "$ro_value" = "$expected_ro_value" ]; then
316    return 0
317  fi
318  return 1
319}
320
321# Misc functions
322# ----------------------------------------------------------------------------
323
324# Returns true if all files in parameters exist.
325# Args: List of files
326ensure_files_exist() {
327  local filename return_value=0
328  for filename in "$@"; do
329    if [ ! -f "$filename" -a ! -b "$filename" ]; then
330      echo "ERROR: Cannot find required file: $filename"
331      return_value=1
332    fi
333  done
334
335  return $return_value
336}
337
338# Check if the 'chronos' user already has a password
339# Args: rootfs
340no_chronos_password() {
341  local rootfs=$1
342  sudo grep -q '^chronos:\*:' "$rootfs/etc/shadow"
343}
344
345trap "cleanup_temps_and_mounts" EXIT
346