From a371d7d8fc8edfeb4def1ef0d684eaef70acc843 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 19 Jun 2026 00:10:12 +0900 Subject: [PATCH] Fix heap-use-after-free in JSON::ResumableParser#partial_value Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/json/ext/parser/parser.c | 35 ++++++++++++++++++++---------- test/json/resumable_parser_test.rb | 20 +++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 136aab6a..fe87d5c4 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/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 52f1356a..6eeabf64 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