Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

device identity #614

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
52c71c9
q-dev: port
piotrbartman Jul 4, 2024
f14f844
q-dev: attachment confirmation PoC
piotrbartman Aug 27, 2024
a7ea7fd
q-dev: port
piotrbartman Jul 31, 2024
b63d801
q-dev: comparison
piotrbartman Aug 1, 2024
29f45c5
q-dev: assignment
piotrbartman Aug 2, 2024
f0e8c85
q-dev: ask-to-attach is attach_automatically
piotrbartman Aug 27, 2024
7700e1f
q-dev: add device_identity to device assignment
piotrbartman Aug 5, 2024
686c806
q-dev: check identity
piotrbartman Aug 5, 2024
083cb8c
q-dev: implementation of attachment confirmation
piotrbartman Aug 27, 2024
e14a620
q-dev: auto-attach only required block devices before vm start
piotrbartman Aug 7, 2024
b910c7f
q-dev: fix attribute name
piotrbartman Aug 7, 2024
53cfe74
q-dev: backward compatible device_protocol
piotrbartman Aug 7, 2024
b97240b
q-dev: add self_identity do device identity
piotrbartman Aug 8, 2024
69ff16c
q-dev: refactor device_protocol.py
piotrbartman Aug 12, 2024
f3c3cae
q-dev: fix events
piotrbartman Aug 15, 2024
8e3822e
q-dev: unify protocol
piotrbartman Aug 15, 2024
7d22664
q-dev: fix test
piotrbartman Aug 15, 2024
a4e1621
q-dev: virtual device
piotrbartman Aug 15, 2024
727ee09
q-dev: device -> devices
piotrbartman Aug 15, 2024
e886cb1
q-dev: matches
piotrbartman Aug 16, 2024
d9ac860
q-dev: backend_name
piotrbartman Aug 16, 2024
f37afb7
q-dev: device_protocol
piotrbartman Aug 17, 2024
18b9502
q-dev: cleanup
piotrbartman Aug 19, 2024
2c110ca
q-dev: fixes
piotrbartman Aug 19, 2024
d3f44ae
q-dev: deny list
piotrbartman Aug 19, 2024
ed705a2
q-dev: assignment.device
piotrbartman Aug 20, 2024
53a3f9a
q-dev: error handling
piotrbartman Aug 20, 2024
5ec0b68
q-dev: add tests
piotrbartman Aug 20, 2024
e47795a
q-dev: fix block auto-attach
piotrbartman Aug 20, 2024
4dbfa28
q-dev: add block devices tests
piotrbartman Aug 26, 2024
3a60033
q-dev: update device_protocol.py
piotrbartman Aug 26, 2024
6b6ca59
q-dev: fix tests and make linter happy
piotrbartman Aug 27, 2024
94836fb
q-dev: update pci tests and cleanup
piotrbartman Aug 27, 2024
bb02ee2
q-dev: update qubes.rng and fix tests
piotrbartman Aug 28, 2024
65f14db
q-dev: Set.required -> Set.assignment
piotrbartman Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,31 @@ ADMIN_API_METHODS_SIMPLE = \
admin.vm.device.pci.Attached \
admin.vm.device.pci.Available \
admin.vm.device.pci.Detach \
admin.vm.device.pci.Set.required \
admin.vm.device.pci.Set.assignment \
admin.vm.device.pci.Unassign \
admin.vm.device.block.Assign \
admin.vm.device.block.Assigned \
admin.vm.device.block.Attach \
admin.vm.device.block.Attached \
admin.vm.device.block.Available \
admin.vm.device.block.Detach \
admin.vm.device.block.Set.required \
admin.vm.device.block.Set.assignment \
admin.vm.device.block.Unassign \
admin.vm.device.usb.Assign \
admin.vm.device.usb.Assigned \
admin.vm.device.usb.Attach \
admin.vm.device.usb.Attached \
admin.vm.device.usb.Available \
admin.vm.device.usb.Detach \
admin.vm.device.usb.Set.required \
admin.vm.device.usb.Set.assignment \
admin.vm.device.usb.Unassign \
admin.vm.device.mic.Assign \
admin.vm.device.mic.Assigned \
admin.vm.device.mic.Attach \
admin.vm.device.mic.Attached \
admin.vm.device.mic.Available \
admin.vm.device.mic.Detach \
admin.vm.device.mic.Set.required \
admin.vm.device.mic.Set.assignment \
admin.vm.device.mic.Unassign \
admin.vm.feature.CheckWithNetvm \
admin.vm.feature.CheckWithTemplate \
Expand Down Expand Up @@ -227,7 +227,7 @@ endif
admin.vm.device.testclass.Unassign \
admin.vm.device.testclass.Attached \
admin.vm.device.testclass.Assigned \
admin.vm.device.testclass.Set.required \
admin.vm.device.testclass.Set.assignment \
admin.vm.device.testclass.Available
install -d $(DESTDIR)/etc/qubes/policy.d/include
install -m 0644 qubes-rpc-policy/admin-local-ro \
Expand Down
12 changes: 6 additions & 6 deletions doc/qubes-devices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ devices, which can be attached to other domains (frontend). Devices can be of
different buses (like 'pci', 'usb', etc.). Each device bus is implemented by
an extension (see :py:mod:`qubes.ext`).

