From b5ccab2093c9bb19ae8564a935e6fd72ec7354cc Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Fri, 20 Feb 2026 10:56:50 +0900 Subject: [PATCH] IO::Buffer#locked: Release lock even when the block raises (#16180) IO::Buffer#locked: Release lock even when the block raises/breaks Previously, `IO::Buffer#locked` leaks the lock when the block raises an exception, or breaks. Fixes: [Bug #21882] --- io_buffer.c | 17 ++++++++++++----- test/ruby/test_io_buffer.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 4bb4685e74..cb35141f47 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1424,6 +1424,17 @@ rb_io_buffer_try_unlock(VALUE self) return 0; } +static VALUE +rb_io_buffer_locked_ensure(VALUE self) +{ + struct rb_io_buffer *buffer = NULL; + TypedData_Get_Struct(self, struct rb_io_buffer, &rb_io_buffer_type, buffer); + + buffer->flags &= ~RB_IO_BUFFER_LOCKED; + + return Qnil; +} + /* * call-seq: locked { ... } * @@ -1466,11 +1477,7 @@ rb_io_buffer_locked(VALUE self) buffer->flags |= RB_IO_BUFFER_LOCKED; - VALUE result = rb_yield(self); - - buffer->flags &= ~RB_IO_BUFFER_LOCKED; - - return result; + return rb_ensure(rb_yield, self, rb_io_buffer_locked_ensure, self); } /* diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 706ce16c42..e272b8a71c 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -930,4 +930,34 @@ class TestIOBuffer < Test::Unit::TestCase assert_equal value, round_trip_value, "#{le_type}/#{be_type}: double-swap should restore original value" end end + + class Bug21882 < RuntimeError; end + def test_locked_exception + buf = IO::Buffer.new(10) + assert_raise(Bug21882, '#locked should propagate exception') do + buf.locked { raise Bug21882 } + end + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end + + def test_locked_break + buf = IO::Buffer.new(10) + assert_equal :ok, (buf.locked { break :ok }) + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end + + def test_locked_throw + buf = IO::Buffer.new(10) + assert_equal :ok, (catch(:bug21882) { buf.locked { throw :bug21882, :ok } }) + + # should be unlocked now and can be locked again + refute_predicate buf, :locked? + buf.locked { } + end end