Skip to content

Commit 3f91317

Browse files
fuzzyrichieclaude
authored andcommitted
Fix integer overflow in the number parser digit counter
json_parse_digits returns the number of digits consumed as an (int)(cursor - start). A JSON number whose digit run is 2^31 or longer (a >= 2GiB document) wraps the counter negative, and the mantissa_digits <= MAX_FAST_INTEGER_SIZE fast-path check on a long wrapped uint64 accumulator then returns a small garbage Integer instead of the number's true value: $ ruby -rjson -e 'p JSON.parse(File.read("huge.json"))[0].bit_length' 63 # a 2_147_483_664 digit number cannot be 63 bits The same wrap also defeats the exponent_digits >= 20 guard, letting a wrapped exponent accumulator reach the exponent. Widen the digit counters from int to ssize_t, so the routing comparisons (<= 19, == 20, > 18, >= 20) see the true count and route huge numbers to the existing large-number paths, which already derive their lengths from the raw text. ssize_t keeps the signed semantics the int counters relied on -- decimal_point_pos starts at -1, and json_decode_float does signed arithmetic in mantissa_digits + exponent < -307 with a negative int64 exponent -- which an unsigned size_t would break, while still being wide enough to hold the true digit count. Not feasible to regression-test from Ruby: the wrap needs a >= 2GiB digit run, and the corrected path converts it through rb_cstr2inum which is quadratic in the digit count. Validated empirically against a 2_147_483_664 digit document: before this change it returns a 63 bit Integer in 0.5s; after it correctly enters the large-number path. All numeric boundary probes (uint64 max, 19/20/21 digits, subnormals, 100k digit bignum) are byte-identical before and after, and the full test suite passes. Note: java/src/json/ext/Parser.java keeps the same int counters; I could not build JRuby here so I left it out of this change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cb82efc commit 3f91317

1 file changed

Lines changed: 9 additions & 9 deletions

File tree

‎ext/json/ext/parser/parser.c‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ NOINLINE(static) VALUE json_decode_large_integer(const char *start, rb_len_t len
11201120
return json_decode_large_number(start, len, json_decode_inum);
11211121
}
11221122

1123-
static inline VALUE json_decode_integer(uint64_t mantissa, int mantissa_digits, bool negative, const char *start, const char *end)
1123+
static inline VALUE json_decode_integer(uint64_t mantissa, ssize_t mantissa_digits, bool negative, const char *start, const char *end)
11241124
{
11251125
if (RB_LIKELY(mantissa_digits <= MAX_FAST_INTEGER_SIZE)) {
11261126
if (RB_LIKELY(!negative)) {
@@ -1164,7 +1164,7 @@ NOINLINE(static) VALUE json_decode_large_float(const char *start, rb_len_t len)
11641164
/* Ruby JSON optimized float decoder using vendored Ryu algorithm
11651165
* Accepts pre-extracted mantissa and exponent from first-pass validation
11661166
*/
1167-
static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, int mantissa_digits, int64_t exponent, bool negative,
1167+
static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, ssize_t mantissa_digits, int64_t exponent, bool negative,
11681168
const char *start, const char *end)
11691169
{
11701170
if (RB_UNLIKELY(config->decimal_class)) {
@@ -1421,7 +1421,7 @@ static inline uint64_t decode_4digits_unrolled(uint32_t val) {
14211421
}
14221422
#endif
14231423

1424-
static inline int json_parse_digits(JSON_ParserState *state, uint64_t *accumulator)
1424+
static inline ssize_t json_parse_digits(JSON_ParserState *state, uint64_t *accumulator)
14251425
{
14261426
const char *start = state->cursor;
14271427

@@ -1454,7 +1454,7 @@ static inline int json_parse_digits(JSON_ParserState *state, uint64_t *accumulat
14541454
state->cursor++;
14551455
}
14561456

1457-
return (int)(state->cursor - start);
1457+
return (ssize_t)(state->cursor - start);
14581458
}
14591459
#endif
14601460

@@ -1463,7 +1463,7 @@ static inline int json_parse_digits(JSON_ParserState *state, uint64_t *accumulat
14631463
*accumulator = *accumulator * 10 + (next_char - '0');
14641464
state->cursor++;
14651465
}
1466-
return (int)(state->cursor - start);
1466+
return (ssize_t)(state->cursor - start);
14671467
}
14681468

14691469
static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig *config, bool negative, const char *start, bool resumable)
@@ -1473,11 +1473,11 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
14731473

14741474
// Variables for Ryu optimization - extract digits during parsing
14751475
int64_t exponent = 0;
1476-
int decimal_point_pos = -1;
1476+
ssize_t decimal_point_pos = -1;
14771477
uint64_t mantissa = 0;
14781478

14791479
// Parse integer part and extract mantissa digits
1480-
int mantissa_digits = json_parse_digits(state, &mantissa);
1480+
ssize_t mantissa_digits = json_parse_digits(state, &mantissa);
14811481

14821482
if (RB_UNLIKELY((first_digit == '0' && mantissa_digits > 1) || (negative && mantissa_digits == 0))) {
14831483
return Qundef;
@@ -1489,7 +1489,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
14891489
decimal_point_pos = mantissa_digits; // Remember position of decimal point
14901490
state->cursor++;
14911491

1492-
int fractional_digits = json_parse_digits(state, &mantissa);
1492+
ssize_t fractional_digits = json_parse_digits(state, &mantissa);
14931493
mantissa_digits += fractional_digits;
14941494

14951495
if (RB_UNLIKELY(!fractional_digits)) {
@@ -1510,7 +1510,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
15101510
}
15111511

15121512
uint64_t abs_exponent = 0;
1513-
int exponent_digits = json_parse_digits(state, &abs_exponent);
1513+
ssize_t exponent_digits = json_parse_digits(state, &abs_exponent);
15141514

15151515
if (RB_UNLIKELY(!exponent_digits)) {
15161516
return Qundef;

0 commit comments

Comments
 (0)