Devices are identified by pair of (backend domain, `ident`), where `ident` is
:py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set.
Devices are identified by pair of (backend domain, `port_id`), where `port_id`
is :py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set.


Device Assignment vs Attachment
Expand Down Expand Up @@ -100,17 +100,17 @@ The microphone cannot be assigned (potentially) to any VM (attempting to attach
Understanding Device Self Identity
----------------------------------

It is important to understand that :py:class:`qubes.device_protocol.Device` does not
It is important to understand that :py:class:`qubes.device_protocol.Port` does not
correspond to the device itself, but rather to the *port* to which the device
is connected. Therefore, when assigning a device to a VM, such as
`sys-usb:1-1.1`, the port `1-1.1` is actually assigned, and thus
*every* devices connected to it will be automatically attached.
Similarly, when assigning `vm:sda`, every block device with the name `sda`
will be automatically attached. We can limit this using :py:meth:`qubes.device_protocol.DeviceInfo.self_identity`, which returns a string containing information
will be automatically attached. We can limit this using :py:meth:`qubes.device_protocol.DeviceInfo.device_id`, which returns a string containing information
presented by the device, such as, `vendor_id`, `product_id`, `serial_number`,
and encoded interfaces. In the case of block devices, `self_identity`
and encoded interfaces. In the case of block devices, `device_id`
consists of the parent port to which the device is connected (if any),
the parent's `self_identity`, and the interface/partition number.
the parent's `device_id`, and the interface/partition number.
In practice, this means that, a partition on a USB drive will only be
automatically attached to a frontend domain if the parent presents
the correct serial number etc., and is connected to a specific port.
Expand Down
4 changes: 2 additions & 2 deletions qubes-rpc-policy/90-admin-default.policy.header
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
!include-service admin.vm.device.mic.Attached * include/admin-local-ro
!include-service admin.vm.device.mic.Available * include/admin-local-ro
!include-service admin.vm.device.mic.Detach * include/admin-local-rwx
!include-service admin.vm.device.mic.Set.required * include/admin-local-rwx
!include-service admin.vm.device.mic.Set.assignment * include/admin-local-rwx
!include-service admin.vm.device.mic.Unassign * include/admin-local-rwx
!include-service admin.vm.device.usb.Assign * include/admin-local-rwx
!include-service admin.vm.device.usb.Assigned * include/admin-local-ro
!include-service admin.vm.device.usb.Attach * include/admin-local-rwx
!include-service admin.vm.device.usb.Attached * include/admin-local-ro
!include-service admin.vm.device.usb.Available * include/admin-local-ro
!include-service admin.vm.device.usb.Detach * include/admin-local-rwx
!include-service admin.vm.device.usb.Set.required * include/admin-local-rwx
!include-service admin.vm.device.usb.Set.assignment * include/admin-local-rwx
!include-service admin.vm.device.usb.Unassign * include/admin-local-rwx

124 changes: 60 additions & 64 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
import qubes.vm
import qubes.vm.adminvm
import qubes.vm.qubesvm
from qubes.device_protocol import Device
from qubes.device_protocol import (
VirtualDevice, UnknownDevice, DeviceAssignment, AssignmentMode)


class QubesMgmtEventsDispatcher:
Expand Down Expand Up @@ -1211,14 +1212,15 @@
raise qubes.exc.QubesException("qubesd shutdown in progress")
raise
if self.arg:
devices = [dev for dev in devices if dev.ident == self.arg]
devices = [dev for dev in devices if dev.port_id == self.arg]
# no duplicated devices, but device may not exist, in which case
# the list is empty
self.enforce(len(devices) <= 1)
devices = self.fire_event_for_filter(devices, devclass=devclass)
dev_info = {dev.ident: dev.serialize().decode() for dev in devices}
return ''.join('{} {}\n'.format(ident, dev_info[ident])
for ident in sorted(dev_info))
dev_info = {f'{dev.port_id}:{dev.device_id}':
dev.serialize().decode() for dev in devices}
return ''.join(f'{port_id} {dev_info[port_id]}\n'
for port_id in sorted(dev_info))

@qubes.api.method('admin.vm.device.{endpoint}.Assigned', endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
Expand All @@ -1237,7 +1239,7 @@
if self.arg:
select_backend, select_ident = self.arg.split('+', 1)
device_assignments = [dev for dev in device_assignments
if (str(dev.backend_domain), dev.ident)
if (str(dev.backend_domain), dev.port_id)
== (select_backend, select_ident)]
# no duplicated devices, but device may not exist, in which case
# the list is empty
Expand All @@ -1246,12 +1248,13 @@
device_assignments, devclass=devclass)

dev_info = {
f'{assignment.backend_domain}+{assignment.ident}':
(f'{assignment.backend_domain}'
f'+{assignment.port_id}:{assignment.device_id}'):
assignment.serialize().decode('ascii', errors="ignore")
for assignment in device_assignments}

return ''.join('{} {}\n'.format(ident, dev_info[ident])
for ident in sorted(dev_info))
return ''.join('{} {}\n'.format(port_id, dev_info[port_id])
for port_id in sorted(dev_info))

@qubes.api.method(
'admin.vm.device.{endpoint}.Attached',
Expand All @@ -1272,21 +1275,24 @@
if self.arg:
select_backend, select_ident = self.arg.split('+', 1)
device_assignments = [dev for dev in device_assignments
if (str(dev.backend_domain), dev.ident)
if (str(dev.backend_domain), dev.port_id)
== (select_backend, select_ident)]
# no duplicated devices, but device may not exist, in which case
# the list is empty
self.enforce(len(device_assignments) <= 1)
device_assignments = self.fire_event_for_filter(device_assignments,
devclass=devclass)
device_assignments = [
a.clone(device=self.app.domains[a.backend_name
].devices[devclass][a.port_id]) for a in device_assignments]
device_assignments = self.fire_event_for_filter(
device_assignments, devclass=devclass)

dev_info = {
f'{assignment.backend_domain}+{assignment.ident}':
(f'{assignment.backend_domain}'
f'+{assignment.port_id}:{assignment.device_id}'):
assignment.serialize().decode('ascii', errors="ignore")
for assignment in device_assignments}

return ''.join('{} {}\n'.format(ident, dev_info[ident])
for ident in sorted(dev_info))
return ''.join('{} {}\n'.format(port_id, dev_info[port_id])
for port_id in sorted(dev_info))

# Assign/Unassign action can modify only persistent state of running VM.
# For this reason, write=True
Expand All @@ -1295,11 +1301,7 @@
scope='local', write=True)
async def vm_device_assign(self, endpoint, untrusted_payload):
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
backend_domain, ident = self.arg.split('+', 1)
# may raise KeyError, either on domain or ident
dev = self.app.domains[backend_domain].devices[devclass][ident]
dev = self.load_device_info(devclass)

assignment = qubes.device_protocol.DeviceAssignment.deserialize(
untrusted_payload, expected_device=dev
Expand All @@ -1315,28 +1317,36 @@
await self.dest.devices[devclass].assign(assignment)
self.app.save()

def load_device_info(self, devclass) -> VirtualDevice:
# qrexec already verified that no strange characters are in self.arg
_dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains)
if _dev.port_id == '*' or _dev.device_id == '*':
return _dev
# load all info, may raise KeyError, either on domain or port_id
try:
dev = self.app.domains[
_dev.backend_domain].devices[devclass][_dev.port_id]
if isinstance(dev, UnknownDevice):
return _dev
return dev
except KeyError:
return _dev

Check warning on line 1333 in qubes/api/admin.py

View check run for this annotation

Codecov / codecov/patch

qubes/api/admin.py#L1331-L1333

Added lines #L1331 - L1333 were not covered by tests

# Assign/Unassign action can modify only persistent state of running VM.
# For this reason, write=True
@qubes.api.method(
'admin.vm.device.{endpoint}.Unassign',
endpoints=(
ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
no_payload=True, scope='local', write=True)
no_payload=True, scope='local', write=True)
async def vm_device_unassign(self, endpoint):
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
backend_domain, ident = self.arg.split('+', 1)
# may raise KeyError; if a device isn't found, it will be UnknownDevice
# instance - but allow it, otherwise it will be impossible to unassign
# an already removed device
dev = self.app.domains[backend_domain].devices[devclass][ident]
dev = self.load_device_info(devclass)
assignment = DeviceAssignment(dev)

self.fire_event_for_permission(device=dev, devclass=devclass)

assignment = qubes.device_protocol.DeviceAssignment(
dev.backend_domain, dev.ident, devclass)
await self.dest.devices[devclass].unassign(assignment)
self.app.save()

Expand All @@ -1350,20 +1360,13 @@
scope='local', execute=True)
async def vm_device_attach(self, endpoint, untrusted_payload):
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
backend_domain, ident = self.arg.split('+', 1)
# may raise KeyError, either on domain or ident
dev = self.app.domains[backend_domain].devices[devclass][ident]

