io_buffer: Reimplement dcompact for IO::Buffer

The `source` field in IO::Buffer can have a String or an IO::Buffer
object, if not nil.

- When the `source` is a String object. The `base` field points to the
  memory location of the String content, which can be embedded in
  RSTRING, and in that case, GC compaction can move the memory region
  along with the String object.

  Thus, IO::Buffer needs to pin the `source` object to prevent `base`
  pointer from becoming invalid.

- When the `source` is an IO::Buffer, then `base` is a pointer to a
  malloced or mmapped memory region, managed by the source IO::Buffer.
  In this case, we don't need to pin the source IO::Buffer object,
  since the referred memory region won't get moved by GC.

Closes: [Bug #21210]
This commit is contained in:
Kasumi Hanazuki 2025-04-01 19:45:37 +00:00 committed by Jean Boussier
parent 4cc58c3a6f
commit 8aac19d598
Notes: git 2025-06-17 05:57:14 +00:00
2 changed files with 25 additions and 3 deletions

View file

@ -273,10 +273,18 @@ io_buffer_free(struct rb_io_buffer *buffer)
} }
static void static void
rb_io_buffer_type_mark(void *_buffer) rb_io_buffer_type_mark_and_move(void *_buffer)
{ {
struct rb_io_buffer *buffer = _buffer; struct rb_io_buffer *buffer = _buffer;
rb_gc_mark(buffer->source); if (buffer->source != Qnil) {
if (RB_TYPE_P(buffer->source, T_STRING)) {
// The `source` String has to be pinned, because the `base` may point to the embedded String content,
// which can be otherwise moved by GC compaction.
rb_gc_mark(buffer->source);
} else {
rb_gc_mark_and_move(&buffer->source);
}
}
} }
static void static void
@ -303,9 +311,10 @@ rb_io_buffer_type_size(const void *_buffer)
static const rb_data_type_t rb_io_buffer_type = { static const rb_data_type_t rb_io_buffer_type = {
.wrap_struct_name = "IO::Buffer", .wrap_struct_name = "IO::Buffer",
.function = { .function = {
.dmark = rb_io_buffer_type_mark, .dmark = rb_io_buffer_type_mark_and_move,
.dfree = rb_io_buffer_type_free, .dfree = rb_io_buffer_type_free,
.dsize = rb_io_buffer_type_size, .dsize = rb_io_buffer_type_size,
.dcompact = rb_io_buffer_type_mark_and_move,
}, },
.data = NULL, .data = NULL,
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,

View file

@ -693,4 +693,17 @@ class TestIOBuffer < Test::Unit::TestCase
buf.set_string('a', 0, 0) buf.set_string('a', 0, 0)
assert_predicate buf, :empty? assert_predicate buf, :empty?
end end
# https://bugs.ruby-lang.org/issues/21210
def test_bug_21210
omit "compaction is not supported on this platform" unless GC.respond_to?(:compact)
str = +"hello"
buf = IO::Buffer.for(str)
assert_predicate buf, :valid?
GC.verify_compaction_references(expand_heap: true, toward: :empty)
assert_predicate buf, :valid?
end
end end