From 2cebc2cdbe5683b41e5848570ca25ac9f25d2a69 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 2 Feb 2026 22:06:44 +0100 Subject: [PATCH] Ensure `Generator::State` is kept on the stack Fix: https://github.com/ruby/json/issues/929 When calling `cState_partial_generate` from `mHash_to_json` or other `to_json` funcs, `VState` becomes unreachable very quickly, hence the compiler may optimize it out of the stack, and make it invisible to the GC stack scanning. This is particularly liekly given how aggressively we inline. Repro: ```ruby require 'json' test_data = { "flag" => true, "data" => 10000.times.map { [1.0] }, :flag => false, } 10.times do test_data.to_json end ``` But in practice the cause was just that the issued warning calls Hash#inspect on a big hash, which triggers GC. So it can be triggered even more reliably with: ```ruby require 'json' module JSON module Common def self.on_mixed_keys_hash(...) GC.start end end end test_data = { "flag" => true, "data" => 10000.times.map { [1.0] }, :flag => false, } test_data.to_json ``` --- CHANGES.md | 3 +++ ext/json/ext/generator/generator.c | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8b51c585..0ff0d517 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ### Unreleased +* Fix a potential crash in very specific circumstance if GC triggers during a call to `to_json` + without first invoking a user defined `#to_json` method. + ### 2025-12-11 (2.18.0) * Add `:allow_control_characters` parser options, to allow JSON strings containing unescaped ASCII control characters (e.g. newlines). diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index dbba99c4..ad925986 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1540,7 +1540,9 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, .obj = obj, .func = func }; - return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data); + VALUE result = rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data); + RB_GC_GUARD(self); + return result; } /* call-seq: