Skip to content

Commit 479ac3d

Browse files
committed
Use ProtocolError on invalid requests
The exceptions ProtocolError and PermissionDenied are both raised by qubesd to indicate various problems with requests. However, both cause clients to print the extremely unhelpful "Got empty response from qubesd" message. ProtocolError must be used only for client *programming* errors (bugs), not for problems that the client could not have detected before making the request. PermissionDenied must be used only for service authorization denials. Therefore, a API handler can be arranged as: - Validation: ProtocolError otherwise - fire_event_for_permission(): PermissionDenied if unauthorized - Action Ideally, validation that may leak existence of a system property should be done after asking for administrative permission, but then it would not be possible to pass only safe values to "admin-api:" events. If we are already leaking existence of a property, it makes sense to provide a useful exception class for it. Fixes: QubesOS/qubes-issues#10345
1 parent 741d111 commit 479ac3d

File tree

11 files changed

+593
-279
lines changed

11 files changed

+593
-279
lines changed

doc/qubes-exc.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ call for effect). Instead consider writing in negative form, implying expected
4343
state: "Domain is not running" instead of "Domain is paused" (yeah, what's wrong
4444
with that?).
4545

46-
Also avoid implying the personhood of the computer, including adressing user in
46+
Also avoid implying the personhood of the computer, including addressing user in
4747
second person. For example, write "Sending message failed" instead of "I failed
4848
to send the message".
4949

qubes/api/__init__.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import re
2828
import shutil
2929
import socket
30+
import string
3031
import struct
3132
import traceback
3233
from typing import Union, Any
@@ -189,6 +190,11 @@ def __init__(
189190
self._handler = candidates[0]
190191
self._running_handler = None
191192

193+
# pylint: disable=invalid-name
194+
self.EXC_ARG_NOT_IN_DEST_VOLUMES = "{} volumes".format(self.dest.name)
195+
# pylint: disable=invalid-name
196+
self.EXC_ARG_NOT_IN_POOLS = "pools"
197+
192198
@classmethod
193199
def list_methods(cls, select_method=None):
194200
for attr in dir(cls):
@@ -239,26 +245,76 @@ def fire_event_for_filter(self, iterable, **kwargs):
239245
"""Fire an event on the source qube to filter for permission"""
240246
return apply_filters(iterable, self.fire_event_for_permission(**kwargs))
241247

248+
def enforce_arg(
249+
self,
250+
wants: Union[None, bool, str, list, tuple] = None,
251+
short_reason: str = "",
252+
) -> None:
253+
"""Enforce argument to be absent or be in iterable."""
254+
if wants is None:
255+
self.enforce(not self.arg, reason="Argument is present")
256+
elif wants is True:
257+
self.enforce(self.arg, reason="Argument is empty")
258+
else:
259+
assert isinstance(self.arg, str)
260+
assert short_reason
261+
if wants is None or isinstance(wants, bool):
262+
assertion = self.arg is wants
263+
elif isinstance(wants, str):
264+
assertion = self.arg == wants
265+
else:
266+
assertion = self.arg in wants
267+
self.enforce(
268+
assertion,
269+
reason="Argument not in {!r}".format(short_reason),
270+
)
271+
272+
def enforce_dest_dom0(self, wants: bool = True) -> None:
273+
"""Enforce destination to be or not to be dom0."""
274+
if wants:
275+
self.enforce(
276+
self.dest.name == "dom0", reason="Destination is not dom0"
277+
)
278+
else:
279+
self.enforce(self.dest.name != "dom0", reason="Destination is dom0")
280+
242281
@staticmethod
243-
def enforce(predicate):
244-
"""An assert replacement, but works even with optimisations."""
282+
def enforce(predicate, reason: str = "") -> None:
283+
"""If predicate is false, raise an exception to terminate handling
284+
the request.
285+
286+
This will raise :py:class:`ProtocolError` if the predicate is false.
287+
See the documentation of that class for details.
288+
289+
:param str reason: Exception motive.
290+
"""
245291
if not predicate:
246-
raise PermissionDenied()
292+
raise ProtocolError(reason)
247293

248294
def validate_size(
249-
self, untrusted_size: bytes, allow_negative: bool = False
295+
self, untrusted_size: bytes, name: str, allow_negative: bool = False
250296
) -> int:
251-
self.enforce(isinstance(untrusted_size, bytes))
297+
allowed_chars = string.ascii_letters + string.digits + "-_. "
298+
assert all(c in allowed_chars for c in name)
299+
if not isinstance(untrusted_size, bytes):
300+
raise ProtocolError(
301+
"Expected {!r} to be of type bytes, got {!r}".format(
302+
name, type(untrusted_size).__name__
303+
)
304+
)
252305
coefficient = 1
253306
if allow_negative and untrusted_size.startswith(b"-"):
254307
coefficient = -1
255308
untrusted_size = untrusted_size[1:]
309+
name_cap = name.capitalize()
256310
if not untrusted_size.isdigit():
257-
raise qubes.exc.ProtocolError("Size must be ASCII digits (only)")
311+
raise ProtocolError(name_cap + " size contains non ASCII digits")
258312
if len(untrusted_size) >= 20:
259-
raise qubes.exc.ProtocolError("Sizes limited to 19 decimal digits")
313+
raise ProtocolError(
314+
name_cap + " size is bigger than 19 decimal digits"
315+
)
260316
if untrusted_size[0] == 48 and untrusted_size != b"0":
261-
raise qubes.exc.ProtocolError("Spurious leading zeros not allowed")
317+
raise ProtocolError(name_cap + " contains spurious leading zeros")
262318
return int(untrusted_size) * coefficient
263319

264320

@@ -345,21 +401,15 @@ async def respond(self, src, meth, dest, arg, *, untrusted_payload):
345401

346402
# except clauses will fall through to transport.abort() below
347403

348-
except PermissionDenied:
349-
self.app.log.warning(
350-
"permission denied for call %s+%s (%s → %s) "
351-
"with payload of %d bytes",
352-
meth,
353-
arg,
354-
src,
355-
dest,
356-
len(untrusted_payload),
357-
)
358-
359-
except ProtocolError:
404+
except (PermissionDenied, ProtocolError) as exc:
405+
log_prefix = None
406+
if isinstance(exc, PermissionDenied):
407+
log_prefix = "permission denied"
408+
elif isinstance(exc, ProtocolError):
409+
log_prefix = "protocol error"
360410
self.app.log.warning(
361-
"protocol error for call %s+%s (%s → %s) "
362-
"with payload of %d bytes",
411+
"%s for call %s+%s (%s → %s) with payload of %d bytes",
412+
log_prefix,
363413
meth,
364414
arg,
365415
src,

0 commit comments

Comments
 (0)