From de8ff209765c08ce6b78efe699ecc9761fbede43 Mon Sep 17 00:00:00 2001 From: Wojtek Porczyk Date: Mon, 28 Aug 2017 14:24:48 +0200 Subject: [PATCH] qubes: Allow for explicit closing of objects --- qubes/__init__.py | 15 ++++++ qubes/app.py | 121 ++++++++++++++++++++++++++++++------------ qubes/events.py | 2 + qubes/tools/qubesd.py | 2 +- qubes/vm/__init__.py | 16 ++++++ 5 files changed, 122 insertions(+), 34 deletions(-) diff --git a/qubes/__init__.py b/qubes/__init__.py index aedd6ba8..cbaebdd0 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -689,6 +689,21 @@ class PropertyHolder(qubes.events.Emitter): # pylint: disable=no-member self.log.fatal(msg) + + def close(self): + super().close() + + # Remove all properties -- somewhere in them there are cyclic + # references. This just removes all the properties, just in case. + # They are removed directly, bypassing write_once. + for prop in self.property_list(): + # pylint: disable=protected-access + try: + delattr(self, prop._attr_name) + except AttributeError: + pass + + # pylint: disable=wrong-import-position from qubes.vm import VMProperty from qubes.app import Qubes diff --git a/qubes/app.py b/qubes/app.py index fd0eee58..e6be4f64 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -32,6 +32,7 @@ import subprocess import sys import tempfile import time +import traceback import uuid import lxml.etree @@ -138,20 +139,22 @@ class VirConnectWrapper(object): class VMMConnection(object): '''Connection to Virtual Machine Manager (libvirt)''' - def __init__(self, offline_mode=None): + def __init__(self, app, offline_mode=None): ''' :param offline_mode: enable/disable offline mode; default is to enable when running in chroot as root, otherwise disable ''' - self._libvirt_conn = None - self._xs = None - self._xc = None + self.app = app if offline_mode is None: offline_mode = bool(os.getuid() == 0 and os.stat('/') != os.stat('/proc/1/root/.')) self._offline_mode = offline_mode + self._libvirt_conn = None + self._xs = None + self._xc = None + @property def offline_mode(self): '''Check or enable offline mode (do not actually connect to vmm)''' @@ -218,36 +221,15 @@ class VMMConnection(object): self.init_vmm_connection() return self._xc - def register_event_handlers(self, app): - '''Register libvirt event handlers, which will translate libvirt - events into qubes.events. This function should be called only in - 'qubesd' process and only when mainloop has been already set. - ''' - self.libvirt_conn.domainEventRegisterAny( - None, # any domain - libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE, - self._domain_event_callback, - app - ) - - @staticmethod - def _domain_event_callback(_conn, domain, event, _detail, opaque): - '''Generic libvirt event handler (virConnectDomainEventCallback), - translate libvirt event into qubes.events. - ''' - app = opaque - try: - vm = app.domains[domain.name()] - except KeyError: - # ignore events for unknown domains - return - - if event == libvirt.VIR_DOMAIN_EVENT_STOPPED: - vm.fire_event('domain-shutdown') - - def __del__(self): + def close(self): + libvirt.registerErrorHandler(None, None) + if self._xs: + self._xs.close() + self._xs = None if self._libvirt_conn: self._libvirt_conn.close() + self._libvirt_conn = None + self._xc = None # and pray it will get garbage-collected class QubesHost(object): @@ -398,6 +380,12 @@ class VMCollection(object): self._dict = dict() + def close(self): + del self.app + self._dict.clear() + del self._dict + + def __repr__(self): return '<{} {!r}>'.format( self.__class__.__name__, list(sorted(self.keys()))) @@ -729,6 +717,10 @@ class Qubes(qubes.PropertyHolder): **kwargs): #: logger instance for logging global messages self.log = logging.getLogger('app') + self.log.debug('init() -> %#x', id(self)) + self.log.debug('stack:') + for frame in traceback.extract_stack(): + self.log.debug('%s', frame) self._extensions = qubes.ext.get_extensions() @@ -742,7 +734,7 @@ class Qubes(qubes.PropertyHolder): self.pools = {} #: Connection to VMM - self.vmm = VMMConnection(offline_mode=offline_mode) + self.vmm = VMMConnection(self, offline_mode=offline_mode) #: Information about host system self.host = QubesHost(self) @@ -759,6 +751,7 @@ class Qubes(qubes.PropertyHolder): self.__load_timestamp = None self.__locked_fh = None + self._domain_event_callback_id = None #: jinja2 environment for libvirt XML templates self.env = jinja2.Environment( @@ -910,6 +903,40 @@ class Qubes(qubes.PropertyHolder): self._release_lock() + def close(self): + '''Deconstruct the object and break circular references + + After calling this the object is unusable, not even for saving.''' + + self.log.debug('close() <- %#x', id(self)) + for frame in traceback.extract_stack(): + self.log.debug('%s', frame) + + super().close() + + if self._domain_event_callback_id is not None: + self.vmm.libvirt_conn.domainEventDeregisterAny( + self._domain_event_callback_id) + self._domain_event_callback_id = None + + # Only our Lord, The God Almighty, knows what references + # are kept in extensions. + del self._extensions + + for vm in self.domains: + vm.close() + self.domains.close() + del self.domains + + self.vmm.close() + del self.vmm + + del self.host + + if self.__locked_fh: + self._release_lock() + + def _acquire_lock(self, for_save=False): assert self.__locked_fh is None, 'double lock' @@ -1129,6 +1156,34 @@ class Qubes(qubes.PropertyHolder): raise qubes.exc.QubesException('No driver %s for pool %s' % (driver, name)) + def register_event_handlers(self): + '''Register libvirt event handlers, which will translate libvirt + events into qubes.events. This function should be called only in + 'qubesd' process and only when mainloop has been already set. + ''' + self._domain_event_callback_id = ( + self.vmm.libvirt_conn.domainEventRegisterAny( + None, # any domain + libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE, + self._domain_event_callback, + None)) + + def _domain_event_callback(self, _conn, domain, event, _detail, _opaque): + '''Generic libvirt event handler (virConnectDomainEventCallback), + translate libvirt event into qubes.events. + ''' + if not self.events_enabled: + return + + try: + vm = self.domains[domain.name()] + except KeyError: + # ignore events for unknown domains + return + + if event == libvirt.VIR_DOMAIN_EVENT_STOPPED: + vm.fire_event('domain-shutdown') + @qubes.events.handler('domain-pre-delete') def on_domain_pre_deleted(self, event, vm): # pylint: disable=unused-argument diff --git a/qubes/events.py b/qubes/events.py index dc3f56fd..792372be 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -106,6 +106,8 @@ class Emitter(object, metaclass=EmitterMeta): self.events_enabled = False self.__handlers__ = collections.defaultdict(set) + def close(self): + self.events_enabled = False def add_handler(self, event, func): '''Add event handler to subject's class. diff --git a/qubes/tools/qubesd.py b/qubes/tools/qubesd.py index ea651bc1..77b74864 100644 --- a/qubes/tools/qubesd.py +++ b/qubes/tools/qubesd.py @@ -35,7 +35,7 @@ def main(args=None): loop.close() raise - args.app.vmm.register_event_handlers(args.app) + args.app.register_event_handlers() if args.debug: qubes.log.enable_debug() diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 4694efad..16193729 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -297,6 +297,22 @@ class BaseVM(qubes.PropertyHolder): if hasattr(self, 'name'): self.init_log() + def close(self): + super().close() + + if self._qdb_connection_watch is not None: + asyncio.get_event_loop().remove_reader( + self._qdb_connection_watch.watch_fd()) + self._qdb_connection_watch.close() + del self._qdb_connection_watch + + del self.app + del self.features + del self.storage + # TODO storage may have circ references, but it doesn't leak fds + del self.devices + del self.tags + def load_extras(self): # features for node in self.xml.xpath('./features/feature'):