#!/bin/bash
#
# Copyright (C) 2012-2026 Red Hat, Inc. All rights reserved.
#
# This file is part of LVM2.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Author: Peter Rajnoha <prajnoha at redhat.com>
#
# Script for deactivating block devices
#
# Requires:
#   bash >= 4.0 (associative array support)
#   util-linux {
#       lsblk >= 2.22 (lsblk -s support)
#       umount
#   }
#   dmsetup >= 1.02.68 (--retry option support)
#   lvm >= 2.2.89 (activation/retry_deactivation config support)
#

shopt -s dotglob nullglob

PATH="/sbin:/usr/sbin:/bin:/usr/bin"

TOOL=blkdeactivate

DEV_DIR="/dev"
SYS_BLK_DIR="/sys/block"

MDADM="/sbin/mdadm"
MOUNTPOINT="/usr/bin/mountpoint"
MPATHD="/sbin/multipathd"
UMOUNT="/bin/umount"
VDO="/bin/vdo"

sbindir="/usr/bin"

if test -x "$UMOUNT" && "$UMOUNT" --help | grep -q -- "--all-targets" ; then
	UMOUNT_OPTS=("--all-targets")
	FINDMNT=()
else
	UMOUNT_OPTS=()
	FINDMNT=(/bin/findmnt -r --noheadings -u -o TARGET)
fi
DMSETUP_OPTS=()
LVM_OPTS=()
MDADM_OPTS=()
MPATHD_OPTS=()
VDO_OPTS=()

LSBLK=(/bin/lsblk -r --noheadings -o "TYPE,KNAME,NAME,MOUNTPOINT")
# Deepest mountpoints first; do not use sort -u on MOUNTPOINT: lsblk uses
# the same label (e.g. [SWAP]) for every active swap, and -u would keep one
# line so other swap devices would miss device_umount() and SKIP_DEVICE_LIST.
SORT_MNT=(/bin/sort -r -k 4)

# Do not show tool errors by default (only done/skipping summary
# message provided by this script) and no verbose mode by default.
ERRORS=0
VERBOSE=0

# Do not unmount mounted devices by default.
DO_UMOUNT=0

# Deactivate each LV separately by default (not the whole VG).
LVM_DO_WHOLE_VG=0
# Do not retry LV deactivation by default.
LVM_CONFIG="activation{retry_deactivation=0}"

# Do not wait for MD RAID device resync, recovery or reshape.
MDRAID_DO_WAIT=0

# Do not disable queueing if set on multipath devices.
MPATHD_DO_DISABLEQUEUEING=0

#
# List of device names and/or VGs to be skipped.
# Device name is the KNAME from lsblk output.
#
# If deactivation of any device fails, it's automatically
# added to the SKIP_DEVICE_LIST.  When whole VGs are being
# deactivated, a failed VG is added to the SKIP_VG_LIST.
#
# These lists provide device tree pruning to skip
# particular device/VG deactivation that failed already.
# (lists are associative arrays!)
#
declare -A SKIP_DEVICE_LIST=()
declare -A SKIP_VG_LIST=()
declare -A MOUNTED_DEVICE_LIST=()
declare -A MOUNTED_VG_LIST=()

#
# List of specified device arguments already successfully deactivated.
# It prevents repeated arguments from being reported as missing devices.
# (list is an associative array!)
#
declare -A DEACTIVATED_ARGS=()

#
# List of mountpoints to be skipped. Any device that is mounted on the mountpoint
# listed here will be added to SKIP_DEVICE_LIST automatically.
# (list is an associative array!)
#
declare -A SKIP_UMOUNT_LIST=(
	["/"]=1
	["/lib"]=1 ["/lib64"]=1
	["/bin"]=1 ["/sbin"]=1
	["/var"]=1 ["/var/log"]=1
	["/usr"]=1
	["/usr/lib"]=1 ["/usr/lib64"]=1
	["/usr/sbin"]=1 ["/usr/bin"]=1
)

