Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 63 additions & 13 deletions autorun/wipe-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,23 +171,62 @@ wipe_drive() {

verify_wipe() {
local dev="$1"
local samples=1024
local dtype="${2:-}"
local samples="${VERIFY_SAMPLES:-1024}"
local block_size=512
local dev_size
local dev_size nblocks
dev_size=$(blockdev --getsize64 "/dev/$dev" 2>/dev/null || echo 0)
[[ "$dev_size" -eq 0 ]] && return 1
nblocks=$((dev_size / block_size))
[[ "$nblocks" -le 0 ]] && return 1

local all_zero=true
for _ in $(seq 1 "$samples"); do
local offset=$((RANDOM * RANDOM % (dev_size / block_size)))
local data
data=$(dd if="/dev/$dev" bs=$block_size skip="$offset" count=1 2>/dev/null | od -A n -t x1 | tr -d ' \n')
if [[ -n "$data" ]] && ! echo "$data" | grep -qP '^(00|ff)+$'; then
all_zero=false
break
# Expected post-wipe read pattern depends on the sanitization method:
# - Overwrite methods (nwipe random — used for hdd/usb-flash and the
# fallback) leave high-entropy RANDOM data. Expecting zeros there is
# wrong and ALWAYS fails, so we only assert the sample is not an
# unwritten all-0x00 block (the signature of a region the wipe missed).
# - Erase methods (nvme sanitize block-erase, ATA secure-erase, and the
# SED crypto-erase variants) canonically read back as all-0x00 (some
# controllers use 0xFF), so we require that.
# nwipe's own --verify=last is the authoritative pattern check; this is a
# coarse, independent read-back sample across the whole device.
# NOTE: SED crypto-erase read-back is controller-dependent; it is treated
# as an erase-family (zero) check here to match prior behavior. If a SED
# controller leaves ciphertext, revisit this mapping in a follow-up.
local expect
case "$dtype" in
nvme-ssd | sata-ssd | nvme-ssd-sed | sata-ssd-sed) expect="zero" ;;
*) expect="random" ;;
esac

local i offset data
for ((i = 0; i < samples; i++)); do
# Full-width offset. RANDOM is only 15 bits, so the old RANDOM*RANDOM
# (max ~1.07e9 blocks ≈ 550GB) never sampled the tail of larger drives.
# Combine three draws for 45 bits of range, which covers any real drive.
offset=$(((RANDOM << 30 | RANDOM << 15 | RANDOM) % nblocks))
# A failed read must NOT be silently treated as a passing sample: with
# pipefail set, a dd I/O error propagates through the pipeline and we
# fail verification rather than skipping the block.
# od -v is REQUIRED: without it od collapses repeated 16-byte lines to a
# "*" marker, so an all-0x00 (or all-0xFF) block never yields a clean run
# of hex pairs and the pattern check silently fails on wiped drives.
if ! data=$(dd if="/dev/$dev" bs="$block_size" skip="$offset" count=1 2>/dev/null | od -A n -v -t x1 | tr -d ' \n'); then
return 1
fi
[[ -z "$data" ]] && return 1
if [[ "$expect" == "zero" ]]; then
if ! printf '%s\n' "$data" | grep -qP '^(00)+$|^(ff)+$'; then
return 1
fi
else
# All-0x00 => unwritten/zeroed region the overwrite did not cover.
if printf '%s\n' "$data" | grep -qP '^(00)+$'; then
return 1
fi
fi
done
$all_zero
return 0
}

