feat: Add consistency test script

This commit is contained in:
Tommaso Gagliardoni 2025-10-19 22:24:05 +02:00
parent edee3842e5
commit 35522bc595
3 changed files with 421 additions and 2 deletions

View file

@ -36,6 +36,8 @@ debug:
test:
make -C shufflecake-userland test
@echo "Running test scripts... This step requires superuser privileges."
@bash tests/consistency.sh
install:
# Just a placeholder for installation, for now equivalent to `make`

View file

@ -79,6 +79,8 @@ main: $(MAIN_BIN)
.PHONY: test
test: $(TEST_BIN)
@echo "Launching compiled tests"
@./$(TEST_LINK)
.PHONY: link_msg
link_msg:
@ -101,8 +103,6 @@ $(TEST_BIN): $(PROJ_OBJS_NO_MAIN) $(TEST_OBJS) | link_msg
@$(CC) $^ -o $@ $(LDFLAGS)
@rm -f $(TEST_LINK)
@ln -s $@ $(TEST_LINK)
@echo "Done, launching tests"
@./$(TEST_LINK)
# Cancel implicit rule
%.o : %.c

417
tests/consistency.sh Executable file
View file

@ -0,0 +1,417 @@
#!/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/>.
# Benchmarking script for Shufflecake
# Variables
SCRIPTNAME=$(basename "$0")
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
LOOP_FILENAME="${SCRIPT_DIR}/sflc-test-loop-file.img"
LOOP_DEVICE=""
BLOCK_DEVICE=""
SFLCVOLUME=""
MNTPOINT=""
TIMEFORMAT='%3R'
SFLCPATH=""
SFLCNAME=""
DMSFLC_INSTALLED=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."
# TODO: add slice recovery check
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 if mounted
if [ -n "$MNTPOINT" ] && grep -qs "$MNTPOINT" /proc/mounts; then
echo "Unmounting \"$MNTPOINT\" ..."
umount "$MNTPOINT"
fi
# Remove mount point directory if it exists
if [ -n "$MNTPOINT" ] && [ -d "$MNTPOINT" ]; then
rmdir "$MNTPOINT"
fi
# Close shufflecake volume if open. Check if the SFLCVOLUME variable
# was set and if the corresponding device path still exists.
if [ -n "$SFLCVOLUME" ] && [ -b "$SFLCVOLUME" ]; 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
}
# 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() {
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
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"
}
# Finds the sflc volume that was created last
find_sflcvolname() {
local files=(/dev/mapper/sflc_*)
# Check if the glob expanded to any existing files
if [ ! -e "${files[0]}" ]; then
echo -e "${RED}ERROR: No sflc_ devices found in /dev/mapper !${NC}" >&2
return 1
fi
# Use printf to list one file per line, then sort and get the last one
printf '%s\n' "${files[@]}" | sort -t '_' -k 2n,2 -k 3n,3 | tail -n 1
return 0
}
# 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() {
TESTNAME="sflc-test"
RUNTIME="10" # running time in seconds
DATASIZE="100M"
TESTFILENAME="testfile"
echo "Starting benchmark 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}"
echo "Shufflecake device initialized. Opening hidden volume (nr. 2)..."
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
echo -e "${GREEN}Action open took $etime seconds.${NC}"
SFLCVOLUME=$(find_sflcvolname)
if [ $? -ne 0 ]; then exit 1; fi # Error message is already in the function
echo "Shufflecake Lite volume opened as \"$SFLCVOLUME\". Formatting with ext4..."
mkfs.ext4 "$SFLCVOLUME" &> /dev/null
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: mkfs.ext4 failed on \"$SFLCVOLUME\".${NC}" >&2; exit 1; fi
udevadm settle
echo "Volume \"$SFLCVOLUME\" formatted. Mounting that..."
MNTPOINT=$(realpath "./sflc_mnt")
mkdir -p "$MNTPOINT"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Could not create mountpoint \"$MNTPOINT\".${NC}" >&2; exit 1; fi
mount "$SFLCVOLUME" "$MNTPOINT"
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Mount failed for \"$SFLCVOLUME\" on \"$MNTPOINT\".${NC}" >&2; exit 1; fi
echo "Volume mounted at \"$MNTPOINT\". Starting fio tests..."
# TEST 01: random read
echo "Test 01: random read with a queue of 32 4kiB blocks on a file (${RUNTIME}s)..."
OUTPUT=$(fio --name="${TESTNAME}-r-rnd" --ioengine=libaio --iodepth=32 --rw=randread --bs=4k --numjobs=1 --direct=1 --size="$DATASIZE" --runtime="$RUNTIME" --time_based --end_fsync=1 --filename="${MNTPOINT}/${TESTFILENAME}" --output-format=json | jq '.jobs[] | {name: .jobname, read_iops: .read.iops, read_bw: .read.bw}')
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Fio test 01 failed.${NC}" >&2; exit 1; fi
printf "${GREEN}%s${NC}\n" "$OUTPUT"
echo "Shufflecake Lite fio tests ended."
# The cleanup trap will handle unmounting and closing.
}
#####################################################################
# 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}"
# ARGUMENT PARSING
while [ "$#" -gt 0 ]; do
case "$1" in
--help|-?)
print_help
;;
*)
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
# 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 [ -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 confirm; then
do_test
else
echo "Aborting..."
fi
# The cleanup trap will automatically run on exit.
# END SCRIPT