Fixes #28260.
Avoid reading past the supplied end pointer in two places:
- matching JSON keywords with strncmp;
- validating number prefixes with firstDigit[1].
Also update the UniValue unit test and parse_univalue fuzz target to exercise bounded input.
Fixes #28260.
Avoid reading past the supplied end pointer in two places:
Also update the UniValue unit test and parse_univalue fuzz target to exercise bounded input.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35281.
<!--021abf342d371248e50ceaed478a90ca-->
See the guideline and AI policy for information on the review process.
| Type | Reviewers |
|---|---|
| Concept ACK | sedited |
| Stale ACK | carloantinarella |
If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
17 | @@ -18,7 +18,9 @@ void initialize_parse_univalue() 18 | 19 | FUZZ_TARGET(parse_univalue, .init = initialize_parse_univalue) 20 | { 21 | - const std::string random_string(buffer.begin(), buffer.end()); 22 | + const char* const buffer_data{ 23 | + buffer.empty() ? "" : reinterpret_cast<const char*>(buffer.data())};
why the ??
You're right, it's not needed. I fixed it in a new commit.
<!--85328a0da195eb286784d51f73fa0af9-->
đźš§ At least one of the CI tasks failed.
<sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/25806359727/job/75815150297</sub>
<sub>LLM reason (✨ experimental): CI failed because the IWYU (include-what-you-use) check flagged missing/wrong includes and intentionally returned non-zero exit status (“Failure generated from IWYU”).</sub>
<details><summary>Hints</summary>
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
</details>
213 | + expect_json_token("false", /*size=*/5, JTOK_KW_FALSE, /*expected_consumed=*/5); 214 | + 215 | + expect_json_token("-0", 1, JTOK_ERR); 216 | + expect_json_token("-x", 1, JTOK_ERR); 217 | + expect_json_token("01", /*size=*/1, JTOK_NUMBER, /*expected_consumed=*/1, "0"); 218 | + expect_json_token("-01", /*size=*/2, JTOK_NUMBER, /*expected_consumed=*/2, "-0");
Would it be the case to add also few tests for fractions and exp numbers?
Added in commit 28f609c7bb — eight tests covering fractions, exponents, and combined, using the same size/end-pointer pattern as the existing tests in this function.
tACK 28f609c7bb78c233356470b9399c35b3183563a6
Thanks for the added tests. One comment: my suggestion is to squash the two commits into a single one, as they do not seem to represent different logical steps.
94 | @@ -95,21 +95,23 @@ enum jtokentype getJsonToken(std::string& tokenVal, unsigned int& consumed, 95 | 96 | case 'n': 97 | case 't': 98 | - case 'f': 99 | - if (!strncmp(raw, "null", 4)) { 100 | + case 'f': { 101 | + const std::string_view raw_view{raw, static_cast<size_t>(end - raw)};
I would prefer if this used the more straight forward std::string_view raw_view{raw, end} than doing pointer arithmetic.
230 | + expect_json_token("-1e-5", /*size=*/5, JTOK_NUMBER, /*expected_consumed=*/5, "-1e-5"); 231 | + expect_json_token("1e", /*size=*/2, JTOK_ERR); 232 | + 233 | + // fractions + exponents combined 234 | + expect_json_token("1.5e3", /*size=*/5, JTOK_NUMBER, /*expected_consumed=*/5, "1.5e3"); 235 | + expect_json_token("-1.5e-3", /*size=*/7, JTOK_NUMBER, /*expected_consumed=*/7, "-1.5e-3");
Isn't this tested already in the fixture files?
204 | + 205 | +void get_json_token_end_test() 206 | +{ 207 | + expect_json_token("null", 3, JTOK_ERR); 208 | + expect_json_token("true", 3, JTOK_ERR); 209 | + expect_json_token("false", 4, JTOK_ERR);
Is this really worthwhile? Seems like these kind of cases are better covered in the fuzz test anyway.
208 | + expect_json_token("true", 3, JTOK_ERR); 209 | + expect_json_token("false", 4, JTOK_ERR); 210 | + 211 | + expect_json_token("null", /*size=*/4, JTOK_KW_NULL, /*expected_consumed=*/4); 212 | + expect_json_token("true", /*size=*/4, JTOK_KW_TRUE, /*expected_consumed=*/4); 213 | + expect_json_token("false", /*size=*/5, JTOK_KW_FALSE, /*expected_consumed=*/5);
Isn't this covered in the test fixtures already?
Concept ACK
I think most of these unit tests are redundant however. Also not sure we should be introducing a new utility for this. I think the only cases actually adding a bit of coverage is the leading -01 case. Maybe that should just be covered by some more test fixture files?
EDIT:
It would also be good to mention that this is fixing two separate out of bounds reads.
@ferminquant can you respond to the review here? It's been a month.
242 | @@ -194,6 +243,7 @@ int main(int argc, char* argv[])
243 |
244 | unescape_unicode_test();
245 | no_nul_test();
can be removed as well, given the fuzz test, and the other tests.
Thanks for the review. Done in commit 7f633b3beaa7778d0e2dcc87b9cd2baf5490c939:
The focused UniValue test builds and passes locally.
This should be a string view on a buffer, like in the fuzz test
Well, this is adding back the null-byte
193 | + 194 | + char number[] = "-01"; 195 | + assert(val.read({number, 2})); 196 | + assert(val.isNum()); 197 | + assert(val.getValStr() == "-0"); 198 | +}
Seems fine, but this should either be a correctly-sized buffer (without null-term), or it should be moved to a fixture file.
Thanks, fixed in d107f950e8:
The focused UniValue test passes locally.
Avoid reading past the supplied end pointer when matching JSON keywords and validating number prefixes.
Also update the UniValue test and parse_univalue fuzz target to exercise bounded input.
65 | @@ -67,15 +66,14 @@ 66 | #include <string_view> 67 | #include <tuple> 68 | 69 | -static std::string rtrim(std::string s) 70 | +static std::string_view rtrim(std::string_view s) 71 | { 72 | - s.erase(s.find_last_not_of(" \n\r\t") + 1); 73 | - return s; 74 | + return s.substr(0, s.find_last_not_of(" \n\r\t") + 1);
Either this needs to be copied into a fresh buffer (so that truly no data follows), or the tests need to be adjusted to not have trailing whitespace.