# Bash can't properly handle '[' and ']' used as a subscript
# within the '()' initialization - it needs to be done separately!
SKIP_UMOUNT_LIST["[SWAP]"]=1

usage() {
	cat <<-EOF
	  ${TOOL}: Utility to deactivate block devices

	  ${TOOL} [options] [device...]
	    - Deactivate block device tree.
	      If devices are specified, deactivate only supplied devices and their holders.

	  Options:
	    -e | --errors                       Show errors reported from tools
	    -h | --help                         Show this help message
	    -d | --dmoptions     DM_OPTIONS     Comma separated DM specific options
	    -l | --lvmoptions    LVM_OPTIONS    Comma separated LVM specific options
	    -m | --mpathoptions  MPATH_OPTIONS  Comma separated DM-multipath specific options
	    -r | --mdraidoptions MDRAID_OPTIONS Comma separated MD RAID specific options
	    -o | --vdooptions    VDO_OPTIONS    Comma separated VDO specific options
	    -u | --umount                       Unmount the device if mounted
	    -v | --verbose                      Verbose mode (also implies -e)

	  Device specific options:
	    DM_OPTIONS:
	      retry           retry removal several times in case of failure
	      force           force device removal
	    LVM_OPTIONS:
	      retry           retry removal several times in case of failure
	      wholevg         deactivate the whole VG when processing an LV
	    MDRAID_OPTIONS:
	      wait            wait for resync, recovery or reshape to complete first
	    MPATH_OPTIONS:
	      disablequeueing disable queueing on all DM-multipath devices first
	    VDO_OPTIONS:
	      configfile=file use specified VDO configuration file

	EOF

	exit
}

error() {
	printf '%s\n' "$TOOL: $*" >&2
	exit 1
}

require_arg() {
	test $# -ge 2 || error "$1 requires an argument."
}

add_device_to_skip_list() {
	SKIP_DEVICE_LIST+=(["$kname"]=1)
}

add_device_to_mounted_list() {
	MOUNTED_DEVICE_LIST+=(["$kname"]=1)
}

add_vg_to_skip_list() {
	SKIP_VG_LIST+=(["$DM_VG_NAME"]=1)
}

add_vg_to_mounted_list() {
	MOUNTED_VG_LIST+=(["$DM_VG_NAME"]=1)
}