host_info() {
Expand All @@ -196,8 +235,19 @@ host_info() {
manufacturer=$(dmidecode -s system-manufacturer 2>/dev/null || echo "unknown")
model=$(dmidecode -s system-product-name 2>/dev/null || echo "unknown")
service_tag=$(dmidecode -s system-serial-number 2>/dev/null || echo "unknown")
printf '{"hostname":"%s","manufacturer":"%s","model":"%s","service_tag":"%s"}' \
"$hostname" "$manufacturer" "$model" "$service_tag"
# Build JSON with python3 so quotes/backslashes/newlines in DMI strings are
# properly escaped. Raw printf produced invalid JSON when a DMI value (e.g.
# manufacturer) contained a double-quote, which crashed init_audit's
# json.loads() and aborted audit initialization. Values are passed as argv
# (not interpolated into the program text) so they cannot inject code.
python3 -c '
import json, sys
print(json.dumps({
"hostname": sys.argv[1],
"manufacturer": sys.argv[2],
"model": sys.argv[3],
"service_tag": sys.argv[4],
}))' "$hostname" "$manufacturer" "$model" "$service_tag"
}

init_audit() {
Expand Down
2 changes: 1 addition & 1 deletion autorun/wipe-now.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ for entry in "${drives[@]}"; do

verify_ok="false"
if [[ "$result" == "success" ]]; then
if verify_wipe "$name"; then
if verify_wipe "$name" "$dtype"; then
verify_ok="true"
fi
fi
Expand Down
2 changes: 1 addition & 1 deletion autorun/wipe-wizard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ for entry in "${confirmed[@]}"; do

verify_ok="false"
if [[ "$result" == "success" ]]; then
if verify_wipe "$name"; then
if verify_wipe "$name" "$dtype"; then
verify_ok="true"
log "Verification passed for /dev/$name"
else
Expand Down
177 changes: 177 additions & 0 deletions tests/test_verify_wipe.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/bin/bash
# Tests for host_info() JSON escaping and the method-aware verify_wipe() in
# wipe-lib.sh. External commands (dmidecode, blockdev, dd) are stubbed via a
# PATH-prepended mock dir; od/tr/grep run for real.

set -euo pipefail

MOCK_DIR="$(mktemp -d)"
trap 'rm -rf "$MOCK_DIR"' EXIT

# --- Mock dmidecode: returns whatever MOCK_DMI_VALUE holds (verbatim) --------
cat << 'MOCK_EOF' > "$MOCK_DIR/dmidecode"
#!/bin/bash
printf '%s\n' "$MOCK_DMI_VALUE"
MOCK_EOF
chmod +x "$MOCK_DIR/dmidecode"

# --- Mock blockdev: MOCK_DEV_SIZE bytes -------------------------------------
cat << 'MOCK_EOF' > "$MOCK_DIR/blockdev"
#!/bin/bash
# called as: blockdev --getsize64 /dev/X
printf '%s\n' "${MOCK_DEV_SIZE:-0}"
MOCK_EOF
chmod +x "$MOCK_DIR/blockdev"

# --- Mock dd: emits one block per MOCK_DD_MODE, records skip offsets ---------
# MOCK_DD_MODE: zero | ff | random | allzero | fail
cat << 'MOCK_EOF' > "$MOCK_DIR/dd"
#!/bin/bash
skip=0
for a in "$@"; do
case "$a" in
skip=*) skip="${a#skip=}" ;;
esac
done
[[ -n "${MOCK_SKIP_LOG:-}" ]] && printf '%s\n' "$skip" >> "$MOCK_SKIP_LOG"
case "${MOCK_DD_MODE:-random}" in
fail) exit 1 ;;
zero|allzero) head -c 512 /dev/zero ;;
ff) head -c 512 /dev/zero | tr '\000' '\377' ;;
random) head -c 512 /dev/urandom ;;
esac
MOCK_EOF
chmod +x "$MOCK_DIR/dd"

export PATH="$MOCK_DIR:$PATH"

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck disable=SC1091
source "$REPO_ROOT/autorun/wipe-lib.sh"

fails=0
pass() { echo "PASS: $1"; }
failt() {
echo "FAIL: $1"
fails=$((fails + 1))
}

# Keep verify_wipe loops cheap.
export VERIFY_SAMPLES=64

echo "== host_info() JSON escaping =="

# A DMI value with an embedded double-quote used to produce invalid JSON.
export MOCK_DMI_VALUE='Dell Inc. "Special" Edition'
hi="$(host_info)"
if printf '%s' "$hi" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
pass "host_info emits valid JSON when DMI value contains double-quotes"
else
failt "host_info produced invalid JSON: $hi"
fi

