/src/qubes-os/qubes-core-qrexec/libqrexec/toml.c
Line | Count | Source (jump to first uncovered line) |
1 | | #include <stdlib.h> |
2 | | #include <stdio.h> |
3 | | #include <string.h> |
4 | | #include <assert.h> |
5 | | #include <errno.h> |
6 | | #include <stddef.h> |
7 | | #include <limits.h> |
8 | | |
9 | | #include "libqrexec-utils.h" |
10 | | #include "private.h" |
11 | | |
12 | | // A trivial parser for a subset of TOML |
13 | 0 | static bool qubes_isspace(unsigned char c) { |
14 | 0 | return c == ' ' || c == '\t'; |
15 | 0 | } |
16 | | |
17 | | enum toml_type { |
18 | | TOML_TYPE_INVALID, // error |
19 | | TOML_TYPE_BOOL, |
20 | | TOML_TYPE_INTEGER, |
21 | | TOML_TYPE_STRING, |
22 | | }; |
23 | | |
24 | | union toml_data { |
25 | | char *string; // allocated with malloc() |
26 | | bool boolean; |
27 | | unsigned long long integer; |
28 | | }; |
29 | | |
30 | | static enum toml_type parse_toml_value( |
31 | | const char *file, |
32 | | size_t line, |
33 | | unsigned char *value_to_parse, |
34 | 0 | union toml_data *value) { |
35 | 0 | enum toml_type ty = TOML_TYPE_INVALID; |
36 | 0 | char *end = NULL; |
37 | |
|
38 | 0 | switch (value_to_parse[0]) { |
39 | 0 | case '0': |
40 | 0 | ty = TOML_TYPE_INTEGER; |
41 | 0 | value->integer = 0; |
42 | 0 | end = (char *)(value_to_parse + 1); |
43 | 0 | break; |
44 | 0 | case '1' ... '9': |
45 | 0 | ty = TOML_TYPE_INTEGER; |
46 | 0 | errno = 0; |
47 | 0 | value->integer = strtoull((char *)value_to_parse, &end, 10); |
48 | 0 | if (errno) { |
49 | 0 | PERROR("%s:%zu: strtoull()", file, line); |
50 | 0 | return TOML_TYPE_INVALID; |
51 | 0 | } |
52 | 0 | break; |
53 | 0 | case 't': |
54 | 0 | ty = TOML_TYPE_BOOL; |
55 | 0 | if (strncmp((char *)value_to_parse, "true", 4) == 0) { |
56 | 0 | value->boolean = true; |
57 | 0 | end = (char *)(value_to_parse + 4); |
58 | 0 | break; |
59 | 0 | } else { |
60 | 0 | LOG(ERROR, "%s:%zu: Unexpected unquoted string", file, line); |
61 | 0 | return TOML_TYPE_INVALID; |
62 | 0 | } |
63 | 0 | case 'f': |
64 | 0 | ty = TOML_TYPE_BOOL; |
65 | 0 | if (strncmp((char *)value_to_parse, "false", 5) == 0) { |
66 | 0 | value->boolean = false; |
67 | 0 | end = (char *)(value_to_parse + 5); |
68 | 0 | break; |
69 | 0 | } else { |
70 | 0 | LOG(ERROR, "%s:%zu: Unexpected unquoted string", file, line); |
71 | 0 | return TOML_TYPE_INVALID; |
72 | 0 | } |
73 | 0 | case '"': |
74 | 0 | LOG(ERROR, "%s:%zu: Double-quoted strings not implemented, use single quotes", file, line); |
75 | 0 | return TOML_TYPE_INVALID; |
76 | 0 | case '\'': |
77 | 0 | ty = TOML_TYPE_STRING; |
78 | 0 | if (value_to_parse[1] == '\'' && value_to_parse[2] == '\'') { |
79 | 0 | LOG(ERROR, "%s:%zu: Triple-quoted strings not implemented", file, line); |
80 | 0 | return TOML_TYPE_INVALID; |
81 | 0 | } |
82 | 0 | end = strchr((char *)value_to_parse + 1, '\''); |
83 | 0 | if (end == NULL) { |
84 | 0 | LOG(ERROR, "%s:%zu: Unterminated quoted string", file, line); |
85 | 0 | return TOML_TYPE_INVALID; |
86 | 0 | } else { |
87 | 0 | size_t to_alloc = (size_t)(end - (char *)value_to_parse); |
88 | 0 | value->string = malloc(to_alloc); |
89 | 0 | if (value->string == NULL) { |
90 | 0 | LOG(ERROR, "%s:%zu: Out of memory copying value", file, line); |
91 | 0 | return TOML_TYPE_INVALID; |
92 | 0 | } |
93 | 0 | memcpy(value->string, value_to_parse + 1, to_alloc - 1); |
94 | 0 | value->string[to_alloc - 1] = 0; |
95 | 0 | end++; |
96 | 0 | break; |
97 | 0 | } |
98 | 0 | default: |
99 | 0 | if (value_to_parse[0] >= ' ' && value_to_parse[0] <= '~') { |
100 | 0 | LOG(ERROR, "%s:%zu: Unsupported start of value: '%c'", file, line, value_to_parse[0]); |
101 | 0 | } else { |
102 | 0 | LOG(ERROR, "%s:%zu: Unsupproted byte at start of value: %d", file, line, value_to_parse[0]); |
103 | 0 | } |
104 | 0 | return TOML_TYPE_INVALID; |
105 | 0 | } |
106 | 0 | while (qubes_isspace(*end)) { |
107 | 0 | end++; |
108 | 0 | } |
109 | 0 | if (*end == '\0' || *end == '#') |
110 | 0 | return ty; |
111 | 0 | if (ty == TOML_TYPE_INTEGER) { |
112 | 0 | LOG(ERROR, "%s:%zu: Unexpected junk after integer; note that only decimal integers with no excess leading zeros are supported", file, line); |
113 | 0 | } else { |
114 | 0 | LOG(ERROR, "%s:%zu: Unexpected junk after value", file, line); |
115 | 0 | } |
116 | 0 | if (ty == TOML_TYPE_STRING) |
117 | 0 | free(value->string); |
118 | 0 | return TOML_TYPE_INVALID; |
119 | 0 | } |
120 | | |
121 | 0 | static bool qubes_is_key_byte(unsigned char c) { |
122 | 0 | switch (c) { |
123 | 0 | case '0' ... '9': |
124 | 0 | case 'A' ... 'Z': |
125 | 0 | case 'a' ... 'z': |
126 | 0 | case '.': |
127 | 0 | case '-': |
128 | 0 | case '_': |
129 | 0 | return true; |
130 | 0 | default: |
131 | 0 | return false; |
132 | 0 | } |
133 | 0 | } |
134 | | |
135 | | static void toml_invalid_type(enum toml_type ty, const char *file, size_t lineno, const char *msg) |
136 | 0 | { |
137 | 0 | const char *bad_type; |
138 | 0 | switch (ty) { |
139 | 0 | case TOML_TYPE_INVALID: |
140 | 0 | bad_type = "<invalid value>"; |
141 | 0 | break; |
142 | 0 | case TOML_TYPE_BOOL: |
143 | 0 | bad_type = "Boolean"; |
144 | 0 | break; |
145 | 0 | case TOML_TYPE_INTEGER: |
146 | 0 | bad_type = "Integer"; |
147 | 0 | break; |
148 | 0 | case TOML_TYPE_STRING: |
149 | 0 | bad_type = "String"; |
150 | 0 | break; |
151 | 0 | default: |
152 | 0 | abort(); |
153 | 0 | } |
154 | 0 | LOG(ERROR, "%s:%zu: %s not valid for %s", file, lineno, bad_type, msg); |
155 | 0 | } |
156 | | |
157 | | static bool toml_check_dup_key(bool *seen_already, const char *file, size_t lineno, const char *msg) |
158 | 0 | { |
159 | 0 | if (*seen_already) { |
160 | 0 | LOG(ERROR, "%s:%zu: Key %s already seen", file, lineno, msg); |
161 | 0 | return true; |
162 | 0 | } |
163 | 0 | *seen_already = true; |
164 | 0 | return false; |
165 | 0 | } |
166 | | |
167 | 0 | static void toml_value_free(union toml_data *value, enum toml_type ty) { |
168 | 0 | if (ty == TOML_TYPE_STRING) { |
169 | 0 | free(value->string); |
170 | 0 | value->string = NULL; |
171 | 0 | } |
172 | 0 | } |
173 | | |
174 | | int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session, char **user, bool *send_service_descriptor, |
175 | | bool *exit_on_service_eof, bool *exit_on_client_eof) |
176 | 0 | { |
177 | 0 | int result = -1; /* assume problem */ |
178 | 0 | FILE *config_file = fopen(config_full_path, "re"); |
179 | 0 | if (!config_file) { |
180 | 0 | PERROR("Failed to load %s", config_full_path); |
181 | 0 | return -1; |
182 | 0 | } |
183 | | |
184 | 0 | char *current_line = NULL; |
185 | 0 | size_t lineno = 0; |
186 | 0 | size_t bufsize = 0; |
187 | 0 | ssize_t signed_linelen; |
188 | 0 | bool seen_wait_for_session = false; |
189 | 0 | bool seen_user = false; |
190 | 0 | bool seen_skip_service_descriptor = false; |
191 | 0 | bool seen_exit_on_client_eof = false; |
192 | 0 | bool seen_exit_on_service_eof = false; |
193 | 0 | *wait_for_session = 0; |
194 | 0 | *send_service_descriptor = true; |
195 | 0 | #define CHECK_DUP_KEY(v) do { \ |
196 | 0 | if (toml_check_dup_key(&(v), config_full_path, lineno, current_line)) { \ |
197 | 0 | toml_value_free(&value, ty); \ |
198 | 0 | goto bad; \ |
199 | 0 | } \ |
200 | 0 | } while (0); |
201 | 0 | #define CHECK_TYPE(v, msg) do { \ |
202 | 0 | if ((v) != ty) { \ |
203 | 0 | toml_invalid_type(v, config_full_path, lineno, msg); \ |
204 | 0 | toml_value_free(&value, ty); \ |
205 | 0 | goto bad; \ |
206 | 0 | } \ |
207 | 0 | } while (0); |
208 | |
|
209 | 0 | while ((signed_linelen = getline(¤t_line, &bufsize, config_file)) != -1) { |
210 | 0 | lineno++; |
211 | | /* Other negative values are invalid. If nothing at all is read that means EOF. */ |
212 | 0 | if (signed_linelen < 1) { |
213 | 0 | LOG(ERROR, "%s:%zu:getline returned invalid value %zd (libc bug?)", |
214 | 0 | config_full_path, lineno, signed_linelen); |
215 | 0 | abort(); |
216 | 0 | } |
217 | 0 | size_t linelen = (size_t)signed_linelen; |
218 | | /* Check for NUL in line */ |
219 | 0 | if (strlen(current_line) != linelen) { |
220 | 0 | LOG(ERROR, "%s:%zu:NUL byte in line", config_full_path, lineno); |
221 | 0 | goto bad; |
222 | 0 | } |
223 | | |
224 | | /* Chop off trailing \n (and \r if present) */ |
225 | 0 | if (linelen > 0 && current_line[linelen - 1] == '\n') { |
226 | 0 | linelen--; |
227 | 0 | current_line[linelen] = '\0'; |
228 | 0 | if (linelen > 0 && current_line[linelen - 1] == '\r') { |
229 | 0 | linelen--; |
230 | 0 | current_line[linelen] = '\0'; |
231 | 0 | } |
232 | 0 | } else { |
233 | 0 | LOG(INFO, "%s:%zu:missing newline at EOF", config_full_path, lineno); |
234 | 0 | } |
235 | | // Multi-line strings not yet implented, so just chop off trailing whitespace |
236 | 0 | while (linelen > 1 && qubes_isspace(current_line[linelen - 1])) { |
237 | 0 | linelen--; |
238 | 0 | current_line[linelen] = '\0'; |
239 | 0 | } |
240 | 0 | switch (current_line[0]) { |
241 | 0 | case '\0': |
242 | 0 | case '#': |
243 | | // Skip comments and blank lines |
244 | 0 | continue; |
245 | 0 | case 'a' ... 'z': |
246 | 0 | case 'A' ... 'Z': |
247 | 0 | break; |
248 | 0 | case '[': |
249 | 0 | LOG(ERROR, "%s:%zu: TOML section headers not supported", config_full_path, lineno); |
250 | 0 | goto bad; |
251 | 0 | case ' ': |
252 | 0 | case '\t': |
253 | 0 | LOG(ERROR, "%s:%zu: Unexpected whitespace at start of line", config_full_path, lineno); |
254 | 0 | goto bad; |
255 | 0 | default: |
256 | 0 | if (current_line[0] > ' ' && current_line[0] <= '~') { |
257 | 0 | LOG(ERROR, "%s:%zu: Invalid character '%c' at start of key", config_full_path, lineno, current_line[0]); |
258 | 0 | } else { |
259 | 0 | LOG(ERROR, "%s:%zu: Invalid byte 0x%x at start of key", config_full_path, lineno, current_line[0]); |
260 | 0 | } |
261 | 0 | goto bad; |
262 | 0 | } |
263 | 0 | unsigned char *key_cursor = (unsigned char *)current_line; |
264 | 0 | do { |
265 | 0 | key_cursor++; |
266 | 0 | } while (qubes_is_key_byte(key_cursor[0])); |
267 | 0 | int const key_len = key_cursor - (unsigned char *)current_line; |
268 | 0 | while (qubes_isspace(key_cursor[0])) |
269 | 0 | key_cursor++; |
270 | 0 | if (key_cursor[0] != '=') { |
271 | 0 | LOG(ERROR, "%s:%zu: Missing '=' after key", config_full_path, lineno); |
272 | 0 | goto bad; |
273 | 0 | } |
274 | 0 | do { |
275 | 0 | key_cursor++; |
276 | 0 | } while (qubes_isspace(key_cursor[0])); |
277 | 0 | current_line[key_len] = '\0'; |
278 | 0 | union toml_data value; |
279 | 0 | enum toml_type ty = parse_toml_value(config_full_path, lineno, key_cursor, &value); |
280 | 0 | if (ty == TOML_TYPE_INVALID) |
281 | 0 | goto bad; |
282 | | |
283 | 0 | if (strcmp(current_line, "wait-for-session") == 0) { |
284 | 0 | CHECK_DUP_KEY(seen_wait_for_session); |
285 | 0 | if (ty == TOML_TYPE_INTEGER) { |
286 | 0 | if (value.integer < 2) { |
287 | 0 | *wait_for_session = (int)value.integer; |
288 | 0 | continue; |
289 | 0 | } |
290 | 0 | LOG(ERROR, "%s:%zu: Unsupported integer value %llu for boolean", config_full_path, lineno, value.integer); |
291 | 0 | goto bad; |
292 | 0 | } else { |
293 | 0 | CHECK_TYPE(TOML_TYPE_BOOL, "wait-for-session"); |
294 | 0 | *wait_for_session = value.boolean; |
295 | 0 | } |
296 | 0 | } else if (strcmp(current_line, "exit-on-client-eof") == 0) { |
297 | 0 | CHECK_DUP_KEY(seen_exit_on_client_eof); |
298 | 0 | CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-client-eof"); |
299 | 0 | *exit_on_client_eof = value.boolean; |
300 | 0 | } else if (strcmp(current_line, "exit-on-service-eof") == 0) { |
301 | 0 | CHECK_DUP_KEY(seen_exit_on_service_eof); |
302 | 0 | CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-service-eof"); |
303 | 0 | *exit_on_service_eof = value.boolean; |
304 | 0 | } else if (strcmp(current_line, "skip-service-descriptor") == 0) { |
305 | 0 | CHECK_DUP_KEY(seen_skip_service_descriptor); |
306 | 0 | CHECK_TYPE(TOML_TYPE_BOOL, "skip-service-descriptor"); |
307 | 0 | *send_service_descriptor = !value.boolean; |
308 | 0 | } else if (strcmp(current_line, "force-user") == 0) { |
309 | 0 | CHECK_DUP_KEY(seen_user); |
310 | 0 | CHECK_TYPE(TOML_TYPE_STRING, "user name or user ID"); |
311 | 0 | *user = value.string; |
312 | 0 | } else { |
313 | 0 | LOG(ERROR, "%s:%zu: Unsupported key %s", config_full_path, lineno, current_line); |
314 | 0 | toml_value_free(&value, ty); |
315 | 0 | } |
316 | 0 | } |
317 | | |
318 | 0 | result = 1; |
319 | 0 | bad: |
320 | 0 | free(current_line); |
321 | 0 | fclose(config_file); |
322 | 0 | return result; |
323 | 0 | } |