blob: 43e4003b4d9b0e3cc57e19bdac12328a1ad73ead [file] [log] [blame]
#!/bin/sh
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This runs from the factory install/reset shim. This MUST be run
# from USB, in developer mode. This script contains functions to erase
# securly the disk and verify it has been erased properly.
# if used inside the test harness, 2 variables are defined:
# TEST_DELAY: to reduce the amount of time checking for the eMMC to be ready
# again,
# TEST_FIO_OUTPUT: to send the output of fio to a known file. By default,
# if the file is generated with $(mktemp fio_output_XXXXXX)
. /usr/share/misc/chromeos-common.sh
# Return value for partial success when strict is set.
STRICT_RETURN_VALUE=100
# Outputs the NVMe namespace block devices that belong to the given
# character device.
# Arguments:
# - NVMe character device without preceding /dev/ (ex: nvme0)
# Outputs NVMe namespaces' block devices (ex: /dev/nvme0n1)
get_nvme_block_devs() {
local nvme="$1"
local block_dev
for block_dev in /sys/block/${nvme}*; do
echo "/dev/${block_dev##*/}"
done
}
# Outputs the size of given NVMe character device
# Arguments:
# - NVMe character device (ex: /dev/nvme0)
get_nvme_char_device_size() {
local disk="$1"
local dev_size="$(nvme id-ctrl --output-format=json "${dev}" | jq ".tnvmcap")"
if [ ${dev_size} -eq 0 ]; then
# This may not be exact size of the disk because there might be some
# unused blocks.
local blk blk_size
for blk in $(get_nvme_block_devs "${disk##*/}"); do
blk_size="$(blockdev --getsize64 ${blk})"
: $(( dev_size += blk_size ))
done
fi
echo "${dev_size}"
}
# Get the supported erase mode for ATA disk.
# Arguments:
# - ATA device to query
# Outputs supported erase mode: "--security-erase" or
# "--security-erase-enhanced", empty string if not supported or unknown.
get_ata_supported_erase_mode() {
local disk="$1"
local func
local supported_mode
for func in "supported" "supported: enhanced erase"; do
hdparm -I "${disk}" \
| grep -A10 "^Security" \
| grep -q "^[[:blank:]]\+${func}$"
if [ $? -eq 0 ]; then
if [ "${func}" = "supported" ]; then
supported_mode="--security-erase"
else
supported_mode="--security-erase-enhanced"
fi
fi
done
echo "${supported_mode}"
}
# Check if the ATA device supports block erase sanitize.
# Arguments:
# - device to query
# Return true if the operation is supported, false if not supported.
is_sata_sanitize_supported() {
local dev="$1"
hdparm -I "${dev}" | grep -q "BLOCK_ERASE_EXT command"
return $?
}
# Check if the most recent sanitize operation was completed successfully
# Arguments:
# - device to query
# This is blocking until the sanitize has finished. Return true if the most
# recent sanitize operation was successful, false otherwise.
is_sata_sanitize_successful() {
local dev="$1"
hdparm --sanitize-status "${disk}" \
| grep -iq "Last Sanitize Operation Completed Without Error"
}
# Check if the NVMe device supports block erase sanitize.
# Arguments:
# - device to query (block device or character device)
# Return true if the operation is supported, false if not supported.
is_nvme_sanitize_supported() {
local dev="$1"
# Check bit #1. If it is set to 1, device supports block erase sanitize.
local sanicap=$(nvme id-ctrl --output-format=json "${dev}" \
| jq ".sanicap")
test $(( sanicap / 2 % 2 )) -eq 1
return $?
}
# Check if the device is currently being sanitized.
# Arguments:
# - device to query
# Return true if is being sanitized, false otherwise.
is_nvme_sanitize_in_progress() {
local dev="$1"
# Check last 3 bits of the sanitize status value. If it is set to 010, the
# sanitize operation is in progress.
local status=$(nvme sanitize-log "${dev}" \
| grep "(SSTAT)" \
| grep -oEi "(0x)?[[:xdigit:]]+$")
test $(( status % 8 )) -eq 2
return $?
}
# Check if the most recent sanitize operation was completed successfully
# Arguments:
# - device to query
# Return true if the most recent sanitize operation was successful, false
# otherwise.
is_nvme_sanitize_successful() {
local dev="$1"
# Check last 3 bits of the sanitize status value. If it is set to 001,
# sanitize operation was successful.
local status=$(nvme sanitize-log "${dev}" \
| grep "(SSTAT)" \
| grep -oEi "(0x)?[[:xdigit:]]+$")
test $(( status % 8 )) -eq 1
return $?
}
# Output the progress of nvme sanitize command. This value indicates the
# fraction complete of the sanitize operation. The value is the numerator of
# the fraction complete that has 65536 as its denominator. This value is set
# to FFFFh (65536) if there is no sanitize in progress.
# Arguments:
# - device to query
get_nvme_sanitize_progress() {
local dev="$1"
nvme sanitize-log "${dev}" | grep "(SPROG)" | grep -oEi "[[:xdigit:]]+$"
}
# Return useful bits of the MMC status
#
# Return some status bits that indicates if the device has completed
# outstanding commands.
#
# if 'mmc status get' fails returns 0, which is an invalid status.
get_mmc_status() {
local status
status=$(mmc status get "$1" | sed -nre 's/^SEND_STATUS response: (.*)/\1/p')
# The state is defined in chapter 6.13 of eMMC rev 5.
# Ideally, we should check that all the error bits to be set to 0.
# Now, reading in more details, eMMC device are not garantee to be always
# valid (see X or R mode) or some bit are reserved.
# Therefore, we limit only to flags that are always valid:
# bit 6: EXCEPTION_EVENT: set to 0
# bit 8: READY_FOR_DATA: set to 1 (0 while sanitizing)
# bit 9-12: CURRENT_STATE: set to 4 (Tran), set to 7 (Prg while sanitizing)
printf "0x%08x\n" $((status & 0x00001F40))
}
# Erase an MMC device using firmware functions
#
# Ask the device to trim all sectors.
# Then, ask the device to physically erase all trimmed sectors.
# Arguments:
# - block device to erase
# - strict: if set to "1", then only returns success if sanitize successfully
# physically erases the device when security is supported. If left
# empty or set to "0", returns success when sanitize succeeds
# whether security is supported or not.
secure_erase_mmc() {
local disk="$1"
local strict="$2"
local delay=${TEST_DELAY:-5}
local count
local secure
local rc
# Mark all location as unused -- try secure first.
for secure in "-s" ""; do
blkdiscard ${secure} "${disk}"
rc=$?
if [ ${rc} -eq 0 ]; then
break
fi
done
if [ ${rc} -ne 0 ]; then
echo "security not supported, just doing overwrite"
fi
# Physically erase unused locations.
# 0x00000900 equals to READY_FOR_DATA=1 and CURRENT_STATE=4 (Tran)
mmc_orig_status=$(get_mmc_status "${disk}")
if [ "${mmc_orig_status}" != "0x00000900" ]; then
echo "Not ready for sanitize: status ${mmc_orig_status}."
return 1
fi
mmc sanitize "${disk}" || return $?
count=120 # wait up to 10 minutes
mmc_status="0xffffffff"
while [ "${mmc_status}" != "${mmc_orig_status}" -a ${count} -gt 0 ]; do
sleep "${delay}"
mmc_status=$(get_mmc_status "${disk}")
: $(( count -= 1 ))
done
if [ "${mmc_status}" != "${mmc_orig_status}" ]; then
echo "Device is stuck sanitizing: status ${mmc_status}."
return 1
fi
if [ "${strict}" = "1" ] && [ ${rc} -ne 0 ]; then
return ${STRICT_RETURN_VALUE}
else
return 0
fi
}
# Erase an ATA device using internal firmware function
#
# To trigger the ATA SECURE ERASE function, the disk must be
# in security mode SEC4 (aka locked) or SEC5 (aka secured).
# Disks are usually in SEC1 (unsecured).
# First put the disk in SEC5 then Erase it, that put it back in SEC1.
# Arguments:
# - block device to erase
# - strict: if set to "1", then only returns success if enhanced security
# erase is supported and succeeds. Security-erase does not guarantee
# to erase unallocated sectors whereas enhanced security erase does.
# If left empty or set to "0", returns success if any of the
# security erase succeed.
secure_erase_sata() {
local disk="$1"
local strict="$2"
local temp_password="chromeos"
local erase_mode="$(get_ata_supported_erase_mode "${disk}")"
local partial_success_return_val=0
if [ "${strict}" = "1" ]; then
partial_success_return_val=${STRICT_RETURN_VALUE}
fi
if is_sata_sanitize_supported "${disk}"; then
hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase "${disk}"
is_sata_sanitize_successful "${disk}" && return 0
fi
if [ -n "${erase_mode}" ]; then
hdparm --user-master u --security-set-pass \
"${temp_password}" "${disk}" || return $?
hdparm --user-master u "${erase_mode}" \
"${temp_password}" "${disk}" || return $?
else
echo "security not supported, just doing overwrite"
fi
return ${partial_success_return_val}
}
# Erase an NVMe device.
#
# Use nvme sanitize if it is supported. Otherwise try nvme format with crypto
# mode, if that fails, then try to do with user data mode with a timeout
# proportional to the size of the device.
# Arguments:
# - character device to erase
# - strict: if set to "1", then only returns success if sanitize successfully
# physically erases the device. If left empty or set to "0", returns
# success if either sanitize or nvme format succeeds.
secure_erase_nvme() {
local disk="$1"
local strict="$2"
local ses_user="1" # 0: no secure, 1: user data erase, 2: cryptographic erase
local ses_crypto="2"
local base_timeout_in_ms=$(( 60 * 1000 )) # base timeout for 1 minute
local dev_size="$(get_nvme_char_device_size "${disk}")"
local bytes_per_ms=$(( 100 * 1024 )) # 100 MB/s
# Maximum time without any progress during sanitize operation
local sanitize_timeout_in_sec=$(( 20 * 60 )) # sanitize timeout for 20 minutes
# Maximum time to finish format operation. Let timeout be proportional to the
# size of device.
local format_timeout_in_ms=$(( base_timeout_in_ms + dev_size / bytes_per_ms ))
local progress current_progress
# "During a sanitize operation, the host may periodically examine the
# Sanitize Status log page to check for progress, however, the host
# should limit this polling (e.g., to at most once every several minutes)
# to avoid interfering with the progress of the sanitize operation itself."
# See section 8.15 in
# https://www.nvmexpress.org/wp-content/uploads/NVM_Express_Revision_1.3.pdf
# Because of the reason above need to sleep and check the progress during
# sanitize operation. We use 10 seconds for sleeping because the device we
# tested this code on executes the command quite fast. The whole full disk
# wipe process takes a lot of time so we don't want to increase it even more
# by picking a larger value.
local sleep_time_in_sec=10
# Sanitize is preferred over format because it guarantees physical data
# destruction.
if is_nvme_sanitize_supported "${disk}"; then
nvme sanitize "${disk}" --ause --sanact=0x02
count="${sanitize_timeout_in_sec}"
while [ ${count} -gt 0 ] && is_nvme_sanitize_in_progress "${disk}"; do
# See the comment above.
sleep "${sleep_time_in_sec}"
current_progress="$(get_nvme_sanitize_progress "${disk}")"
if [ "${progress}" = "${current_progress}" ]; then
: $(( count -= sleep_time_in_sec ))
else
count="${sanitize_timeout_in_sec}"
fi
progress="${current_progress}"
done
is_nvme_sanitize_successful "${disk}" && return 0
# If block erase sanitize operation fails, issue sanitize command with the
# 'Exit Failure Mode' action to recover from failure.
nvme sanitize "${disk}" --sanact=0x01
count="${sanitize_timeout_in_sec}"
progress=0
while [ ${count} -gt 0 ] && is_nvme_sanitize_in_progress "${disk}"; do
# Check if the sanitize command executed before continuing.
# Also see the comment above.
sleep "${sleep_time_in_sec}"
current_progress="$(get_nvme_sanitize_progress "${disk}")"
if [ "${progress}" = "${current_progress}" ]; then
: $(( count -= sleep_time_in_sec ))
else
count="${sanitize_timeout_in_sec}"
fi
progress="${current_progress}"
done
fi
# If strict is set to 1, only sanitize should return success.
# We want to be able to distinguish between the successes in strict mode
# in order to inform users if their device is physically erased or not.
local success_return_value=0
if [ "${strict}" = "1" ]; then
success_return_value=${STRICT_RETURN_VALUE}
fi
# Format with crypto mode
nvme format "${disk}" --ses "${ses_crypto}" && return ${success_return_value}
# Format with userdata mode
# 0xffffffff means format all namespaces of the given character device.
nvme format "${disk}" --namespace-id=0xffffffff --ses "${ses_user}" \
--timeout "${format_timeout_in_ms}"
local return_val=$?
test ${return_val} -eq 0 && return ${success_return_value}
return ${return_val}
}
# Erase a device using its internal firmware function.
#
# Arguments:
# - device to erase ("/dev/sda", "/dev/mmcblk0", "/dev/nvme0").
# - strict: if set to 1 then only returns success if the physical data
# destruction is guaranteed by the command set that is used. If unset
# or set to "0", returns success if either physical data destruction
# is guaranteed or not.
# Returns:
# 0 if the erase is either not supported or completed.
# !0 if the erase process could not complete or failed.
secure_erase() {
local disk="$1"
local strict="$2"
local disk_type=$(get_device_type "${disk}")
# Identify if MMC or SATA.
case "$disk_type" in
MMC)
secure_erase_mmc "${disk}" "${strict}"
;;
ATA)
secure_erase_sata "${disk}" "${strict}"
;;
NVME)
secure_erase_nvme "${disk}" "${strict}"
;;
*)
echo "Unable to identify the type of disk: -${disk_type}-"
return 1
esac
}
# Use fio to write/verify a pattern.
#
# The first and last 1M of the disk are zeroed, the rest is written
# with a random patter fio can verify latter.
#
# Argument
# - disk: the device to erase
# - disk_size: the size of the device
# - disk_op: "write" to write over the SSD, "verify" to check the SSD
# has been overwritten properly, "verify_disk_wipe" to check
# the SSD has been zeroed.
# Returns:
# fio error code if fio could run, 1 otherwise.
perform_fio_op() {
# Globals, used by factory_secure.fio
local disk="$1"
local disk_size="$2"
local disk_op="$3"
local dev_main_area_end=$(( ${disk_size} - 1048576 ))
local block_size=1048576
local fio_err=0
local fio_output="${TEST_FIO_OUTPUT}"
local fio_regex='/^(secure|fio)/s/.* err= *([[:digit:]]+).*/\1/p'
local input
export FIO_DEV="${disk}"
export FIO_DEV_MAIN_AREA_SIZE=$(( ${disk_size} - 2097152 ))
export OFFSET="1m"
export VERIFY="md5"
if [ -z "${fio_output}" ]; then
fio_output="$(mktemp -t fio_output_XXXXXX)"
fi
case "$disk_op" in
write)
export FIO_VERIFY_ONLY=0
# Erase the begining an the end of the drive. Write random first
# to ensure the data is scrambled.
for input in "urandom" "zero"; do
dd bs="${block_size}" of="${disk}" oflag=dsync iflag=fullblock \
if=/dev/${input} count=1
dd bs="${block_size}" of="${disk}" oflag=dsync iflag=fullblock \
if=/dev/${input} seek=$(( dev_main_area_end / ${block_size} ))
done
;;
verify)
export FIO_VERIFY_ONLY=1
;;
verify_disk_wipe)
export FIO_VERIFY_ONLY=1
export VERIFY="pattern"
export FIO_DEV_MAIN_AREA_SIZE=${disk_size}
export OFFSET="0"
;;
*)
echo "Unsupported operation: -${disk_op}-"
return 1
esac
local fio_script="$(mktemp -t fio_config_XXXXXX)"
cat >"${fio_script}" <<HERE
[secure]
filename=\${FIO_DEV}
ioengine=libaio
iodepth=32
direct=1
readwrite=write
bs=256k
offset=\${OFFSET}
size=\${FIO_DEV_MAIN_AREA_SIZE}
do_verify=1
verify=\${VERIFY}
verify_pattern=0x0
verify_only=\${FIO_VERIFY_ONLY}
HERE
# Write a pattern on the media for future verification.
# fio configuration file use DEV, DEV_MAIN_AREA_SIZE and VERIFY_ONLY.
fio "${fio_script}" --output "${fio_output}" --aux-path="${TMPDIR:-/tmp}"
rm -f "${fio_script}"
fio_err=$(sed -nr "${fio_regex}" "${fio_output}")
if [ -z "${fio_err}" ]; then
echo "-- output of fio not understood --"
cat "${fio_output}"
fio_err=1
elif [ ${fio_err} -ne 0 ]; then
cat "${fio_output}"
if [ $FIO_VERIFY_ONLY -eq 0 ]; then
echo "-- writing pattern failed --"
echo "The storage device is not working properly."
else
# Check if we fail to read the device, or the pattern is wrong.
echo "-- verifying pattern failed --"
if [ ${fio_err} -eq 84 ]; then
echo -n "The storage device has either been tampered with or "
echo "not securely erased properly."
else
echo "Storage device broken: unable to read some sector from it."
fi
fi
else
rm "${fio_output}"
fi
return ${fio_err}
}