is_top_level_device() {
	# top level devices do not have any holders, that is
	# the SYS_BLK_DIR/<device_name>/holders dir is empty
	local files=("$SYS_BLK_DIR/$kname/holders/"*)
	test ${#files[@]} -eq 0
}

# lsblk and findmnt raw output (\xHH encoded blanks, backslashes and
# non-printable characters) must be decoded before the path is used.
unhex_escape() {
	local str=$1 out= hex

	while [[ "$str" == *'\x'* ]]; do
		out+=${str%%\\x*}
		str=${str#*\\x}
		hex=${str:0:2}
		if [[ "$hex" == [0-9a-fA-F][0-9a-fA-F] ]]; then
			printf -v hex "\\x$hex"
			out+=$hex
			str=${str:2}
		else
			out+='\x'
		fi
	done

	printf '%s' "$out$str"
}

device_umount_one() {
	local split_out

	test -z "$mnt" && return 0

	if test -z "${SKIP_UMOUNT_LIST["$mnt"]}" && test "$DO_UMOUNT" -eq 1; then
		echo -n "  [UMOUNT]: unmounting $name ($kname) mounted on $mnt... "
		if "$UMOUNT" "${UMOUNT_OPTS[@]}" "$mnt" >&9 2>&8; then
			echo "done"
		elif "$MOUNTPOINT" -q "$mnt"; then
			echo "skipping"
			add_device_to_skip_list
			return 1
		else
			echo "already unmounted"
		fi
	else
		# Skip by user policy or because -u was not requested: this
		# is not a failure of this script.
		echo "  [SKIP]: unmount of $name ($kname) mounted on $mnt"
		add_device_to_mounted_list
		if test "$devtype" = "lvm" && test "$LVM_DO_WHOLE_VG" -eq 1 &&
		   split_out=$("$DMSETUP" splitname --nameprefixes --noheadings --rows "$name" LVM 2>&8); then
			eval "$split_out"
			add_vg_to_mounted_list
		fi
	fi
}

device_umount() {
	local failures=0
	test "$devtype" != "lvm" && test "${kname:0:3}" != "dm-" &&
		test "${kname:0:2}" != "md" && return 0

	mnt=$(unhex_escape "$mnt")

	# FINDMNT is defined only if umount --all-targets is not available.
	# In that case, read the list of multiple mount points of one device
	# using FINDMNT and unmount it one by one manually.
	if test ${#FINDMNT[@]} -eq 0; then
		device_umount_one
	else
		while read -r mnt; do
			mnt=$(unhex_escape "$mnt")
			device_umount_one || failures=1
		done <<< "$("${FINDMNT[@]}" "$DEV_DIR/$kname")"
		return "$failures"
	fi
}

deactivate_holders() {
	local devtype kname name mnt skip=1 rc

	# Get holders for the device - either a mount or another device.
	# The first line on the lsblk output is the device itself, which is
	# already being deactivated by the caller - skip it.
	while read -r devtype kname name mnt; do
		test "$skip" -eq 1 && { skip=0; continue; }

		test -e "$SYS_BLK_DIR/$kname" || continue
		# check if the device not on the skip list already
		test -z "${SKIP_DEVICE_LIST["$kname"]}" || return 1
		test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 2

		# try to deactivate the holder
		deactivate
		rc=$?
		test "$rc" -eq 0 || return 1
		test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 2
	done <<< "$("${LSBLK[@]}" "$1")"
}

deactivate_holders_or_skip() {
	local rc

	deactivate_holders "$1"
	rc=$?
	test "$rc" -eq 2 || return "$rc"
	add_device_to_mounted_list
	return 0
}

deactivate_dm() {
	local xname=$name
	test -b "$DEV_DIR/mapper/$xname" || return 0
	test -z "${SKIP_DEVICE_LIST["$kname"]}" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	# Skip DM device deactivation if DM tools missing.
	test "$DMSETUP_AVAILABLE" -eq 0 && {
		add_device_to_skip_list
		return 1
	}

	deactivate_holders_or_skip "$DEV_DIR/mapper/$xname" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	echo -n "  [DM]: deactivating $devtype device $xname ($kname)... "
	if "$DMSETUP" "${DMSETUP_OPTS[@]}" remove "$xname" >&9 2>&8; then
		echo "done"
	else
		echo "skipping"
		add_device_to_skip_list
		return 1
	fi
}

deactivate_lvm() {
	local DM_VG_NAME DM_LV_NAME lv_list lv split_out
	local devices holder_rc lv_kname lv_mnt

	# Skip LVM device deactivation if DM tools missing.
	test "$DMSETUP_AVAILABLE" -eq 0 && {
		add_device_to_skip_list
		return 1
	}

	if ! split_out=$("$DMSETUP" splitname --nameprefixes --noheadings --rows "$name" LVM 2>&8); then
		add_device_to_skip_list
		return 1
	fi
	eval "$split_out"
	test -b "$DEV_DIR/$DM_VG_NAME/$DM_LV_NAME" || return 0
	test -z "${SKIP_DEVICE_LIST["$kname"]}" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0
	test -z "${SKIP_VG_LIST["$DM_VG_NAME"]}" || return 1
	test -z "${MOUNTED_VG_LIST["$DM_VG_NAME"]}" || return 0

	if test "$LVM_DO_WHOLE_VG" -eq 0; then
		# Skip LVM device deactivation if LVM tools missing.
		test "$LVM_AVAILABLE" -eq 0 && {
			add_device_to_skip_list
			return 1
		}
		# Deactivating only the LV specified
		deactivate_holders_or_skip "$DEV_DIR/$DM_VG_NAME/$DM_LV_NAME" || {
			add_device_to_skip_list
			return 1
		}
		test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

		echo -n "  [LVM]: deactivating Logical Volume $DM_VG_NAME/$DM_LV_NAME... "
		if "$LVM" lvchange "${LVM_OPTS[@]}" --config "log{prefix=\"\"} $LVM_CONFIG" -aln "$DM_VG_NAME/$DM_LV_NAME" >&9 2>&8; then
			echo "done"
		else
			echo "skipping"
			add_device_to_skip_list
			return 1
		fi

	else
		# Skip LVM VG deactivation if LVM tools missing.
		test "$LVM_AVAILABLE" -eq 0 && {
			add_vg_to_skip_list
			return 1
		}
		# Deactivating the whole VG the LV is part of
		lv_list=$("$LVM" vgs --config "$LVM_CONFIG" --noheadings --rows -o lv_name "$DM_VG_NAME" 2>&8) || {
			add_vg_to_skip_list
			return 1
		}
		for lv in $lv_list; do
			test -b "$DEV_DIR/$DM_VG_NAME/$lv" || continue
			if devices=$("${LSBLK[@]}" --nodeps "$DEV_DIR/$DM_VG_NAME/$lv"); then
				read -r _ lv_kname _ lv_mnt <<< "$devices"
				lv_mnt=$(unhex_escape "$lv_mnt")
				# A mounted LV in this VG may not be in MOUNTED_DEVICE_LIST
				# when only another LV in the VG was given on the command line.
				if test -n "$lv_mnt" ||
				   test -n "${MOUNTED_DEVICE_LIST["$lv_kname"]}"; then
					add_vg_to_mounted_list
					return 0
				fi
			fi
			deactivate_holders "$DEV_DIR/$DM_VG_NAME/$lv" || {
				holder_rc=$?
				test "$holder_rc" -eq 2 || {
					add_vg_to_skip_list
					return 1
				}
				add_vg_to_mounted_list
				return 0
			}
		done

		echo -n "  [LVM]: deactivating Volume Group $DM_VG_NAME... "
		if "$LVM" vgchange "${LVM_OPTS[@]}" --config "log{prefix=\"    \"} $LVM_CONFIG" -aln "$DM_VG_NAME" >&9 2>&8; then
			echo "done"
		else
			echo "skipping"
			add_vg_to_skip_list
			return 1
		fi
	fi
}

deactivate_md() {
	local sync_action xname=$name
	test -b "$DEV_DIR/$xname" || return 0
	test -d "$SYS_BLK_DIR/$kname/md" || return 0
	test -z "${SKIP_DEVICE_LIST["$kname"]}" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	# Skip MD device deactivation if MD tools missing.
	test "$MDADM_AVAILABLE" -eq 0 && {
		add_device_to_skip_list
		return 1
	}

	deactivate_holders_or_skip "$DEV_DIR/$xname" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	echo -n "  [MD]: deactivating $devtype device $kname... "

	test "$MDRAID_DO_WAIT" -eq 1 && {
		read -r sync_action 2>/dev/null < "$SYS_BLK_DIR/$kname/md/sync_action" || sync_action="idle"
		test "$sync_action" != "idle" && {
			echo -n "$sync_action action in progress... "
			if "$MDADM" "${MDADM_OPTS[@]}" -W "$DEV_DIR/$kname" >&9 2>&8; then
				echo -n "complete... "
			else
				test $? -ne 1 && echo -n "failed to wait for $sync_action action... "
			fi
		}
	}

	if "$MDADM" "${MDADM_OPTS[@]}" -S "$xname" >&9 2>&8; then
		echo "done"
	else
		echo "skipping"
		add_device_to_skip_list
		return 1
	fi
}

deactivate_vdo() {
	local xname=$name
	test -b "$DEV_DIR/mapper/$xname" || return 0
	test -z "${SKIP_DEVICE_LIST["$kname"]}" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	# Skip VDO device deactivation if VDO tools missing.
	test "$VDO_AVAILABLE" -eq 0 && {
		add_device_to_skip_list
		return 1
	}

	deactivate_holders_or_skip "$DEV_DIR/mapper/$xname" || return 1
	test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || return 0

	echo -n "  [VDO]: deactivating VDO volume $xname... "
	if "$VDO" stop "${VDO_OPTS[@]}" --name="$xname" >&9 2>&8; then
		echo "done"
	else
		echo "skipping"
		add_device_to_skip_list
		return 1
	fi
}

deactivate() {
	######################################################################
	# DEACTIVATION HOOKS FOR NEW DEVICE TYPES GO HERE!                   #
	#                                                                    #
	# Identify a new device type either by inspecting the TYPE provided  #
	# by lsblk directly ($devtype) or by any other suitable means.       #
	# e.g. the KNAME provided by lsblk ($kname).                         #
	# Available variables: devtype, kname, name, mnt. Then call a        #
	# device-specific deactivation function that handles the exact type. #
	#                                                                    #
	# This device-specific function will certainly need to call          #
	# deactivate_holders first to recursively deactivate any existing    #
	# holders it might have before deactivating the device it processes. #
	######################################################################
	case "$devtype" in
		lvm) deactivate_lvm ;;
		vdo) deactivate_vdo ;;
		*)
			case "$kname" in
				dm-*) deactivate_dm ;;
				md*) deactivate_md ;;
			esac
			;;
	esac
}

