From 5d5d318f4dba838d99605ebfe222a932f20f2ee8 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 18 Jun 2026 13:47:45 +0900 Subject: [PATCH 1/8] [DOC] Fix typo in WeakKeyMap#inspect --- weakmap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weakmap.c b/weakmap.c index 7cef1fd46a63a7..618c88bfa985f8 100644 --- a/weakmap.c +++ b/weakmap.c @@ -842,7 +842,7 @@ wkmap_clear(VALUE self) * call-seq: * map.inspect -> new_string * - * Returns a new String containing informations about the map: + * Returns a new String containing information about the map: * * m = ObjectSpace::WeakKeyMap.new * m[key] = value From 3ee3a41245418e13be970a8abb11bcb895cf1595 Mon Sep 17 00:00:00 2001 From: Daichi Kamiyama <32436625+dak2@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:27:32 +0900 Subject: [PATCH 2/8] ZJIT: Add constant-fold overflow boundary tests for Fixnum bops Cover the cases where folding a Fixnum add/sub/mult would overflow RUBY_FIXNUM_MAX/MIN into a Bignum, asserting the fold is rejected and the FixnumAdd/Sub/Mult node is kept. FixnumDiv is omitted because `test_dont_fold_fixnum_div_negative_one_overflow` already covers its only overflow case (RUBY_FIXNUM_MIN / -1). FixnumMod is omitted, a % b is bounded by |b| and can never overflow. --- zjit/src/hir/opt_tests.rs | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index a418c3008dd491..3d144471fc887f 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -201,6 +201,87 @@ mod hir_opt_tests { "); } + #[test] + fn test_no_fold_fixnum_add_overflow() { + eval(&format!(" + def test + {RUBY_FIXNUM_MAX} + 1 + end + ")); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[4611686018427387903] = Const Value(4611686018427387903) + v12:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1000, +@0x1008, cme:0x1010) + v23:Fixnum = FixnumAdd v10, v12 + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_no_fold_fixnum_sub_underflow() { + eval(&format!(" + def test + {RUBY_FIXNUM_MIN} - 1 + end + ")); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[-4611686018427387904] = Const Value(-4611686018427387904) + v12:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1000, -@0x1008, cme:0x1010) + v23:Fixnum = FixnumSub v10, v12 + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_no_fold_fixnum_mult_overflow() { + eval(&format!(" + def test + {RUBY_FIXNUM_MAX} * 2 + end + ")); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[4611686018427387903] = Const Value(4611686018427387903) + v12:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Integer@0x1000, *@0x1008, cme:0x1010) + v23:Fixnum = FixnumMult v10, v12 + CheckInterrupts + Return v23 + "); + } + #[test] fn test_fold_fixnum_sub_zero() { eval(" From 6a066f58bca76a26acac4bc08bff0d302b97f3e6 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:10:12 +0900 Subject: [PATCH 3/8] [ruby/json] Fix heap-use-after-free in JSON::ResumableParser#partial_value https://github.com/ruby/json/commit/e5fa06aa42 Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/json/parser/parser.c | 35 ++++++++++++++++++++---------- test/json/resumable_parser_test.rb | 20 +++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 136aab6ae764f5..fe87d5c442cf52 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2481,18 +2481,9 @@ static VALUE cResumableParser_clear(VALUE self) return self; } -/* - * call-seq: partial_value -> object - * - * Returns the Ruby objects parsed up to this point: - * parser << '[1, [2, 3,' - * parser.parse # => false - * parser.value # ArgumentError no ready value - * parser.partial_value # => [1, [2, 3]] - */ -static VALUE cResumableParser_partial_value(VALUE self) +static VALUE cResumableParser_partial_value_body(VALUE self) { - JSON_ResumableParser *original_parser = ResumableParser_acquire(self, false); + JSON_ResumableParser *original_parser = cResumableParser_get(self); JSON_ResumableParser parser = *original_parser; parser.state.frames = &parser.frames; @@ -2559,6 +2550,28 @@ static VALUE cResumableParser_partial_value(VALUE self) return partial_result; } +/* + * call-seq: partial_value -> object + * + * Returns the Ruby objects parsed up to this point: + * parser << '[1, [2, 3,' + * parser.parse # => false + * parser.value # ArgumentError no ready value + * parser.partial_value # => [1, [2, 3]] + */ +static VALUE cResumableParser_partial_value(VALUE self) +{ + JSON_ResumableParser *parser = ResumableParser_acquire(self, true); + + int status; + VALUE result = rb_protect(cResumableParser_partial_value_body, self, &status); + parser->in_use = false; + if (status) { + rb_jump_tag(status); + } + return result; +} + /* * call-seq: rest -> string * diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 52f1356a704f9a..6eeabf64597179 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -208,6 +208,26 @@ def test_reentrency_prevented assert_equal "ResumableParser can't be used recursively", error.message end + def test_reentrency_prevented_in_partial_value + parser = nil + callback = ->(o) do + # Arrays are only built while partial_value runs (the scalars were pushed by the + # earlier parse); re-entering here used to corrupt/free the shared frame stack. + parser.parse if o.is_a?(Array) + o + end + parser = new_parser(on_load: callback) + parser << '[1, [2, 3,' + parser.parse + error = assert_raise ArgumentError do + parser.partial_value + end + assert_equal "ResumableParser can't be used recursively", error.message + + # The in_use lock must be released even though partial_value raised. + refute_predicate parser, :value? + end + def test_exception_unlock_parser called = false parser = nil From 0416fb209eaafe9fe9f8c6c1568046d1153c8b28 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:11:05 +0900 Subject: [PATCH 4/8] [ruby/json] Fix JSON::ResumableParser#parse leaking the in_use lock on an empty buffer https://github.com/ruby/json/commit/3927c5ca4c Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/json/parser/parser.c | 1 + test/json/resumable_parser_test.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index fe87d5c442cf52..b67feacbc9e268 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2377,6 +2377,7 @@ static VALUE cResumableParser_parse(VALUE self) { JSON_ResumableParser *parser = ResumableParser_acquire(self, true); if (!parser->buffer) { + parser->in_use = false; return Qfalse; } diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 6eeabf64597179..329bbc34e4a232 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -48,6 +48,21 @@ def test_clear refute_predicate @parser, :value? end + def test_parse_with_empty_buffer_keeps_parser_usable + # parse before any feed must not leak the in_use lock + refute @parser.parse + @parser << '[1, 2, 3]' + assert @parser.parse + assert_equal [1, 2, 3], @parser.value + + # same after a clear with no following feed + @parser.clear + refute @parser.parse + @parser << '[4]' + assert @parser.parse + assert_equal [4], @parser.value + end + def test_parse_document_direct @parser << '[true]' assert_equal true, @parser.parse From f322dd1332cfd1f857e05633eb12aa372caa12c5 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:12:06 +0900 Subject: [PATCH 5/8] [ruby/json] Forbid JSON::ResumableParser#<< while the parser is in use https://github.com/ruby/json/commit/489b8c13b9 Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/json/parser/parser.c | 7 +++++-- test/json/resumable_parser_test.rb | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index b67feacbc9e268..e4f140d7c58c88 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2274,6 +2274,8 @@ static VALUE cResumableParser_initialize(int argc, VALUE *argv, VALUE self) return self; } +static JSON_ResumableParser *ResumableParser_acquire(VALUE self, bool lock); + /* * call-seq: self << string -> self * @@ -2282,13 +2284,14 @@ static VALUE cResumableParser_initialize(int argc, VALUE *argv, VALUE self) static VALUE cResumableParser_feed(VALUE self, VALUE str) { rb_check_frozen(self); + + JSON_ResumableParser *parser = ResumableParser_acquire(self, false); + str = convert_encoding(str); if (!RSTRING_LEN(str)) { return self; } - JSON_ResumableParser *parser = cResumableParser_get(self); - size_t offset = parser->state.cursor - parser->state.start; const size_t remaining = parser->state.end - parser->state.cursor; diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 329bbc34e4a232..40dcb4a46ef169 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -243,6 +243,26 @@ def test_reentrency_prevented_in_partial_value refute_predicate parser, :value? end + def test_feed_during_callback_prevented + parser = nil + callback = ->(o) do + parser << '99' if o == 1 # feeding while a parse is running must be rejected + o + end + parser = new_parser(on_load: callback) + parser << '[1, 2, 3]' + error = assert_raise ArgumentError do + parser.parse + end + assert_equal "ResumableParser can't be used recursively", error.message + + # the lock is released, so the parser stays usable + parser = new_parser + parser << '[1, 2, 3]' + assert parser.parse + assert_equal [1, 2, 3], parser.value + end + def test_exception_unlock_parser called = false parser = nil From 7215da1a635a068a61be7deea59b792abdcc36f2 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:14:30 +0900 Subject: [PATCH 6/8] [ruby/json] Fix JSON::ResumableParser stalling on a NUL byte https://github.com/ruby/json/commit/d91b370b1e Co-Authored-By: Jean Boussier --- ext/json/parser/parser.c | 16 +++++++++---- test/json/resumable_parser_test.rb | 38 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index e4f140d7c58c88..9762fb8157ed0b 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1642,7 +1642,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo state->cursor++; value = json_decode_array(state, config, 0); break; - } else if (resumable && next == 0) { + } else if (resumable && eos(state)) { state->cursor = value_start; return false; } @@ -1691,8 +1691,14 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo } case 0: - return false; - + // peek() returns 0 both at end-of-stream and for a literal NUL byte in the + // buffer. Only a genuine EOS means "feed me more"; a NUL byte that is not at + // EOS is just an invalid character. + if (eos(state)) { + return false; + } else { + raise_syntax_error("unexpected NULL byte: %s", state); + } default: raise_syntax_error("unexpected character: %s", state); } @@ -1807,7 +1813,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - } else if (resumable && next_char == 0) { + } else if (resumable && eos(state)) { return false; } else { raise_syntax_error("expected ',' or ']' after array value", state); @@ -1858,7 +1864,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - } else if (resumable && next_char == 0) { + } else if (resumable && eos(state)) { return false; } else { raise_syntax_error("expected ',' or '}' after object value, got: %s", state); diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 40dcb4a46ef169..b48b9e05611aa8 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -133,6 +133,30 @@ def test_parse_byte_by_byte_numbers assert_resumed_parsing('123 ') end + def test_nul_byte_is_a_syntax_error + # A NUL byte in a structural position must raise, not stall forever waiting for more input + # (peek() returns 0 both at EOS and for a literal NUL byte). + assert_parse_error "\x00" # document value + assert_parse_error "[\x00]" # first array element + assert_parse_error "[1\x00]" # after an array element (',' or ']' expected) + assert_parse_error "[1,\x00]" # array element after ',' + assert_parse_error "{\x00}" # object key + assert_parse_error "{\"a\":1\x00}" # after an object value (',' or '}' expected) + assert_parse_error "{\"a\":1,\x00}" # object key after ',' + end + + def test_incomplete_input_at_structural_positions_resumes + # Counterpart of test_nul_byte_is_a_syntax_error: a genuine EOS at the same positions must + # stay incomplete (return false), not raise -- this is what distinguishes EOS from a NUL. + assert_incomplete "[" + assert_incomplete "[1" + assert_incomplete "[1," + assert_incomplete "{" + assert_incomplete "{\"a\"" + assert_incomplete "{\"a\":1" + assert_incomplete "{\"a\":1," + end + def test_rest @parser << '[1, 2, 3, "unterminated string' refute @parser.parse @@ -316,6 +340,20 @@ def test_buffer_shrink private + def assert_parse_error(json) + parser = new_parser + parser << json + assert_raise(JSON::ParserError, "expected a parse error for #{json.inspect}") do + parser.parse + end + end + + def assert_incomplete(json) + parser = new_parser + parser << json + refute(parser.parse, "expected #{json.inspect} not to produce a value") + end + def assert_partial_value(expected, json) parser = new_parser parser << json From 8c7c8cb9e4529ce29311511e2d8f554022a2d9c2 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:15:19 +0900 Subject: [PATCH 7/8] [ruby/json] Reset all per-parse state in JSON::ResumableParser#clear https://github.com/ruby/json/commit/56d16deb46 Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/json/parser/parser.c | 3 +++ test/json/resumable_parser_test.rb | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 9762fb8157ed0b..48700911e46a1d 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2487,6 +2487,9 @@ static VALUE cResumableParser_clear(VALUE self) parser->frames.head = 0; parser->value_stack.head = 0; parser->state.name_cache.length = 0; + parser->state.current_nesting = 0; + parser->state.in_array = 1; + parser->state.emitted_deprecations = 0; parser->state.start = parser->state.cursor = parser->state.end = NULL; return self; } diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index b48b9e05611aa8..2801544c76a60a 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -63,6 +63,20 @@ def test_parse_with_empty_buffer_keeps_parser_usable assert_equal [4], @parser.value end + def test_clear_resets_nesting_depth + # An unfinished document leaks a nesting level; #clear must reset it so a later shallow + # document is not rejected with a spurious NestingError. + parser = new_parser(max_nesting: 10) + 10.times do + parser << '[1' # opens an array that is never closed before clear + parser.parse + parser.clear + end + parser << '[1]' + assert parser.parse + assert_equal [1], parser.value + end + def test_parse_document_direct @parser << '[true]' assert_equal true, @parser.parse From 0c992796dda10967904ac62510d89acfb62c7219 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 02:44:26 +0900 Subject: [PATCH 8/8] [ruby/json] Fix off-by-one line number in parse error messages cursor_position consumed the newline ending the previous line (post-decrement) before counting lines, so any error past the first line was reported one line too low (e.g. "[1,\n@" reported '@' at line 1 instead of line 2). Count that newline when the column loop breaks on it. https://github.com/ruby/json/commit/82b6b21354 Co-Authored-By: Jean Boussier --- ext/json/parser/parser.c | 1 + test/json/json_parser_test.rb | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 48700911e46a1d..665d3152718b6e 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -593,6 +593,7 @@ static void cursor_position(JSON_ParserState *state, long *line_out, long *colum while (cursor >= state->start) { if (*cursor-- == '\n') { + line++; break; } column++; diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index c1aa2f61567a90..c891dd7c2db99d 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -862,7 +862,7 @@ def test_parse_error_incomplete_hash end def test_parse_error_snippet - omit "C ext only test" unless RUBY_ENGINE == "ruby" + omit "JRuby errors don't contain positions" unless RUBY_ENGINE == "ruby" error = assert_raise(JSON::ParserError) { JSON.parse("あああああああああああああああああああああああ") } assert_equal "unexpected character: 'ああああああああああ' at line 1 column 1", error.message @@ -875,6 +875,15 @@ def test_parse_error_snippet error = assert_raise(JSON::ParserError) { JSON.parse("abcあああああああああああああああああああああああ") } assert_equal "unexpected character: 'abcあああああああああ' at line 1 column 1", error.message + + error = assert_raise(JSON::ParserError) { JSON.parse("[1,\n@") } + assert_equal "unexpected character: '@' at line 2 column 1", error.message + + error = assert_raise(JSON::ParserError) { JSON.parse("[\n 1,\n @\n]") } + assert_equal "unexpected character: '@' at line 3 column 3", error.message + + error = assert_raise(JSON::ParserError) { JSON.parse("@") } + assert_equal "unexpected character: '@' at line 1 column 1", error.message end def test_parse_leading_slash