Skip to content

Commit 6d97051

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 6d97051

1 file changed

Lines changed: 121 additions & 22 deletions

File tree

src/gpg-list-options.c

Lines changed: 121 additions & 22 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,30 +165,40 @@ 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-
107-
for (const char *untrusted_c = optarg; *untrusted_c; untrusted_c++) {
108-
/* char is signed on x86, so the first check is not redundant */
109-
if (*untrusted_c < 0x20 || *untrusted_c > 0x7E)
110-
errx(1, "Non-ASCII byte %" PRIu8 " forbidden in %s option", (uint8_t)*untrusted_c, msg);
111-
}
112-
172+
bool seen_option = false;
113173
for (;;) {
114174
/* Skip leading spaces and commas */
115175
buf_consume_delims(buf);
116-
if (buf->untrusted_cursor[0] == '\0')
117-
return; /* Nothing to do */
176+
if (buf->untrusted_cursor == buf->end) {
177+
break; /* Nothing to do. Caller will NUL-terminate the buffer. */
178+
}
179+
180+
if (seen_option) {
181+
/*
182+
* Add a comma to delimit options.
183+
* Proof that this will not assert:
184+
*
185+
* - Options must be followed by '\0', '=', ',' or ' '.
186+
* - If the option is followed by '\0' the loop exits above.
187+
* - If the option is followed by ' ' or ',', the above buf_consume_delims() call
188+
* advances the cursor, unless it has already been advanced below.
189+
* - If the option is followed by '=', it is required to be followed by ' ' or ','
190+
* after the option argument. buf_consume_delims() will consume it.
191+
*/
192+
buf_putc(buf, ',');
193+
}
194+
seen_option = true;
118195

119196
/* Read the identifier */
120-
uint8_t *const untrusted_option = buf->untrusted_cursor;
121-
size_t const option_len = read_option(untrusted_option);
197+
size_t const option_len = read_option(buf->untrusted_cursor);
122198
if (option_len == 0)
123199
errx(1, "'=' not following a %s option", msg);
200+
/* Slide the option back. */
201+
uint8_t *untrusted_option = buf_put(buf, NULL, option_len);
124202

125203
bool const negated = option_len >= 3 &&
126204
memcmp(untrusted_option, "no-", 3) == 0;
@@ -134,8 +212,6 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
134212
errx(1, "Forbidden %s option %.*s", msg,
135213
(int)option_len, untrusted_option);
136214

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

141217
if (!buf_consume(buf, '='))
@@ -146,6 +222,8 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
146222
if (negated || !p->has_argument)
147223
errx(1, "%s option %.*s does not take an argument", msg,
148224
(int)option_len, untrusted_option);
225+
/* just consumed an '=' so this is okay */
226+
buf_putc(buf, '=');
149227

150228
if (buf->untrusted_cursor[0] && buf->untrusted_cursor[0] != ',')
151229
consume_subpackets_argument(buf);
@@ -155,3 +233,24 @@ void sanitize_option_list(const struct listopt *allowed_list_options, const char
155233
"(found %c)", msg, buf->untrusted_cursor[0]);
156234
}
157235
}
236+
237+
void sanitize_option_list(const struct listopt *allowed_list_options, const char *const msg)
238+
{
239+
if (!strcmp(optarg, "help"))
240+
return; // allow --list-options=help
241+
242+
for (const char *untrusted_c = optarg; *untrusted_c; untrusted_c++) {
243+
/* char is signed on x86, so the first check is not redundant */
244+
if (*untrusted_c < 0x20 || *untrusted_c > 0x7E)
245+
errx(1, "Non-ASCII byte %" PRIu8 " forbidden in %s option", (uint8_t)*untrusted_c, msg);
246+
}
247+
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)