admin: implement admin.vm.volume.Import

Implement this in two parts:
1. Permissions checks, getting a path from appropriate storage pool
2. Actual data import

The first part is done by qubesd in a standard way, but then, instead of
accepting all the data (which may be several GB), return a path to which
a shell script (in practice: `dd` command) will write the data.
Then the script call back to qubesd again to report success/failure and
qubesd response from that call is actually returned to the user.

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.

QubesOS/qubes-issues#2622
This commit is contained in:
Marek Marczykowski-Górecki 2017-05-23 15:38:28 +02:00
parent 46b60dbf42
commit 3cacf290bb
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 111 additions and 6 deletions

View File

@ -176,6 +176,7 @@ endif
ln -s ../../usr/libexec/qubes/qubesd-query-fast \
$(DESTDIR)/etc/qubes-rpc/$$method; \
done
install qubes-rpc/admin.vm.volume.Import $(DESTDIR)/etc/qubes-rpc/
for method in $(ADMIN_API_METHODS); do \
install -m 0644 qubes-rpc-policy/admin-default \
$(DESTDIR)/etc/qubes-rpc/policy/$$method; \

View File

@ -0,0 +1,35 @@
#!/bin/sh
set -e
# use temporary file, because env variables deal poorly with \0 inside
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
qubesd-query -e \
"$QREXEC_REMOTE_DOMAIN" \
"admin.vm.volume.Import" \
"$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)
# now process stdin into this path
if dd bs=4k of="$path" count="$size" iflag=count_bytes \
conv=sparse,notrunc,nocreat,fdatasync; then
status="ok"
else
status="fail"
fi
# send status notification to qubesd, and pass its response to the caller
echo -n "$status" | qubesd-query -c /var/run/qubesd.internal.sock \
"$QREXEC_REMOTE_DOMAIN" \
"internal.vm.volume.ImportEnd" \
"$QREXEC_REQUESTED_TARGET" \
"$1"

View File

@ -307,6 +307,35 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.dest.storage.resize(self.arg, size)
self.app.save()
@qubes.api.method('admin.vm.volume.Import', no_payload=True)
@asyncio.coroutine
def vm_volume_import(self):
'''Import volume data.
Note that this function only returns a path to where data should be
written, actual importing is done by a script in /etc/qubes-rpc
When the script finish importing, it will trigger
internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
payload) and response from that call will be actually send to the
caller.
'''
assert self.arg in self.dest.volumes.keys()
self.fire_event_for_permission()
if not self.dest.is_halted():
raise qubes.exc.QubesVMNotHaltedError(self.dest)
path = self.dest.storage.import_data(self.arg)
assert ' ' not in path
size = self.dest.volumes[self.arg].size
# when we know the action is allowed, inform extensions that it will
# be performed
self.dest.fire_event('domain-volume-import-begin', volume=self.arg)
return '{} {}'.format(size, path)
@qubes.api.method('admin.pool.List', no_payload=True)
@asyncio.coroutine
def pool_list(self):

View File

@ -83,3 +83,27 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
# TODO convert to coroutine
self.dest.cleanup()
@qubes.api.method('internal.vm.volume.ImportEnd')
@asyncio.coroutine
def vm_volume_import_end(self, untrusted_payload):
'''
This is second half of admin.vm.volume.Import handling. It is called
when actual import is finished. Response from this method is sent do
the client (as a response for admin.vm.volume.Import call).
'''
assert self.arg in self.dest.volumes.keys()
success = untrusted_payload == b'ok'
try:
self.dest.storage.import_data_end(self.arg, success=success)
except:
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
succeess=False)
raise
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
succeess=success)
if not success:
raise qubes.exc.QubesException('Data import failed')

View File

@ -553,6 +553,18 @@ class Storage(object):
return self.pools[volume].import_data(self.vm.volumes[volume])
def import_data_end(self, volume, success):
''' Helper function to finish/cleanup data import
(pool.import_data_end( volume))'''
assert isinstance(volume, (Volume, str)), \
"You need to pass a Volume or pool name as str"
if isinstance(volume, Volume):
return self.pools[volume.name].import_data_end(volume,
success=success)
return self.pools[volume].import_data_end(self.vm.volumes[volume],
success=success)
class Pool(object):
''' A Pool is used to manage different kind of volumes (File
@ -624,14 +636,18 @@ class Pool(object):
raise self._not_implemented("export")
def import_data(self, volume):
''' Returns an object that can be `open()`.
Storage implementation may register for
`domain-volume-import-end` event to cleanup after this. The
event will have also success=True|False information.
'''
''' Returns an object that can be `open()`. '''
raise self._not_implemented("import")
def import_data_end(self, volume, success):
''' End data import operation. This may be used by pool
implementation to commit changes, cleanup temporary files etc.
:param success: True if data import was successful, otherwise False
'''
# by default do nothing
pass
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
''' Imports data to a volume in this pool '''
raise self._not_implemented("import_volume")