From 35522bc595203df2fb115a5751fd7788a8159b34 Mon Sep 17 00:00:00 2001 From: Tommaso Gagliardoni Date: Sun, 19 Oct 2025 22:24:05 +0200 Subject: [PATCH] feat: Add consistency test script --- Makefile | 2 + shufflecake-userland/Makefile | 4 +- tests/consistency.sh | 417 ++++++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 2 deletions(-) create mode 100755 tests/consistency.sh diff --git a/Makefile b/Makefile index 6d3666a..016545e 100644 --- a/Makefile +++ b/Makefile @@ -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` diff --git a/shufflecake-userland/Makefile b/shufflecake-userland/Makefile index ee59323..1a3b483 100644 --- a/shufflecake-userland/Makefile +++ b/shufflecake-userland/Makefile @@ -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 diff --git a/tests/consistency.sh b/tests/consistency.sh new file mode 100755 index 0000000..1fc9451 --- /dev/null +++ b/tests/consistency.sh @@ -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 +#  + +#  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 . + +#  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 . + +# 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