assignment = qubes.device_protocol.DeviceAssignment.deserialize(
untrusted_payload, expected_device=dev
)
dev = self.load_device_info(devclass)
assignment = DeviceAssignment.deserialize(
untrusted_payload, expected_device=dev)

self.fire_event_for_permission(
device=dev, devclass=devclass,
required=assignment.required,
attach_automatically=assignment.attach_automatically,
device=dev,
mode=assignment.mode.value,
options=assignment.options
)

Expand All @@ -1379,23 +1382,16 @@
no_payload=True, scope='local', execute=True)
async def vm_device_detach(self, endpoint):
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
backend_domain, ident = self.arg.split('+', 1)
# may raise KeyError; if device isn't found, it will be UnknownDevice
# instance - but allow it, otherwise it will be impossible to detach
# already removed device
dev = self.app.domains[backend_domain].devices[devclass][ident]
dev = self.load_device_info(devclass)

self.fire_event_for_permission(device=dev, devclass=devclass)

assignment = qubes.device_protocol.DeviceAssignment(
dev.backend_domain, dev.ident, devclass)
assignment = qubes.device_protocol.DeviceAssignment(dev)
await self.dest.devices[devclass].detach(assignment)

# Assign/Unassign action can modify only a persistent state of running VM.
# For this reason, write=True
@qubes.api.method('admin.vm.device.{endpoint}.Set.required',
@qubes.api.method('admin.vm.device.{endpoint}.Set.assignment',
endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
scope='local', write=True)
Expand All @@ -1409,20 +1405,20 @@
"""
devclass = endpoint

self.enforce(untrusted_payload in (b'True', b'False'))
# now is safe to eval, since the value of untrusted_payload is trusted
# pylint: disable=eval-used
assignment = eval(untrusted_payload)
del untrusted_payload
allowed_values = {
b'required': AssignmentMode.REQUIRED,
b'ask-to-attach': AssignmentMode.ASK,
b'auto-attach': AssignmentMode.AUTO}
try:
mode = allowed_values[untrusted_payload]
except KeyError:
raise qubes.exc.PermissionDenied()

# qrexec already verified that no strange characters are in self.arg
backend_domain_name, ident = self.arg.split('+', 1)
backend_domain = self.app.domains[backend_domain_name]
dev = Device(backend_domain, ident, devclass)
dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains)

self.fire_event_for_permission(device=dev, assignment=assignment)
self.fire_event_for_permission(device=dev, mode=mode)

await self.dest.devices[devclass].update_required(dev, assignment)
await self.dest.devices[devclass].update_assignment(dev, mode)
self.app.save()

@qubes.api.method('admin.vm.firewall.Get', no_payload=True,
Expand Down
2 changes: 1 addition & 1 deletion qubes/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ def on_domain_pre_deleted(self, event, vm):
assignments = vm.get_provided_assignments()
if assignments:
desc = ', '.join(
assignment.ident for assignment in assignments)
assignment.port_id for assignment in assignments)
raise qubes.exc.QubesVMInUseError(
vm,
'VM has devices assigned to other VMs: ' + desc)
Expand Down
Loading