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 69 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
458c1c1
q-dev: port
piotrbartman Jul 4, 2024
58ae845
q-dev: attachment confirmation PoC
piotrbartman Aug 27, 2024
0005d08
q-dev: port
piotrbartman Jul 31, 2024
167ec8d
q-dev: comparison
piotrbartman Aug 1, 2024
1d0b2ad
q-dev: assignment
piotrbartman Aug 2, 2024
bfbe0b9
q-dev: ask-to-attach is attach_automatically
piotrbartman Aug 27, 2024
a5a7fdb
q-dev: add device_identity to device assignment
piotrbartman Aug 5, 2024
d7302f3
q-dev: check identity
piotrbartman Aug 5, 2024
e481e22
q-dev: implementation of attachment confirmation
piotrbartman Aug 27, 2024
9d82600
q-dev: auto-attach only required block devices before vm start
piotrbartman Aug 7, 2024
5ea5fc6
q-dev: fix attribute name
piotrbartman Aug 7, 2024
6b031a0
q-dev: backward compatible device_protocol
piotrbartman Aug 7, 2024
9053c70
q-dev: add self_identity do device identity
piotrbartman Aug 8, 2024
ba2100e
q-dev: refactor device_protocol.py
piotrbartman Aug 12, 2024
fd6e4a8
q-dev: fix events
piotrbartman Aug 15, 2024
6dd8f56
q-dev: unify protocol
piotrbartman Aug 15, 2024
1302bf9
q-dev: fix test
piotrbartman Aug 15, 2024
1b2934c
q-dev: virtual device
piotrbartman Aug 15, 2024
c93f8c3
q-dev: device -> devices
piotrbartman Aug 15, 2024
efb572e
q-dev: matches
piotrbartman Aug 16, 2024
e09010e
q-dev: backend_name
piotrbartman Aug 16, 2024
93713ed
q-dev: device_protocol
piotrbartman Aug 17, 2024
af77f91
q-dev: cleanup
piotrbartman Aug 19, 2024
1d26a0e
q-dev: fixes
piotrbartman Aug 19, 2024
c2aac2d
q-dev: deny list
piotrbartman Aug 19, 2024
2514885
q-dev: assignment.device
piotrbartman Aug 20, 2024
9f19108
q-dev: error handling
piotrbartman Aug 20, 2024
56559c0
q-dev: add tests
piotrbartman Aug 20, 2024
64e7669
q-dev: fix block auto-attach
piotrbartman Aug 20, 2024
4aac101
q-dev: add block devices tests
piotrbartman Aug 26, 2024
c7d244a
q-dev: update device_protocol.py
piotrbartman Aug 26, 2024
e73d56d
q-dev: fix tests and make linter happy
piotrbartman Aug 27, 2024
b8bee03
q-dev: update pci tests and cleanup
piotrbartman Aug 27, 2024
d62624f
q-dev: update qubes.rng and fix tests
piotrbartman Aug 28, 2024
63489c1
q-dev: Set.required -> Set.assignment
piotrbartman Aug 28, 2024
97084d6
q-dev: deny list drop ins and comments
piotrbartman Sep 24, 2024
4dbf597
q-dev: do not include port id in device identity
piotrbartman Sep 25, 2024
c91295e
q-dev: add error message
piotrbartman Sep 25, 2024
1999c2f
q-dev: do not load device if device_id does not match
piotrbartman Sep 25, 2024
d76b318
q-dev: do not attach unknown device
piotrbartman Sep 30, 2024
daaf839
q-dev: better error message
piotrbartman Sep 30, 2024
1db8f8f
q-dev: remove redundant list
piotrbartman Oct 1, 2024
1267f3f
q-dev: async confirmation
piotrbartman Oct 7, 2024
89925b0
q-dev: sanitize confirmation output
piotrbartman Oct 7, 2024
a851333
q-dev: fire pre-event for assignment
piotrbartman Oct 8, 2024
53b6bd9
q-dev: fix detaching required devices
piotrbartman Oct 8, 2024
cc65d08
q-dev: remove unused import
piotrbartman Oct 8, 2024
be25600
q-dev: update device_protocol.py
piotrbartman Oct 9, 2024
4539f63
q-dev: update device tests
piotrbartman Oct 9, 2024
2b6dfb8
q-dev: update admin api device tests
piotrbartman Oct 11, 2024
4ff7ae3
q-dev: add encoding type
piotrbartman Oct 14, 2024
fd6bd27
q-dev: minor device_protocol fixes
piotrbartman Oct 14, 2024
e6b70ef
q-dev: introduce AnyPort
piotrbartman Oct 14, 2024
1109708
q-dev: devices improvements
piotrbartman Oct 14, 2024
a3b781c
q-dev: add short way to create DeviceAssignment
piotrbartman Oct 14, 2024
1cd1c88
q-dev: rename attach-confirm -> qubes-device-attach-confirm
piotrbartman Oct 14, 2024
9bdeb52
q-dev: keep consistency in fire_event_for_permission
piotrbartman Oct 15, 2024
bee1797
q-dev: fix conflicted attachments
piotrbartman Oct 16, 2024
6dec58d
q-dev: wait for attaching devices during startup and update tests
piotrbartman Oct 17, 2024
5d49926
q-dev: pylint + black
piotrbartman Oct 18, 2024
79001ff
q-dev: fix deny list
piotrbartman Oct 18, 2024
5b38d7f
q-dev: call attach-confirm socket directly
piotrbartman Oct 18, 2024
4a16de5
q-dev: fix block device removing
piotrbartman Oct 21, 2024
ca949bb
q-dev: fix type hint
piotrbartman Oct 25, 2024
600209d
q-dev: update docs
piotrbartman Oct 25, 2024
73ea327
q-dev: less scary device category names
piotrbartman Oct 29, 2024
9a77764
q-dev: fix assignment.devices
piotrbartman Oct 29, 2024
08b899c
q-dev: pylint
piotrbartman Oct 29, 2024
727133f
q-dev: update tests and make pylint happy
piotrbartman Oct 30, 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

