From bc26e743399f70c0d5f2acc85738d69426307e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 23 Sep 2020 00:57:30 +0200 Subject: [PATCH] ext: support for non-service feature advertisement Add an API for VMs to announce support for non-service features. This is very similar to supported-service.* features, but applies to non-service features. This may be also used for announcing support for features that do not use qvm-features framework itself - for example some VM kernel features, installed drivers, packages etc. QubesOS/qubes-issues#6030 --- doc/qubes-features.rst | 24 ++++++++++++ qubes/ext/supported_features.py | 68 +++++++++++++++++++++++++++++++++ qubes/tests/ext.py | 61 +++++++++++++++++++++++++++++ rpm_spec/core-dom0.spec.in | 1 + setup.py | 1 + 5 files changed, 155 insertions(+) create mode 100644 qubes/ext/supported_features.py diff --git a/doc/qubes-features.rst b/doc/qubes-features.rst index 76e74430..451482bf 100644 --- a/doc/qubes-features.rst +++ b/doc/qubes-features.rst @@ -183,6 +183,30 @@ Services and features can be then inspected from dom0 using $ qvm-features my-qube supported-service.my-service 1 + +Announcing supported features +------------------------------ + +For non-service features, there is similar announce mechanis to the above, but +uses ``supported-feature.`` prefix. It works like this: + +1. The TemplateVM (or StandaloneVM) announces +``supported-feature.FEATURE_NAME=1`` (with ``FEATURE_NAME`` replaced with an +actual feature name) using ``qvm-features-request`` tool (see above how to use it). +2. core-admin extension +:py:class:`qubes.ext.supported_features.SupportedFeaturesExtension` records +such requests as features on the same VM. +3. Any tool that wants to check if the feature support is advertised by the VM, +can look into its features. For template-based VMs it is advised to check +also its template - there is a `vm.features.check_with_template()` +function specifically for this. + +Note that (similar to services) it is not necessary for the feature to be +advertised to enable it. In fact, many features does not need any support from +the VM side, so they will work without matching ``supported-feature.`` entry. +Whether a feature requires VM-side support, is documented on case-by-case basis +in `qvm-features` tool manual page. + Module contents --------------- diff --git a/qubes/ext/supported_features.py b/qubes/ext/supported_features.py new file mode 100644 index 00000000..5561be1d --- /dev/null +++ b/qubes/ext/supported_features.py @@ -0,0 +1,68 @@ +# -*- encoding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2020 Marek Marczykowski-Górecki +# +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see . + +"""Extension responsible for announcing supported features""" + +import qubes.ext +import qubes.config + +# pylint: disable=too-few-public-methods + +class SupportedFeaturesExtension(qubes.ext.Extension): + """This extension handles VM announcing non-service features as + 'supported-feature.*' features. + """ + + @qubes.ext.handler('features-request') + def supported_features(self, vm, event, untrusted_features): + """Handle advertisement of supported features""" + # pylint: disable=no-self-use,unused-argument + + if getattr(vm, 'template', None): + vm.log.warning( + 'Ignoring qubes.FeaturesRequest from template-based VM') + return + + new_supported_features = set() + for requested_feature in untrusted_features: + if not requested_feature.startswith('supported-feature.'): + continue + if untrusted_features[requested_feature] == '1': + # only allow to advertise feature as supported, lack of entry + # means feature is not supported + new_supported_features.add(requested_feature) + del untrusted_features + + # if no feature is supported, ignore the whole thing - do not clear + # all features in case of empty request (manual or such) + if not new_supported_features: + return + + old_supported_features = set( + feat for feat in vm.features + if feat.startswith('supported-feature.') and vm.features[feat]) + + for feature in new_supported_features.difference( + old_supported_features): + vm.features[feature] = True + + for feature in old_supported_features.difference( + new_supported_features): + del vm.features[feature] diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index fde1f6c2..48008317 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -395,3 +395,64 @@ class TC_20_Services(qubes.tests.QubesTestCase): 'service.guivm-gui-agent', '') self.assertEqual(os.path.exists(service_path), False) + +class TC_30_SupportedFeatures(qubes.tests.QubesTestCase): + def setUp(self): + super().setUp() + self.ext = qubes.ext.supported_features.SupportedFeaturesExtension() + self.features = {} + specs = { + 'features.get.side_effect': self.features.get, + 'features.items.side_effect': self.features.items, + 'features.__iter__.side_effect': self.features.__iter__, + 'features.__contains__.side_effect': self.features.__contains__, + 'features.__setitem__.side_effect': self.features.__setitem__, + 'features.__delitem__.side_effect': self.features.__delitem__, + } + vmspecs = {**specs, **{ + 'template': None, + 'maxmem': 1024, + 'is_running.return_value': True, + }} + dom0specs = {**specs, **{ + 'name': "dom0", + }} + self.vm = mock.MagicMock() + self.vm.configure_mock(**vmspecs) + self.dom0 = mock.MagicMock() + self.dom0.configure_mock(**dom0specs) + + def test_010_supported_features(self): + self.ext.supported_features(self.vm, 'features-request', + untrusted_features={ + 'supported-feature.test1': '1', # ok + 'supported-feature.test2': '0', # ignored + 'supported-feature.test3': 'some text', # ignored + 'no-feature': '1', # ignored + }) + self.assertEqual(self.features, { + 'supported-feature.test1': True, + }) + + def test_011_supported_features_add(self): + self.features['supported-feature.test1'] = '1' + self.ext.supported_features(self.vm, 'features-request', + untrusted_features={ + 'supported-feature.test1': '1', # ok + 'supported-feature.test2': '1', # ok + }) + # also check if existing one is untouched + self.assertEqual(self.features, { + 'supported-feature.test1': '1', + 'supported-feature.test2': True, + }) + + def test_012_supported_features_remove(self): + self.features['supported-feature.test1'] = '1' + self.ext.supported_features(self.vm, 'features-request', + untrusted_features={ + 'supported-feature.test2': '1', # ok + }) + self.assertEqual(self.features, { + 'supported-feature.test2': True, + }) diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 2169a0a3..1dde97eb 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -425,6 +425,7 @@ done %{python3_sitelib}/qubes/ext/pci.py %{python3_sitelib}/qubes/ext/r3compatibility.py %{python3_sitelib}/qubes/ext/services.py +%{python3_sitelib}/qubes/ext/supported_features.py %{python3_sitelib}/qubes/ext/windows.py %dir %{python3_sitelib}/qubes/tests diff --git a/setup.py b/setup.py index e6fdd8e1..a5d34c36 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ if __name__ == '__main__': 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension', 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension', 'qubes.ext.services = qubes.ext.services:ServicesExtension', + 'qubes.ext.supported_features = qubes.ext.supported_features:SupportedFeaturesExtension', 'qubes.ext.windows = qubes.ext.windows:WindowsFeatures', ], 'qubes.devices': [