Skip to content

Commit 2b4d408

Browse files
committed
custom-persist: add support of metadata
1 parent 7b1069a commit 2b4d408

File tree

2 files changed

+171
-34
lines changed

2 files changed

+171
-34
lines changed

qubes/ext/custom_persist.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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

20+
import os
2021
import qubes.ext
2122
import qubes.config
2223

@@ -39,20 +40,55 @@ def _is_expected_feature(feature) -> bool:
3940
return feature.startswith(FEATURE_PREFIX)
4041

4142
@staticmethod
42-
def _is_valid_key(key, vm) -> bool:
43+
def _check_key(key):
4344
if not key:
44-
vm.log.warning("Got empty custom-persist key, ignoring")
45-
return False
45+
raise qubes.exc.QubesValueError(
46+
"custom-persist key cannot be empty"
47+
)
4648

4749
# QubesDB key length limit
4850
key_maxlen = QDB_KEY_LIMIT - len(QDB_PREFIX)
4951
if len(key) > key_maxlen:
50-
vm.log.warning(
52+
raise qubes.exc.QubesValueError(
5153
"custom-persist key is too long (max {}), ignoring: "
5254
"{}".format(key_maxlen, key)
5355
)
54-
return False
55-
return True
56+
57+
@staticmethod
58+
def _check_value_path(value):
59+
if not os.path.isabs(value):
60+
raise qubes.exc.QubesValueError(
61+
f"invalid path '{value}'"
62+
)
63+
64+
def _check_value(self, value):
65+
if value.startswith('/'):
66+
self._check_value_path(value)
67+
else:
68+
options = value.split(':')
69+
if len(options) < 5 or not options[4].startswith('/'):
70+
raise qubes.exc.QubesValueError(
71+
f"invalid value format: '{value}'"
72+
)
73+
74+
resource_type = options[0]
75+
mode = options[3]
76+
if resource_type not in ('file', 'dir'):
77+
raise qubes.exc.QubesValueError(
78+
f"invalid resource type option '{resource_type}' "
79+
f"in value '{value}'"
80+
)
81+
try:
82+
if not 0 <= int(mode, 8) <= 0o7777:
83+
raise qubes.exc.QubesValueError(
84+
f"invalid mode option '{mode}' in value '{value}'"
85+
)
86+
except ValueError:
87+
raise qubes.exc.QubesValueError(
88+
f"invalid mode option '{mode}' in value '{value}'"
89+
)
90+
91+
self._check_value_path(':'.join(options[4:]))
5692

5793
def _write_db_value(self, feature, value, vm):
5894
vm.untrusted_qdb.write(
@@ -65,9 +101,9 @@ def on_domain_qdb_create(self, vm, event):
65101
"""Actually export features"""
66102
# pylint: disable=unused-argument
67103
for feature, value in vm.features.items():
68-
if self._is_expected_feature(feature) and self._is_valid_key(
69-
self._extract_key_from_feature(feature), vm
70-
):
104+
if self._is_expected_feature(feature):
105+
self._check_key(self._extract_key_from_feature(feature))
106+
self._check_value(value)
71107
self._write_db_value(feature, value, vm)
72108

73109
@qubes.ext.handler("domain-feature-set:*")
@@ -78,8 +114,8 @@ def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None):
78114
if not self._is_expected_feature(feature):
79115
return
80116

81-
if not self._is_valid_key(self._extract_key_from_feature(feature), vm):
82-
return
117+
self._check_key(self._extract_key_from_feature(feature))
118+
self._check_value(value)
83119

84120
if not vm.is_running():
85121
return

qubes/tests/ext.py

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ def test_001_feature_set(self):
11001100
"/newvalue",
11011101
"",
11021102
)
1103+
11031104
self.assertEqual(
11041105
sorted(self.vm.untrusted_qdb.mock_calls),
11051106
[
@@ -1114,49 +1115,149 @@ def test_002_feature_delete(self):
11141115
self.ext.on_domain_feature_delete(
11151116
self.vm, "feature-delete:custom-persist.test", "custom-persist.test"
11161117
)
1118+
self.vm.untrusted_qdb.rm.assert_called_with("/persist/test")
1119+
1120+
def test_003_empty_key(self):
1121+
with self.assertRaises(qubes.exc.QubesValueError) as e:
1122+
self.ext.on_domain_feature_set(
1123+
self.vm,
1124+
"feature-set:custom-persist.",
1125+
"custom-persist.",
1126+
"/test",
1127+
"",
1128+
)
1129+
self.assertEqual(str(e.exception), "custom-persist key cannot be empty")
1130+
self.vm.untrusted_qdb.write.assert_not_called()
1131+
1132+
def test_004_key_too_long(self):
1133+
with self.assertRaises(qubes.exc.QubesValueError) as e:
1134+
self.ext.on_domain_feature_set(
1135+
self.vm,
1136+
"feature-set:custom-persist." + "X" * 55,
1137+
"custom-persist." + "X" * 55,
1138+
"/test",
1139+
"",
1140+
)
1141+
11171142
self.assertEqual(
1118-
self.vm.untrusted_qdb.mock_calls,
1119-
[mock.call.rm("/persist/test")],
1143+
str(e.exception),
1144+
"custom-persist key is too long (max 54), ignoring: " + "X" * 55,
11201145
)
1146+
self.vm.untrusted_qdb.assert_not_called()
11211147

1122-
def test_003_empty_key(self):
1148+
def test_005_other_feature_deletion(self):
1149+
self.ext.on_domain_feature_delete(
1150+
self.vm, "feature-delete:otherfeature.test", "otherfeature.test"
1151+
)
1152+
self.vm.untrusted_qdb.assert_not_called()
1153+
1154+
def test_006_feature_set_while_vm_is_not_running(self):
1155+
self.vm.is_running.return_value = False
11231156
self.ext.on_domain_feature_set(
11241157
self.vm,
1125-
"feature-set:custom-persist.",
1126-
"custom-persist.",
1158+
"feature-set:custom-persist.test",
1159+
"custom-persist.test",
11271160
"/test",
1161+
)
1162+
self.vm.untrusted_qdb.write.assert_not_called()
1163+
1164+
def test_007_feature_set_value_with_option(self):
1165+
self.ext.on_domain_feature_set(
1166+
self.vm,
1167+
"feature-set:custom-persist.test",
1168+
"custom-persist.test",
1169+
"dir:root:root:0755:/var/test",
11281170
"",
11291171
)
1130-
self.vm.untrusted_qdb.assert_not_called()
1131-
self.vm.log.warning.assert_called_once_with(
1132-
"Got empty custom-persist key, ignoring"
1172+
self.vm.untrusted_qdb.write.assert_called_with(
1173+
"/persist/test", "dir:root:root:0755:/var/test"
11331174
)
11341175

1135-
def test_004_key_too_long(self):
1176+
def test_008_feature_set_invalid_path(self):
1177+
with self.assertRaises(qubes.exc.QubesValueError):
1178+
self.ext.on_domain_feature_set(
1179+
self.vm,
1180+
"feature-set:custom-persist.test",
1181+
"custom-persist.test",
1182+
"test",
1183+
"",
1184+
)
1185+
self.vm.untrusted_qdb.write.assert_not_called()
1186+
1187+
def test_009_feature_set_invalid_option_type(self):
1188+
with self.assertRaises(qubes.exc.QubesValueError):
1189+
self.ext.on_domain_feature_set(
1190+
self.vm,
1191+
"feature-set:custom-persist.test",
1192+
"custom-persist.test",
1193+
"bad:root:root:0755:/var/test",
1194+
"",
1195+
)
1196+
self.vm.untrusted_qdb.write.assert_not_called()
1197+
1198+
def test_010_feature_set_invalid_option_mode_too_high(self):
1199+
with self.assertRaises(qubes.exc.QubesValueError):
1200+
self.ext.on_domain_feature_set(
1201+
self.vm,
1202+
"feature-set:custom-persist.test",
1203+
"custom-persist.test",
1204+
"file:root:root:9750:/var/test",
1205+
"",
1206+
)
1207+
self.vm.untrusted_qdb.write.assert_not_called()
1208+
1209+
def test_011_feature_set_invalid_option_mode_negative_high(self):
1210+
with self.assertRaises(qubes.exc.QubesValueError):
1211+
self.ext.on_domain_feature_set(
1212+
self.vm,
1213+
"feature-set:custom-persist.test",
1214+
"custom-persist.test",
1215+
"file:root:root:-755:/var/test",
1216+
"",
1217+
)
1218+
self.vm.untrusted_qdb.write.assert_not_called()
1219+
1220+
def test_012_feature_set_option_mode_without_leading_zero(self):
11361221
self.ext.on_domain_feature_set(
11371222
self.vm,
1138-
"feature-set:custom-persist." + "X" * 55,
1139-
"custom-persist." + "X" * 55,
1140-
"/test",
1223+
"feature-set:custom-persist.test",
1224+
"custom-persist.test",
1225+
"file:root:root:755:/var/test",
11411226
"",
11421227
)
1143-
self.vm.untrusted_qdb.assert_not_called()
1144-
self.vm.log.warning.assert_called_once_with(
1145-
"custom-persist key is too long (max 54), ignoring: " + "X" * 55
1228+
self.vm.untrusted_qdb.write.assert_called_with(
1229+
"/persist/test", "file:root:root:755:/var/test"
11461230
)
11471231

1148-
def test_005_other_feature_deletion(self):
1149-
self.ext.on_domain_feature_delete(
1150-
self.vm, "feature-delete:otherfeature.test", "otherfeature.test"
1232+
def test_013_feature_set_invalid_path_with_option(self):
1233+
with self.assertRaises(qubes.exc.QubesValueError):
1234+
self.ext.on_domain_feature_set(
1235+
self.vm,
1236+
"feature-set:custom-persist.test",
1237+
"custom-persist.test",
1238+
"dir:root:root:0755:var/test",
1239+
"",
1240+
)
1241+
self.vm.untrusted_qdb.write.assert_not_called()
1242+
1243+
def test_014_feature_set_path_with_colon_with_options(self):
1244+
self.ext.on_domain_feature_set(
1245+
self.vm,
1246+
"feature-set:custom-persist.test",
1247+
"custom-persist.test",
1248+
"file:root:root:755:/var/test:dir:with:colon",
1249+
"",
11511250
)
1152-
self.vm.untrusted_qdb.assert_not_called()
1251+
self.vm.untrusted_qdb.write.assert_called()
11531252

1154-
def test_006_feature_set_while_vm_is_not_running(self):
1155-
self.vm.is_running.return_value = False
1253+
def test_015_feature_set_path_with_colon_without_options(self):
11561254
self.ext.on_domain_feature_set(
11571255
self.vm,
11581256
"feature-set:custom-persist.test",
11591257
"custom-persist.test",
1160-
"/test",
1258+
"/var/test:dir:with:colon",
1259+
"",
1260+
)
1261+
self.vm.untrusted_qdb.write.assert_called_with(
1262+
"/persist/test", "/var/test:dir:with:colon"
11611263
)
1162-
self.vm.untrusted_qdb.assert_not_called()

0 commit comments

Comments
 (0)