Skip to content

Commit 20454ba

Browse files
committed
Fix handling out of of range exponent in numbers
Fix: #970 If the parsed exponent overflows a `int32_t` passing it to ryu is incorrect. We could pass it to `rb_cstr_to_dbl` but then Ruby will emit an annoying warning, instead we can coerce to `0.0` and `Inf`.
1 parent 0e99fcb commit 20454ba

3 files changed

Lines changed: 22 additions & 4 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Unreleased
44

5+
* Fix parsing of out of range floats (very large exponents that lead ot either `0.0` or `Inf`).
6+
57
### 2026-03-25 (2.19.3)
68

79
* Fix handling of unescaped control characters preceeded by a backslash.

ext/json/ext/parser/parser.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -855,21 +855,29 @@ NOINLINE(static) VALUE json_decode_large_float(const char *start, long len)
855855
/* Ruby JSON optimized float decoder using vendored Ryu algorithm
856856
* Accepts pre-extracted mantissa and exponent from first-pass validation
857857
*/
858-
static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, int mantissa_digits, int32_t exponent, bool negative,
858+
static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, int mantissa_digits, int64_t exponent, bool negative,
859859
const char *start, const char *end)
860860
{
861861
if (RB_UNLIKELY(config->decimal_class)) {
862862
VALUE text = rb_str_new(start, end - start);
863863
return rb_funcallv(config->decimal_class, config->decimal_method_id, 1, &text);
864864
}
865865

866+
if (RB_UNLIKELY(exponent > INT32_MAX)) {
867+
return CInfinity;
868+
}
869+
870+
if (RB_UNLIKELY(exponent < INT32_MIN)) {
871+
return rb_float_new(0.0);
872+
}
873+
866874
// Fall back to rb_cstr_to_dbl for potential subnormals (rare edge case)
867875
// Ryu has rounding issues with subnormals around 1e-310 (< 2.225e-308)
868876
if (RB_UNLIKELY(mantissa_digits > 17 || mantissa_digits + exponent < -307)) {
869877
return json_decode_large_float(start, end - start);
870878
}
871879

872-
return DBL2NUM(ryu_s2d_from_parts(mantissa, mantissa_digits, exponent, negative));
880+
return DBL2NUM(ryu_s2d_from_parts(mantissa, mantissa_digits, (int32_t)exponent, negative));
873881
}
874882

875883
static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig *config, long count)
@@ -1144,7 +1152,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
11441152
const char first_digit = *state->cursor;
11451153

11461154
// Variables for Ryu optimization - extract digits during parsing
1147-
int32_t exponent = 0;
1155+
int64_t exponent = 0;
11481156
int decimal_point_pos = -1;
11491157
uint64_t mantissa = 0;
11501158

@@ -1188,7 +1196,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
11881196
raise_parse_error_at("invalid number: %s", state, start);
11891197
}
11901198

1191-
exponent = negative_exponent ? -((int32_t)abs_exponent) : ((int32_t)abs_exponent);
1199+
exponent = negative_exponent ? -abs_exponent : abs_exponent;
11921200
}
11931201

11941202
if (integer) {

test/json/json_ryu_fallback_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,12 @@ def test_invalid_numbers_rejected
166166
end
167167
end
168168
end
169+
170+
def test_large_exponent_numbers
171+
assert_equal Float::INFINITY, JSON.parse("1e4294967296")
172+
assert_equal 0.0, JSON.parse("1e-4294967296")
173+
assert_equal 0.0, JSON.parse("99999999999999999e-4294967296")
174+
assert_equal Float::INFINITY, JSON.parse("1e4294967295")
175+
assert_equal Float::INFINITY, JSON.parse("1e4294967297")
176+
end
169177
end

0 commit comments

Comments
 (0)