山本です。

>すると、rb_io_check_writable なんかも io_unread を呼んでいるので、
>位置がずれたりしないでしょうか。

path = "/b.txt"

open(path, "w") do |io|
  10.times do |i|
    io.puts(i)
  end
end

open(path, "rb") do |io|
  p io.read
end

open(path, "r+") do |io|
  io.gets
  io.puts("?")
end

open(path, "rb") do |io|
  p io.read
end

のコードで実行すると

"0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n"
"0\r\n1\r\n2\r\n3\r\n?\r\n5\r\n6\r\n7\r\n8\r\n9\r\n"

となり、期待される

"0\r\n?\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n"

から外れていました。

io_unread に限れば、read(2) する前に tell して位置を
OpenFile に記録することで正確に戻せると思うのですが、
パフォーマンスが気になりますし、依然として IO#seek +
IO#tell は救済できません。

むしろ、read(2) は常にバイナリモードで呼んで、
バッファ内も改行文字の変換を行わず、取り出すときに
初めてモードによって改行文字を変換したりしなかったり
すると、うまくいきそうな気がします。
(でもユーザが得られた文字数から io.seek(io.tell - s.length)
  なんてしてしまうと・・・うーん)