deactivate_all() {
	local devtype kname name mnt devices arg skip=0 failures=0

	echo "Deactivating block devices:"

	test "$MPATHD_RUNNING" -eq 1 && {
		echo -n "  [DM]: disabling queueing on all multipath devices... "
		if "$MPATHD" "${MPATHD_OPTS[@]}" disablequeueing maps 2>&8 | grep -q '^ok$' ; then
			echo "done"
		else
			echo "failed"
			failures=1
		fi
	}

	if test $# -eq 0; then
		#######################
		# Process all devices #
		#######################

		# Unmount all relevant mountpoints first.
		# lsblk output is kept in variables so a failing lsblk is
		# reported; piping it into the here-string would hide the
		# exit status.
		devices=$("${LSBLK[@]}") || {
			echo "$TOOL: unable to list block devices." >&2
			return 1
		}
		while read -r devtype kname name mnt; do
			device_umount || failures=1
		done <<< "$(printf '%s\n' "$devices" | "${SORT_MNT[@]}")"

		# Do deactivate
		devices=$("${LSBLK[@]}" -s) || {
			echo "$TOOL: unable to list block devices." >&2
			return 1
		}
		while read -r devtype kname name mnt; do
			# 'disk' is at the bottom already and it's a real device
			test "$devtype" = "disk" && continue

			# After deactivate() fails, skip=1: lsblk -s lists holders
			# under the failed node before the next unrelated stack.
			# is_top_level_device() is true when holders/ is empty, i.e.
			# this line is a new stack root -- stop skipping there.
			test "$skip" -eq 1 && {
				is_top_level_device || continue
				skip=0
			}

			# check if the device is not on the skip list already
			test -z "${SKIP_DEVICE_LIST["$kname"]}" &&
				test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || continue

			# try to deactivate; on failure skip its subtree below
			deactivate || { skip=1; failures=1; }
		done <<< "$devices"
	else
		##################################
		# Process only specified devices #
		##################################

		while test $# -ne 0; do
			arg=$1
			shift

			# Skip empty arguments; DEACTIVATED_ARGS["$arg"] with an empty
			# subscript makes bash report "bad array subscript".  The option
			# parser already ignores "" the same way.
			test -n "$arg" || continue

			# Unmount all relevant mountpoints first.
			# A missing device is reported below, so an lsblk
			# failure here is not fatal by itself.
			if devices=$("${LSBLK[@]}" "$arg"); then
				while read -r devtype kname name mnt; do
					device_umount || failures=1
				done <<< "$(printf '%s\n' "$devices" | "${SORT_MNT[@]}")"
			fi

			# Do deactivate
			# Single dm device tree deactivation.
			if test -b "$arg"; then
				devices=$("${LSBLK[@]}" --nodeps "$arg") || {
					echo "$TOOL: unable to list device \"$arg\"." >&2
					failures=1
					continue
				}
				read -r devtype kname name mnt <<< "$devices"

				# check if the device is not on the skip list already
				test -z "${SKIP_DEVICE_LIST["$kname"]}" &&
					test -z "${MOUNTED_DEVICE_LIST["$kname"]}" || continue

				if deactivate; then
					DEACTIVATED_ARGS+=(["$arg"]=1)
				else
					failures=1
				fi
			elif test -n "${DEACTIVATED_ARGS["$arg"]}"; then
				echo "$arg: already deactivated"
			else
				echo "$arg: device not found"
				failures=1
			fi
		done
	fi

	return "$failures"
}

