Skip to content

rb_io_buffer_free_locked calls rb_bug when Ruby code holds a lock #43

Description

@himura467

Summary

rb_fiber_scheduler_io_read_memory wraps borrowed memory with rb_io_buffer_new_locked, which starts at a lock count of one, passes the buffer to a user defined Fiber::Scheduler#io_read, and releases it with rb_io_buffer_free_locked. That function calls rb_bug unless the lock count is exactly one, and the scheduler can raise the count with #locked.

class TinySched
  def io_read(io, buffer, length, offset)
    Fiber.new { buffer.locked { Fiber.yield } }.resume   # lock count 1 -> 2
    0
  end
  def io_wait(io, events, timeout) = events
  def kernel_sleep(duration = nil) = nil
  def block(blocker, timeout = nil) = nil
  def unblock(blocker, fiber) = nil
  def close = nil
  def fiber(&block) = Fiber.new(blocking: false, &block).tap(&:resume)
end

Fiber.set_scheduler(TinySched.new)
Fiber.schedule { r, w = IO.pipe; w.close; r.read(16) }
[BUG] rb_io_buffer_free_locked: expected lock count 1, got 2
ruby 4.1.0dev (2026-08-22T20:13:33Z master 37325e9f7a) +PRISM [arm64-darwin25]

No C extension is involved. rb_bug terminates the process immediately and cannot be rescued, and it is documented for interpreter bugs: doc/extension.rdoc says it "should be called under the situation caused by the bug in the interpreter", and doc/extension.ja.rdoc says it is for situations that can only arise from such a bug. This state is reachable by calling a public method.

rb_fiber_scheduler_io_write_memory, io_pread_memory and io_pwrite_memory have the same shape.

Why it matters here

SPEC.md section 4 lets the bridge multiplex requests onto fibers through a Fiber::Scheduler, and lets the application supply its own. A scheduler that leaves a lock on a buffer it was handed takes the whole process down, which section 1 forbids: an exception in application code must never crash the host.

It also makes the documented new_locked and free_locked pair unusable for any wrapper the bridge exposes to application code, since the application can raise the lock count the same way.

Proposal

Stop calling rb_bug for a state that ordinary Ruby code can produce. Preventing the state, for example by making the wrapper unlockable from Ruby, and handling the outstanding locks inside free_locked both seem possible. Which one fits depends on #44.

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream/rubyAn upstream issue in CRuby

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions