Skip to content

Commit 8394915

Browse files
committed
Clean up list options before passing them to GnuPG
GnuPG's parsing of list options is not well-documented. The current behavior is consistent with that of GnuPG itself, but it depends on various GnuPG implementation details. Avoid this by only passing cleaned-up "canonical" list options. This means that options are separated with a single comma, as are a list of subpacket arguments. To simplify the code, whether a list option argument is in double quotes is preserved. This modifies the option in-place, rather than making a copy. A bounded buffer abstraction and assertions are used to validate that data is not overwritten until after it has already been read and therefore will not be needed again. This also ensures that no out of bounds writes occur. Fixes QubesOS/qubes-issues#10540
1 parent 26cf2b7 commit 8394915

1 file changed

Lines changed: 115 additions & 16 deletions

File tree

src/gpg-list-options.c

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,66 @@
33
#include <assert.h>
44
#include <inttypes.h>
55
#include <stdlib.h>
6+
#include <errno.h>
7+
#include <stdio.h>
68

79
struct lexbuf {
810
uint8_t *untrusted_cursor;
11+
uint8_t *outbuf;
12+
const uint8_t *const end;
913
};
1014

15+
static void buf_assert_invariants(const struct lexbuf *const buf)
16+
{
17+
assert(buf != NULL);
18+
assert(buf->untrusted_cursor != NULL);
19+
assert(buf->outbuf != NULL);
20+
assert(buf->end != NULL);
21+
assert(buf->outbuf <= buf->untrusted_cursor);
22+
assert(buf->untrusted_cursor <= buf->end);
23+
assert(*buf->end == '\0');
24+
}
25+
1126
static bool buf_consume(struct lexbuf *const buf, uint8_t const expected)
1227
{
28+
buf_assert_invariants(buf);
29+
assert(expected != '\0');
1330
bool ret = buf->untrusted_cursor[0] == expected;
1431
buf->untrusted_cursor += ret;
1532
return ret;
1633
}
1734

