From 3619eed4515cf51161cfa7c57be4f330cd07e377 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sat, 9 May 2020 14:04:47 -0700 Subject: Add cli-tests framework Add a framework for writing automated tests of the fscrypt command-line tool. See cli-tests/README.md for details. --- cli-tests/run.sh | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100755 cli-tests/run.sh (limited to 'cli-tests/run.sh') diff --git a/cli-tests/run.sh b/cli-tests/run.sh new file mode 100755 index 0000000..909b645 --- /dev/null +++ b/cli-tests/run.sh @@ -0,0 +1,299 @@ +#!/bin/bash +# +# run.sh - run the fscrypt command-line interface tests +# +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +# Use extra-strict mode. +set -e -u -o pipefail + +# Ensure we're in the cli-tests/ directory. +cd "$(dirname "$0")" + +# Names of the test devices. +# Variables with these names are exported to the tests. +DEVICES=(DEV DEV_ROOT) + +# Names of the mountpoint of each test device. +# Variables with these names are exported to the tests. +MOUNTS=(MNT MNT_ROOT) + +# Name of the test user. This user will be created and deleted by this script. +# This variable is exported to the tests. +TEST_USER=fscrypt-test-user + +# The temporary directory to use. +# This variable is exported to the tests. +TMPDIR=$(mktemp -d /tmp/fscrypt.XXXXXX) + +# The loopback devices that correspond to each test device. +LOOPS=() + +# Update the expected output files to match the actual output? +UPDATE_OUTPUT=false + +LONGOPTS_ARRAY=( +'help' +'update-output' +) +LONGOPTS=$(echo "${LONGOPTS_ARRAY[*]}" | tr ' ' ,) + +cleanup() +{ + local mnt loop + + # Unmount all the test filesystems. + for mnt in "${MOUNTS[@]}"; do + mnt="$TMPDIR/$mnt" + if mountpoint "$mnt" &> /dev/null; then + umount "$mnt" + fi + done + + # Delete the loopback device of each test device. + for loop in "${LOOPS[@]}"; do + losetup -d "$loop" + done + + # Delete all temporary files. + rm -rf "${TMPDIR:?}"/* +} + +cleanup_full() +{ + cleanup + rm -rf "$TMPDIR" + userdel "$TEST_USER" || true +} + +# Filters the output of the test script to make the output consistent on every +# run of the test. For example, references to the mountpoint like +# /tmp/fscrypt.4OTb6y/MNT will be replaced with simply MNT, since the name of +# the temporary directory is different every time. +filter_test_output() +{ + local sedscript="" + local raw_output=$TMPDIR/raw-test-output + local i + + cat > "$raw_output" + + # Filter mountpoint and device names. + for i in "${!DEVICES[@]}"; do + sedscript+="s@$TMPDIR/${MOUNTS[$i]}@${MOUNTS[$i]}@g;" + sedscript+="s@${LOOPS[$i]}@${DEVICES[$i]}@g;" + done + + # Filter the path to fscrypt.conf. + sedscript+="s@$FSCRYPT_CONF@FSCRYPT_CONF@g;" + + # Filter policy and protector descriptors. + sedscript+=$(grep -E -o '\<([a-f0-9]{16})|([a-f0-9]{32})\>' \ + "$raw_output" \ + | awk '{ printf "s@\\<" $1 "\\>@desc" NR "@g;" }') + + # Filter any other paths in TMPDIR. + sedscript+="s@$TMPDIR@TMPDIR@g;" + + sed -e "$sedscript" "$raw_output" +} + +# Prepares to run a test script. +setup_for_test() +{ + local i dev_var mnt_var img mnt loop + + # Start with a clean state. + cleanup + + # ../bin/fscrypt might not be accessible to $TEST_USER. Copy it into + # $TMPDIR so that $TEST_USER is guaranteed to have access to it. + mkdir "$TMPDIR/bin" + cp ../bin/fscrypt "$TMPDIR/bin/" + chmod 755 "$TMPDIR" "$TMPDIR/bin" "$TMPDIR/bin/fscrypt" + + # Create the test filesystems and mountpoints. + LOOPS=() + for i in "${!DEVICES[@]}"; do + dev_var=${DEVICES[$i]} + mnt_var=${MOUNTS[$i]} + img="$TMPDIR/$dev_var" + if ! mkfs.ext4 -O encrypt -F -b 4096 -I 256 "$img" $((1<<15)) \ + &> "$TMPDIR/mkfs.out" + then + cat 1>&2 "$TMPDIR/mkfs.out" + exit 1 + fi + loop=$(losetup --find --show "$img") + LOOPS+=("$loop") + export "$dev_var=$loop" + mnt="$TMPDIR/$mnt_var" + export "$mnt_var=$mnt" + mkdir -p "$mnt" + mount "$loop" "$mnt" + done + + # Give the tests their own "root" mount for storing login protectors, so + # they don't use the real "/". + export FSCRYPT_ROOT_MNT="$MNT_ROOT" + + # Enable consistent output mode. + export FSCRYPT_CONSISTENT_OUTPUT="1" + + # Give the tests their own fscrypt.conf. + export FSCRYPT_CONF="$TMPDIR/fscrypt.conf" + fscrypt setup --time=1ms > /dev/null + + # The tests assume kernel support for v2 policies. + if ! grep -q '"policy_version": "2"' "$FSCRYPT_CONF"; then + cat 1>&2 << EOF +ERROR: Can't run these tests because your kernel doesn't support v2 policies. +You need kernel v5.4 or later. +EOF + exit 1 + fi + + # Set up the test filesystems that aren't already set up. + fscrypt setup "$MNT" > /dev/null +} + +run_test() +{ + local t=$1 + + # Run the test script. + set +e + "./$1.sh" |& filter_test_output > "$t.out.actual" + status=${PIPESTATUS[0]} + set -e + + # Check for failure status. + if [ "$status" != 0 ]; then + echo 1>&2 "FAILED: $t [exited with failure status $status]" + if [ -s "$t.out.actual" ]; then + if (( $(wc -l "$t.out.actual" | cut -f1 -d' ') > 10 )); then + echo 1>&2 "Last 10 lines of test output:" + tail -n10 "$t.out.actual" | sed 1>&2 's/^/ /' + echo 1>&2 + echo 1>&2 "See $t.out.actual for the full output." + else + echo 1>&2 "Test output:" + sed 1>&2 's/^/ /' < "$t.out.actual" + fi + fi + exit 1 + fi + + # Check for output mismatch. + if ! cmp "$t.out" "$t.out.actual" &> /dev/null; then + if $UPDATE_OUTPUT; then + cp "$t.out.actual" "$t.out" + echo "Updated $t.out" + else + echo 1>&2 "FAILED: $t [output mismatch]" + echo 1>&2 "Differences between $t.out and $t.out.actual:" + echo 1>&2 + diff 1>&2 "$t.out" "$t.out.actual" + exit 1 + fi + fi + rm -f "$t.out.actual" +} + +usage() +{ + cat << EOF +Usage: run.sh [--update-output] [TEST_SCRIPT_NAME]..." +EOF + exit 1 +} + +if ! options=$(getopt -o "" -l "$LONGOPTS" -- "$@"); then + usage +fi +eval set -- "$options" +while (( $# >= 1 )); do + case "$1" in + --update-output) + UPDATE_OUTPUT=true + ;; + --) + shift + break + ;; + --help|*) + usage + ;; + esac + shift +done + +if [ "$(id -u)" != 0 ]; then + echo 1>&2 "ERROR: You must be root to run these tests." + exit 1 +fi + +# Check for prerequisites. +PREREQ_CMDS=(mkfs.ext4 expect keyctl) +PREREQ_PKGS=(e2fsprogs expect keyutils) +for i in ${!PREREQ_CMDS[*]}; do + if ! type -P "${PREREQ_CMDS[$i]}" > /dev/null; then + cat 1>&2 << EOF +ERROR: You must install the '${PREREQ_PKGS[$i]}' package to run these tests. + Try a command like 'sudo apt-get install ${PREREQ_PKGS[$i]}'. +EOF + exit 1 + fi +done + +# Use a consistent umask. +umask 022 + +# Use a consistent locale, to prevent output mismatches. +export LANG=C +export LC_ALL=C + +# Always cleanup fully on exit. +trap cleanup_full EXIT + +# Create a test user, so that we can test non-root use of fscrypt. Give them a +# password, so that we can test creating login passphrase protected directories. +userdel "$TEST_USER" &> /dev/null || true +useradd "$TEST_USER" +echo "$TEST_USER:TEST_USER_PASS" | chpasswd +export TEST_USER + +# Let the tests use $TMPDIR if they need it. +export TMPDIR + +# Make it so that running 'fscrypt' in the tests runs the correct binary. +export PATH="$TMPDIR/bin:$PATH" + +if (( $# >= 1 )); then + # Tests specified on command line. + tests=("$@") +else + # No tests specified on command line. Just run everything. + tests=(t_*.sh) +fi +for t in "${tests[@]}"; do + t=${t%.sh} + echo "Running $t" + setup_for_test + run_test "$t" +done + +echo "All tests passed!" -- cgit v1.2.3