Project

General

Profile

Actions

Bug #22239

closed

Multiple buffer clear errors in IO

Bug #22239: Multiple buffer clear errors in IO

Added by YO4 (Yoshinao Muramatsu) about 2 months ago. Updated 24 days ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:126360]

Description

While testing my changes on the Windows platform to use an encoding converter for newline conversion for "r", I discovered a failure in test_reopen. This corresponds to case (3) below.

I asked an LLM to investigate similar cases and related code that raised concerns, and was able to identify several issues, so I am reporting them here. I'll submit a PR later.
I used Hy3 (High) via opencode GO to perform this survey and create the test code. (For transparency—this is not an advertisement.)

case(1)
In io.c, free_io_buffer only clears ptr and left off, len and capa as they were.
Since clear_readconv releases the character buffer through it, READ_CHAR_PENDING() kept reporting pending characters after the buffer had been released.
As a result, every operation that clears the code converter (IO#rewind, IO#seek, IO#pos=, IO#flush, IO#binmode, IO#tell, IO#set_encoding, ...) left the IO in a state where byte oriented reads raise IOError.
Also IO#eof? answers false at the end of file and IO#sysseek refuses to work.

File.binwrite("t", "foo")
open("t", "rt") do |f|
  f.ungetc(f.getc)
  f.rewind
  f.getbyte # => 'IO#getbyte': byte oriented read for character buffered IO (IOError)
end

case(2)
IO#reopen(io) only flushes the write buffer when the IO is writable, and never unreads the read buffer.
The buffer happens to be dropped by the io_seek after dup2, but only when the other stream is readable and seekable, so a read-write IO keeps stale bytes otherwise.

File.binwrite("t", "0123456789")
f = File.open("t", "r+b")
f.getbyte
IO.pipe do |r, w|
  w.write("ABC"); w.close
  f.reopen(r)
  f.read # => "123456789ABC" instead of "ABC"
end

case(3)
IO#reopen(name) discards the byte buffer but keeps the code converter and the character buffer, so characters left over from the previous stream leak into the reopened one.

File.binwrite("t", "foo")
open(File::NULL, "rt") do |f|
  f.ungetc("a")
  f.reopen("t")
  f.read # => "afoo" instead of "foo"
end

case(4)
IO#reopen(io) relies on flush_before_seek to drop the buffers, but io_unread returns at once when rbuf is empty and therefore never reaches clear_codeconv.
With characters pending in cbuf and nothing in rbuf they leak into the reopened stream.

File.write("t", "foo\n")
open(File::NULL, "rt") do |f|
  f.ungetc("a")
  open("t", "rt") {|f2| f.reopen(f2)}
  f.gets # => "afoo\n" instead of "foo\n"
end

case(5)
Unlike IO#rewind, IO#seek and IO#pos= do not clear the character buffer.
As io_unread returns at once when rbuf is empty, an ungotten character survives the repositioning while it does not in binary mode.

File.write("t", "foo\n")
open("t", "rt") do |f|
  f.ungetc("a")
  f.seek(2, :SET)
  f.getc # => "a" instead of "o"
end

Updated by hsbt (Hiroshi SHIBATA) 24 days ago Actions #2

  • Status changed from Open to Closed

PR ruby/ruby#18276 landed on 2026-08-11 as git|fa0edeb757d3b7edca3bf5fda8eb85d22476ac49 and this ticket was never closed. I checked all five cases across that commit: each one fails before it and gives the expected result after (102, "ABC", "foo", "foo\n", "o").

The fix is on master only, so it ships in Ruby 4.1.0 and is not in 4.0.x.

Actions

Also available in: PDF Atom