diff --git a/linux/systemd/Makefile b/linux/systemd/Makefile index 690fb8ab..7cebae40 100644 --- a/linux/systemd/Makefile +++ b/linux/systemd/Makefile @@ -12,3 +12,4 @@ install: cp qubes-reload-firewall@.service $(DESTDIR)$(UNITDIR) cp qubes-reload-firewall@.timer $(DESTDIR)$(UNITDIR) cp qubes-qmemman.service $(DESTDIR)$(UNITDIR) + cp qubesd.service $(DESTDIR)$(UNITDIR) diff --git a/linux/systemd/qubesd.service b/linux/systemd/qubesd.service new file mode 100644 index 00000000..0e8d54cc --- /dev/null +++ b/linux/systemd/qubesd.service @@ -0,0 +1,10 @@ +[Unit] +Description=Qubes OS daemon + +[Service] +Type=notify +ExecStart=/usr/bin/qubesd +StandardOutput=syslog + +[Install] +WantedBy=multi-user.target diff --git a/qubes/libvirtaio.py b/qubes/libvirtaio.py new file mode 100644 index 00000000..b2077815 --- /dev/null +++ b/qubes/libvirtaio.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 + +import asyncio +import collections +import ctypes +import functools +import itertools +import logging +import signal + +import libvirt + +import pdb + +ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p +ctypes.pythonapi.PyCapsule_GetPointer.argtypes = ( + ctypes.py_object, ctypes.c_char_p) + +virFreeCallback = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + +try: + asyncio.ensure_future +except AttributeError: + asyncio.ensure_future = asyncio.async + +class LibvirtAsyncIOEventImpl(object): + class Callback(object): + _iden_counter = itertools.count() + def __init__(self, impl, cb, opaque, *args, **kwargs): + super().__init__(*args, **kwargs) + self.iden = next(self._iden_counter) + self.impl = impl + self.cb = cb + self.opaque = opaque + + assert self.iden not in self.impl.callbacks, \ + 'found {} callback: {!r}'.format( + self.iden, self.impl.callbacks[self.iden]) + self.impl.callbacks[self.iden] = self + + def close(self): + self.impl.log.debug('callback %d close(), scheduling ff', self.iden) + + # Now this is cheating but we have no better option. + caps_cb, caps_opaque, caps_ff = self.opaque + ff = virFreeCallback(ctypes.pythonapi.PyCapsule_GetPointer( + caps_ff, b'virFreeCallback')) + + if ff: + real_opaque = ctypes.pythonapi.PyCapsule_GetPointer( + caps_opaque, b'void*') + self.impl.loop.call_soon(ff, real_opaque) + + + class FDCallback(Callback): + def __init__(self, *args, descriptor, event, **kwargs): + super().__init__(*args, **kwargs) + self.descriptor = descriptor + self.event = event + + self.descriptor.callbacks[self.iden] = self + + def close(self): + del self.descriptor.callbacks[self.iden] + super().close() + + + class TimeoutCallback(Callback): + def __init__(self, *args, timeout=None, **kwargs): + super().__init__(*args, **kwargs) + self.timeout = timeout + self.task = None + + @asyncio.coroutine + def timer(self): + while True: + try: + t = self.timeout * 1e-3 + self.impl.log.debug('sleeping %r', t) + yield from asyncio.sleep(t) + except asyncio.CancelledError: + self.impl.log.debug('timer %d cancelled', self.iden) + break + self.cb(self.iden, self.opaque) + self.impl.log.debug('timer %r callback ended', self.iden) + + def start(self): + self.impl.log.debug('timer %r start', self.iden) + if self.task is not None: + return + self.task = asyncio.ensure_future(self.timer()) + + def stop(self): + self.impl.log.debug('timer %r stop', self.iden) + if self.task is None: + return + self.task.cancel() + self.task = None + + def close(self): + self.stop() + super().close() + + + class DescriptorDict(dict): + class Descriptor(object): + def __init__(self, loop, fd): + self.loop = loop + self.fd = fd + self.callbacks = {} + + self.loop.add_reader( + self.fd, self.handle, libvirt.VIR_EVENT_HANDLE_READABLE) + self.loop.add_writer( + self.fd, self.handle, libvirt.VIR_EVENT_HANDLE_WRITABLE) + + def close(self): + self.loop.remove_reader(self.fd) + self.loop.remove_writer(self.fd) + + def handle(self, event): + for callback in self.callbacks.values(): + if callback.event is not None and callback.event & event: + callback.cb( + callback.iden, self.fd, event, callback.opaque) + + def __init__(self, loop): + super().__init__() + self.loop = loop + def __missing__(self, fd): + descriptor = self.Descriptor(loop, fd) + self[fd] = descriptor + return descriptor + + def __init__(self, loop): + self.loop = loop + self.callbacks = {} + self.descriptors = self.DescriptorDict(self.loop) + self.log = logging.getLogger(self.__class__.__name__) + + def register(self): + libvirt.virEventRegisterImpl( + self.add_handle, self.update_handle, self.remove_handle, + self.add_timeout, self.update_timeout, self.remove_timeout) + + def add_handle(self, fd, event, cb, opaque): + self.log.debug('add_handle(fd=%d, event=%d, cb=%r, opaque=%r)', + fd, event, cb, opaque) + callback = self.FDCallback(self, cb, opaque, + descriptor=self.descriptors[fd], event=event) + return callback.iden + + def update_handle(self, watch, event): + self.log.debug('update_handle(watch=%d, event=%d)', watch, event) + self.callbacks[watch].event = event + + def remove_handle(self, watch): + self.log.debug('remove_handle(watch=%d)', watch) + callback = self.callbacks.pop(watch) + callback.close() + + # libvirt-python.git/libvirt-override.c suggests that the opaque value + # should be returned. This is horribly wrong, because this would cause + # instant execution of ff callback, which is prohibited by libvirt's + # C API documentation. We therefore intentionally return None. + return None + + def add_timeout(self, timeout, cb, opaque): + self.log.debug('add_timeout(timeout=%d, cb=%r, opaque=%r)', + timeout, cb, opaque) + if timeout <= 0: + # TODO we could think about registering timeouts of -1 as a special + # case and emulate 0 somehow (60 Hz?) + self.log.warning('will not add timer with timeout %r', timeout) + return -1 + + callback = self.TimeoutCallback(self, cb, opaque, timeout=timeout) + callback.start() + return callback.iden + + def update_timeout(self, timer, timeout): + self.log.debug('update_timeout(timer=%d, timeout=%d)', timer, timeout) + callback = self.callbacks[timer] + callback.timeout = timeout + if timeout > 0: + callback.start() + else: + callback.stop() + + def remove_timeout(self, timer): + self.log.debug('remove_timeout(timer=%d)', timer) + callback = self.callbacks.pop(timer) + callback.close() + + # See remove_handle() + return None diff --git a/qubes/tools/qubesd.py b/qubes/tools/qubesd.py new file mode 100644 index 00000000..d06efe05 --- /dev/null +++ b/qubes/tools/qubesd.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3.6 + +import asyncio +import functools +import io +import json +import operator +import os +import reprlib +import signal +import socket +import sys +import types + +import qubes +import qubes.libvirtaio +import qubes.utils +import qubes.vm.qubesvm + +QUBESD_SOCK = '/var/run/qubesd.sock' + + +class ProtocolRepr(reprlib.Repr): + def repr1(self, x, level): + if isinstance(x, qubes.vm.qubesvm.QubesVM): + x = x.name + return super().repr1(x, level) + + def repr_str(self, x, level): + '''Warning: this is incompatible with python 3 wrt to b'' ''' + return "'{}'".format(''.join( + chr(c) + if 0x20 < c < 0x7f and c not in (ord("'"), ord('\\')) + else '\\x{:02x}'.format(c) + for c in x.encode())) + + def repr_Label(self, x, level): + return self.repr1(x.name, level) + + +class ProtocolError(AssertionError): + '''Raised when something is wrong with data received''' + pass + +class PermissionDenied(Exception): + '''Raised deliberately by handlers when we decide not to cooperate''' + pass + + +def not_in_api(func): + func.not_in_api = True + return func + +class QubesMgmt(object): + def __init__(self, app, src, method, dest, arg): + self.app = app + + self.src = self.app.domains[src.decode('ascii')] + self.dest = self.app.domains[dest.decode('ascii')] + self.arg = arg.decode('ascii') + + self.prepr = ProtocolRepr() + + self.method = method.decode('ascii') + + untrusted_func_name = self.method + if untrusted_func_name.startswith('mgmt.'): + untrusted_func_name = untrusted_func_name[5:] + untrusted_func_name = untrusted_func_name.lower().replace('.', '_') + + if untrusted_func_name.startswith('_') \ + or not '_' in untrusted_func_name: + raise ProtocolError( + 'possibly malicious function name: {!r}'.format( + untrusted_func_name)) + + try: + untrusted_func = getattr(self, untrusted_func_name) + except AttributeError: + raise ProtocolError( + 'no such attribute: {!r}'.format( + untrusted_function_name)) + + if not isinstance(untrusted_func, types.MethodType): + raise ProtocolError( + 'no such method: {!r}'.format( + untrusted_function_name)) + + if getattr(untrusted_func, 'not_in_api', False): + raise ProtocolError( + 'attempt to call private method: {!r}'.format( + untrusted_function_name)) + + self.execute = untrusted_func + del untrusted_func_name + del untrusted_func + + # + # PRIVATE METHODS, not to be called via RPC + # + + @not_in_api + def fire_event_for_permission(self, *args, **kwargs): + return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method), + self.dest, self.arg, *args, **kwargs) + + @not_in_api + def repr(self, *args, **kwargs): + return self.prepr.repr(*args, **kwargs) + + # + # ACTUAL RPC CALLS + # + + def vm_list(self, untrusted_payload): + assert self.dest.name == 'dom0' + assert not self.arg + assert not untrusted_payload + del untrusted_payload + + domains = self.app.domains + for selector in self.fire_event_for_permission(): + domains = filter(selector, domains) + + return ''.join('{} class={} state={}\n'.format( + self.repr(vm), + vm.__class__.__name__, + vm.get_power_state()) + for vm in sorted(domains)) + + def vm_property_get(self, untrusted_payload): + assert self.arg in self.dest.property_list() + assert not untrusted_payload + del untrusted_payload + + self.fire_event_for_permission() + + try: + value = getattr(self.dest, self.arg) + except AttributeError: + return 'default=True ' + else: + return 'default={} {}'.format( + str(dest.property_is_default(self.arg)), + self.repr(self.value)) + + +class QubesDaemonProtocol(asyncio.Protocol): + buffer_size = 65536 + + def __init__(self, *args, app, **kwargs): + super().__init__() + self.app = app + self.untrusted_buffer = io.BytesIO() + self.untrusted_buffer_trusted_len = 0 + + def connection_made(self, transport): + print('connection_made()') + self.transport = transport + + def connection_lost(self, exc): + print('connection_lost(exc={!r})'.format(exc)) + self.untrusted_buffer.close() + + def data_received(self, untrusted_data): + print('data_received(untrusted_data={!r})'.format(untrusted_data)) + if self.untrusted_buffer_trusted_len + len(untrusted_data) \ + > self.buffer_size: + print(' request too long') + self.transport.close() + return + + self.untrusted_buffer_trusted_len += \ + self.untrusted_buffer.write(untrusted_data) + + def eof_received(self): + print('eof_received()') + try: + src, method, dest, arg, untrusted_payload = \ + self.untrusted_buffer.getvalue().split(b'\0', 4) + except ValueError: + # TODO logging + return + + try: + mgmt = QubesMgmt(self.app, src, method, dest, arg) + response = mgmt.execute(untrusted_payload=untrusted_payload) + except PermissionDenied as err: + # TODO logging + return + except ProtocolError as err: + # TODO logging + print(repr(err)) + return + except AssertionError: + # TODO logging + print(repr(err)) + return + + self.transport.write(response.encode('ascii')) + try: + self.transport.write_eof() + except NotImplementedError: + pass + + +def sighandler(loop, signame, server): + print('caught {}, exiting'.format(signame)) + server.close() + loop.stop() + +parser = qubes.tools.QubesArgumentParser(description='Qubes OS daemon') + +def main(args=None): + args = parser.parse_args(args) + loop = asyncio.get_event_loop() + + qubes.libvirtaio.LibvirtAsyncIOEventImpl(loop).register() + + try: + os.unlink(QUBESD_SOCK) + except FileNotFoundError: + pass + old_umask = os.umask(0o007) + server = loop.run_until_complete(loop.create_unix_server( + functools.partial(QubesDaemonProtocol, app=args.app), QUBESD_SOCK)) + os.umask(old_umask) + del old_umask + + for signame in ('SIGINT', 'SIGTERM'): + loop.add_signal_handler(getattr(signal, signame), + sighandler, loop, signame, server) + + qubes.utils.systemd_notify() + + try: + loop.run_forever() + loop.run_until_complete(server.wait_closed()) + finally: + loop.close() + +if __name__ == '__main__': + main() diff --git a/qubes/utils.py b/qubes/utils.py index b20d3843..bfa5970f 100644 --- a/qubes/utils.py +++ b/qubes/utils.py @@ -26,6 +26,7 @@ import random import string import os import re +import socket import subprocess import pkg_resources @@ -163,3 +164,15 @@ def random_string(length=5): ''' Return random string consisting of ascii_leters and digits ''' return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) + +def systemd_notify(): + '''Notify systemd''' + nofity_socket = os.getenv('NOTIFY_SOCKET') + if not nofity_socket: + return + if nofity_socket.startswith('@'): + nofity_socket = '\0' + nofity_socket[1:] + s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + s.connect(nofity_socket) + s.sendall(b'READY=1') + s.close() diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index 7a039e52..9b0354ab 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -208,6 +208,7 @@ fi /usr/bin/qvm-* /usr/bin/qubes-* /usr/bin/qmemmand +/usr/bin/qubesd %dir %{python3_sitelib}/qubes-*.egg-info %{python3_sitelib}/qubes-*.egg-info/* @@ -223,8 +224,9 @@ fi %{python3_sitelib}/qubes/devices.py %{python3_sitelib}/qubes/dochelpers.py %{python3_sitelib}/qubes/events.py -%{python3_sitelib}/qubes/firewall.py %{python3_sitelib}/qubes/exc.py +%{python3_sitelib}/qubes/firewall.py +%{python3_sitelib}/qubes/libvirtaio.py %{python3_sitelib}/qubes/log.py %{python3_sitelib}/qubes/rngdoc.py %{python3_sitelib}/qubes/tarwriter.py @@ -264,6 +266,7 @@ fi %{python3_sitelib}/qubes/tools/qubes_create.py %{python3_sitelib}/qubes/tools/qubes_monitor_layout_notify.py %{python3_sitelib}/qubes/tools/qubes_prefs.py +%{python3_sitelib}/qubes/tools/qubesd.py %{python3_sitelib}/qubes/tools/qvm_block.py %{python3_sitelib}/qubes/tools/qvm_backup.py %{python3_sitelib}/qubes/tools/qvm_backup_restore.py @@ -386,6 +389,7 @@ fi %{_unitdir}/qubes-netvm.service %{_unitdir}/qubes-qmemman.service %{_unitdir}/qubes-vm@.service +%{_unitdir}/qubesd.service %{_unitdir}/qubes-reload-firewall@.service %{_unitdir}/qubes-reload-firewall@.timer %attr(2770,root,qubes) %dir /var/lib/qubes