Skip to content

Instantly share code, notes, and snippets.

@smoser
Last active January 14, 2026 21:23
Show Gist options
  • Select an option

  • Save smoser/8c65b8771d5ab1d99c44c285323dfff6 to your computer and use it in GitHub Desktop.

Select an option

Save smoser/8c65b8771d5ab1d99c44c285323dfff6 to your computer and use it in GitHub Desktop.
backdoor-image

backdoor-image

Description

backdoor-image can be used to easily add user with passwordless sudo access to a image or a root filesystem.

Operating on an image requires the 'mount-image-callback' tool from cloud-utils. That can be installed on ubuntu via apt-get install -qy cloud-image-utils.

Usage

Usage: backdoor-image [ options ] target

   add a 'backdoor' user to a image or filesystem at 'target'

   options:
      --import-id U      use 'ssh-import-id' to get ssh public keys
                         may be used more than once.
      --force            required to operate on / filesystem
      --password P       set password P, implies --password-auth
      --password-auth    enable password auth
      --pubkeys  F       add public keys from file 'F'
                         default: ~/.ssh/id_rsa.pub unless --password
                         or --import-id specified
      --user      U      use user 'U' (default: 'backdoor')
#!/bin/bash
# shellcheck disable=SC2015,SC2166
VERBOSITY=0
TEMP_D=""
DEFAULT_USER="backdoor"
error() { echo "$@" 1>&2; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] target
add a 'backdoor' user to a image or filesystem at 'target'
options:
--import-id U use 'ssh-import-id' to get ssh public keys
may be used more than once.
--force required to operate on / filesystem
--password P set password P, implies --password-auth
--password-auth enable password auth
--pubkeys F add public keys from file 'F'
default: ~/.ssh/id_rsa.pub unless --password
or --import-id specified
--user U use user 'U' (default: '${DEFAULT_USER}')
--root-password P set root password to P
EOF
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
cleanup() {
[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}
debug() {
local level=${1}; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
mod_sshd_bool() {
local cfg="$1" kn="$2" target="$3" dry=${4:-false}
local ws=$' \t' msg=""
local match="^\([#]\{0,1\}\)[#$ws]*$kn\([$ws]\+\)\(yes\|no\)"
local cur="" hsh="#"
cur=$(sed -n "s/$match/\1\3/p" "$cfg") ||
{ error "failed to read $cfg"; return 1; }
if [ -n "$cur" ]; then
# shellcheck disable=SC2295
case "$cur" in
"#$target") msg="uncommenting line '$kn $target'";;
"#*") msg="uncommenting and changing '$kn ${cur#$hsh}' to '$target'";;
"$target") msg="nochange";;
"*") msg="changing '$cur' to '$target'";;
esac
if [ "$msg" = "nochange" ]; then
debug 1 "no change to $cfg necessary"
else
debug 1 "updating $cfg: $msg"
$dry && return
sed -i "s/$match/$kn\2${target}/" "$cfg" ||
{ error "failed to update $cfg"; return 1; }
fi
else
debug 1 "appending entry for '$kn $target' to $cfg"
$dry && return
echo "$kn $target" >> "$cfg" ||
{ error "failed to append entry to $cfg"; return 1; }
fi
return 0
}
test_mod_sshd_cfg() {
local kn="PasswordAuthentication"
echo "#$kn yes" > f1
echo "#$kn no" > f2
echo "$kn yes" > f3
echo "$kn no" > f4
: > f5
for f in f1 f2 f3 f4 f5; do
mod_sshd_bool "$f" PasswordAuthentication yes true
done
}
add_group_ent() {
local group="$1" gid="$2" fgroup="$3" dry="${4:-false}"
local grent="$group:x:$gid:"
if grep -q "^$group:" "$fgroup"; then
debug 1 "remove $group from group file"
$dry || sed -i "/^$group:/d" "$fgroup" ||
{ error "failed to remove user from group"; return 1; }
fi
debug 1 "append entry to group: $grent"
if ! $dry; then
echo "$grent" >> "$fgroup" ||
{ error "failed to update group file"; return 1; }
fi
return 0
}
add_passwd_ent() {
local user="$1" uid="$2" gid="$3" home="$4" shell="$5" fpasswd="$6"
local dry=${7:-false}
if grep -q "^$user:" "$fpasswd"; then
debug 1 "remove $user from password file"
$dry || sed -i "/^$user:/d" "$fpasswd" ||
{ error "failed to remove user from password file"; return 1; }
fi
local pwent="$user:x:$uid:$gid:backdoor:$home:$shell"
debug 1 "append entry to passwd: $pwent"
if ! $dry; then
echo "$pwent" >> "$fpasswd" ||
{ error "failed to update passwd file"; return 1; }
fi
}
encrypt_pass() {
local pass="$1"
command -v openssl >/dev/null ||
{ error "need openssl for password encryption"; return 1; }
enc=$(openssl passwd -6 "$pass") ||
{ error "failed running 'openssl passwd -6 '$pass'"; return 1; }
[ -n "${enc}" ]
_RET="$enc"
}
add_shadow_ent() {
local user="$1" pass="$2" fshadow="$3" dry="$4"
local encrypt_pre="\$6\$" shent="" encpass="" pwchange=""
# if input was '$6$' format, just use it verbatum
# shellcheck disable=SC2295
if [ "${pass#${encrypt_pre}}" != "${pass}" ]; then
debug 1 "using encrypted password from cmdline"
encpass="$pass"
else
encrypt_pass "$pass" && encpass="$_RET" ||
{ error "failed to encrypt password"; return 1; }
fi
# pwchange is number of days since 1970
pwchange=$(($(date +"(%Y-1970)*365 + 10#%j")))
shent="$user:$encpass:$pwchange:0:99999:7:::"
if grep -q "^$user:" "$fshadow"; then
debug 1 "remove $user from shadow file"
$dry || sed -i "/^$user:/d" "$fshadow" ||
{ error "failed to remove user from shadow"; return 1; }
fi
debug 1 "append entry to shadow: $shent"
if ! $dry; then
echo "$shent" >> "$fshadow" ||
{ error "failed to update shadow file"; return 1; }
fi
return 0
}
add_sudo_ent() {
local user="$1" mp="$2" dry="$3"
local target="/etc/sudoers.d/99-$user"
local ent="$user ALL=(ALL) NOPASSWD:ALL"
local start="#BACKDOOR_START_${user}"
local end="#BACKDOOR_end_${user}"
local content=""
content=$(printf "%s\n%s\n%s\n" "$start" "$ent" "$end")
debug 1 "add sudoers ($mp,$target): $ent"
if ! $dry; then
local d="${target%/*}"
[ -d "$mp/$d" ] || mkdir -m 0755 "$mp/$d" ||
{ error "failed to create $d"; return 1; }
rm -f "$mp/$target" &&
( umask 226 && echo "$content" > "$mp/$target" ) ||
{ error "failed to add sudoers entry to $target"; return 1; }
fi
}
add_user() {
local user="$1" pass="$2" uid="$3" gid="$4" home="$5" shell="$6"
local rootd="$7" dry="${8:-false}"
local fpasswd="$rootd/etc/passwd" fshadow="$rootd/etc/shadow"
local fgroup="$rootd/etc/group"
[ -f "$fpasswd" ] || { error "no password file"; return 1; }
[ -f "$fshadow" ] || { error "no shadow file"; return 1; }
[ -f "$fgroup" ] || { error "no group file"; return 1; }
local group="$user" f="" t=""
if [ -z "$shell" ]; then
shell=/bin/sh
if [ -e "$rootd/bin/bash" ] && [ -x "$rootd/bin/bash" ]; then
shell=/bin/bash
fi
fi
add_passwd_ent "$user" "$uid" "$gid" "$home" "$shell" "$fpasswd" "$dry" || return 1
add_group_ent "$group" "$gid" "$fgroup" "$dry" || return 1
add_shadow_ent "$user" "$pass" "$fshadow" "$dry" || return 1
debug 1 "create $rootd/home/$user"
if ! $dry; then
mkdir -p "$rootd/home/$user" &&
chown "$uid:$gid" "$rootd/home/$user" ||
{ error "failed to make home dir"; return 1; }
for f in "$rootd/etc/skel/".* "$rootd/etc/skel/"*; do
[ -e "$f" ] || continue
t="$rootd/home/$user/${f##*/}"
[ ! -e "$t" ] || continue
cp -a "$f" "$t" && chown -R "$uid:$gid" "$t" ||
{ error "failed to copy $f to $t"; return 1; }
done
fi
}
add_user_keys() {
local keys="$1" dir="$2" ownership="$3" dry="${4:-false}"
debug 1 "add ssh keys to $dir with $ownership"
$dry && return
mkdir -p "$dir" &&
cp "$keys" "$dir/authorized_keys" &&
chmod 600 "$dir/authorized_keys" &&
chown "$ownership" "$dir" "$dir/authorized_keys" &&
chmod 700 "$dir" ||
{ error "failed to add user keys"; return 1; }
if [ $VERBOSITY -ge 1 ]; then
debug 1 "added ssh keys:"
sed "s,^,| ," "$keys"
fi
}
gen_ssh_keys() {
local mp="$1" types="${2:-rsa}" dry="${3:-false}"
local ktype="" file="" ftmpl="/etc/ssh/ssh_host_%s_key" out=""
for ktype in $types; do
file=${ftmpl//%s/$ktype}
if [ -f "$mp/$file" ]; then
debug 2 "existing key for $mp/$file"
continue
fi
debug 1 "ssh-keygen -t $ktype -N '' -f '$file' -C backdoor"
$dry && continue
out=$(ssh-keygen -t "$ktype" -N '' -f "$mp/$file" -C backdoor 2>&1) || {
error "$out"
error "failed generate keytype $ktype";
return 1;
}
out=$(ssh-keygen -l -f "$mp/$file")
debug 1 "$out"
done
}
apply_changes() {
local mp="$1" user="$2" password="$3" pwauth="$4" pubkeys="$5" shell="$6" rootpw="$7"
local dry="${8:-false}"
local home="/home/$user" key=""
local uid="9999" gid="9999"
local sshcfg="$mp/etc/ssh/sshd_config"
[ -f "$sshcfg" ] ||
{ error "$sshcfg did no exist"; return 1; }
key="PubkeyAuthentication"
mod_sshd_bool "$sshcfg" "$key" "yes" "$dry" ||
{ error "failed to set $key to yes"; return 1; }
if $pwauth; then
key="PasswordAuthentication"
mod_sshd_bool "$sshcfg" "$key" "yes" "$dry" ||
{ error "failed to set $key to yes"; return 1; }
fi
gen_ssh_keys "$mp" "rsa" "$dry" || return 1
add_user "$user" "$password" "$uid" "$gid" "$home" "$shell" "$mp" "$dry" || return 1
[ -z "$pubkeys" ] ||
add_user_keys "$pubkeys" "$mp/$home/.ssh" "$uid:$gid" || return 1
add_sudo_ent "$user" "$mp" "$dry" || return 1
if [ -n "$rootpw" ]; then
add_shadow_ent "root" "$rootpw" "$mp/etc/shadow" "$dry" || return 1
fi
}
dump_ssh_keys() {
local u="" url=""
for u in "$@"; do
url="https://github.com/$u.keys"
if command -v curl >/dev/null; then
curl --silent "$url" || {
stderr "failed curl $url"
return 1
}
elif command -v wget >/dev/null; then
wget --quiet -O- "$url" || {
stderr "failed wget -O- $url"
return 1
}
else
stderr "no http client to get keys from $url"
return 1
fi
done
}
main() {
short_opts="hvp:"
long_opts="help,dry-run,force,import-id:,password:,root-password:,password-auth,pubkeys:,partition:,shell:,user:,verbose"
getopt_out=$(getopt --name "${0##*/}" \
--options "${short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
bad_Usage
local user="" password="" pwauth=false pubkeys="" import_ids="" shell=""
local target="" pkfile="" force=false dry=false rootpw=""
user="${DEFAULT_USER}"
local args="" rootpart=2
args=( "$@" )
unset "args[${#args[@]}-1]"
while [ $# -ne 0 ]; do
cur=${1}; next=${2};
case "$cur" in
-h|--help) Usage ; exit 0;;
--dry-run) dry=true;;
--force) force=true;;
--import-id)
import_ids="${import_ids:+${import_ids} }$next";
shift;;
-p|--partition) rootpart=$next; shift;;
--password) password=$next; shift;;
--password-auth) pwauth=true;;
--root-password) rootpw="$next";;
--pubkeys) pubkeys=$next; shift;;
--shell) shell="$next"; shift;;
--user) user=$next; shift;;
-v|--verbose) VERBOSITY=$((VERBOSITY+1));;
--) shift; break;;
esac
shift;
done
[ $# -ne 0 ] || { bad_Usage "must provide image"; return 1; }
[ $# -ge 2 ] && { bad_Usage "too many arguments: $*"; return 1; }
[ "$(id -u)" = "0" ] ||
{ error "sorry, must be root"; return 1; }
target="$1"
if [ -d "$target" ]; then
if [ "$target" -ef "/" ] && ! $force; then
error "you must specify --force to operate on /"
return 1
fi
elif [ -f "$target" ]; then
local vopt="" mic="mount-image-callback"
if [ ${VERBOSITY} -ge 2 ]; then
vopt="-v"
fi
if command -v "$mic" >/dev/null 2>&1; then
:
elif [ -x "${0%/*}/$mic" ]; then
PATH="${0%/*}:$PATH"
else
error "No '$mic' in PATH"
return 1
fi
exec "$mic" "--partition=$rootpart" $vopt -- "$target" "$0" "${args[@]}" _MOUNTPOINT_
else
[ -f "$target" ] || { error "$target: not a file"; return 1; }
fi
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
{ error "failed to make tempdir"; return 1; }
trap cleanup EXIT
pkfile="${TEMP_D}/pubkeys"
if [ -z "$password" -a -z "$pubkeys" -a -z "$import_ids" ]; then
local pk=""
for pk in ~/.ssh/id_25519.pub ~/.ssh/id_rsa.pub ; do
if [ -f "$pk" ]; then
pubkeys="${pubkeys} $pk"
debug 1 "added $pk to pubkeys"
break
fi
done
pubkeys=${pubkeys# }
if [ -z "$pubkeys" ]; then
error "must specify one of --password, --pubkeys, --import-id"
error "either pass an argument or create ~/.ssh/id_rsa.pub"
return 1
fi
fi
if [ -n "$pubkeys" ]; then
cp "$pubkeys" "$pkfile" ||
{ error "failed to copy $pubkeys"; return 1; }
fi
if [ -n "$import_ids" ]; then
# shellcheck disable=SC2086
dump_ssh_keys ${import_ids} > "$pkfile.i" &&
cat "$pkfile.i" >> "$pkfile" ||
{ error "failed to import ssh users: $import_ids"; return 1; }
fi
[ -f "$pkfile" ] || pkfile=""
[ -z "$password" ] || pwauth=true
apply_changes \
"$target" "$user" "$password" "$pwauth" "$pkfile" "$shell" "$rootpw" ||
{ error "failed to apply changes"; return 1; }
error "added user '$user' to $target"
[ -n "$password" ] && error "set password to $password."
$pwauth && error "enabled password auth" ||
error "did not enable password auth"
[ -n "$rootpw" ] && error "set root password"
[ -n "$pubkeys" ] && error "added pubkeys from $pubkeys."
[ -n "$import_ids" ] && error "imported github ssh keys for $import_ids to $user"
return 0
}
main "$@"
#!/bin/bash
# shellcheck disable=SC2015,SC2166,SC2162
VERBOSITY=0
TEMP_D=""
UMOUNTS=( )
QEMU_DISCONNECT=""
CR=$'\n'
TAB=$'\t'
error() { echo "$@" 1>&2; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] file cmd [ args ]
mount a file to a temporary mount point and then
invoke the provided cmd with args
supported 'file' are:
file : any disk format supported by qemu-nbd
the temporary mountpoint will be put in an a environment variable
named MOUNTPOINT.
if any of the arguments are the literal string '_MOUNTPOINT_', then
they will be replaced with the mount point. Example:
${0##*/} my.img chroot _MOUNTPOINT_ /bin/sh
Additionally, the helper program 'mchroot' will be added to the path
and can be used effectively as 'chroot _MOUNTPOINT_':
${0##*/} my.img mchroot
options:
-v | --verbose increase verbosity
-h | --help print this message.
--read-only use read-only mount.
-C | --cd-mountpoint change dir to mountpoint before executing cmd.
-m | --mountpoint MP mount to directory MP rather than a temp dir
--overlay mount via overlayfs
-P | --partition PARTNUM mount partition PARTNUM (default 'auto')
if 'auto', then mount part 1 if image is
partitioned otherwise mount image
-p | --proc bind mount /proc
-s | --sys bind mount /sys
-d | --dev bind mount /dev
--system-mounts bind mount /sys, /proc, /dev
--system-resolvconf copy host's resolvconf into /etc/resolvconf
--format FMT specify the format of the image.
default is to automatically determine
EOF
}
# umount_r(mp) : unmount any filesystems under r
# this is useful to unmount a chroot that had sys, proc ... mounted
umount_r() {
local p
for p in "$@"; do
[ -n "$p" ] || continue
tac /proc/mounts | sh -c '
p=$1
didumount=0
while read s mp t opt a b ; do
[ "${mp}" = "${p}" -o "${mp#${p}/}" != "${mp}" ] ||
continue
umount "$mp" || exit 1
didumount=1
done
[ $didumount -eq 1 ] || exit 1
exit 0' umount_r "${p%/}"
# shellcheck disable=SC2181
[ $? -eq 0 ] || return
done
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
disconnect_qemu() {
[ -n "$QEMU_DISCONNECT" ] || return 0
local out="" nbd="$QEMU_DISCONNECT"
debug 1 "disconnecting $nbd"
local pid="" pfile="/sys/block/${nbd#/dev/}/pid"
{ read pid < "$pfile" ; } >/dev/null 2>&1
[ -n "$pid" -a ! -d "/proc/$pid" ] &&
error "qemu-nbd process seems to have died. was '$pid'"
out=$(qemu-nbd --disconnect "$nbd" 2>&1) &&
QEMU_DISCONNECT="" || {
error "failed to disconnect $nbd";
error "$out"
return 1;
}
}
do_umounts() {
local um="" fails=0 mydir="$PWD/" mounts="" i=0
mounts=( "$@" )
for((i=${#mounts[@]}-1;i>=0;i--)); do
um=${mounts[$i]}
um=$(readlink -f "$um") || {
error "WARNING: failed to get full path to '$um'";
fails=$((fails+1))
continue;
}
# shellcheck disable=SC2295
[ "${mydir#${um}/}" != "${mydir}" ] && {
error "WARNING: leaving '$mydir' to unmount $um";
cd /
}
umount_r "$um" || {
error "WARNING: unmounting filesystem at $um failed!"
fails=$((fails+1))
}
done
return $fails
}
cleanup() {
if [ "${#UMOUNTS[@]}" -ne 0 ]; then
debug 2 "umounts: ${UMOUNTS[*]}"
do_umounts "${UMOUNTS[@]}" ||
{ error "failed cleaning up mounts"; return 1; }
fi
disconnect_qemu
rm -Rf "$TEMP_D" || error "removal of temp dir failed!"
}
debug() {
local level="$1"; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
get_image_format() {
local img="$1" out=""
out=$(qemu-img info "$img") &&
out=$(echo "$out" | awk '$0 ~ /^file format:/ { print $3 }') &&
_RET="$out"
}
get_partition() {
# return in _RET the 'auto' partition for a image.
# _RET=partition number for a partitioned image
# _RET=0 for unpartitioned
local img="$1" out=""
_RET_ERR=""
out=$(LANG=C sfdisk --list -uS "$img" 2>&1) || {
_RET_ERR="$?:$out"
return 1;
}
if echo "$out" | grep -q 'Device.*Start.*End'; then
_RET=1
else
_RET=0
fi
}
add_bin() {
cat > "$1" || { error "failed to write to $1"; return 1; }
chmod 755 "$1" || { error "failed to set perms on $1"; return 1; }
}
add_helpers() {
local d="$1"
[ -d "$1" ] || mkdir -p "$1"
add_bin "$d/mchroot" <<"EOF" || return 1
#!/bin/sh
if [ $# -eq 0 ]; then
for p in ${SHELL#/} bin/bash bin/sh; do
[ -x "${MOUNTPOINT}/$p" ] && set -- "/$p" && break
done
[ $# -ne 0 ] || { echo "no shell found in $MOUNTPOINT/bin" 1>&2; exit 1; }
fi
exec chroot "$MOUNTPOINT" "$@"
EOF
return
}
mount_overlay() {
local lower="$1" upper="$2" workdir="$3"
local olayopts="lowerdir=$lower,upperdir=$upper"
# 3.18+ require 'workdir=' option.
case "$(uname -r)" in
2*|3.1[01234567]*|3.[0-9].*) :;;
*) olayopts="${olayopts},workdir=$workdir"
mkdir -p "$workdir" ||
{ _ERR="Failed to create workdir '$workdir'"; return 1; }
;;
esac
local cmd="" fstype="" ret="" out="" fsfile="/proc/filesystems"
_ERR=""
for fstype in overlay overlayfs; do
cmd=( mount -t "$fstype" -o "$olayopts" "$lower" "$upper" )
debug 2 "attempting '$fstype' mount with: ${cmd[*]}"
out=$("${cmd[@]}" 2>&1)
ret=$?
if [ $ret -eq 0 ]; then
debug 1 "mounted '$fstype' via $fstype: ${cmd[*]}"
return 0
fi
_ERR="${_ERR}Failed [$ret]: ${cmd[*]}:${CR}"
_ERR="${_ERR}$out${CR}"
if [ -r "$fsfile" ] && grep -q "${TAB}${fstype}$" "$fsfile"; then
# this failed and we have support in kernel. do not try further.
return $ret
fi
done
return $ret
}
assert_nbd_support() {
if [ ! -e /sys/block/nbd0 ] && ! grep -q nbd /proc/modules; then
debug 1 "trying to load nbd module"
modprobe nbd >/dev/null 2>&1
has_cmd udevadm && udevadm settle >/dev/null 2>&1
fi
[ -e /sys/block/nbd0 ] || {
error "Cannot use nbd: no nbd kernel support."
return 1;
}
}
find_unused_nbd() {
# return a path to an unused nbd device (/dev/nbd?)
local f roflag=""
for f in /sys/block/nbd*; do
[ -d "$f" -a ! -f "$f/pid" ] &&
{ _RET="/dev/${f##*/}"; return 0; }
done
error "failed to find an nbd device"
return 1;
}
connect_nbd() {
local img="$1" fmt="$2" ptnum="${3:-auto}" rwmode="${3:-ro}"
local nbd="" pidfile="" pid="" ret="" roflag="" nptnum="" i=""
# shellcheck disable=SC2178
local cmd=""
if [ "$rwmode" = "ro" ]; then
roflag="--read-only"
fi
# yes, there is a race condition here.
find_unused_nbd || return
nbd="$_RET"
cmd=( qemu-nbd ${roflag:+"$roflag"} "--format=$fmt" --connect "$nbd" "$img" )
"${cmd[@]}" && QEMU_DISCONNECT="$nbd"
ret=$?
if [ $ret -ne 0 ]; then
error "Failed [$ret]: $*"
return $ret
fi
pidfile="/sys/block/${nbd#/dev/}/pid"
if [ ! -f "$pidfile" ]; then
debug 1 "waiting on pidfile for $nbd in $pidfile"
i=0
while [ ! -f "$pidfile" ] && i=$((i+1)); do
if [ $i -eq 200 ]; then
error "giving up on pidfile $pidfile for $nbd"
disconnect_qemu
return 1
fi
sleep .1
debug 2 "."
done
fi
read pid < "$pidfile" && debug 2 "pid for $nbd is $pid" || {
error "reading pid from $pidfile for $nbd failed!";
disconnect_qemu
return 1
}
debug 1 "connected $img_in ($fmt) ${rwmode} to $nbd. waiting for device."
local out="" tries=40
# This can fail due to udev events, but we ignore that. We need to ensure
# it happens for where it doesnt happen automatically (LP: #1741300)
out=$(blockdev --rereadpt "$nbd" 2>&1) ||
debug 1 "blockdev rereadpt $nbd failed"
has_cmd udevadm && udevadm settle
i=0
while i=$((i+1)); do
get_partition "$nbd" && nptnum="$_RET" && break
[ $i -eq $tries ] && {
error "gave up on $nbd"
if [ -n "$_RET_ERR" ]; then
error "get_partition error: $_RET_ERR"
fi
disconnect_qemu
return 1
}
if [ -n "$_RET_ERR" ] && [ "$VERBOSITY" -gt 1 ]; then
error "get_partition $i/$tries error: $_RET_ERR"
fi
[ $((i%10)) -eq 0 ] &&
debug 1 "waiting for $nbd to be ready."
sleep .1
done
if [ "${ptnum}" = "auto" ]; then
if [ "$nptnum" = "0" ]; then
debug 1 "unpartitioned disk."
else
debug 1 "partitioned disk."
fi
ptnum=$nptnum
else
if [ "$nptnum" = "0" -a "$ptnum" != "0" ]; then
error "img $img does not appear partitioned but ptnum=$ptnum provided."
return 1
fi
fi
if [ "$ptnum" -ne 0 ]; then
mdev="${nbd}p${ptnum}"
else
mdev="${nbd}"
fi
i=0
while :; do
[ -b "$mdev" ] && break
i=$((i+1))
[ $i -eq 100 ] && {
error "gave up on waiting for $mdev"
disconnect_qemu
return 1
}
[ $((i%10)) -eq 0 ] &&
debug 1 "waiting for $mdev part=$ptnum to be ready."
sleep .1
done
_RET_NBD="$nbd"
_RET_PT="$ptnum"
_RET_DEV="$mdev"
}
mount_nbd() {
local img="$1" mp="$2" fmt="$3" ptnum="$4" rwmode="${5:-rw}" opts="$6"
if [ -z "$fmt" ]; then
get_image_format "$img" && fmt="$_RET" || {
error "failed to get image format for '$img' (try --format)"
return 1
}
fi
assert_nbd_support || return
connect_nbd "$img" "$fmt" "$ptnum" "$rwmode" || return
local ptnum="$_RET_PT" mdev="$_RET_DEV" nbd="$_RET_NBD"
# shellcheck disable=SC2086
if ( set -f; mount -o "$rwmode" $opts "$mdev" "$img_mp" ); then
debug 1 "mounted $mdev via qemu-nbd $nbd at $img_mp"
else
error "failed to mount $mdev"
return 1
fi
}
mount_callback_umount() {
local img_in="$1" out="" mp="" ret="" img="" readonly=false
local opts="" bmounts="" system_resolvconf=false ptnum=auto
local cd_mountpoint=false fmt="" overlay=false rwmode="rw"
local img_mp=""
short_opts="Cdhm:P:psSv"
long_opts="cd-mountpoint,dev,help,format:,mountpoint:,overlay,partition:,proc,read-only,sys,system-mounts,system-resolvconf,verbose"
getopt_out=$(getopt -n "${0##*/}" \
-o "${short_opts}" -l "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
{ bad_Usage; return 1; }
while [ $# -ne 0 ]; do
cur=${1}; next=${2};
case "$cur" in
-C|--cd-mountpoint) cd_mountpoint=true;;
-d|--dev) bmounts="${bmounts:+${bmounts} }/dev";;
--format) fmt=$next;;
-h|--help) Usage ; exit 0;;
-m|--mountpoint) mp=$next;;
-P|--partition) ptnum=$next;;
--overlay) overlay=true;;
-p|--proc) bmounts="${bmounts:+${bmounts} }/proc";;
-s|--sys) bmounts="${bmounts:+${bmounts} }/sys";;
-S|--system-mounts) bmounts="/dev /proc /sys";;
--system-resolvconf) system_resolvconf=true;;
-v|--verbose) VERBOSITY=$((VERBOSITY+1));;
--opts) opts="${opts} $next"; shift;;
--read-only) readonly=true; rwmode="ro";;
--) shift; break;;
esac
shift;
done
[ $# -ge 2 ] || { bad_Usage "must provide image and cmd"; return 1; }
$readonly && { $system_resolvconf && ! $overlay; } && {
error "--read-only is incompatible with system-resolvconf";
error "maybe try with --overlay"
return 1;
}
img_in="$1"
shift 1
if [ "${img_in#lxd:}" != "${img_in}" -a ! -f "${img_in}" ]; then
error "${img_in}: lxd is no longer supported."
return 1;
fi
img=$(readlink -f "$img_in") ||
{ error "failed to get full path to $img_in"; return 1; }
[ -f "$img" ] ||
{ error "$img: not a file"; return 1; }
[ "$(id -u)" = "0" ] ||
{ error "sorry, must be root"; return 1; }
trap cleanup EXIT
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
{ error "failed to make tempdir"; return 1; }
if [ -z "$mp" ]; then
mp="${TEMP_D}/mp"
mkdir "$mp" || return
else
[ -d "$mp" ] ||
{ error "mountpoint '$mp': not a directory"; return 1; }
mp=$(readlink -f "$mp") || {
error "failed to get full path to provided mountpoint";
return 1;
}
fi
if $overlay; then
img_mp="${TEMP_D}/underlay"
mkdir -p "$img_mp" || return
else
img_mp=$mp
fi
out=""
# shellcheck disable=SC2086
if [ "$ptnum" = "auto" -o "$ptnum" = "0" ] &&
out=$(set -f; mount -o "loop,$rwmode" $opts "$img" "$img_mp" 2>&1); then
debug 1 "mounted simple fs image $rwmode in '$img_in' at $img_mp"
UMOUNTS[${#UMOUNTS[@]}]="$img_mp"
else
local hasqemu=false
command -v "qemu-nbd" >/dev/null 2>&1 && hasqemu=true
if ! $hasqemu; then
error "simple mount of '$img_in' failed."
error "if this is not a simple unpartitioned raw image, then"
error "you must have qemu-nbd (apt-get install qemu-utils)"
if [ -n "$out" ]; then
error "mount failed with: $out"
fi
return 1
fi
mount_nbd "$img" "$img_mp" "$fmt" "$ptnum" "$rwmode" "$opts" || return
UMOUNTS[${#UMOUNTS[@]}]="$img_mp"
fi
if $overlay; then
mount_overlay "$img_mp" "$mp" "${TEMP_D}/workdir" || {
[ -n "${_ERR}" ] && error "${_ERR}"
error "Unable to mount overlay filesystem. Maybe no kernel support?"
return 1
}
UMOUNTS[${#UMOUNTS[@]}]="$mp"
fi
local bindmp=""
for bindmp in $bmounts; do
[ -d "$mp${bindmp}" ] || mkdir "$mp${bindmp}" ||
{ error "failed mkdir $bindmp in mount"; return 1; }
mount --bind "$bindmp" "$mp${bindmp}" ||
{ error "failed bind mount '$bindmp'"; return 1; }
UMOUNTS[${#UMOUNTS[@]}]="$mp${bindmp}"
debug 1 "mounted $bindmp to $mp${bindmp}"
done
if ${system_resolvconf}; then
local rcf="$mp/etc/resolv.conf"
debug 1 "replacing /etc/resolvconf"
if [ -e "$rcf" -o -L "$rcf" ]; then
local trcf="$rcf.${0##*/}.$$"
rm -f "$trcf" &&
mv "$rcf" "$trcf" && ORIG_RESOLVCONF="$trcf" ||
{ error "failed mv $rcf"; return 1; }
fi
cp "/etc/resolv.conf" "$rcf" ||
{ error "failed copy /etc/resolv.conf"; return 1; }
fi
# shellcheck disable=SC2178
local cmd="" arg=""
cmd=( )
for arg in "$@"; do
if [ "${arg}" = "_MOUNTPOINT_" ]; then
debug 1 "replaced string _MOUNTPOINT_ in arguments arg ${#cmd[@]}"
arg=$mp
fi
cmd[${#cmd[@]}]="$arg"
done
if [ "${cmd[0]##*/}" = "bash" -o "${cmd[0]##*/}" = "sh" ] &&
[ ${#cmd[@]} -eq 0 ]; then
debug 1 "invoking shell ${cmd[0]}"
error "MOUNTPOINT=$mp"
fi
add_helpers "$TEMP_D/bin" "$SUBUID" "$SUBGID" || {
error "failed to add helpers to $TEMP_D";
return 1;
}
PATH="$TEMP_D/bin:$PATH"
local startwd="$PWD"
debug 1 "invoking: MOUNTPOINT=$mp" "${cmd[@]}"
# shellcheck disable=SC2194
if [ "${cd_mountpoint}" = "true" ]; then
cd "$mp" || {
error "cd $mp failed"
return 1;
}
fi
MOUNTPOINT="$mp" "${cmd[@]}"
ret=$?
cd "$startwd" || :
if ${system_resolvconf}; then
local rcf="$mp/etc/resolv.conf"
cmp -s "/etc/resolv.conf" "$rcf" >/dev/null ||
error "WARN: /etc/resolv.conf changed in image!"
rm "$rcf" &&
{ [ -z "$ORIG_RESOLVCONF" ] || mv "$ORIG_RESOLVCONF" "$rcf"; } ||
{ error "failed to restore /etc/resolv.conf"; return 1; }
fi
debug 1 "cmd returned $ret. unmounting $mp"
do_umounts "${UMOUNTS[@]}" && UMOUNTS=( ) ||
{ error "failed umount $img"; return 1; }
if [ -n "$QEMU_DISCONNECT" ]; then
disconnect_qemu || return 1;
fi
return $ret
}
mount_callback_umount "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment