From 3cacf290bb72c19f91cde0883b3a80f1c4e9eb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 23 May 2017 15:38:28 +0200 Subject: [PATCH] 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 --- Makefile | 1 + qubes-rpc/admin.vm.volume.Import | 35 ++++++++++++++++++++++++++++++++ qubes/api/admin.py | 29 ++++++++++++++++++++++++++ qubes/api/internal.py | 24 ++++++++++++++++++++++ qubes/storage/__init__.py | 28 +++++++++++++++++++------ 5 files changed, 111 insertions(+), 6 deletions(-) create mode 100755 qubes-rpc/admin.vm.volume.Import diff --git a/Makefile b/Makefile index 0b06cc8f..29ca0f24 100644 --- a/Makefile +++ b/Makefile @@ -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; \ diff --git a/qubes-rpc/admin.vm.volume.Import b/qubes-rpc/admin.vm.volume.Import new file mode 100755 index 00000000..afde7875 --- /dev/null +++ b/qubes-rpc/admin.vm.volume.Import @@ -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" diff --git a/qubes/api/admin.py b/qubes/api/admin.py index e58bc928..712b75a7 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -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): diff --git a/qubes/api/internal.py b/qubes/api/internal.py index 68cad53c..b9bad5dd 100644 --- a/qubes/api/internal.py +++ b/qubes/api/internal.py @@ -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') diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 2d818114..ac56ba8a 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -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")