pseudodisk/cleanup.sh
2025-10-20 14:16:14 +02:00

444 lines
12 KiB
Bash
Executable File

#!/bin/bash
# Forensic Disk Image Cleanup Helper
# Safely unmounts and detaches loop devices
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
if [ "$EUID" -ne 0 ]; then
print_error "This script must be run as root (use sudo)"
exit 1
fi
echo ""
echo "=========================================="
echo " Forensic Disk Cleanup Tool"
echo "=========================================="
echo ""
# Function to get user loop devices (excluding system paths)
get_user_loop_devices() {
losetup -l -n -O NAME,BACK-FILE | grep -v "/var/lib/snapd" | grep -v "/snap/" | grep -v "^$" | awk '{if (NF >= 2) print $0}'
}
# Function to display user loop devices nicely
show_user_loop_devices() {
local devices=$(get_user_loop_devices)
if [ -z "$devices" ]; then
echo "No user loop devices found (system devices filtered out)"
return 1
fi
echo "Active disk images:"
echo ""
printf "%-15s %s\n" "LOOP DEVICE" "IMAGE FILE"
echo "------------------------------------------------------------"
while IFS= read -r line; do
local device=$(echo "$line" | awk '{print $1}')
local file=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//')
printf "%-15s %s\n" "$device" "$file"
done <<< "$devices"
echo ""
return 0
}
# Helper to list processes blocking a device (if possible)
list_blocking_processes() {
local dev="$1"
if command -v fuser >/dev/null 2>&1; then
fuser -v "$dev" 2>/dev/null || true
elif command -v lsof >/dev/null 2>&1; then
lsof "$dev" 2>/dev/null || true
else
print_info "Install 'psmisc' (provides fuser) or 'lsof' to list blocking processes"
fi
}
# Function to unmount all partitions of a loop device
unmount_loop_partitions() {
local loop_device=$1
local unmounted=0
# Build a list of partition nodes for this loop device
local parts=()
for p in "${loop_device}p"* "${loop_device}"[0-9]*; do
# Only consider block device nodes that actually exist
if [ -b "$p" ]; then
parts+=("$p")
fi
done
if [ ${#parts[@]} -eq 0 ]; then
print_info "No partition block devices found for $loop_device"
return 0
fi
for part in "${parts[@]}"; do
# Determine if the partition is mounted
local mount_point
mount_point=$(findmnt -n -o TARGET --source "$part" 2>/dev/null || true)
if [ -n "$mount_point" ]; then
print_info "Unmounting $part from $mount_point"
if umount "$part" 2>/dev/null; then
print_success "Unmounted $part"
unmounted=$((unmounted + 1))
else
print_warning "Regular umount failed for $part - attempting lazy unmount"
if umount -l "$part" 2>/dev/null; then
print_success "Lazy-unmounted $part"
unmounted=$((unmounted + 1))
else
print_warning "Failed to unmount $part even with lazy unmount"
fi
fi
else
print_info "Partition $part does not appear to be mounted"
fi
done
# Attempt to remove partition mappings so kernel releases device nodes
if command -v partx >/dev/null 2>&1; then
partx -d "$loop_device" 2>/dev/null || true
elif command -v kpartx >/dev/null 2>&1; then
kpartx -d "$loop_device" 2>/dev/null || true
fi
# Give udev a moment to remove stale device nodes
if command -v udevadm >/dev/null 2>&1; then
udevadm settle 2>/dev/null || true
fi
sleep 1
return $unmounted
}
# Function to detach loop device
detach_loop_device() {
local loop_device=$1
local force=${2:-false} # second optional argument: "true" to forcibly kill blockers
print_info "Detaching $loop_device"
# If the device is not present in losetup listing, consider it already detached
if ! losetup -l -n -O NAME 2>/dev/null | awk '{print $1}' | grep -qxF "$loop_device"; then
print_success "Loop device already detached"
return 0
fi
# Try a normal detach first
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device"
return 0
fi
print_warning "Initial detach failed for $loop_device; attempting recovery steps"
# Try removing partition mappings and let udev settle
if command -v partx >/dev/null 2>&1; then
partx -d "$loop_device" 2>/dev/null || true
elif command -v kpartx >/dev/null 2>&1; then
kpartx -d "$loop_device" 2>/dev/null || true
fi
if command -v udevadm >/dev/null 2>&1; then
udevadm settle 2>/dev/null || true
fi
sleep 1
# Second detach attempt
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device"
return 0
fi
# If allowed, try to kill processes referencing the loop device (SIGTERM then SIGKILL)
if [ "$force" = "true" ]; then
if command -v fuser >/dev/null 2>&1; then
print_info "Killing processes using $loop_device (SIGTERM)"
fuser -k -TERM "$loop_device" 2>/dev/null || true
sleep 1
# Try detach again
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device after killing blockers"
return 0
fi
print_info "Killing any remaining processes using $loop_device (SIGKILL)"
fuser -k -KILL "$loop_device" 2>/dev/null || true
sleep 1
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device after force-killing blockers"
return 0
fi
elif command -v lsof >/dev/null 2>&1; then
print_info "Listing processes using $loop_device via lsof"
lsof "$loop_device" 2>/dev/null || true
local pids
pids=$(lsof -t "$loop_device" 2>/dev/null || true)
if [ -n "$pids" ]; then
print_info "Sending SIGTERM to: $pids"
kill -TERM $pids 2>/dev/null || true
sleep 1
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device after killing blockers"
return 0
fi
print_info "Sending SIGKILL to: $pids"
kill -KILL $pids 2>/dev/null || true
sleep 1
if losetup -d "$loop_device" 2>/dev/null; then
print_success "Detached $loop_device after force-killing blockers"
return 0
fi
fi
else
print_warning "No 'fuser' or 'lsof' available; cannot automatically kill blocking processes"
fi
fi
print_error "Failed to detach $loop_device. The device is likely still referenced by processes or the kernel"
print_info "Processes referencing $loop_device (if any):"
list_blocking_processes "$loop_device"
return 1
}
# Automatic mode
auto_cleanup() {
local devices=$(get_user_loop_devices)
if [ -z "$devices" ]; then
print_info "No user loop devices to clean up"
return 0
fi
echo "The following loop devices will be cleaned up:"
echo ""
local count=0
while IFS= read -r line; do
local device=$(echo "$line" | awk '{print $1}')
local file=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//')
echo " [$((count+1))] $device -> $file"
count=$((count+1))
done <<< "$devices"
echo ""
read -p "Clean up all $count device(s)? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
print_info "Cancelled"
return 0
fi
echo ""
local success=0
local failed=0
while IFS= read -r line; do
local device=$(echo "$line" | awk '{print $1}')
local file=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//')
echo "Processing: $device"
unmount_loop_partitions "$device" || true
# In automatic cleanup assume we are allowed to force-kill blockers when necessary
if detach_loop_device "$device" "true"; then
success=$((success+1))
else
failed=$((failed+1))
fi
echo ""
done <<< "$devices"
echo "=========================================="
print_success "Cleaned up: $success device(s)"
if [ $failed -gt 0 ]; then
print_warning "Failed: $failed device(s)"
fi
echo "=========================================="
}
# Manual mode - specific file
manual_cleanup() {
local target=$1
if [ ! -f "$target" ]; then
print_error "File not found: $target"
return 1
fi
# Find loop device associated with this file
local loop_device=$(losetup -l -n -O NAME,BACK-FILE | grep "$(realpath $target)" | awk '{print $1}')
if [ -z "$loop_device" ]; then
print_warning "No loop device found for: $target"
print_info "The file may already be detached"
return 0
fi
print_info "Found loop device: $loop_device"
echo ""
unmount_loop_partitions "$loop_device" || true
# Ask user whether to force if needed
read -p "Force-kill processes using $loop_device if necessary? (y/N): " force_ans
force_ans=${force_ans:-N}
if [ "$force_ans" = "y" ] || [ "$force_ans" = "Y" ]; then
detach_loop_device "$loop_device" "true"
else
detach_loop_device "$loop_device"
fi
echo ""
print_success "Cleanup complete for: $target"
}
# Interactive mode
interactive_cleanup() {
local devices=$(get_user_loop_devices)
if [ -z "$devices" ]; then
print_info "No user loop devices to clean up"
return 0
fi
echo "Select a device to clean up:"
echo ""
local -a device_array
local -a file_array
local count=0
while IFS= read -r line; do
local device=$(echo "$line" | awk '{print $1}')
local file=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//')
device_array[$count]=$device
file_array[$count]=$file
echo " [$((count+1))] $device -> $file"
count=$((count+1))
done <<< "$devices"
echo " [a] Clean up ALL"
echo " [q] Quit"
echo ""
read -p "Enter selection: " selection
if [ "$selection" = "q" ]; then
print_info "Cancelled"
return 0
fi
if [ "$selection" = "a" ]; then
echo ""
auto_cleanup
return 0
fi
# Validate numeric input
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $count ]; then
print_error "Invalid selection"
return 1
fi
local idx=$((selection-1))
local device="${device_array[$idx]}"
local file="${file_array[$idx]}"
echo ""
print_info "Cleaning up: $device -> $file"
echo ""
unmount_loop_partitions "$device" || true
read -p "Force-kill processes using $device if necessary? (y/N): " force_ans
force_ans=${force_ans:-N}
if [ "$force_ans" = "y" ] || [ "$force_ans" = "Y" ]; then
detach_loop_device "$device" "true"
else
detach_loop_device "$device"
fi
echo ""
print_success "Cleanup complete"
}
# Main menu
main() {
# Check if any user loop devices exist
if ! show_user_loop_devices; then
exit 0
fi
echo "Cleanup Options:"
echo " 1) Select specific device (interactive)"
echo " 2) Clean up all user devices (automatic)"
echo " 3) Enter filename manually"
echo " 4) Quit"
echo ""
read -p "Select option [1-4]: " option
echo ""
case $option in
1)
interactive_cleanup
;;
2)
auto_cleanup
;;
3)
read -p "Enter disk image filename: " filename
echo ""
manual_cleanup "$filename"
;;
4)
print_info "Cancelled"
;;
*)
print_error "Invalid option"
exit 1
;;
esac
echo ""
# Show final state
if get_user_loop_devices >/dev/null 2>&1; then
echo "Remaining loop devices:"
show_user_loop_devices
else
print_success "All user loop devices cleaned up"
fi
}
# Run main
main