get_dmopts() {
	local IFS=',' opt

	for opt in $1; do
		case "$opt" in
			"") ;;
			"retry") DMSETUP_OPTS+=("--retry") ;;
			"force") DMSETUP_OPTS+=("--force") ;;
			*) error "$opt: unknown DM option" ;;
		esac
	done
}

get_lvmopts() {
	local IFS=',' opt

	for opt in $1; do
		case "$opt" in
			"") ;;
			"retry") LVM_CONFIG="activation{retry_deactivation=1}" ;;
			"wholevg") LVM_DO_WHOLE_VG=1 ;;
			*) error "$opt: unknown LVM option" ;;
		esac
	done
}

get_mdraidopts() {
	local IFS=',' opt

	for opt in $1; do
		case "$opt" in
			"") ;;
			"wait") MDRAID_DO_WAIT=1 ;;
			*) error "$opt: unknown MD RAID option" ;;
		esac
	done
}

get_mpathopts() {
	local IFS=',' opt

	for opt in $1; do
		case "$opt" in
			"") ;;
			"disablequeueing") MPATHD_DO_DISABLEQUEUEING=1 ;;
			*) error "$opt: unknown DM-multipath option" ;;
		esac
	done
}

get_vdoopts() {
	local IFS=',' opt tmp

	for opt in $1; do
		case "$opt" in
			"") ;;
			configfile=*) tmp=${opt#*=}; VDO_OPTS+=("--confFile=${tmp%%,*}") ;;
			*) error "$opt: unknown VDO option" ;;
		esac
	done
}

