Skip to content

Reading a freed IO::Buffer silently returns empty data instead of raising IO::Buffer::InvalidatedError #8

Description

@himura467

Observed behavior

Reading a freed IO::Buffer does not raise; it silently returns empty data. Verified on current master: ruby 4.1.0dev (2026-07-02T09:24:35Z master 7c69f93d53) +PRISM [arm64-darwin25].

b = IO::Buffer.new(16)
b.free
b.get_string  # => "" (no exception)

b2 = IO::Buffer.for("frozen str")
b2.free
b2.get_string # => "" (no exception)

The same holds for buffers created from C via rb_io_buffer_new(ptr, len, RB_IO_BUFFER_EXTERNAL | RB_IO_BUFFER_READONLY) and freed via rb_io_buffer_free, as verified end-to-end through this bridge's stash test (host/fixture.rb, /stash then /stash-read).

Root cause in CRuby

IO::Buffer::InvalidatedError already exists for exactly this concept, but it is never raised for freed buffers (io_buffer.c, master 7c69f93d53):

  • io_buffer_validate (io_buffer.c:990) performs real validation only for slices (buffer->source != Qnil); any non-slice buffer, including a freed one with base == NULL, unconditionally passes.
  • io_buffer_get_bytes_for_reading (io_buffer.c:1074) then maps the NULL base to (NULL, 0).
  • io_buffer_get_string (io_buffer.c:2840) happily builds a string from 0 bytes, so the caller sees "".

Why it matters for NSGI

Buffer invalidation is the bridge's core safety mechanism: request fields are zero-copy views over host-borrowed memory, and the bridge frees every handed-out buffer when the request completes. Memory safety holds today (freeing nulls the base pointer, so no dangling read is possible), but the failure mode for a misbehaving app is wrong:

  • An app that stashes a request buffer and reads it later gets "" back, a silent data bug that can go unnoticed indefinitely.
  • Raising IO::Buffer::InvalidatedError would turn the lifetime violation into an immediately debuggable exception, which is what our SPEC wants to promise app authors ("holding a buffer past the request raises on access").

Proposed upstream improvement

Make io_buffer_validate (or io_buffer_validate_for_reading/_for_writing) treat a freed buffer as invalid, so access raises the existing IO::Buffer::InvalidatedError. A buffer legitimately created with zero length should keep returning empty data; only buffers whose memory was freed/detached after allocation would raise. Since IO::Buffer is explicitly experimental, this behavioral tightening seems fair game.

References

  • Bridge invalidation logic: ext/nsgi/src/lib.rs (protected_invalidate)
  • End-to-end proof: host/src/main.rs (exercise_stash) + host/fixture.rb

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream/rubyAn upstream issue in CRuby

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions