From 4c8255d185472872fa4252aa7db411670616e50c Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 3 Jun 2026 15:46:00 -0600 Subject: [PATCH 1/2] RUBY-3893 Fix heap use-after-free in put_array RARRAY_PTR is unsafe to hold across any call that may invoke the Ruby runtime or GC. The loop body called rb_funcall twice per element (for bson_type and to_bson), so a callback that mutated the array and triggered GC compaction could free the backing store while the cached pointer was still in use. Replace the raw pointer walk with rb_ary_entry, which re-derives the element from the live array object on every access. Also snapshot the array length before the loop and raise RuntimeError if it changes, rather than silently producing corrupted BSON output. --- ext/bson/write.c | 13 ++++++++----- spec/bson/array_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ext/bson/write.c b/ext/bson/write.c index c426129a1..d8e3aa0a6 100644 --- a/ext/bson/write.c +++ b/ext/bson/write.c @@ -649,7 +649,6 @@ VALUE rb_bson_byte_buffer_put_array(VALUE self, VALUE array){ size_t new_position = 0; int32_t new_length = 0; size_t position = 0; - VALUE *array_element = NULL; TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); Check_Type(array, T_ARRAY); @@ -657,12 +656,16 @@ VALUE rb_bson_byte_buffer_put_array(VALUE self, VALUE array){ /* insert length placeholder */ pvt_put_int32(b, 0); - array_element = RARRAY_PTR(array); + int32_t original_len = (int32_t)RARRAY_LEN(array); - for(int32_t index=0; index < RARRAY_LEN(array); index++, array_element++){ - pvt_put_type_byte(b, *array_element); + for (int32_t index = 0; index < original_len; index++) { + if ((int32_t)RARRAY_LEN(array) != original_len) { + rb_raise(rb_eRuntimeError, "array modified during BSON serialization"); + } + VALUE element = rb_ary_entry(array, index); + pvt_put_type_byte(b, element); pvt_put_array_index(b, index); - pvt_put_field(b, self, *array_element); + pvt_put_field(b, self, element); } pvt_put_byte(b, 0); diff --git a/spec/bson/array_spec.rb b/spec/bson/array_spec.rb index 3aca70b5e..42a28269a 100644 --- a/spec/bson/array_spec.rb +++ b/spec/bson/array_spec.rb @@ -75,6 +75,30 @@ end end + context 'when an array element mutates the array size during serialization', + if: BSON::ByteBuffer.new.respond_to?(:put_array) do + let(:array) { Array.new(10, 1) } + + before do + evil = Class.new do + define_method(:initialize) { |a| @array = a; @mutated = false } + define_method(:bson_type) do + unless @mutated + @mutated = true + @array.replace(Array.new(200_000, 0)) + end + BSON::Int32::BSON_TYPE + end + define_method(:to_bson) { |buffer| 123.to_bson(buffer) } + end + array[0] = evil.new(array) + end + + it 'raises a RuntimeError' do + expect { array.to_bson }.to raise_error(RuntimeError, /array modified during BSON serialization/) + end + end + context 'when array contains value of an unserializable class' do class ArraySpecUnserializableClass end From 5ef23e4c4d2ec2b5c00cf870e694ab711ff450b6 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 3 Jun 2026 16:34:13 -0600 Subject: [PATCH 2/2] RUBY-3893 Fix original_len truncation and GC visibility in put_array Keep original_len as long and raise RangeError for arrays exceeding INT32_MAX, preventing truncation from silently corrupting the modification check. Mark element as volatile VALUE so the conservative GC can see it on the C stack during ALLOC_N calls in pvt_put_array_index. --- ext/bson/write.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ext/bson/write.c b/ext/bson/write.c index d8e3aa0a6..2be127bc2 100644 --- a/ext/bson/write.c +++ b/ext/bson/write.c @@ -656,13 +656,16 @@ VALUE rb_bson_byte_buffer_put_array(VALUE self, VALUE array){ /* insert length placeholder */ pvt_put_int32(b, 0); - int32_t original_len = (int32_t)RARRAY_LEN(array); + long original_len = RARRAY_LEN(array); + if (original_len > INT32_MAX) { + rb_raise(rb_eRangeError, "array too large for BSON serialization"); + } - for (int32_t index = 0; index < original_len; index++) { - if ((int32_t)RARRAY_LEN(array) != original_len) { + for (int32_t index = 0; index < (int32_t)original_len; index++) { + if (RARRAY_LEN(array) != original_len) { rb_raise(rb_eRuntimeError, "array modified during BSON serialization"); } - VALUE element = rb_ary_entry(array, index); + volatile VALUE element = rb_ary_entry(array, index); pvt_put_type_byte(b, element); pvt_put_array_index(b, index); pvt_put_field(b, self, element);