140 changes: 65 additions & 75 deletions qubes/api/admin.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be also Admin API methods for reading/writing deny list. If nothing more fancy, then at least similar to qrexec policy ones that:

  1. Verify syntax when writing
  2. Allow race-free edits (read returns a "token" being hash of the file, and write gets the token to verify if nobody changed it in the meantime).

If you prefer to add it in a separate PR, convert this note to an issue.

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 @@ -1218,14 +1219,15 @@ async def vm_device_available(self, endpoint):
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 @@ -1244,7 +1246,7 @@ async def vm_device_list(self, endpoint):
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 @@ -1253,12 +1255,13 @@ async def vm_device_list(self, endpoint):
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 @@ -1279,21 +1282,24 @@ async def vm_device_attached(self, endpoint):
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 @@ -1302,48 +1308,49 @@ async def vm_device_attached(self, endpoint):
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
)
untrusted_payload, expected_device=dev)

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

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
if _dev.device_id not in ('*', dev.device_id):
return _dev
return dev
except KeyError:
return _dev

# 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
dev = self.load_device_info(devclass)
assignment = DeviceAssignment(dev)

# 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]

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

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

Expand All @@ -1357,22 +1364,12 @@ async def vm_device_unassign(self, endpoint):
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,
options=assignment.options
)
device=dev, mode=assignment.mode.value, options=assignment.options)

await self.dest.devices[devclass].attach(assignment)

Expand All @@ -1386,23 +1383,16 @@ async def vm_device_attach(self, endpoint, untrusted_payload):
no_payload=True, scope='local', execute=True)
async def vm_device_detach(self, endpoint):
devclass = endpoint
dev = self.load_device_info(devclass)

# 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]

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

assignment = qubes.device_protocol.DeviceAssignment(
dev.backend_domain, dev.ident, devclass=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 @@ -1416,20 +1406,20 @@ async def vm_device_set_required(self, endpoint, untrusted_payload):
"""
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 @@ -1542,7 +1542,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