# And the escaped value round-trips intact.
got="$(printf '%s' "$hi" | python3 -c 'import json,sys; print(json.load(sys.stdin)["manufacturer"])')"
if [[ "$got" == 'Dell Inc. "Special" Edition' ]]; then
pass "host_info preserves the quoted manufacturer value"
else
failt "host_info mangled the value: got [$got]"
fi

# Backslashes must not break JSON either.
export MOCK_DMI_VALUE='Back\slash \ Co'
if host_info | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
pass "host_info emits valid JSON when DMI value contains backslashes"
else
failt "host_info produced invalid JSON for a backslash value"
fi

echo "== verify_wipe() method-aware pattern =="

export MOCK_DEV_SIZE=$((512 * 1000000)) # ~512MB, plenty of blocks

# Overwrite methods: random data must PASS, all-zero must FAIL.
export MOCK_DD_MODE=random
if verify_wipe "sdX" "hdd"; then
pass "hdd (random overwrite): random data verifies OK (no false negative)"
else
failt "hdd random data was wrongly rejected"
fi

export MOCK_DD_MODE=allzero
if ! verify_wipe "sdX" "hdd"; then
pass "hdd (random overwrite): all-zero (unwritten) region fails verification"
else
failt "hdd all-zero region wrongly passed verification"
fi

export MOCK_DD_MODE=random
if ! verify_wipe "sdX" "usb-flash"; then
failt "usb-flash random data was wrongly rejected"
else
pass "usb-flash (random overwrite): random data verifies OK"
fi

# Erase methods: zeros/0xFF must PASS, random must FAIL.
export MOCK_DD_MODE=zero
if verify_wipe "sdX" "nvme-ssd"; then
pass "nvme-ssd (block erase): all-zero verifies OK"
else
failt "nvme-ssd all-zero was wrongly rejected"
fi

export MOCK_DD_MODE=ff
if verify_wipe "sdX" "sata-ssd"; then
pass "sata-ssd (secure erase): all-0xFF verifies OK"
else
failt "sata-ssd all-0xFF was wrongly rejected"
fi

export MOCK_DD_MODE=random
if ! verify_wipe "sdX" "nvme-ssd"; then
pass "nvme-ssd (block erase): leftover random data fails verification"
else
failt "nvme-ssd random data wrongly passed verification"
fi

echo "== verify_wipe() read-failure detection =="

export MOCK_DD_MODE=fail
if ! verify_wipe "sdX" "hdd"; then
pass "dd read failure fails verification (not silently skipped)"
else
failt "dd read failure was silently treated as a pass"
fi

echo "== verify_wipe() zero-size guard =="
export MOCK_DEV_SIZE=0
export MOCK_DD_MODE=random
if ! verify_wipe "sdX" "hdd"; then
pass "zero-size device fails verification"
else
failt "zero-size device wrongly passed"
fi

echo "== verify_wipe() full-width offset coverage (>500GB) =="
# 2TB device: the old RANDOM*RANDOM ceiling (~1.07e9 blocks) could not reach
# past ~550GB. Assert sampled offsets exceed that ceiling.
export MOCK_DEV_SIZE=$((2 * 1000 * 1000 * 1000 * 1000)) # 2 TB
export MOCK_DD_MODE=random
export MOCK_SKIP_LOG="$MOCK_DIR/skips.log"
: > "$MOCK_SKIP_LOG"
VERIFY_SAMPLES=500 verify_wipe "sdX" "hdd" || true
max_skip=$(sort -n "$MOCK_SKIP_LOG" | tail -1)
old_ceiling=1073676289 # 32767 * 32767
if [[ "${max_skip:-0}" -gt "$old_ceiling" ]]; then
pass "offsets reach past the old RANDOM*RANDOM ceiling (max=$max_skip > $old_ceiling)"
else
failt "offsets never exceeded old ceiling (max=${max_skip:-0}); tail of large drives unreachable"
fi
unset MOCK_SKIP_LOG

if [[ $fails -eq 0 ]]; then
echo "All verify_wipe/host_info tests passed."
else
echo "$fails test(s) failed."
/bin/false
fi
Loading