From e30f894df97fb4f603713c95b003995065c0a76e Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 27 Apr 2016 17:17:32 +0200 Subject: [PATCH 1/6] Add Volume.removable field --- qubes/storage/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 0c528a5f..e3d3d424 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -58,13 +58,15 @@ class Volume(object): script = None usage = 0 - def __init__(self, name, pool, volume_type, vid=None, size=0, **kwargs): + def __init__(self, name, pool, volume_type, vid=None, size=0, + removable=False, **kwargs): super(Volume, self).__init__(**kwargs) self.name = str(name) self.pool = str(pool) self.vid = vid self.size = size self.volume_type = volume_type + self.removable = removable def __xml__(self): return lxml.etree.Element('volume', **self.config) From baaac858bc47c6ddfcca7d8b72e57356a26c157c Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Sun, 22 May 2016 21:42:54 +0200 Subject: [PATCH 2/6] Add DomainPool - All domain pool volumes are removable volumes - DomainVolume uses device name as vid --- qubes/storage/domain.py | 82 +++++++++++++++++++++++++++++++++++++++++ rpm_spec/core-dom0.spec | 1 + 2 files changed, 83 insertions(+) create mode 100644 qubes/storage/domain.py diff --git a/qubes/storage/domain.py b/qubes/storage/domain.py new file mode 100644 index 00000000..b5a275d6 --- /dev/null +++ b/qubes/storage/domain.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python2 +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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. +# +''' Manages block devices in a domain ''' + +from qubes.storage import Pool, Volume + + +class DomainPool(Pool): + ''' This pool manages all the block devices of a domain. + + The devices are queried through :py:module:`qubesdb` + ''' + + driver = 'domain' + + def __init__(self, vm): + self.vm = vm + super(DomainPool, self).__init__(name='p_' + vm.name) + + @property + def volumes(self): + ''' Queries qubesdb and returns volumes for `self.vm` ''' + + qdb = self.vm.qdb + if not self.vm.is_running(): + return [] + untrusted_qubes_devices = qdb.list('/qubes-block-devices/') + # because we get each path 3 x times as + # /qubes-block-devices/foo/{desc,mode,size} we need to merge this + untrusted_devices = {} + for untrusted_device_path in untrusted_qubes_devices: + _, _, untrusted_name, untrusted_atr = untrusted_device_path.split( + '/', 4) + if untrusted_name not in untrusted_devices.keys(): + untrusted_devices[untrusted_name] = { + untrusted_atr: qdb.read(untrusted_device_path) + } + else: + untrusted_devices[untrusted_name][untrusted_atr] = qdb.read( + untrusted_device_path) + + return [DomainVolume(untrusted_n, self.name, **untrusted_atrs) + for untrusted_n, untrusted_atrs in untrusted_devices.items()] + + def clone(self, source, target): + raise NotImplementedError + + +class DomainVolume(Volume): + ''' A volume provided by a block device in an domain ''' + + def __init__(self, name, pool, desc, mode, size): + if mode == 'w': + volume_type = 'read-write' + else: + volume_type = 'read-only' + + super(DomainVolume, self).__init__(desc, + pool, + volume_type, + vid=name, + size=size, + removable=True) diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index 9bedef4a..bb49a41c 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -234,6 +234,7 @@ fi %dir %{python_sitelib}/qubes/storage %{python_sitelib}/qubes/storage/__init__.py* %{python_sitelib}/qubes/storage/file.py* +%{python_sitelib}/qubes/storage/domain.py* %{python_sitelib}/qubes/storage/kernels.py* %dir %{python_sitelib}/qubes/tools From ddf040ae64bacebe1f38e263bd9039d05dc0c6ba Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 27 Apr 2016 19:39:02 +0200 Subject: [PATCH 3/6] Do not serialize the domain pool config --- qubes/app.py | 4 +++- qubes/storage/domain.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/qubes/app.py b/qubes/app.py index 804545b1..8a785035 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -708,7 +708,9 @@ class Qubes(qubes.PropertyHolder): pools_xml = lxml.etree.Element('pools') for pool in self.pools.values(): - pools_xml.append(pool.__xml__()) + xml = pool.__xml__() + if xml is not None: + pools_xml.append(xml) element.append(pools_xml) diff --git a/qubes/storage/domain.py b/qubes/storage/domain.py index b5a275d6..a84be882 100644 --- a/qubes/storage/domain.py +++ b/qubes/storage/domain.py @@ -64,6 +64,9 @@ class DomainPool(Pool): def clone(self, source, target): raise NotImplementedError + def __xml__(self): + return None + class DomainVolume(Volume): ''' A volume provided by a block device in an domain ''' From 3f5a92772a45e47777ceca051da3a00ee46617db Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Sun, 22 May 2016 21:43:11 +0200 Subject: [PATCH 4/6] A QubesVM always has an empty DomainPool - A DomainPool is initialized by QubesVM after Storage initialization on a `domain-load` event --- qubes/vm/qubesvm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 6b5135ec..2fbde264 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -51,6 +51,8 @@ import qubes.vm import qubes.vm.mix.net import qubes.tools.qvm_ls +from qubes.storage.domain import DomainPool + qmemman_present = False try: import qubes.qmemman.client @@ -516,6 +518,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): # Initialize VM image storage class self.storage = qubes.storage.Storage(self) + vm_pool = DomainPool(self) + self.app.pools[vm_pool.name] = vm_pool @qubes.events.handler('property-set:label') From 35974a5dbfe87b55d973332afa7d34726273dc65 Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 25 May 2016 16:28:03 +0200 Subject: [PATCH 5/6] DomainPool check the untrusted data from qubes-db --- qubes/storage/domain.py | 51 +++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/qubes/storage/domain.py b/qubes/storage/domain.py index a84be882..e7fc33ca 100644 --- a/qubes/storage/domain.py +++ b/qubes/storage/domain.py @@ -21,6 +21,8 @@ # ''' Manages block devices in a domain ''' +import string + from qubes.storage import Pool, Volume @@ -41,25 +43,50 @@ class DomainPool(Pool): ''' Queries qubesdb and returns volumes for `self.vm` ''' qdb = self.vm.qdb + safe_set = set(string.letters + string.digits + string.punctuation) + allowed_attributes = {'desc': string.printable, + 'mode': string.letters, + 'size': string.digits} if not self.vm.is_running(): return [] untrusted_qubes_devices = qdb.list('/qubes-block-devices/') # because we get each path 3 x times as # /qubes-block-devices/foo/{desc,mode,size} we need to merge this - untrusted_devices = {} + devices = {} for untrusted_device_path in untrusted_qubes_devices: - _, _, untrusted_name, untrusted_atr = untrusted_device_path.split( - '/', 4) - if untrusted_name not in untrusted_devices.keys(): - untrusted_devices[untrusted_name] = { - untrusted_atr: qdb.read(untrusted_device_path) - } - else: - untrusted_devices[untrusted_name][untrusted_atr] = qdb.read( - untrusted_device_path) + if not all(c in safe_set for c in untrusted_device_path): + msg = ("%s vm's device path name contains unsafe characters. " + "Skipping it.") + self.vm.log.warning(msg % self.vm.name) + continue - return [DomainVolume(untrusted_n, self.name, **untrusted_atrs) - for untrusted_n, untrusted_atrs in untrusted_devices.items()] + # name can be trusted because it was checked as a part of + # untrusted_device_path check above + _, _, name, untrusted_atr = untrusted_device_path.split('/', 4) + + if untrusted_atr in allowed_attributes.keys(): + atr = untrusted_atr + else: + msg = ('{!s} has an unknown qubes-block-device atr {!s} ' + 'Skipping it') + self.vm.log.error(msg.format(self.vm.name, untrusted_atr)) + continue + + untrusted_value = qdb.read(untrusted_device_path) + allowed_characters = allowed_attributes[atr] + if all(c in allowed_characters for c in untrusted_value): + value = untrusted_value + else: + msg = ("{!s} vm's device path {!s} contains unsafe characters") + self.vm.log.error(msg.format(self.vm.name, atr)) + + if name not in devices.keys(): + devices[name] = {} + + devices[name][atr] = value + + return [DomainVolume(n, self.name, **atrs) + for n, atrs in devices.items()] def clone(self, source, target): raise NotImplementedError From 17790c32bb08f182caa768ef260d796a5fd5e28e Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 1 Jun 2016 17:28:55 +0200 Subject: [PATCH 6/6] Fix DomainPool missing a continue --- qubes/storage/domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qubes/storage/domain.py b/qubes/storage/domain.py index e7fc33ca..e2ab388c 100644 --- a/qubes/storage/domain.py +++ b/qubes/storage/domain.py @@ -79,6 +79,7 @@ class DomainPool(Pool): else: msg = ("{!s} vm's device path {!s} contains unsafe characters") self.vm.log.error(msg.format(self.vm.name, atr)) + continue if name not in devices.keys(): devices[name] = {}