35+
/*
36+
* Add this amount of bytes to the buffer.
37+
* Advances the output cursor.
38+
* Returns pointer to the start of the data.
39+
* Pass NULL to shift data in-place and advance the input cursor too.
40+
*/
41+
static uint8_t *buf_put(struct lexbuf *buf, const uint8_t *ptr, size_t size)
42+
{
43+
buf_assert_invariants(buf);
44+
uint8_t *const outbuf = buf->outbuf;
45+
if (size != 0) {
46+
if (ptr != NULL) {
47+
assert((size_t)(buf->untrusted_cursor - outbuf) >= size);
48+
memmove(outbuf, ptr, size);
49+
} else {
50+
assert((size_t)(buf->end - buf->untrusted_cursor) >= size);
51+
memmove(outbuf, buf->untrusted_cursor, size);
52+
buf->untrusted_cursor += size;
53+
}
54+
buf->outbuf = outbuf + size;
55+
}
56+
return outbuf;
57+
}
58+
59+
static void buf_putc(struct lexbuf *buf, uint8_t c)
60+
{
61+
buf_assert_invariants(buf);
62+
assert(buf->outbuf < buf->untrusted_cursor);
63+
*buf->outbuf++ = c;
64+
}
65+
1866
static bool is_delim(uint8_t const untrusted_c)
1967
{
2068
return untrusted_c == ' ' || untrusted_c == ',';
@@ -23,27 +71,39 @@ static bool is_delim(uint8_t const untrusted_c)
2371
/* Skip spaces and commas */
2472
static void buf_consume_delims(struct lexbuf *const buf)
2573
{
74+
buf_assert_invariants(buf);
2675
while (is_delim(buf->untrusted_cursor[0]))
2776
buf->untrusted_cursor++;
2877
}
2978

3079
static void consume_subpacket_number(struct lexbuf *const buf)
3180
{
81+
buf_assert_invariants(buf);
3282
char *endptr = NULL;
33-
long untrusted_subpacket_number = strtol((const char *)buf->untrusted_cursor, &endptr, 10);
83+
errno = 0;
84+
const uint8_t *const last = buf->untrusted_cursor;
85+
long untrusted_subpacket_number = strtol((const char *)last, &endptr, 10);
3486
if (endptr == NULL)
3587
abort();
36-
if ((const uint8_t *)endptr == buf->untrusted_cursor)
88+
// Safety: strtol() guarantees that this is not past the NUL terminator.
89+
buf->untrusted_cursor = (uint8_t *)endptr;
90+
if ((const uint8_t *)endptr == last)
3791
errx(1, "Invalid character in subpacket number list");
38-
if (untrusted_subpacket_number < 1 || untrusted_subpacket_number > 127)
92+
if (errno || untrusted_subpacket_number < 1 || untrusted_subpacket_number > 127)
3993
errx(1, "Subpacket number not valid (must be between 1 and 127 inclusive, got %ld)",
4094
untrusted_subpacket_number);
95+
char outbuf[4];
96+
int r = snprintf(outbuf, sizeof(outbuf), "%ld", untrusted_subpacket_number);
97+
if (r < 1 || r > 3)
98+
errx(1, "snprintf failed");
4199
switch (*endptr) {
42100
case '"':
43101
case ',':
44102
case ' ':
45103
case '\0':
46-
buf->untrusted_cursor = (uint8_t *)endptr;
104+
// This cannot fail an assertion because snprintf()
105+
// always uses the fewest bytes possible.
106+
buf_put(buf, (const uint8_t *)outbuf, (size_t)r);
47107
return;
48108
default:
49109
errx(1, "Invalid character %c following subpacket number", *endptr);
@@ -53,13 +113,21 @@ static void consume_subpacket_number(struct lexbuf *const buf)
53113
static void consume_subpackets_argument(struct lexbuf *const buf)
54114
{
55115
if (buf_consume(buf, '"')) {
116+
buf_putc(buf, '"');
56117
if (!strchr((const char *)buf->untrusted_cursor, '"'))
57118
errx(1, "Unterminated quoted option argument");
58119
buf_consume_delims(buf);
120+
bool seen_subpacket_number = false;
59121
while (!buf_consume(buf, '"')) {
122+
if (*buf->untrusted_cursor == '\0')
123+
errx(1, "Unterminated quoted option argument");
124+
if (seen_subpacket_number)
125+
buf_putc(buf, ',');
126+
seen_subpacket_number = true;
60127
consume_subpacket_number(buf);
61128
buf_consume_delims(buf);
62129
}
130+
buf_putc(buf, '"');
63131
} else {
64132
consume_subpacket_number(buf);
65133
}
@@ -97,13 +165,10 @@ static const struct listopt *find_option(const uint8_t *const untrusted_option,
97165
return options;
98166
}
99167

100-
void sanitize_option_list(const struct listopt *allowed_list_options, const char *const msg)
168+
static void fixup_option_list(const struct listopt *allowed_list_options,
169+
const char *const msg,
170+
struct lexbuf *const buf)
101171
{
102-
if (!strcmp(optarg, "help"))
103-
return; // allow --list-options=help
104-
struct lexbuf buf_ = { .untrusted_cursor = (uint8_t *)optarg };
105-
struct lexbuf *const buf = &buf_;
106-
107172
for (const char *untrusted_c = optarg; *untrusted_c; untrusted_c++) {
108173
/* char is signed on x86, so the first check is not redundant */
109174
if (*untrusted_c < 0x20 || *untrusted_c > 0x7E)
@@ -113,14 +178,34 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
113178
for (;;) {
114179
/* Skip leading spaces and commas */
115180
buf_consume_delims(buf);
116-
if (buf->untrusted_cursor[0] == '\0')
117-
return; /* Nothing to do */
181+
if (buf->untrusted_cursor == buf->end) {
182+
/* Avoid failing an assertion if nothing was stripped. */
183+
if (buf->untrusted_cursor != buf->outbuf)
184+
buf_putc(buf, '\0');
185+
break; /* Nothing to do */
186+
}
187+
188+
if (buf->outbuf != (const uint8_t *)optarg) {
189+
/*
190+
* Add a comma to delimit options.
191+
* Proof that this will not assert:
192+
*
193+
* - Options must be followed by '\0', '=', ',' or ' '.
194+
* - If the option is followed by '\0' the loop exits above.
195+
* - If the option is followed by ' ' or ',', the above buf_consume_delims() call
196+
* advances the cursor, unless it has already been advanced below.
197+
* - If the option is followed by '=', it is required to be followed by ' ' or ','
198+
* after the option argument. buf_consume_delims() will consume it.
199+
*/
200+
buf_putc(buf, ',');
201+
}
118202

119203
/* Read the identifier */
120-
uint8_t *const untrusted_option = buf->untrusted_cursor;
121-
size_t const option_len = read_option(untrusted_option);
204+
size_t const option_len = read_option(buf->untrusted_cursor);
122205
if (option_len == 0)
123206
errx(1, "'=' not following a %s option", msg);
207+
/* Slide the option back. */
208+
uint8_t *untrusted_option = buf_put(buf, NULL, option_len);
124209

125210
bool const negated = option_len >= 3 &&
126211
memcmp(untrusted_option, "no-", 3) == 0;
@@ -134,8 +219,6 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
134219
errx(1, "Forbidden %s option %.*s", msg,
135220
(int)option_len, untrusted_option);
136221

137-
buf->untrusted_cursor += option_len;
138-
139222
while (buf_consume(buf, ' ')) {}
140223

141224
if (!buf_consume(buf, '='))
@@ -146,6 +229,8 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
146229
if (negated || !p->has_argument)
147230
errx(1, "%s option %.*s does not take an argument", msg,
148231
(int)option_len, untrusted_option);
232+
/* just consumed an '=' so this is okay */
233+
buf_putc(buf, '=');
149234

150235
if (buf->untrusted_cursor[0] && buf->untrusted_cursor[0] != ',')
151236
consume_subpackets_argument(buf);
@@ -155,3 +240,17 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
155240
"(found %c)", msg, buf->untrusted_cursor[0]);
156241
}
157242
}
243+
244+
void sanitize_option_list(const struct listopt *allowed_list_options, const char *const msg)
245+
{
246+
if (!strcmp(optarg, "help"))
247+
return; // allow --list-options=help
248+
struct lexbuf buf = {
249+
.untrusted_cursor = (uint8_t *)optarg,
250+
.outbuf = (uint8_t *)optarg,
251+
.end = (const uint8_t *)optarg + strlen(optarg),
252+
};
253+
fixup_option_list(allowed_list_options, msg, &buf);
254+
/* NUL terminate the buffer */
255+
memset(buf.outbuf, 0, (size_t)(buf.end - buf.outbuf));
256+
}

0 commit comments

Comments
 (0)