Skip to content

Commit fc3abcf

Browse files
authored
Fix four out-of-bounds reads in the lexer and tag URI validation (#547)
Found by running `tests/fuzz_test` under `libFuzzer` and `AddressSanitizer`. Each of these reads past the end of the input buffer and reproduces on develop with an input of at most 35 bytes: * "!<" - `extract_tag_name()` advances past `m_end_itr` before the scanning loop's end test can fire. * "!!a%41" - `uri_encoding::validate()` advances the iterator twice per percent escape, so an escape at the end of the URI moves it past end. The character after a valid `%XX` was also never validated. * "%TAG ! " - `scan_tag_directive()` dereferences `m_cur_itr` after `skip_white_spaces()` may have consumed the rest of the buffer. * "\"\\\\\\\"" - the backslash counting loop in `determine_double_quoted_scalar_range()` never examines the token's first character, so an odd run of backslashes is read as even and the scalar parser is handed a token one byte short. Two results change for inputs that did not crash: "!!a%41^" is now rejected and "!!%41" is now accepted. Fixes #546
1 parent f51e0b5 commit fc3abcf

4 files changed

Lines changed: 79 additions & 8 deletions

File tree

include/fkYAML/detail/encodings/uri_encoding.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ class uri_encoding {
3737
return false;
3838
}
3939

40+
// validate_octets() advances `current` past the last octet it consumed. Without
41+
// moving it back, the loop's own ++current skips the character which follows the
42+
// escape sequence, and, when the escape ends the sequence, moves `current` one
43+
// past `end` so that the loop condition never holds and reads out of bounds.
44+
--current;
4045
continue;
4146
}
4247

