Skip to content

Commit 2531edd

Browse files
committed
Ignore invalid GUI related features and options
resolves: QubesOS/qubes-issues#7730
1 parent ee57bf1 commit 2531edd

File tree

2 files changed

+76
-17
lines changed

2 files changed

+76
-17
lines changed

qubesadmin/tools/qvm_start_daemon.py

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,63 @@
3939
import qubesadmin.vm
4040
from qubesadmin.tools import xcffibhelpers
4141

42+
# pylint: disable=wrong-import-position
43+
from Xlib import XK, X
44+
from Xlib.display import Display
45+
4246
GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
4347
PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
4448
QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'
4549

50+
def validator_key_sequence(sequence: str) -> bool:
51+
""" xside.c key sequence validation is not case sensitive and supports more
52+
choices than Global Config's limited choices, so we replicate it here """
53+
if not isinstance(sequence, str):
54+
return False
55+
while '-' in sequence:
56+
modifier, sequence = sequence.split('-', 1)
57+
if not modifier.lower() in \
58+
['shift', 'ctrl', 'alt', 'mod1', 'mod2', 'mod3', 'mod4']:
59+
return False
60+
return Display().keysym_to_keycode(XK.string_to_keysym(sequence)) != \
61+
X.NoSymbol
62+
63+
def validator_trayicon_mode(mode: str) -> bool:
64+
""" xside.c tray mode validation is replicated here """
65+
if not isinstance(mode, str):
66+
return False
67+
if mode in ['bg', 'border1', 'border2', 'tint']:
68+
return True
69+
if mode.startswith('tint'):
70+
if mode[4:] in ['+border1', '+border2', '+saturation50', '+whitehack']:
71+
return True
72+
return False
73+
74+
def validator_color(color: str) -> bool:
75+
""" xside.c `parse_color` validation code is replicated here """
76+
if not isinstance(color, str):
77+
return False
78+
if re.match(r"^0[xX](?:[0-9a-fA-F]{3}){1,2}$", color.strip()):
79+
# Technically `parse_color` could parse values such as `0x0`
80+
# Xlib.xobject.colormap conventions & standards are different.
81+
return True
82+
if Display().screen().default_colormap.alloc_named_color(color) is not None:
83+
return True
84+
return False
85+
4686
GUI_DAEMON_OPTIONS = [
47-
('allow_fullscreen', 'bool'),
48-
('override_redirect_protection', 'bool'),
49-
('override_redirect', 'str'),
50-
('allow_utf8_titles', 'bool'),
51-
('secure_copy_sequence', 'str'),
52-
('secure_paste_sequence', 'str'),
53-
('windows_count_limit', 'int'),
54-
('trayicon_mode', 'str'),
55-
('window_background_color', 'str'),
56-
('startup_timeout', 'int'),
57-
('max_clipboard_size', 'int'),
87+
('allow_fullscreen', 'bool', (lambda x: isinstance(x, bool))),
88+
('override_redirect_protection', 'bool', (lambda x: isinstance(x, bool))),
89+
('override_redirect', 'str', (lambda x: x in ['allow', 'disable'])),
90+
('allow_utf8_titles', 'bool', (lambda x: isinstance(x, bool))),
91+
('secure_copy_sequence', 'str', validator_key_sequence),
92+
('secure_paste_sequence', 'str', validator_key_sequence),
93+
('windows_count_limit', 'int', (lambda x: isinstance(x, int) and x > 0)),
94+
('trayicon_mode', 'str', validator_trayicon_mode),
95+
('window_background_color', 'str', validator_color),
96+
('startup_timeout', 'int', (lambda x: isinstance(x, int) and x >= 0)),
97+
('max_clipboard_size', 'int', \
98+
(lambda x: isinstance(x, int) and 256 <= x <= 256000)),
5899
]
59100

60101
formatter = logging.Formatter(
@@ -108,12 +149,12 @@ def retrieve_gui_daemon_options(vm, guivm):
108149

109150
options = {}
110151

111-
for name, kind in GUI_DAEMON_OPTIONS:
112-
feature_value = vm.features.get(
113-
'gui-' + name.replace('_', '-'), None)
152+
for name, kind, validator in GUI_DAEMON_OPTIONS:
153+
feature = 'gui-' + name.replace('_', '-')
154+
feature_value = vm.features.get(feature, None)
114155
if feature_value is None:
115-
feature_value = guivm.features.get(
116-
'gui-default-' + name.replace('_', '-'), None)
156+
feature = 'gui-' + name.replace('_', '-')
157+
feature_value = guivm.features.get(feature, None)
117158
if feature_value is None:
118159
continue
119160

@@ -126,6 +167,15 @@ def retrieve_gui_daemon_options(vm, guivm):
126167
else:
127168
assert False, kind
128169

170+
if not validator(value):
171+
message = f"{vm.name}: Ignoring invalid feature:\n" \
172+
f"{feature}={feature_value}"
173+
log.error(message)
174+
if not sys.stdout.isatty():
175+
subprocess.run(['notify-send', '-a', 'Qubes GUI Daemon', \
176+
'--icon', 'dialog-warning', message], check=False)
177+
continue
178+
129179
options[name] = value
130180
return options
131181

@@ -141,7 +191,7 @@ def serialize_gui_daemon_options(options):
141191
'',
142192
'global: {',
143193
]
144-
for name, kind in GUI_DAEMON_OPTIONS:
194+
for name, kind, validator in GUI_DAEMON_OPTIONS:
145195
if name in options:
146196
value = options[name]
147197
if kind == 'bool':
@@ -153,6 +203,14 @@ def serialize_gui_daemon_options(options):
153203
else:
154204
assert False, kind
155205

206+
if not validator(value):
207+
message = f"Ignoring invalid GUI property:\n{name}={value}"
208+
log.error(message)
209+
if not sys.stdout.isatty():
210+
subprocess.run(['notify-send', '-a', 'Qubes GUI Daemon', \
211+
'--icon', 'dialog-warning', message], check=False)
212+
continue
213+
156214
lines.append(' {} = {};'.format(name, serialized))
157215
lines.append('}')
158216
lines.append('')

rpm_spec/qubes-core-admin-client.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Requires: python%{python3_pkgversion}-qubesdb
4242
Requires: python%{python3_pkgversion}-rpm
4343
Requires: python%{python3_pkgversion}-tqdm
4444
Requires: python%{python3_pkgversion}-pyxdg
45+
Requires: python%{python3_pkgversion}-xlib
4546
Conflicts: qubes-core-dom0 < 4.3.0
4647
Conflicts: qubes-manager < 4.0.6
4748

0 commit comments

Comments
 (0)