#!/usr/bin/env bash # Fully erase a device or erase its partition-table and ZFS metadata. set -euo pipefail readonly DEFAULT_PASSES=10 readonly MIB=$((1024 * 1024)) readonly METADATA_MIB=4 readonly METADATA_BYTES=$((METADATA_MIB * MIB)) usage() { cat <<'EOF' Usage: sudo ./erase-zfs.sh [--wipe|--metadata] [--source zeroes|random] [--passes ] [--verify] /dev/ Fully overwrites the target device ten times by default. --metadata overwrites only the first and last 4 MiB, covering partition tables and ZFS vdev labels. Options: --wipe Fully overwrite the device (default). --metadata Overwrite only partition-table and ZFS metadata regions. --source Write zeroes (default) or random data. --passes Use a positive number of overwrite passes (default: 10). --verify Run signature, ZFS-label, and pool-discovery checks after wiping. -h, --help Show this help message. EOF } die() { printf 'Error: %s\n' "$*" >&2 exit 1 } wipe_mode=wipe write_source=/dev/zero source_name=zeroes passes=$DEFAULT_PASSES verify=false device= while [[ $# -gt 0 ]]; do case $1 in --wipe) wipe_mode=wipe shift ;; --metadata) wipe_mode=metadata shift ;; --source) [[ $# -ge 2 ]] || die '--source requires zeroes or random.' case $2 in zeroes) write_source=/dev/zero source_name=zeroes ;; random) write_source=/dev/urandom source_name=random ;; *) die "invalid source '$2'; use zeroes or random." ;; esac shift 2 ;; --passes) [[ $# -ge 2 ]] || die '--passes requires a positive integer.' [[ $2 =~ ^[1-9][0-9]*$ ]] || die "invalid pass count '$2'; use a positive integer." passes=$2 shift 2 ;; --verify) verify=true shift ;; -h|--help) usage exit 0 ;; -*) die "unknown option '$1'." ;; *) [[ -z $device ]] || die 'specify exactly one block device.' device=$1 shift ;; esac done [[ -n $device ]] || { usage >&2; exit 2; } [[ $EUID -eq 0 ]] || die 'run as root (for example, with sudo).' [[ -b $device ]] || die "'$device' is not a block device." [[ -r $write_source ]] || die "'$write_source' is not readable." for command in blockdev dd findmnt lsblk zpool; do command -v "$command" >/dev/null 2>&1 || die "required command '$command' was not found." done if [[ $verify == true ]]; then for command in wipefs zdb; do command -v "$command" >/dev/null 2>&1 || die "--verify requires '$command'." done if [[ $source_name == zeroes ]]; then command -v cmp >/dev/null 2>&1 || die "--verify with zeroes requires 'cmp'." fi fi if findmnt --noheadings --source "$device" >/dev/null 2>&1; then die "'$device' is mounted; unmount it before continuing." fi if lsblk --noheadings --raw --output MOUNTPOINT "$device" | grep -q '[^[:space:]]'; then die "'$device' or one of its children is mounted; unmount it before continuing." fi if zpool status -P 2>/dev/null | grep -Fq -- "$device"; then die "'$device' belongs to an imported ZFS pool; export or detach it before continuing." fi size_bytes=$(blockdev --getsize64 "$device") (( size_bytes > 0 )) || die "'$device' has no addressable capacity." if [[ $wipe_mode == metadata ]]; then (( size_bytes >= 2 * METADATA_BYTES )) || die "'$device' is smaller than $((2 * METADATA_MIB)) MiB." fi printf '\nWARNING: this permanently performs a %s wipe on:\n %s\nusing %s data for %d passes.\n\n' \ "$wipe_mode" "$device" "$source_name" "$passes" lsblk --output NAME,SIZE,TYPE,MOUNTPOINTS "$device" read -r -p "Type exactly 'ERASE $device' to continue: " confirmation [[ $confirmation == "ERASE $device" ]] || die 'confirmation did not match; no data was changed.' wipe_metadata() { local last_offset=$((size_bytes - METADATA_BYTES)) dd if="$write_source" of="$device" bs="$MIB" count="$METADATA_MIB" iflag=fullblock conv=fsync,notrunc status=none dd if="$write_source" of="$device" bs=1 count="$METADATA_BYTES" seek="$last_offset" iflag=fullblock conv=fsync,notrunc status=none } wipe_full_device() { local whole_mib=$((size_bytes / MIB)) local remainder=$((size_bytes % MIB)) if (( whole_mib > 0 )); then dd if="$write_source" of="$device" bs="$MIB" count="$whole_mib" iflag=fullblock conv=fsync,notrunc status=progress fi if (( remainder > 0 )); then dd if="$write_source" of="$device" bs=1 count="$remainder" seek=$((whole_mib * MIB)) iflag=fullblock conv=fsync,notrunc status=none fi } for ((pass = 1; pass <= passes; pass++)); do printf 'Pass %d/%d...\n' "$pass" "$passes" if [[ $wipe_mode == wipe ]]; then wipe_full_device else wipe_metadata fi done sync printf 'Completed %d %s %s passes on %s.\n' "$passes" "$source_name" "$wipe_mode" "$device" [[ $verify == true ]] || exit 0 verification_failed=false printf '\nVerification: checking for remaining signatures.\n' if wipefs --all --no-act "$device"; then printf 'PASS: wipefs found no signatures.\n' else printf 'FAIL: wipefs reported one or more signatures.\n' >&2 verification_failed=true fi printf '\nVerification: checking for readable ZFS labels.\n' if zdb -l "$device"; then printf 'FAIL: zdb found a readable ZFS label.\n' >&2 verification_failed=true else printf 'PASS: zdb found no readable ZFS label.\n' fi printf '\nVerification: checking whether ZFS discovers an importable pool near this device.\n' discovery_output= if ! discovery_output=$(zpool import -d "$(dirname -- "$device")" 2>&1); then : fi if grep -Eq '^[[:space:]]*pool:[[:space:]]' <<<"$discovery_output"; then printf '%s\n' "$discovery_output" printf 'FAIL: zpool found an importable pool, possibly using this device.\n' >&2 verification_failed=true elif grep -Fq 'no pools available to import' <<<"$discovery_output"; then printf 'PASS: zpool found no importable pools in the device directory.\n' else printf '%s\n' "$discovery_output" >&2 printf 'FAIL: zpool discovery did not produce a conclusive result.\n' >&2 verification_failed=true fi if [[ $wipe_mode == wipe && $source_name == zeroes ]]; then printf '\nVerification: comparing the full device with zeroes.\n' if cmp --silent /dev/zero "$device"; then printf 'PASS: the complete readable device contains zeroes.\n' else printf 'FAIL: the complete readable device is not all zeroes.\n' >&2 verification_failed=true fi fi [[ $verification_failed == false ]] || exit 1 printf '\nVerification completed successfully.\n'