set_env() {
	# Fail early when the tools needed for device enumeration are missing
	# instead of silently reporting success without doing anything.
	test -x "${LSBLK[0]}" || error "${LSBLK[0]} is required."
	test -x "${SORT_MNT[0]}" || error "${SORT_MNT[0]} is required."
	if test ${#FINDMNT[@]} -ne 0; then
		test -x "${FINDMNT[0]}" || error "${FINDMNT[0]} is required."
	fi
	if test "$DO_UMOUNT" -eq 1; then
		test -x "$UMOUNT" || error "$UMOUNT is required for unmounting."
		test -x "$MOUNTPOINT" || error "$MOUNTPOINT is required for unmounting."
	fi

	# fd 8: tool stderr (shown with -e/-v)
	if test "$ERRORS" -eq 1; then
		exec 8>&2
	else
		exec 8>/dev/null
	fi

	# fd 9: tool stdout (shown with -v)
	if test "$VERBOSE" -eq 1; then
		exec 9>&1
		UMOUNT_OPTS+=("-v")
		DMSETUP_OPTS+=("-vvvv")
		LVM_OPTS+=("-vvvv")
		MDADM_OPTS+=("-vv")
		MPATHD_OPTS+=("-v" "3")
		VDO_OPTS+=("--verbose")
	else
		exec 9>/dev/null
	fi

	LVM_AVAILABLE=0
	MDADM_AVAILABLE=0
	VDO_AVAILABLE=0
	DMSETUP_AVAILABLE=0
	MPATHD_RUNNING=0

	test -x "$LVM" && LVM_AVAILABLE=1
	test -x "$MDADM" && MDADM_AVAILABLE=1
	test -x "$VDO" && VDO_AVAILABLE=1
	test -x "$DMSETUP" && DMSETUP_AVAILABLE=1
	test "$MPATHD_DO_DISABLEQUEUEING" -eq 1 &&
		test -x "$MPATHD" &&
		"$MPATHD" show daemon 2>&8 | grep -q "running" &&
		MPATHD_RUNNING=1
}

# TRUSTED_PATH: keep validate_path() in sync with scripts/fsadm.sh.
# Reject a path a non-root user could modify or replace between this
# check and its use: every component of the canonical path must be
# root-owned and must not be group or world writable.  The sticky bit
# is accepted for directories (e.g. /tmp), where other users cannot
# remove entries owned by root.
validate_path() {
	local NAME=$1
	local NODE=$2
	local MODE

	while :; do
		MODE=$(stat -c '%u %a' "$NODE") ||
			error "$NAME \"$NODE\" is not accessible."
		test "${MODE%% *}" = "0" ||
			error "$NAME \"$NODE\" must be owned by root."
		MODE=${MODE##* }
		test $(( 0$MODE & 022 )) -eq 0 ||
			{ test -d "$NODE" && test $(( 0$MODE & 01000 )) -ne 0; } ||
			error "$NAME \"$NODE\" must not be group or world writable."
		test "$NODE" = "/" && break
		NODE=${NODE%/*}
		test -n "$NODE" || NODE=/
	done
}

validate_override() {
	local OPATH VAL

	VAL=${!1-}
	test -z "$VAL" && return 0
	test "${VAL#/}" != "$VAL" ||
		error "$1 must be an absolute path."

	OPATH=$(readlink -f "$VAL") ||
		error "$1 \"$VAL\" must be accessible and owned by root."

	validate_path "$1" "$OPATH"

	if ! test -f "$OPATH" || ! test -x "$OPATH"; then
		error "$1 \"$OPATH\" must be an executable file."
	fi

	# Run the validated canonical path, not the original one, so that a
	# symlink cannot be repointed at a different binary after this check.
	printf -v "$1" '%s' "$OPATH"
}

while test $# -ne 0; do
	case "$1" in
		"") ;;
		-e|--errors) ERRORS=1 ;;
		-h|--help) usage ;;
		-d|--dmoptions) require_arg "$@"; shift; get_dmopts "$1" ;;
		-l|--lvmoptions) require_arg "$@"; shift; get_lvmopts "$1" ;;
		-m|--mpathoptions) require_arg "$@"; shift; get_mpathopts "$1" ;;
		-r|--mdraidoptions) require_arg "$@"; shift; get_mdraidopts "$1" ;;
		-o|--vdooptions) require_arg "$@"; shift; get_vdoopts "$1" ;;
		-u|--umount) DO_UMOUNT=1 ;;
		-v|--verbose) VERBOSE=1 ; ERRORS=1 ;;
		-vv) VERBOSE=1 ; ERRORS=1 ; set -x ;;
		*) break ;;
	esac
	shift
done

validate_override LVM_BINARY
validate_override DMSETUP_BINARY

DMSETUP="${DMSETUP_BINARY:-$sbindir/dmsetup}"
LVM="${LVM_BINARY:-$sbindir/lvm}"

set_env
deactivate_all "$@"
