Skip to content

Commit 6cd9523

Browse files
committed
Merge remote-tracking branch 'origin/pr/643'
* origin/pr/643: device interface denied list: reformat code device interface denied list: make add/remove idempotent device interface denied list: special value "all" for removing all items device interface denied list: allow hexdigits device interface denied list: update no-payload/no-argument tests device interface denied list: test renumbering device interface denied list: device denied as qube property Pull request description: Delete the file and drop-in folder for the denied device interfaces list and move the list to `qvm-prefs`. Solves QubesOS/qubes-issues/issues/9674
2 parents 3e17b7c + ffbe0c5 commit 6cd9523

File tree

8 files changed

+296
-76
lines changed

8 files changed

+296
-76
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ ADMIN_API_METHODS_SIMPLE = \
9292
admin.vm.device.mic.Detach \
9393
admin.vm.device.mic.Set.assignment \
9494
admin.vm.device.mic.Unassign \
95+
admin.vm.device.denied.List \
96+
admin.vm.device.denied.Add \
97+
admin.vm.device.denied.Remove \
9598
admin.vm.feature.CheckWithNetvm \
9699
admin.vm.feature.CheckWithTemplate \
97100
admin.vm.feature.CheckWithAdminVM \

qubes/api/admin.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
UnknownDevice,
5151
DeviceAssignment,
5252
AssignmentMode,
53+
DeviceInterface,
5354
)
5455

5556

@@ -1630,6 +1631,83 @@ async def vm_device_set_required(self, endpoint, untrusted_payload):
16301631
await self.dest.devices[devclass].update_assignment(dev, mode)
16311632
self.app.save()
16321633

1634+
@qubes.api.method(
1635+
"admin.vm.device.denied.List", no_payload=True, scope="local", read=True
1636+
)
1637+
async def vm_device_denied_list(self):
1638+
"""
1639+
List all denied device interfaces for the VM.
1640+
1641+
Returns a newline-separated string.
1642+
"""
1643+
self.enforce(not self.arg)
1644+
1645+
self.fire_event_for_permission()
1646+
1647+
denied = self.dest.devices_denied
1648+
return "\n".join(map(repr, DeviceInterface.from_str_bulk(denied)))
1649+
1650+
@qubes.api.method("admin.vm.device.denied.Add", scope="local", write=True)
1651+
async def vm_device_denied_add(self, untrusted_payload):
1652+
"""
1653+
Add device interface(s) to the denied list for the VM.
1654+
1655+
Payload:
1656+
Encoded device interface (can be repeated without any separator).
1657+
"""
1658+
payload = untrusted_payload.decode("ascii", errors="strict")
1659+
to_add = DeviceInterface.from_str_bulk(payload)
1660+
1661+
if len(set(to_add)) != len(to_add):
1662+
raise qubes.exc.QubesValueError(
1663+
"Duplicated device interfaces in payload."
1664+
)
1665+
1666+
self.fire_event_for_permission(interfaces=to_add)
1667+
1668+
to_add_enc = "".join(map(repr, to_add))
1669+
1670+
prev = self.dest.devices_denied
1671+
# "auto" ignoring of duplications
1672+
self.dest.devices_denied = self.dest.devices_denied + to_add_enc
1673+
# do not save if nothing changed
1674+
if prev != self.dest.devices_denied:
1675+
self.app.save()
1676+
1677+
@qubes.api.method(
1678+
"admin.vm.device.denied.Remove", scope="local", write=True
1679+
)
1680+
async def vm_device_denied_remove(self, untrusted_payload):
1681+
"""
1682+
Remove device interface(s) from the denied list for the VM.
1683+
1684+
Payload:
1685+
Encoded device interface (can be repeated without any separator).
1686+
If payload is "all", all interfaces are removed.
1687+
"""
1688+
denied = DeviceInterface.from_str_bulk(self.dest.devices_denied)
1689+
1690+
payload = untrusted_payload.decode("ascii", errors="strict")
1691+
if payload != "all":
1692+
to_remove = DeviceInterface.from_str_bulk(payload)
1693+
else:
1694+
to_remove = denied.copy()
1695+
1696+
if len(set(to_remove)) != len(to_remove):
1697+
raise qubes.exc.QubesValueError(
1698+
"Duplicated device interfaces in payload."
1699+
)
1700+
1701+
# may contain missing values
1702+
self.fire_event_for_permission(interfaces=to_remove)
1703+
1704+
# ignore missing values
1705+
new_denied = "".join(repr(i) for i in denied if i not in to_remove)
1706+
1707+
if new_denied != self.dest.devices_denied:
1708+
self.dest.devices_denied = new_denied
1709+
self.app.save()
1710+
16331711
@qubes.api.method(
16341712
"admin.vm.firewall.Get", no_payload=True, scope="local", read=True
16351713
)

qubes/device_protocol.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from typing import TYPE_CHECKING
3939