include/fkYAML/detail/input/lexical_analyzer.hpp

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,11 @@ class lexical_analyzer {
554554
// extract a tag prefix.
555555
//
556556

557+
// skip_white_spaces() above may have consumed the rest of the input buffer.
558+
if FK_YAML_UNLIKELY (m_cur_itr == m_end_itr) {
559+
emit_error("invalid TAG directive is found.");
560+
}
561+
557562
m_token_begin_itr = m_cur_itr;
558563
const char* p_tag_prefix_begin = m_cur_itr;
559564
switch (*m_cur_itr) {
@@ -684,7 +689,9 @@ class lexical_analyzer {
684689
case '<':
685690
// Verbatim tags (!<TAG>)
686691
is_verbatim = true;
687-
++m_cur_itr;
692+
if FK_YAML_UNLIKELY (++m_cur_itr == m_end_itr) {
693+
emit_error("verbatim tag (!<TAG>) must be ended with \'>\'.");
694+
}
688695
break;
689696
default:
690697
// Either local tags (!suffix) or named handles (!tag!suffix)
@@ -816,12 +823,18 @@ class lexical_analyzer {
816823
// * even number of backslashes -> double quotation mark IS NOT escaped (e.g., "\\"")
817824
uint32_t backslash_counts = 0;
818825
const char* p = m_token_begin_itr + (pos - 1);
819-
do {
820-
if (*p-- != '\\') {
826+
for (;;) {
827+
if (*p != '\\') {
821828
break;
822829
}
823830
++backslash_counts;
824-
} while (p != m_token_begin_itr);
831+
if (p == m_token_begin_itr) {
832+
// the first character of the token has just been counted. Stopping here
833+
// also keeps `p` from being decremented past the beginning of the token.
834+
break;
835+
}
836+
--p;
837+
}
825838
is_closed = ((backslash_counts & 1u) == 0); // true: even, false: odd
826839
}
827840

single_include/fkYAML/node.hpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,11 @@ class uri_encoding {
13101310
return false;
13111311
}
13121312

1313+
// validate_octets() advances `current` past the last octet it consumed. Without
1314+
// moving it back, the loop's own ++current skips the character which follows the
1315+
// escape sequence, and, when the escape ends the sequence, moves `current` one
1316+
// past `end` so that the loop condition never holds and reads out of bounds.
1317+
--current;
13131318
continue;
13141319
}
13151320

@@ -3782,6 +3787,11 @@ class lexical_analyzer {
37823787
// extract a tag prefix.
37833788
//
37843789

3790+
// skip_white_spaces() above may have consumed the rest of the input buffer.
3791+
if FK_YAML_UNLIKELY (m_cur_itr == m_end_itr) {
3792+
emit_error("invalid TAG directive is found.");
3793+
}
3794+
37853795
m_token_begin_itr = m_cur_itr;
37863796
const char* p_tag_prefix_begin = m_cur_itr;
37873797
switch (*m_cur_itr) {
@@ -3912,7 +3922,9 @@ class lexical_analyzer {
39123922
case '<':
39133923
// Verbatim tags (!<TAG>)
39143924
is_verbatim = true;
3915-
++m_cur_itr;
3925+
if FK_YAML_UNLIKELY (++m_cur_itr == m_end_itr) {
3926+
emit_error("verbatim tag (!<TAG>) must be ended with \'>\'.");
3927+
}
39163928
break;
39173929
default:
39183930
// Either local tags (!suffix) or named handles (!tag!suffix)
@@ -4044,12 +4056,18 @@ class lexical_analyzer {
40444056
// * even number of backslashes -> double quotation mark IS NOT escaped (e.g., "\\"")
40454057
uint32_t backslash_counts = 0;
40464058
const char* p = m_token_begin_itr + (pos - 1);
4047-
do {
4048-
if (*p-- != '\\') {
4059+
for (;;) {
4060+
if (*p != '\\') {
40494061
break;
40504062
}
40514063
++backslash_counts;
4052-
} while (p != m_token_begin_itr);
4064+
if (p == m_token_begin_itr) {
4065+
// the first character of the token has just been counted. Stopping here
4066+
// also keeps `p` from being decremented past the beginning of the token.
4067+
break;
4068+
}
4069+
--p;
4070+
}
40534071
is_closed = ((backslash_counts & 1u) == 0); // true: even, false: odd
40544072
}
40554073

tests/unit_test/test_fuzz_regression.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,39 @@ TEST_CASE("FuzzRegression") {
118118
p_end = p_begin + sizeof(input);
119119
REQUIRE_THROWS_AS(root = fkyaml::node::deserialize(p_begin, p_end), fkyaml::invalid_encoding);
120120
}
121+
122+
SUBCASE("verbatim tag which ends the input buffer") {
123+
const char input[] = "!<";
124+
p_begin = input;
125+
p_end = input + sizeof(input) - 1;
126+
REQUIRE_THROWS_AS(root = fkyaml::node::deserialize(p_begin, p_end), fkyaml::parse_error);
127+
}
128+
129+
SUBCASE("TAG directive without a tag prefix") {
130+
const char input[] = "%TAG ! ";
131+
p_begin = input;
132+
p_end = input + sizeof(input) - 1;
133+
REQUIRE_THROWS_AS(root = fkyaml::node::deserialize(p_begin, p_end), fkyaml::parse_error);
134+
}
135+
136+
SUBCASE("percent escape at the end of a tag URI") {
137+
const char input[] = "!!a%41";
138+
p_begin = input;
139+
p_end = input + sizeof(input) - 1;
140+
REQUIRE_NOTHROW(root = fkyaml::node::deserialize(p_begin, p_end));
141+
}
142+
143+
SUBCASE("invalid URI character right after a percent escape") {
144+
const char input[] = "!!a%41^";
145+
p_begin = input;
146+
p_end = input + sizeof(input) - 1;
147+
REQUIRE_THROWS_AS(root = fkyaml::node::deserialize(p_begin, p_end), fkyaml::parse_error);
148+
}
149+
150+
SUBCASE("double quoted scalar closed by an escaped quotation mark") {
151+
const char input[] = "\"\\\\\\\"";
152+
p_begin = input;
153+
p_end = input + sizeof(input) - 1;
154+
REQUIRE_THROWS_AS(root = fkyaml::node::deserialize(p_begin, p_end), fkyaml::parse_error);
155+
}
121156
}

0 commit comments

Comments
 (0)