Skip to content

Commit da9b5f2

Browse files
committed
2 parents c38bce7 + 166d2b3 commit da9b5f2

3 files changed

Lines changed: 224 additions & 16 deletions

File tree

‎fuzz/from_chars.cc‎

Lines changed: 181 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,205 @@
11
#include "fast_float/fast_float.h"
22
#include <fuzzer/FuzzedDataProvider.h>
3+
#include <cstdint>
4+
#include <cstdio>
5+
#include <cstdlib>
6+
#include <cstring>
37
#include <string>
48
#include <system_error>
9+
#include <type_traits>
10+
#include <vector>
11+
12+
#if defined(__has_include)
13+
#if __has_include(<charconv>) && __cplusplus >= 201703L
14+
#include <charconv>
15+
#define FASTFLOAT_FUZZ_CHARCONV 1
16+
#endif
17+
#endif
18+
19+
static void fail(char const *check) {
20+
fprintf(stderr, "fast_float fuzz failure: %s\n", check);
21+
abort();
22+
}
523

624
fast_float::chars_format arbitrary_format(FuzzedDataProvider &fdp) {
725
using fast_float::chars_format;
8-
switch (fdp.ConsumeIntegralInRange<int>(0, 3)) {
26+
chars_format format;
27+
switch (fdp.ConsumeIntegralInRange<int>(0, 7)) {
928
case 0:
10-
return chars_format::scientific;
29+
format = chars_format::scientific;
1130
break;
1231
case 1:
13-
return chars_format::fixed;
32+
format = chars_format::fixed;
1433
break;
1534
case 2:
16-
return chars_format::fixed;
35+
format = chars_format::json;
36+
break;
37+
case 3:
38+
format = chars_format::json_or_infnan;
39+
break;
40+
case 4:
41+
format = chars_format::fortran;
1742
break;
43+
case 5:
44+
format = chars_format::javascript;
45+
break;
46+
case 6:
47+
format = chars_format::javascript_sloppy;
48+
break;
49+
default:
50+
format = chars_format::general;
51+
break;
52+
}
53+
if (fdp.ConsumeBool()) {
54+
format = format | chars_format::allow_leading_plus;
55+
}
56+
if (fdp.ConsumeBool()) {
57+
format = format | chars_format::skip_white_space;
58+
}
59+
return format;
60+
}
61+
62+
template <typename T> uint64_t bits_of(T value) {
63+
uint64_t bits = 0;
64+
memcpy(&bits, &value, sizeof(T));
65+
return bits;
66+
}
67+
68+
// The same ASCII text must parse identically for every character type: same
69+
// error code, same number of consumed code units, bit-identical value.
70+
template <typename UC>
71+
void check_widened(std::string const &input,
72+
fast_float::from_chars_result_t<char> expected,
73+
double expected_value, fast_float::chars_format format) {
74+
std::vector<UC> wide;
75+
wide.reserve(input.size());
76+
for (char c : input) {
77+
wide.push_back(UC(static_cast<unsigned char>(c)));
78+
}
79+
double value = 0.0;
80+
auto result = fast_float::from_chars(wide.data(), wide.data() + wide.size(),
81+
value, format);
82+
if (result.ec != expected.ec ||
83+
(result.ptr - wide.data()) != (expected.ptr - input.data())) {
84+
fail("wide result disagrees with char result");
85+
}
86+
if (expected.ec == std::errc() && bits_of(value) != bits_of(expected_value)) {
87+
fail("wide value disagrees with char value");
88+
}
89+
}
90+
91+
// Every number format accepted by the library is pure ASCII, so a successful
92+
// parse must never consume a code unit >= 0x80. A violation means some helper
93+
// inspected only the low byte of a wide code unit (the bug behind #391).
94+
// The generated sequences keep an ASCII low byte but take hostile high bits:
95+
// top-bit-set units (near the minimum of a signed wchar_t, the shape behind
96+
// #422), sign-extended negative units (the shape behind #395), and non-ASCII
97+
// code points whose low byte is an ASCII digit (the shape behind #391).
98+
template <typename UC>
99+
void check_arbitrary_units(FuzzedDataProvider &fdp,
100+
fast_float::chars_format format) {
101+
using unsigned_uc = typename std::make_unsigned<UC>::type;
102+
size_t count = fdp.ConsumeIntegralInRange<size_t>(0, 64);
103+
std::vector<UC> units;
104+
units.reserve(count);
105+
for (size_t i = 0; i < count; ++i) {
106+
unsigned_uc unit = fdp.ConsumeIntegral<uint8_t>();
107+
switch (fdp.ConsumeIntegralInRange<int>(0, 4)) {
108+
case 0: // top bit set: near the minimum value when UC is signed
109+
unit = unsigned_uc(unit | (unsigned_uc(1) << (sizeof(UC) * 8 - 1)));
110+
break;
111+
case 1: // all high bits set: a small negative value when UC is signed
112+
unit = unsigned_uc(unit | ~unsigned_uc(0xFF));
113+
break;
114+
case 2: // arbitrary high byte: a non-ASCII code point, ASCII low byte
115+
unit = unsigned_uc(
116+
unit | unsigned_uc(unsigned_uc(fdp.ConsumeIntegral<uint8_t>()) << 8));
117+
break;
118+
default: // plain ASCII unit
119+
break;
120+
}
121+
units.push_back(UC(unit));
122+
}
123+
if (units.empty()) {
124+
return;
125+
}
126+
UC const *begin = units.data();
127+
UC const *end = begin + units.size();
128+
129+
double as_double = 0.0;
130+
auto result = fast_float::from_chars(begin, end, as_double, format);
131+
if (result.ec == std::errc()) {
132+
for (UC const *p = begin; p != result.ptr; ++p) {
133+
if (static_cast<unsigned_uc>(*p) >= 0x80) {
134+
fail("float parse consumed a non-ASCII code unit");
135+
}
136+
}
137+
}
138+
uint16_t as_uint16 = 0;
139+
auto int_result = fast_float::from_chars(begin, end, as_uint16);
140+
if (int_result.ec == std::errc()) {
141+
for (UC const *p = begin; p != int_result.ptr; ++p) {
142+
if (static_cast<unsigned_uc>(*p) >= 0x80) {
143+
fail("integer parse consumed a non-ASCII code unit");
144+
}
145+
}
146+
}
147+
}
148+
149+
// fast_float's integer parsing is specified to match std::from_chars.
150+
template <typename T> void check_int(std::string const &input, int base) {
151+
T value{};
152+
auto result = fast_float::from_chars(
153+
input.data(), input.data() + input.size(), value, base);
154+
#ifdef FASTFLOAT_FUZZ_CHARCONV
155+
T expected{};
156+
auto std_result = std::from_chars(input.data(), input.data() + input.size(),
157+
expected, base);
158+
if (result.ec != std_result.ec || result.ptr != std_result.ptr) {
159+
fail("integer result disagrees with std::from_chars");
18160
}
19-
return chars_format::general;
161+
if (result.ec == std::errc() && value != expected) {
162+
fail("integer value disagrees with std::from_chars");
163+
}
164+
#else
165+
static_cast<void>(result);
166+
#endif
167+
}
168+
169+
template <typename T>
170+
void check_int_all_bases(std::string const &input, int base) {
171+
check_int<T>(input, 10);
172+
check_int<T>(input, base);
20173
}
21174

22175
extern "C" int LLVMFuzzerTestOneInput(uint8_t const *data, size_t size) {
23176
FuzzedDataProvider fdp(data, size);
24177
fast_float::chars_format format = arbitrary_format(fdp);
25-
double result_d = 0.0;
178+
int base = fdp.ConsumeIntegralInRange<int>(2, 36);
179+
26180
std::string input_d = fdp.ConsumeRandomLengthString(128);
27-
auto answer = fast_float::from_chars(
181+
double result_d = 0.0;
182+
auto answer_d = fast_float::from_chars(
28183
input_d.data(), input_d.data() + input_d.size(), result_d, format);
29-
std::string input_f = fdp.ConsumeRandomLengthString(128);
30184
float result_f = 0.0;
31-
answer = fast_float::from_chars(
32-
input_f.data(), input_f.data() + input_f.size(), result_f, format);
33-
int result_i = 0;
185+
fast_float::from_chars(input_d.data(), input_d.data() + input_d.size(),
186+
result_f, format);
187+
check_widened<wchar_t>(input_d, answer_d, result_d, format);
188+
check_widened<char16_t>(input_d, answer_d, result_d, format);
189+
check_widened<char32_t>(input_d, answer_d, result_d, format);
190+
34191
std::string input_i = fdp.ConsumeRandomLengthString(128);
35-
answer = fast_float::from_chars(input_i.data(),
36-
input_i.data() + input_i.size(), result_i);
192+
check_int_all_bases<uint8_t>(input_i, base);
193+
check_int_all_bases<int8_t>(input_i, base);
194+
check_int_all_bases<uint16_t>(input_i, base);
195+
check_int_all_bases<int16_t>(input_i, base);
196+
check_int_all_bases<uint32_t>(input_i, base);
197+
check_int_all_bases<int32_t>(input_i, base);
198+
check_int_all_bases<uint64_t>(input_i, base);
199+
check_int_all_bases<int64_t>(input_i, base);
200+
201+
check_arbitrary_units<wchar_t>(fdp, format);
202+
check_arbitrary_units<char16_t>(fdp, format);
203+
check_arbitrary_units<char32_t>(fdp, format);
37204
return 0;
38-
}
205+
}

‎include/fast_float/parse_number.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ clinger_fast_path_impl(am_mant_t const mantissa, am_pow_t const exponent,
248248
// Only std::float16_t can overflow here (e.g., "656e2"); let the
249249
// slow path report result_out_of_range.
250250
if (binary_format<T>::fast_path_can_overflow() &&
251-
value > (std::numeric_limits<T>::max)()) {
251+
value > std::numeric_limits<T>::max()) {
252252
return false;
253253
}
254254
}
@@ -590,7 +590,7 @@ FASTFLOAT_CONSTEXPR20
590590
const auto is_negative = mantissa < 0;
591591
// Negate as unsigned: -mantissa overflows for the most negative int64_t.
592592
const auto m = is_negative ? am_mant_t(0) - static_cast<am_mant_t>(mantissa)
593-
: static_cast<am_mant_t>(mantissa);
593+
: static_cast<am_mant_t>(mantissa);
594594
#else
595595
FASTFLOAT_ASSUME(mantissa >= 0);
596596
const auto m = static_cast<am_mant_t>(mantissa);

‎tests/fixedwidthtest.cpp‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,48 @@
22
#include <iostream>
33
#include <vector>
44
#include <cstring>
5+
#include <limits>
6+
#include <string_view>
57
#include "fast_float/fast_float.h"
68
#include <cstdint>
79

810
#if __cplusplus >= 202300L
911
#include <stdfloat>
1012
#endif
1113

14+
// If no characters match the pattern, value must be left unmodified
15+
// (see https://github.com/fastfloat/fast_float/issues/424).
16+
template <typename T> bool check_invalid_input_preserves_value() {
17+
std::vector<std::string_view> const invalid_inputs{"x", "", "-", ".", "e1",
18+
"-x", "na", "+"};
19+
for (auto const &f : invalid_inputs) {
20+
T result = T(42);
21+
auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result);
22+
if (answer.ec != std::errc::invalid_argument || answer.ptr != f.data()) {
23+
std::cerr << "Expected invalid_argument for input: \"" << f << "\""
24+
<< std::endl;
25+
return false;
26+
}
27+
if (result != T(42)) {
28+
std::cerr << "Value modified on invalid input: \"" << f << "\" got "
29+
<< result << std::endl;
30+
return false;
31+
}
32+
}
33+
// Out-of-range input must still store the documented value.
34+
std::string_view const too_large = "1e999";
35+
T result = T(42);
36+
auto answer = fast_float::from_chars(
37+
too_large.data(), too_large.data() + too_large.size(), result);
38+
if (answer.ec != std::errc::result_out_of_range ||
39+
result != std::numeric_limits<T>::infinity()) {
40+
std::cerr << "Expected infinity and result_out_of_range for \"1e999\""
41+
<< std::endl;
42+
return false;
43+
}
44+
return true;
45+
}
46+
1247
int main() {
1348
// Write some testcases for the parsing of floating point numbers in the
1449
// float32_t type. We use the from_chars function defined in this library.
@@ -33,6 +68,9 @@ int main() {
3368
return EXIT_FAILURE;
3469
}
3570
}
71+
if (!check_invalid_input_preserves_value<std::float32_t>()) {
72+
return EXIT_FAILURE;
73+
}
3674
#else
3775
std::cout << "No std::float32_t type available." << std::endl;
3876
#endif
@@ -60,6 +98,9 @@ int main() {
6098
return EXIT_FAILURE;
6199
}
62100
}
101+
if (!check_invalid_input_preserves_value<std::float64_t>()) {
102+
return EXIT_FAILURE;
103+
}
63104
#else
64105
std::cout << "No std::float64_t type available." << std::endl;
65106
#endif

0 commit comments

Comments
 (0)