core-admin/qubes-rpc/admin.vm.volume.Import
Pawel Marczewski e9b97e42b1
import: check exact size of copied data
The import will error out if there is not enough data, or too
much data provided.
2020-01-23 09:48:58 +01:00

97 lines
3.3 KiB
Bash
Executable File

#!/bin/bash
#
# This Admin API call is implemented as a custom script, instead of dumb
# passthrough to qubesd because it may get huge amount of data (whole root.img
# for example). qubesd cannot handle it because:
# 1. It loads the whole payload into memory, before even start looking at it
# (and later, do not allow to modify/append it).
# 2. There is 64kB limit on payload size that qubesd can handle (because of
# point 1).
# 3. Performance reasons (qubesd is not optimized for performance, passing
# such large data stream through it would take ages).
#
# The whole admin.vm.volume.Import consists of:
# 1. Permissions checks, getting a path from appropriate storage pool (done
# by qubesd)
# 2. Actual data import (done by this script, using dd)
# 3. Report final result, produce final response to the caller (done by
# qubesd)
#
# This way we do not pass all the data through qubesd, but still can
# control the process from there in a meaningful way. Note that the last
# part (second call to qubesd) may perform all kind of verification (like
# a signature check on the data, or so) and can also prevent VM from
# starting (hooking also domain-pre-start event) from not verified image.
#
# Note that this script implements two calls:
# - admin.vm.volume.Import
# - admin.vm.volume.ImportWithSize
# In the case of admin.vm.ImportWithSize, the first line of payload is then
# data size in bytes. This is so that we can already notify qubesd to create a
# volume with the new size.
set -e
# make dd output consistent
export LC_ALL=C
# use temporary file, because env variables deal poorly with \0 inside
tmpfile=$(mktemp)
trap 'rm -f $tmpfile' EXIT
requested_size=""
if [[ "${0##*/}" == admin.vm.volume.ImportWithSize ]]; then
read -r requested_size
fi
echo -n "$requested_size" | qubesd-query -c /var/run/qubesd.internal.sock \
"$QREXEC_REMOTE_DOMAIN" \
"internal.vm.volume.ImportBegin" \
"$QREXEC_REQUESTED_TARGET" \
"$1" >"$tmpfile"
# exit if qubesd returned an error (not '0\0')
if [ "$(head -c 2 "$tmpfile" | xxd -p)" != "3000" ]; then
cat "$tmpfile"
exit 1
fi
size=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 1)
path=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 2)
error=""
# now process stdin into this path
if ! sudo dd bs=128K of="$path" count="$size" iflag=count_bytes,fullblock \
conv=sparse,notrunc,nocreat,fdatasync 2>"$tmpfile"; then
error="error copying data"
fi
# Examine dd's output and check if number of bytes copied matches
if [ -z "$error" ]; then
bytes_copied=$(tail -n1 "$tmpfile" | cut -d ' ' -f 1)
if [ "$bytes_copied" -ne "$size" ]; then
error="not enough data (copied $bytes_copied bytes, expected $size bytes)"
fi
fi
# Check if there is nothing more to be read from stdin
if [ -z "$error" ]; then
if ! dd of="$tmpfile" bs=1 count=1 status=none || \
[ "$(stat -c %s "$tmpfile")" -ne 0 ]; then
error="too much data (expected $size bytes)"
fi
fi
if [ -z "$error" ]; then
status="ok"
else
status="fail\n$error"
fi
# send status notification to qubesd, and pass its response to the caller
echo -ne "$status" | qubesd-query -c /var/run/qubesd.internal.sock \
"$QREXEC_REMOTE_DOMAIN" \
"internal.vm.volume.ImportEnd" \
"$QREXEC_REQUESTED_TARGET" \
"$1"