Coverage Report

Created: 2025-04-22 06:16

/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(&current_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
}