shufflecake/tests/consistency.sh
2025-10-21 17:00:07 +02:00

745 lines
26 KiB
Bash
Executable file

#!/usr/bin/env bash
# Copyright The Shufflecake Project Authors (2022)
# Copyright The Shufflecake Project Contributors (2022)
# Copyright Contributors to the The Shufflecake Project.
# See the AUTHORS file at the top-level directory of this distribution and at
# <https://www.shufflecake.net/permalinks/shufflecake-c/AUTHORS>
# This file is part of the program shufflecake-c, which is part of the Shufflecake
# Project. Shufflecake is a plausible deniability (hidden storage) layer for
# Linux. See <https://www.shufflecake.net>.
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 2 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>.
# Consistency test script for Shufflecake
# NOTE: there is an undocumented --auto option that skips all user interaction and uses loop devices for the tests
# Variables
SCRIPTNAME=$(basename "$0")
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
LOOP_FILENAME="${SCRIPT_DIR}/sflc-test-loop-file.img"
HASHFILE1="${SCRIPT_DIR}/testfile1.sha256"
HASHFILE2="${SCRIPT_DIR}/testfile2.sha256"
LOOP_DEVICE=""
BLOCK_DEVICE=""
SFLCVOLUME1=""
MNTPOINT1=""
SFLCVOLUME2=""
MNTPOINT2=""
TIMEFORMAT='%3R'
SFLCPATH=""
SFLCNAME=""
DMSFLC_INSTALLED=false
AUTO_MODE=false
# Colors
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No color
# Help
print_help() {
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
echo -e "${BLUE}Usage: ${SCRIPTNAME} [OPTION]... [BLOCKDEVICE]${NC}"
echo " "
echo "This script is used to test consistency of Shufflecake on this machine."
echo "This script is part of the Shufflecake test suite."
echo "Shufflecake is a plausible deniability (hidden storage) layer for Linux."
echo -e "Visit ${BLUE}https://www.shufflecake.net${NC} for more info and documentation."
echo " "
echo "This script requires root because it operates on block devices, please run it "
echo -e "with ${BLUE}sudo${NC}. It does the following:"
echo "1) Creates a Shufflecake (Lite) device with two volumes."
echo "2) Opens both volumes, formats them, and mounts them."
echo "3) Writes two large random files into each volume, saves their checksums."
echo "4) Unmounts and closes the used volume."
echo "5) Re-opens both devices, reads the files, and checks they didn't change."
echo "6) Deletes the files, then close the volumes."
echo "7) Re-re-opens both devices, and checks that free slices have been recovered."
echo " "
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
echo "You must have already compiled and installed Shufflecake in order to run this."
echo "The script will search for Shufflecake either in the default installation "
echo "directory, or in the current directory, or in the parent directory (one level "
echo -e "above this). If the module ${BLUE}dm-sflc${NC} is not loaded, the script "
echo "will load it, execute, and then unload it."
echo " "
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
echo "You can pass the path to a block device as an optional argument, otherwise the "
echo "script will ask for one. If no path is provided, the script will create a 2 GiB"
echo "local file and use it to back loop devices as a virtual block devices to be "
echo "formatted with the appropriate tools. The file will be removed at the end."
echo " "
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
echo -e "${BLUE}WARNING: ALL CONTENT OF THE PROVIDED BLOCK DEVICE WILL BE ERASED!${NC}"
echo " "
exit 0
}
# Function for debugging
bpx() {
if [ -z "$1" ]; then
echo "BPX: Paused. Press any key to continue..." >&2
else
echo "BPX: $1. Press any key to continue..." >&2
fi
read -n1 -s
}
# Show usage
usage() {
echo -e "Use ${BLUE}${SCRIPTNAME} --help${NC} for usage and help."
}
# Clean up function, called by trap on exit
cleanup() {
echo "Exiting and cleaning..."
# Unmount filesystem 1 if mounted
if [ -n "$MNTPOINT1" ] && grep -qs "$MNTPOINT1" /proc/mounts; then
echo "Unmounting \"$MNTPOINT1\" ..."
umount "$MNTPOINT1"
fi
# Unmount filesystem 2 if mounted
if [ -n "$MNTPOINT2" ] && grep -qs "$MNTPOINT2" /proc/mounts; then
echo "Unmounting \"$MNTPOINT2\" ..."
umount "$MNTPOINT2"
fi
# Remove mount point directory 1 if it exists
if [ -n "$MNTPOINT1" ] && [ -d "$MNTPOINT1" ]; then
rmdir "$MNTPOINT1"
fi
# Remove mount point directory 2 if it exists
if [ -n "$MNTPOINT2" ] && [ -d "$MNTPOINT2" ]; then
rmdir "$MNTPOINT2"
fi
# Close shufflecake volume if open. Check if *either* SFLCVOLUME variable
# was set and if *either* corresponding device path still exists.
local sflc_open=false
if [ -n "$SFLCVOLUME1" ] && [ -b "$SFLCVOLUME1" ]; then sflc_open=true; fi
if [ -n "$SFLCVOLUME2" ] && [ -b "$SFLCVOLUME2" ]; then sflc_open=true; fi
if [ "$sflc_open" = true ]; then
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
fi
# Unload kernel module if we loaded it
unload_dmsflc
# Detach loop device if created
if [[ -n "$LOOP_DEVICE" ]]; then
echo "Detaching \"$LOOP_DEVICE\" ..."
losetup -d "$LOOP_DEVICE"
echo "Deleting \"$LOOP_FILENAME\" ..."
rm -f "$LOOP_FILENAME"
echo "Loop device detached and backing file deleted."
fi
# Clean up hash files
rm -f "$HASHFILE1" "$HASHFILE2" 2>/dev/null
}
# Set trap to run cleanup function on exit
trap cleanup EXIT SIGHUP SIGINT SIGTERM
# Check that this script is run as root
check_sudo() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root.${NC}"
usage
exit 1
fi
}
# Find the path of Shufflecake executable
find_sflc_path() {
local cmd="shufflecake"
# Check if the command exists in the current directory
if [[ -x "./${cmd}" ]]; then
SFLCPATH=$(realpath ./)
SFLCNAME=$(realpath "./${cmd}")
return
fi
# Check if the command exists in the parent directory
if [[ -x "../${cmd}" ]]; then
SFLCPATH=$(realpath ../)
SFLCNAME=$(realpath "../${cmd}")
return
fi
# Check if the command exists in the directories listed in PATH
IFS=':' read -ra dirs <<< "$PATH"
for dir in "${dirs[@]}"; do
if [[ -x "${dir}/${cmd}" ]]; then
SFLCPATH=$(realpath "$dir")
SFLCNAME=$(realpath "${dir}/${cmd}")
return
fi
done
# If the command was not found, print an error message
echo -e "${RED}ERROR: Command '${cmd}' not found${NC}." >&2
exit 1
}
# Find and load module dm-sflc
load_dmsflc() {
local mod="dm-sflc"
# Kernel uses underscores, but module files may use hyphens.
local mod_name_loaded="${mod//-/_}"
# First, make sure that dm-mod is loaded
modprobe dm_mod
# Check if the module is already loaded
if lsmod | grep -q "^${mod_name_loaded} "; then
DMSFLC_INSTALLED=true
echo "Module '${mod}' is already loaded."
return
fi
# Try loading from system modules first
if modprobe "$mod" &> /dev/null; then
echo "Module '${mod}' loaded from system modules."
return
fi
# If not, look for the module file and try to load it
local mod_file="${mod}.ko"
# Check if the module file exists in the current directory
if [[ -f "./${mod_file}" ]]; then
insmod "$(realpath "./${mod_file}")" || exit 1
echo "Module '${mod}' loaded from current directory."
return
fi
# Check if the module file exists in the parent directory
if [[ -f "../${mod_file}" ]]; then
insmod "$(realpath "../${mod_file}")" || exit 1
echo "Module '${mod}' loaded from parent directory."
return
fi
# If the module file was not found, print an error message
echo -e "${RED} ERROR: Module file '${mod_file}' not found.${NC}." >&2
exit 1
}
# Unload dm-sflc if it was loaded by this script
unload_dmsflc() {
local mod="dm-sflc"
# Kernel uses underscores, but module files may use hyphens.
local mod_name_loaded="${mod//-/_}"
if [ "$DMSFLC_INSTALLED" = true ]; then
echo "Module '${mod}' will not be unloaded because it was already present when the script started."
return
fi
if lsmod | grep -q "^${mod_name_loaded} "; then
echo "Unloading module '${mod}'..."
rmmod "${mod_name_loaded}" || echo -e "${RED}Warning: Failed to unload module '${mod}'.${NC}" >&2
fi
}
# Function to check if argument is a block device
check_block_device() {
if [ -b "$1" ]; then
echo "OK, block device path \"$1\" is valid." >&2
else
echo -e "${RED}Error: \"$1\" is not a valid block device, aborting.${NC}"
usage
exit 1
fi
}
# Function to create loop device
create_loop_device() {
# Check for 2 GiB of free disk space ***
echo "Checking for 2 GiB of free disk space..." >&2
local required_kb=2097152 # 2 GiB = 2048 * 1024 KiB
local free_kb
free_kb=$(df -k "$SCRIPT_DIR" | awk 'NR==2 {print $4}')
if [ "$free_kb" -lt "$required_kb" ]; then
echo -e "${RED}Error: Not enough free disk space to create 2 GiB loop file.${NC}" >&2
echo "Required: $required_kb KiB, Available: $free_kb KiB in $SCRIPT_DIR" >&2
exit 1
fi
echo "OK, sufficient disk space available." >&2
echo "I will now try to create a file \"$LOOP_FILENAME\" ..." >&2
if [ -e "$LOOP_FILENAME" ]; then
echo -e "${RED}Error: Impossible to generate file, \"$LOOP_FILENAME\" already exists.${NC}"
exit 1
fi
dd if=/dev/zero of="$LOOP_FILENAME" bs=1M count=2048 &> /dev/null
echo "Writing of empty file complete. I will now try to attach it to a new loop device..." >&2
# Create the loop device
LOOP_DEVICE=$(losetup -f --show "$LOOP_FILENAME")
if [ $? -ne 0 ] || [ -z "$LOOP_DEVICE" ]; then
echo -e "${RED}Error: Failed to create loop device.${NC}"
exit 1
fi
echo "Successfully created loop device \"$LOOP_DEVICE\" ." >&2
echo "$LOOP_DEVICE"
}
# Function for user confirmation
confirm() {
while true; do
echo -e "${BLUE}Are you sure you want to proceed? All data on disk \"$BLOCK_DEVICE\" will be erased. (y/n)${NC}"
read -r response
case "$response" in
[yY] | [yY][eE][sS]) # Responded Yes
return 0 # Return 0 for Yes (success, convention for bash scripting)
;;
[nN] | [nN][oO]) # Responded No
return 1 # Return 1 for No (error, convention for bash scripting)
;;
*) # Responded something else
echo "Please press only (y)es or (n)o."
;;
esac
done
}
# Tests
do_test() {
local TESTFILE1="testfile.100M"
local TESTFILE2="testfile.1400M"
# Use global HASHFILE1 and HASHFILE2
MNTPOINT1=$(realpath "./sflc_mnt_1") # Use global
MNTPOINT2=$(realpath "./sflc_mnt_2") # Use global
echo "Starting consistency test for Shufflecake Lite..."
echo "Initializing block device \"$BLOCK_DEVICE\" with two Shufflecake Lite volumes (--skip-randfill)..."
etime=$( (time echo -e "passwd1\npasswd2" | "$SFLCNAME" --skip-randfill -n 2 init "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake init failed.${NC}" >&2; exit 1; fi
echo -e "${GREEN}Action init took $etime seconds.${NC}"
# --- STAGE 1: OPEN, CREATE, HASH ---
echo -e "${BLUE}--- STAGE 1: Opening volumes, creating files, and calculating hashes ---${NC}"
echo "Opening all volumes..."
local BEFORE_OPEN
BEFORE_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake open failed.${NC}" >&2; exit 1; fi
local AFTER_OPEN
AFTER_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
local NEW_VOLUMES
NEW_VOLUMES=$(comm -13 <(echo "$BEFORE_OPEN") <(echo "$AFTER_OPEN"))
if [ $(echo "$NEW_VOLUMES" | wc -l) -ne 2 ]; then
echo -e "${RED}ERROR: Expected 2 new volumes to be created, but found $(echo "$NEW_VOLUMES" | wc -l).${NC}" >&2
exit 1
fi
# Assign to global vars for cleanup. `sort -V` and `sed` ensure sflc_..._1 is vol1.
SFLCVOLUME1=$(echo "$NEW_VOLUMES" | sed -n '1p')
SFLCVOLUME2=$(echo "$NEW_VOLUMES" | sed -n '2p')
echo -e "${GREEN}Action open took $etime seconds.${NC}"
echo "Found volume 1: $SFLCVOLUME1"
echo "Found volume 2: $SFLCVOLUME2"
# --- Volume 1 ---
echo "Formatting $SFLCVOLUME1 with ext4..."
mkfs.ext4 -F "$SFLCVOLUME1" &> /dev/null # -F to force, non-interactive
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: mkfs.ext4 failed on \"$SFLCVOLUME1\".${NC}" >&2; exit 1; fi
udevadm settle
mkdir -p "$MNTPOINT1"
mount "$SFLCVOLUME1" "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
echo "Volume 1 mounted at $MNTPOINT1."
echo "Creating 100MB test file on volume 1..."
dd if=/dev/urandom of="${MNTPOINT1}/${TESTFILE1}" bs=1M count=100 &> /dev/null
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: dd failed for volume 1.${NC}" >&2; exit 1; fi
echo "Calculating SHA256 hash for 100MB file..."
sha256sum "${MNTPOINT1}/${TESTFILE1}" | cut -d' ' -f1 > "$HASHFILE1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: sha256sum failed for volume 1.${NC}" >&2; exit 1; fi
echo "Hash for volume 1 saved to $HASHFILE1."
# --- Volume 2 ---
echo "Formatting $SFLCVOLUME2 with ext4..."
mkfs.ext4 -F "$SFLCVOLUME2" &> /dev/null # -F to force, non-interactive
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: mkfs.ext4 failed on \"$SFLCVOLUME2\".${NC}" >&2; exit 1; fi
udevadm settle
mkdir -p "$MNTPOINT2"
mount "$SFLCVOLUME2" "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
echo "Volume 2 mounted at $MNTPOINT2."
echo "Creating 1400MB test file on volume 2..."
dd if=/dev/urandom of="${MNTPOINT2}/${TESTFILE2}" bs=1M count=1400 &> /dev/null
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: dd failed for volume 2.${NC}" >&2; exit 1; fi
echo "Calculating SHA256 hash for 1400MB file..."
sha256sum "${MNTPOINT2}/${TESTFILE2}" | cut -d' ' -f1 > "$HASHFILE2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: sha256sum failed for volume 2.${NC}" >&2; exit 1; fi
echo "Hash for volume 2 saved to $HASHFILE2."
# --- STAGE 2: UNMOUNT, CLOSE ---
echo -e "${BLUE}--- STAGE 2: Unmounting and closing volumes ---${NC}"
umount "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT1"
umount "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT2"
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake close failed.${NC}" >&2; exit 1; fi
# Clear the globals so the cleanup trap doesn't try to re-close/re-umount
SFLCVOLUME1=""
SFLCVOLUME2=""
MNTPOINT1=""
MNTPOINT2=""
echo "Volumes unmounted and closed."
# --- STAGE 3: RE-OPEN, RE-MOUNT, CHECK ---
echo -e "${BLUE}--- STAGE 3: Re-opening volumes and checking file integrity ---${NC}"
echo "Re-opening all volumes..."
BEFORE_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake (re-)open failed.${NC}" >&2; exit 1; fi
AFTER_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
NEW_VOLUMES=$(comm -13 <(echo "$BEFORE_OPEN") <(echo "$AFTER_OPEN"))
if [ $(echo "$NEW_VOLUMES" | wc -l) -ne 2 ]; then
echo -e "${RED}ERROR: Expected 2 new volumes to be re-opened, but found $(echo "$NEW_VOLUMES" | wc -l).${NC}" >&2
exit 1
fi
# Re-assign to global vars for cleanup
SFLCVOLUME1=$(echo "$NEW_VOLUMES" | sed -n '1p')
SFLCVOLUME2=$(echo "$NEW_VOLUMES" | sed -n '2p')
echo -e "${GREEN}Action (re-)open took $etime seconds.${NC}"
echo "Found volume 1: $SFLCVOLUME1"
echo "Found volume 2: $SFLCVOLUME2"
# --- Volume 1 Check ---
MNTPOINT1=$(realpath "./sflc_mnt_1") # Re-set global for cleanup
mkdir -p "$MNTPOINT1"
mount "$SFLCVOLUME1" "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Re-mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
echo "Volume 1 re-mounted at $MNTPOINT1."
echo "Checking hash for 100MB file..."
local ORIGINAL_HASH1
ORIGINAL_HASH1=$(cat "$HASHFILE1")
local NEW_HASH1
NEW_HASH1=$(sha256sum "${MNTPOINT1}/${TESTFILE1}" | cut -d' ' -f1)
if [ "$ORIGINAL_HASH1" == "$NEW_HASH1" ]; then
echo -e "${GREEN}SUCCESS: Volume 1 file hash matches!${NC}"
else
echo -e "${RED}FAILURE: Volume 1 file hash MISMATCH!${NC}" >&2
echo "Original: $ORIGINAL_HASH1" >&2
echo "New: $NEW_HASH1" >&2
exit 1
fi
# --- Volume 2 Check ---
MNTPOINT2=$(realpath "./sflc_mnt_2") # Re-set global for cleanup
mkdir -p "$MNTPOINT2"
mount "$SFLCVOLUME2" "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Re-mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
echo "Volume 2 re-mounted at $MNTPOINT2."
echo "Checking hash for 1400MB file..."
local ORIGINAL_HASH2
ORIGINAL_HASH2=$(cat "$HASHFILE2")
local NEW_HASH2
NEW_HASH2=$(sha256sum "${MNTPOINT2}/${TESTFILE2}" | cut -d' ' -f1)
if [ "$ORIGINAL_HASH2" == "$NEW_HASH2" ]; then
echo -e "${GREEN}SUCCESS: Volume 2 file hash matches!${NC}"
else
echo -e "${RED}FAILURE: Volume 2 file hash MISMATCH!${NC}" >&2
echo "Original: $ORIGINAL_HASH2" >&2
echo "New: $NEW_HASH2" >&2
exit 1
fi
# --- STAGE 4: CHECK SLICE RECOVERY (GARBAGE COLLECTION) ---
echo -e "${BLUE}--- STAGE 4: Checking slice recovery (garbage collection) ---${NC}"
# 1. Get the BDEV_ID (Major:Minor) for the underlying block device
local MAJOR MINOR BDEV_ID
MAJOR=$(ls -l "$BLOCK_DEVICE" | awk '{print $5}' | tr -d ',')
MINOR=$(ls -l "$BLOCK_DEVICE" | awk '{print $6}')
BDEV_ID="${MAJOR}:${MINOR}"
local SLICE_FILE="/sys/module/dm_sflc/bdevs/${BDEV_ID}/free_slices"
if [ ! -f "$SLICE_FILE" ]; then
echo -e "${RED}FAILURE: Cannot find slice file at $SLICE_FILE!${NC}" >&2
echo "Device ID was ${BDEV_ID}. Is the dm-sflc module loaded correctly?" >&2
exit 1
fi
# 2. Get the number of free slices *before* deleting files
local SLICES_BEFORE
SLICES_BEFORE=$(cat "$SLICE_FILE")
if ! [[ "$SLICES_BEFORE" =~ ^[0-9]+$ ]]; then
echo -e "${RED}FAILURE: Invalid content in $SLICE_FILE: $(cat "$SLICE_FILE")${NC}" >&2
exit 1
fi
echo "Free slices before delete: $SLICES_BEFORE"
# 3. Delete the files
echo "Deleting test files..."
rm -f "${MNTPOINT1}/${TESTFILE1}"
rm -f "${MNTPOINT2}/${TESTFILE2}"
sleep 1
sync # Ensure all I/O is flushed
sleep 1
# 4. Run fstrim to notify Shufflecake of the freed blocks
echo "Running fstrim on $MNTPOINT1 (ext4)..."
fstrim "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}WARNING: fstrim failed on \"$MNTPOINT1\". Slice recovery test may fail.${NC}" >&2; fi
echo "Running fstrim on $MNTPOINT2 (ext4)..."
fstrim "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}WARNING: fstrim failed on \"$MNTPOINT2\". Slice recovery test may fail.${NC}" >&2; fi
sleep 1
sync # Ensure all I/O is flushed
sleep 1
# 5. Unmount and close the device
echo "Unmounting volumes..."
umount "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT1"
umount "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT2"
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake close failed.${NC}" >&2; exit 1; fi
sleep 1
# Clear globals so cleanup trap doesn't complain
SFLCVOLUME1=""
SFLCVOLUME2=""
MNTPOINT1=""
MNTPOINT2=""
echo "Volumes unmounted and closed."
# 6. Re-open the device to trigger garbage collection
echo "Re-opening device to trigger slice recovery..."
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake (re-re-)open failed.${NC}" >&2; exit 1; fi
echo -e "${GREEN}Action (re-re-)open took $etime seconds.${NC}"
sleep 1
# Re-populate globals for the final cleanup trap
local ALL_OPEN_VOLS
ALL_OPEN_VOLS=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
if [ $(echo "$ALL_OPEN_VOLS" | wc -l) -ne 2 ]; then
echo -e "${RED}ERROR: Expected 2 volumes to be re-re-opened, but found $(echo "$ALL_OPEN_VOLS" | wc -l).${NC}" >&2
exit 1
fi
SFLCVOLUME1=$(echo "$ALL_OPEN_VOLS" | sed -n '1p')
SFLCVOLUME2=$(echo "$ALL_OPEN_VOLS" | sed -n '2p')
echo "Found volume 1: $SFLCVOLUME1"
echo "Found volume 2: $SFLCVOLUME2"
# Proactively mount volumes to prevent OS auto-mounter from grabbing them
echo "Proactively mounting volumes to prevent OS auto-mount..."
MNTPOINT1=$(realpath "./sflc_mnt_1") # Re-set global for cleanup
mkdir -p "$MNTPOINT1"
mount "$SFLCVOLUME1" "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Proactive re-mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
MNTPOINT2=$(realpath "./sflc_mnt_2") # Re-set global for cleanup
mkdir -p "$MNTPOINT2"
mount "$SFLCVOLUME2" "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Proactive re-mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
echo "Volumes re-mounted."
# 7. Check the slice file again
if [ ! -f "$SLICE_FILE" ]; then
echo -e "${RED}FAILURE: Cannot find slice file at $SLICE_FILE after re-open!${NC}" >&2
exit 1
fi
local SLICES_AFTER
SLICES_AFTER=$(cat "$SLICE_FILE")
if ! [[ "$SLICES_AFTER" =~ ^[0-9]+$ ]]; then
echo -e "${RED}FAILURE: Invalid content in $SLICE_FILE after re-open: $(cat "$SLICE_FILE")${NC}" >&2
exit 1
fi
echo "Free slices after recovery: $SLICES_AFTER"
# 8. Do the math and check
local SLICES_RECOVERED
SLICES_RECOVERED=$((SLICES_AFTER - SLICES_BEFORE))
local TOTAL_DELETED=1500 # 100MiB + 1400MiB = 1500 slices
local EXPECTED_RECOVERY=1200 # 80% of 1500
echo "Total slices deleted (approx): $TOTAL_DELETED"
echo "Slices recovered: $SLICES_RECOVERED"
echo "Expected recovery (>= 80%): $EXPECTED_RECOVERY"
if [ "$SLICES_RECOVERED" -ge "$EXPECTED_RECOVERY" ]; then
echo -e "${GREEN}SUCCESS: Slice recovery test passed! Recovered $SLICES_RECOVERED slices (>= $EXPECTED_RECOVERY).${NC}"
else
echo -e "${RED}FAILURE: Slice recovery test FAILED! Recovered only $SLICES_RECOVERED slices (< $EXPECTED_RECOVERY).${NC}" >&2
# Umount (failure case)
echo "Unmounting volumes after test failure..."
umount "$MNTPOINT1" &> /dev/null
rmdir "$MNTPOINT1" &> /dev/null
umount "$MNTPOINT2" &> /dev/null
rmdir "$MNTPOINT2" &> /dev/null
MNTPOINT1=""
MNTPOINT2=""
exit 1
fi
# Umount (success case)
echo "Unmounting volumes..."
umount "$MNTPOINT1"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT1"
umount "$MNTPOINT2"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
rmdir "$MNTPOINT2"
# Clear mount globals so the final trap doesn't try to unmount them again
MNTPOINT1=""
MNTPOINT2=""
echo -e "${GREEN}--- ALL CONSISTENCY TESTS PASSED ---${NC}"
# The cleanup trap will handle closing the sflc device.
}
#####################################################################
# MAIN SCRIPT BODY STARTS HERE
#####################################################################
# BANNER
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
echo -e "${BLUE}===============================================================================${NC}"
echo -e "${BLUE} Consistency Test for Shufflecake Lite${NC}"
echo -e "${BLUE}===============================================================================${NC}"
while [ "$#" -gt 0 ]; do
case "$1" in
--help|-?)
print_help
;;
--auto)
AUTO_MODE=true
;;
*)
if [ -n "$BLOCK_DEVICE" ]; then
echo -e "${RED}Error: You can only specify one block device.${NC}" >&2
usage
exit 1
fi
BLOCK_DEVICE="$1"
;;
esac
shift
done
# Check for conflicting options
if [ "$AUTO_MODE" = true ] && [ -n "$BLOCK_DEVICE" ]; then
echo -e "${RED}Error: --auto cannot be used when a BLOCKDEVICE is specified.${NC}" >&2
usage
exit 1
fi
# PRELIMINARY CHECKS
check_sudo
echo -e "${BLUE}Initializing Shufflecake...${NC}"
echo "Searching Shufflecake executable..."
find_sflc_path
echo "Shufflecake executable found at \"$SFLCNAME\"."
echo "Searching and loading dm-sflc module..."
load_dmsflc
echo "Module dm-sflc status determined. DMSFLC_INSTALLED flag is: $DMSFLC_INSTALLED."
echo " "
# DETERMINE BLOCK DEVICE
if [ "$AUTO_MODE" = true ]; then
echo "Auto-mode enabled. Creating a local file and loop device..."
LOOP_DEVICE=$(create_loop_device)
BLOCK_DEVICE="$LOOP_DEVICE"
elif [ -z "$BLOCK_DEVICE" ]; then
echo "Now you will be asked to enter the path for a block device to be used for the "
echo "benchmarks (all content will be erased). If no path is provided (default"
echo "choice), then the script will create a 1 GiB file in the current directory and "
echo "use it to back a loop device instead, then the file will be removed at the end."
echo " "
echo -n "Please enter the path for a block device (default: none): "
read -r BLOCK_DEVICE
if [ -z "$BLOCK_DEVICE" ]; then
echo "No path provided, creating a local file and loop device..."
LOOP_DEVICE=$(create_loop_device)
BLOCK_DEVICE="$LOOP_DEVICE"
fi
fi
check_block_device "$BLOCK_DEVICE"
# MAIN PROGRAM
if [ "$AUTO_MODE" = true ]; then
echo "Auto-mode: Skipping confirmation and running test."
do_test
elif confirm; then
do_test
else
echo "Aborting..."
fi
# The cleanup trap will automatically run on exit.
# END SCRIPT