4040
import qubes.utils
41-
from qubes.exc import ProtocolError
41+
from qubes.exc import ProtocolError, QubesValueError
4242

4343
if TYPE_CHECKING:
4444
from qubes.vm.qubesvm import QubesVM
@@ -693,7 +693,12 @@ def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
693693
f"for given {devclass=}",
694694
file=sys.stderr,
695695
)
696-
ifc_full = devclass[0] + ifc_padded
696+
if not all(c in string.hexdigits + "*" for c in ifc_padded):
697+
raise ProtocolError("Invalid characters in interface encoding")
698+
devclass_code = devclass[0].lower()
699+
if devclass_code not in string.ascii_lowercase:
700+
raise ProtocolError("Invalid characters in devclass encoding")
701+
ifc_full = devclass_code + ifc_padded
697702
else:
698703
known_devclasses = {
699704
"p": "pci",
@@ -733,6 +738,19 @@ def unknown(cls) -> "DeviceInterface":
733738
"""Value for unknown device interface."""
734739
return cls("?******")
735740

741+
@staticmethod
742+
def from_str_bulk(interfaces: Optional[str]) -> List["DeviceInterface"]:
743+
interfaces = interfaces or ""
744+
if len(interfaces) % 7 != 0:
745+
raise QubesValueError(
746+
f"Invalid length of {interfaces=} "
747+
f"(is {len(interfaces)}, expected multiple of 7)",
748+
)
749+
return [
750+
DeviceInterface(interfaces[i : i + 7])
751+
for i in range(0, len(interfaces), 7)
752+
]
753+
736754
def __repr__(self):
737755
return self._interface_encoding
738756

@@ -1120,11 +1138,7 @@ def _deserialize(
11201138

11211139
if "interfaces" in properties:
11221140
interfaces = properties["interfaces"]
1123-
interfaces = [
1124-
DeviceInterface(interfaces[i : i + 7])
1125-
for i in range(0, len(interfaces), 7)
1126-
]
1127-
properties["interfaces"] = interfaces
1141+
properties["interfaces"] = DeviceInterface.from_str_bulk(interfaces)
11281142

11291143
if "parent_ident" in properties:
11301144
properties["parent"] = Port(

qubes/devices.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@
7373
)
7474
from qubes.exc import ProtocolError
7575

76-
DEVICE_DENY_LIST = "/etc/qubes/device-deny.list"
77-
7876

7977
class DeviceNotAssigned(qubes.exc.QubesException, KeyError):
8078
"""

qubes/ext/admin.py

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
# You should have received a copy of the GNU Lesser General Public
1818
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
1919
import importlib
20-
import os
2120

2221
import qubes.api
2322
import qubes.api.internal
@@ -27,7 +26,6 @@
2726
from qrexec.policy import utils, parser
2827

2928
from qubes.device_protocol import DeviceInterface
30-
from qubes.devices import DEVICE_DENY_LIST
3129

3230

3331
class JustEvaluateAskResolution(parser.AskResolution):
@@ -184,44 +182,11 @@ def on_device_attach(self, vm, event, dest, arg, device, mode, **kwargs):
184182
if mode != "manual":
185183
return
186184

187-
# load device deny list
188-
deny = {}
189-
AdminExtension._load_deny_list(deny, DEVICE_DENY_LIST)
190-
191-
# load drop ins
192-
drop_in_path = DEVICE_DENY_LIST + ".d"
193-
if os.path.isdir(drop_in_path):
194-
for deny_list_name in os.listdir(drop_in_path):
195-
deny_list_path = os.path.join(drop_in_path, deny_list_name)
196-
197-
if os.path.isfile(deny_list_path):
198-
AdminExtension._load_deny_list(deny, deny_list_path)
199-
200-
# check if any presented interface is on deny list
201-
for interface in deny.get(dest.name, set()):
202-
pattern = DeviceInterface(interface)
185+
# check if any presented interface is on denied list
186+
denied = set(DeviceInterface.from_str_bulk(dest.devices_denied))
187+
for pattern in denied:
203188
for devint in device.interfaces:
204189
if pattern.matches(devint):
205190
raise qubes.exc.PermissionDenied(
206191
f"Device exposes a banned interface: {devint}"
207192
)
208-
209-
@staticmethod
210-
def _load_deny_list(deny: dict, path: str) -> None:
211-
try:
212-
with open(path, "r", encoding="utf-8") as file:
213-
for line in file:
214-
line = line.strip()
215-
216-
# skip comments
217-
if line.startswith("#"):
218-
continue
219-
220-
if line:
221-
name, *values = line.split()
222-
values = " ".join(values).replace(",", " ").split()
223-
values = [v for v in values if len(v) > 0]
224-
225-
deny[name] = deny.get(name, set()).union(set(values))
226-
except IOError:
227-
pass

0 commit comments

Comments
 (0)