【一人 bugs.ruby Advent Calendar 2021】番外編: 今年みた Ruby のバグ報告【25日目】

一人 bugs.ruby Advent Calendar 2021 25日目の記事になります。
今日で Advent Calendar も最後という事で今回は今年みた Ruby のバグをいくつか紹介してみようと思います。
またこれから紹介する修正済みのバグは Ruby 3.1 ではなくて古い Ruby でもバックポートされている可能性があるので注意してください(〜で修正済みと書かれていても RUby 2.7.x 系でバックポートされていて修正済みの可能性があります。

[Bug #18377] Integer#times has different behavior depending on the size of the integer

Integer#+ を書き換えると特定の値で Integer#times の挙動に影響を与えるというバグ報告です。

# これは問題がない
(2**1).times do
  Integer.undef_method(:+)
  Integer.define_method(:+) do |_other|
    puts "my custom add"
  end
end

# FIXNUM を越える値に対して `times` を呼び出すと Integer#+ を呼び出してエラーになる
# `times': undefined method `<' for nil:NilClass (NoMethodError)
(2**65).times do
  Integer.undef_method(:+)
  Integer.define_method(:+) do |_other|
    # ここが呼び出されるようになる
    puts "my custom add"
  end
end

内部で Integer#+ を呼び出すようになっていたところを呼び出さないようにして対応済みです。

[Bug #17675] StringIO#each_byte doesn't check for readabilty while iterating

IO がクローズしているのにイテレーションが処理されてしまうというバグ報告です。

require "stringio"

strio = StringIO.new("1234")
strio.each_byte do |byte|
  puts byte
  # ここでクローズしているがイテレーションは引き続き処理されている
  strio.close
end
# => 49
#    50
#    51
#    52

この問題は修正済みで最新版では IOError が発生するようになります。

require "stringio"

strio = StringIO.new("1234")
strio.each_byte do |byte|
  puts byte
  strio.close
end
# => 49
#    error: `each_byte': not opened for reading (IOError)

[Bug #18343] empty hash passed to Array#pack causes Segmentation fault (2.6)

Ruby 2.4 ~ 2.6 で Array#pack に空の Hash を渡すと Segmentation fault が発生するというバグ報告です。

# これで segv する
[0].pack('c', {})

この問題は Ruby 2.7 以降では再現せず Ruby 2.6 は現状セキリュティサポートのみなのでこのチケットは閉じられています。

[Bug #18292] 3.1.0-dev include cause Module to be marked as initialized

これは Ruby 3.1.0-dev で発生したバグになります。
次のように Module を継承して include した後に super を呼ぶとエラーになってしまうバグです。

class Mod1 < Module
  def initialize(...)
    super
  end
end
p Mod1.new
# => #<Mod1:0x000055b6dc5a5d00>

class Mod2 < Module
  def initialize(...)
    include Enumerable
    super
  end
end
p Mod2.new
# 3.0.2     => #<Mod2:0x000055b6dc5a59e0>
# 3.1.0-dev => error: `initialize': already initialized module (TypeError)

以下のコードだけでもエラーになったので include 後に Module#initialize を呼ぶとダメなのかも?

class Mod2 < Module
  def initialize(...)
    include Enumerable
    super
  end
end
p Mod2.new
# 3.0.2     => #<Mod2:0x000055b6dc5a59e0>
# 3.1.0-dev => error: `initialize': already initialized module (TypeError)

これはまだ最新の 3.1.0-dev でも再現していたので Ruby 3.1 でも残ったままになってしまっているかも。

[Bug #18329] Calling super to non-existent method dumps core

存在しない super を呼び出すとコアダンプするというバグ報告です。
次のコードを Ruby 3.0.2 で実行すると segv します。

module Probes
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def probe(*methods)
      prepend(probing_module(methods))
    end

    def probing_module(methods)
      Module.new do
        methods.each do |method|
          define_method(method) do |*args, **kwargs, &block|
            super(*args, **kwargs, &block)
          end
        end
      end
    end
  end
end

class Probed
  include Probes

  probe :danger!, :missing

  def danger!
    raise "BOOM"
  end
end

5.times do
  subject = Probed.new
  subject.danger! rescue RuntimeError
  subject.missing rescue NoMethodError
end

ちょっと分かりづらいので最小構成にすると以下のような感じです。

class Probed
  def self.probing_module(methods)
    Module.new do
      methods.each do |method|
        define_method(method) do |*args, **kwargs, &block|
          super(*args, **kwargs, &block)
        end
      end
    end
  end

  prepend probing_module [:danger!, :missing]

  def danger!
  end
end

subject = Probed.new
subject.danger!

# ここで存在しない super を呼び出している
subject.missing rescue NoMethodError

# ここで segv
subject.danger!

この問題は既に修正されていて Ruby 3.0.3 にもバックポートされています。

[Bug #18243] Ractor.make_shareable does not freeze the receiver of a Proc but allows accessing ivars of it

次のように Ractor 内で別の Ractor のオブジェクトが書き換えられてしまうバグ報告です。

class C
  attr_accessor :foo
  def setter_proc
    Ractor.make_shareable(proc {})
#     Ractor.make_shareable(-> v { @foo = v })
  end
end

c = C.new
c.foo = 1
p c
# => #<C:0x0000559bf1df5880 @foo=1>

# インスタンス変数を書き換える proc を生成
# c 自体は freeze されてない
proc = c.setter_proc
p c.frozen?
# => false

# Ractor 内で setter_proc を呼び出すと c のオブジェクトが書き換えら得てしまう
# これがバグ
Ractor.new(proc) { |s| s.call(42) }.take
p c
# => #<C:0x0000559bf1df5880 @foo=42>

この問題は修正済みで生成 Procself が共有可能オブジェクトかどうかまで参照するようになりました。

# OK: self は参照可能オブジェクトである
Ractor.make_shareable(nil.instance_eval { -> {} })

# NG: self は参照可能オブジェクトではない
# error: `make_shareable': Proc's self is not shareable: #<Proc:0x00007fb6e26e5460 /tmp/vDlYFAi/48:5 (lambda)> (Ractor::IsolationError)
Ractor.make_shareable(-> {})

[Bug #17719] Irregular evaluation order in hash literals

Hash リテラルで同名のキーが存在する場合に評価順が左からにならないバグ報告です。
これは Ruby 3.1 で修正されました。

ary = []
{ a: ary << 1, b: ary << 2, a: ary << 3 }
pp ary
# Ruby 3.0 => [1, 3, 2]
# Ruby 3.1 => [1, 2, 3]

[Bug #1823] Conversion to float not working for object with to_f method

Thread#join で引数を Float に変換しているが #to_f が呼ばれていないというバグ報告です。
Ruby 3.0 以降から再現するようになりました。

class Something
  def to_f
    0.1
  end
end

# error: `join': can't convert Something into Float (TypeError)
Thread.new{ }.join(Something.new)

この問題は Ruby 3.1 で修正済みです。

[Bug #18180] opt_newarray_min/max instructions ignore refined methods

以下のようなケースで Refinements が正しく反映されていないというバグ報告です。

module M
  refine Array do
    def min; :min; end
    def max; :max; end
  end
end

using M

# これは Refinements が適用される
pp [1, 2, 3].min    # => :min

# これは Refinements が適用されない
pp [1+0, 2, 3].min  # => 1

これはレシーバがリテラルの場合に最適化を行っていて Refinements が反映されなくなってしまっているが原因らしいです。
これは Ruby 3.1 で修正済みです。

module M
  refine Array do
    def min; :min; end
    def max; :max; end
  end
end

using M

pp [1+0, 2, 3].min
# Ruby 3.0 => 1
# Ruby 3.1 => :min

[Bug #17048] Calling initialize_copy on live modules leads to crashes

以下のコードで Ruby がクラッシュするというバグ報告です。

loop do
  m = Module.new do
    prepend Module.new
    def hello
    end
  end

  klass = Class.new { include m }
  m.send(:initialize_copy, Module.new)
  GC.start

  klass.new.hello rescue nil
end

上記のように Module#initialize_copy を呼び出すとクラッシュする可能性があるらしいです。
Ruby 3.1 では Module#initialize_copy を呼び出すと TypeError が発生するように修正されました。

module A
end

# error: `initialize_copy': already initialized module (TypeError)
A.send(:initialize_copy, Module.new) # fine, no one inherits from A

[Bug #18160] IndexError raised from MatchData#{offset,begin,end} does not keep the encoding of the argument

MatchData#{offset,begin,end} で発生した IndexErrorエンコーディングを保持してないバグ報告です。

pp RUBY_VERSION  # => "3.0.2"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: \xE3\x81\x82"

これは修正されて Ruby 3.0.3 以降では意図する文字コードで出力されます。

pp RUBY_VERSION  # => "3.0.3"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: あ"

[Bug #18084] JSON.dump can crash VM.

次のように再帰的な HashJSON.dump に渡すと VM がクラッシュするバグ報告です。

require 'json'

x = {}
# 自身に自身を割り当てる
x[:x] = x

# machine stack overflow in critical region (fatal)
p JSON.dump(x)

これは Ruby 2.7 から再現しており、Ruby 2.7 以前は SystemStackError が発生していました。

require 'json'

x = {}
x[:x] = x

# Ruby 2.6 の場合
# error: stack level too deep (SystemStackError)
p JSON.dump(x)

これは修正されて Ruby 3.1 からは SystemStackError が発生します。

[Bug #18080] Syntax error on one-line pattern matching

次のように『カッコで囲まれていないパラメータを持つメソッドの戻り値を右代入で使用するとシンタックスエラーになる』というバグ報告です。

# パラメータがなかったり、カッコが付いている場合は OK
p do
end => a
p a #=> nil

p(1) do
end => a
p a #=> 1

# カッコがないパラメータがある場合はシンタックスエラーになる
p 1 do
end => a
#=>
# syntax error, unexpected =>, expecting end-of-input
# end => a
#    ^~

# これは1行 in でも同様にシンタックスエラーになる
p 1 do
end in a
#=>
# syntax error, unexpected `in', expecting end-of-input
# end in a
#     ^~

このエラーは意図的ではないが修正するのは難しいらしいです。

[Bug #18053] Crashes and infinite loops when generating partial backtraces in Ruby 3.0+

以下のコードを Ruby 3.0 以降で実行すると segv するというバグ報告です。

def foo
  caller_locations(2, 1).inspect # this will segv
  # caller_locations(2, 1)[0].path # this will infinite loop
end

1.times.map { 1.times.map { foo } }

これは Ruby 3.0 の最適化のバグらしいです。
Ruby 3.0.3 以降では修正済みです。

[Bug #18031] Nested TracePoint#enable with target crashes

以下のように TracePoint がネストしているとクラッシュするというバグ報告です。

one = TracePoint.new(:call) {}
two = TracePoint.new(:call) {}

obj = Object.new
obj.define_singleton_method(:foo) {} # a bmethod

foo = obj.method(:foo)
# ここでクラッシュする
one.enable(target: foo) do
  two.enable(target: foo) {}
end

修正 PR は既にあるんですが、いくつか問題がありまだマージされていません。

[Bug #16243 完了] case/when is slower than if on MRI

Ruby 2.6.5 で if 文よりも case when 文の方が遅いというバグ報告です。

# frozen_string_literal: true
require "benchmark/ips"

def deep_dup_case(obj)
  case obj
  when Integer, Float, TrueClass, FalseClass, NilClass
    obj
  when String
    obj.dup
  when Array
    obj.map { |e| deep_dup_case(e) }
  when Hash
    duped = obj.dup
    duped.each_pair do |key, value|
      duped[key] = deep_dup_case(value)
    end
  else
    obj.dup
  end
end

def deep_dup_if(obj)
  if Integer === obj || Float === obj || TrueClass === obj || FalseClass === obj || NilClass === obj
    obj
  elsif String === obj
    obj.dup
  elsif Array === obj
    obj.map { |e| deep_dup_if(e) }
  elsif Hash === obj
    duped = obj.dup
    duped.each_pair do |key, value|
      duped[key] = deep_dup_if(value)
    end
    duped
  else
    obj.dup
  end
end


obj = { "class" => "FooWorker", "args" => [1, 2, 3, "foobar"], "jid" => "123987123" }

Benchmark.ips do |x|
  x.report("deep_dup_case") do
    deep_dup_case(obj)
  end

  x.report("deep_dup_if") do
    deep_dup_if(obj)
  end

  x.compare!
end
__END__
Warming up --------------------------------------
       deep_dup_case    37.767k i/100ms
         deep_dup_if    41.802k i/100ms
Calculating -------------------------------------
       deep_dup_case    408.046k (± 0.9%) i/s -      2.077M in   5.090997s
         deep_dup_if    456.657k (± 0.9%) i/s -      2.299M in   5.035040s

Comparison:
         deep_dup_if:   456657.4 i/s
       deep_dup_case:   408046.1 i/s - 1.12x  slower

この問題は Ruby 3.1 で改善されています。

[Bug #14817] TracePoint#parameters for bmethod's return event should return the same value as its Method#parameters

TracePoint:return イベント時に TracePoint#parameters で正しく値が取得できないバグ報告です。

define_method(:bm) {|a|}

p method_parameters: method(:bm).parameters
# => {:method_parameters=>[[:req, :a]]}

trace = TracePoint.new(:call, :return){|tp|
  mid = tp.method_id
  if mid == :bm
    p mid: mid, event: tp.event, tp_parameters: tp.parameters
  end
}
trace.enable{
  bm(0)
}

# :call 時は parameters が取得できているが
# :return 時は parameters が取得できてない
# output:
# {:mid=>:bm, :event=>:call, :tp_parameters=>[[:req, :a]]}
# {:mid=>:bm, :event=>:return, :tp_parameters=>[]}

TracePoint#parameters だけではなくて define_method + TracePoint 全般の問題らしいです。

define_method(:bm) {|a|}

trace = TracePoint.new(:call, :return){|tp|
  p [tp.event, tp.lineno] if tp.method_id == :bm
}
trace.enable{
  bm(0)
}
# output:
# [:call, 1]
# [:return, 7] #=> [:return, 1] になるべき?

この問題は Ruby 3.1 で修正済みです。

[Bug #14391] Integer#digitsが遅い

Integer#digits が遅いというバグ報告です。
Integer#to_s と比較してもかなり遅いらしい。

(9999**9999).to_s.chars.map(&:to_i).reverse # 0.030225秒
(9999**9999).digits # 1.187126秒 (40倍)

(99999**99999).to_s.chars.map(&:to_i).reverse # 1.888218秒
(99999**99999).digits # 195.594539秒 (100倍)

ちなみに Integer#digits は各桁を配列として返すメソッドになります。

pp 16.digits    # => [6, 1]
pp 1234.digits  # => [4, 3, 2, 1]

この問題は Ruby 3.1 で修正済みです。

[Bug #17767] Cloned ENV inconsistently returns ENV or self

ENVENV.clone したオブジェクトで挙動に一貫性がないというバグ報告です。

cloned_env = ENV.clone

p ENV.each_key{}.equal?(ENV) #=> true
p cloned_env.each_key{}.equal?(cloned_env) #=> true

ENV.delete('TEST')

err = ENV.fetch('TEST') rescue $!
p err.receiver.equal?(ENV) #=> true
err = cloned_env.fetch('TEST') rescue $!
p err.receiver.equal?(cloned_env) #=> false

ENV['TEST'] = 'TRUE'
p ENV.select!{ false }.equal?(ENV) #=> true

cloned_env['TEST'] = 'TRUE'
p cloned_env.select!{ false }.equal?(cloned_env) #=> false

このチケットがきっかけで Ruby 3.1 から以下のように挙動が変わりました。

[Bug #17951] Collisions in Proc#hash values for blocks defined at the same line

Proc#hash の値が同じ値になるケースがあるというバグ報告です。

require 'set'

def capture(&block)
  block
end

# 同じブロックを大量に生成する
blocks = Array.new(1000) { capture { :foo } }

hashes = blocks.map(&:hash).uniq
ids = blocks.map(&:object_id).uniq
equality = blocks.map { blocks[0].eql?(_1) }.tally
hash = blocks.to_h { [_1, nil] }
set = blocks.to_set

# hash が一意であれば hashes.size == 1000 になるはずだがなっていない
puts(hashes.size)      # => 11
puts(ids.size)         # => 1000
puts(equality.inspect) # => {true=>1, false=>999}
puts(hash.size)        # => 1000
puts(set.size)         # => 1000

この問題は Ruby 3.1 で修正済みです。

[Bug #17889] Enumerator::Lazy#with_index should return size

Enumerator::Lazy#with_index の戻り値に対して size を呼ぶと意図しない値が返ってきたというバグ報告です。

p Enumerator::Lazy.new([1, 2, 3], 3){|y, v| y << v}.with_index.size
# 期待する値 => 3
# 実際の値   => nil

この問題は Ruby 3.0.2 で修正済みです。

[Bug #17857] when 0r and when 0i do not match with case 0

0r === 00i === 0true を返すが case-when でマッチしないというバグ報告です。

# これは true を返す
p 0r === 0  # => true
p 0i === 0  # => true

# しかし case-when では 0r などにマッチしない
case 0
when 0r
  p :hoge
when 0i
  p :foo
else
  p :bar
end
# 期待する挙動 => :hoge
# 実際の挙動   => :bar

これは最適化のバグらしく、最適化を無効にして実行すると問題なく動作します。

# 最適化を無効にして Ruby のコードを実行する
RubyVM::InstructionSequence.compile(<<END, specialized_instruction: false).eval
case 0
when 0r
  p :hoge
when 0i
  p :foo
else
  p :bar
end
# => :hoge
END

この問題は Ruby 3.1 で修正済みです。

[Bug #17814] inconsistent Array.zip behavior

以下のように Array#zip だとイテレーションが1回余計に呼ばれているというバグ報告です。

i = 0
# 1 ずつ増えるカウンタ
e = Enumerator.produce { i += 1 }

# 1つ余計にイテレーションが発生する
p [0, 0, 0, 0].zip e
# => [[0, 1], [0, 2], [0, 3], [0, 4]]
p i
# 期待する挙動 => 4
# 実際の挙動   => 5

# Enumerable#zip だと再現しない
p [0, 0, 0, 0].each.zip e
# => [[0, 6], [0, 7], [0, 8], [0, 9]]
p i
# => 9

Enumerable#zip だと問題ないので対応する場合はこっちを使うとよさそう。
この問題は Ruby 3.1 で修正済みです。

[Bug #4443] odd evaluation order in a multiple assignment

以下のように多重代入した時に先に右辺のメソッドが呼び出されるというバグ報告です。

def foo
  p :foo
  []
end
def bar
  p :bar
end

# bar -> foo という順に評価される
x, foo[0] = bar, 0
# output:
# :bar
# :foo

# これは foo -> bar という順になる
foo[0] = bar
# output:
# :foo
# :bar

10年前のチケットで Ruby 3.1 で修正されました。
Ruby 3.1 だと以下のような挙動になります。

def foo
  p :foo
  []
end
def bar
  p :bar
end

# Ruby 3.1だと foo -> bar と評価されるようになった
x, foo[0] = bar, 0
# output:
# :foo
# :bar

[Bug #17754] NoMethodError#to_s makes segmentation fault when Module#name returns non string value

以下のように .name が文字列以外を返した場合に SEGV するというバグ報告です。

class C
  def self.name
    42
    # これなら OK
    # "42"
  end
end
# C に対して NoMethodError なエラーが発生すると SEGV する
C.this_method_does_not_exist

これは Ruby 3.0.0 で再現し、Ruby 3.0.1 では修正済みです。

[Bug #17756] StringScanner#charpos makes segmentation fault when target.byteslice returns non string value

以下のように StringScanner を使用すると SEGV するというバグ報告です。

require 'strscan'
string = 'ruby'
scnanner = StringScanner.new(string)
pre = Module.new do
  def byteslice(*args)
  end
end
string.singleton_class.prepend(pre)
scnanner.charpos

この問題は Ruby 3.0.3 で修正済みです。

[Bug #17739] Array#sort! changes the order even if the receiver raises FrozenError in given block

Array#sort! のブロック内でレシーバを freeze すると例外が発生するがソート済みになっているというバグ報告です。

array = [1, 2, 3, 4, 5]
begin
  array.sort! do |a, b|
    array.freeze if a == 3
    1
  end
rescue => err
  # 例外が発生する
  p err #=> #<FrozenError: can't modify frozen Array: [5, 4, 3, 2, 1]>
end

# 例外が発生してもソート済みになっている
p array #=> [5, 4, 3, 2, 1]

ちなみに break した場合はそこまでのソートになっている

array = [1, 2, 3, 4, 5]
array.sort! do |a, b|
  break if a == 3
  1
end

# 途中までソートされた状態
p array #=> [3, 4, 2, 1, 5]

この問題は Ruby 3.1 で修正済みです。

[Bug #17719] Irregular evaluation order in hash literals

Hash リテラルでキーが重複している場合に以下のような評価順になるというバグ報告です。

{ foo: p(1), bar: p(2), foo: p(3) }
# => 1
#    3
#    2

Ruby では左から右に評価されるのが一般的なので Ruby 3.1 では左から右に向かって評価されるように修正されました。

{ foo: p(1), bar: p(2), foo: p(3) }
# => 1
#    2
#    3

[Bug #17652] GC compaction crash on mprotect

以下のコードを実行した時に GC compaction でクラッシュするというバグ報告です。

GC.auto_compact = true

times = 20_000_000
arr = Array.new(times)
times.times do |i|
  arr[i] = "#{i}"
end

arr = Array.new(1_000_000, 42)
GC.start

puts "ok"

この問題は Ruby 3.1 で修正済みです。

[Bug #17661] IO#each will segfault when if file is closed inside an each_byte block

以下のように File#each_byte 内で File#close すると segv するというバグ報告です。

file = File.open(__FILE__)
file.each_byte do |byte|
  file.close
end

この問題は Ruby 3.0.3 で修正済みです。

[Bug #17667] Module#name needs synchronization

Module#name は非同期処理に対応していないので以下のようにすると segv するというバグ報告です。

class C
  @iv = 1
end
Ractor.new {
  loop {
    C.name
  }
}

class C
  0.step { |i|
    instance_variable_set("@iv#{i}", i)
  }
end

この問題はまだ未修正のようです。

[Bug #17649] defined? invokes method once for each syntactic element around it

defined? の式で複数回メソッドが呼ばれることがあるというバグ報告です。
以下の例だと x メソッドが defined? 時に複数回呼ばれることがあるらしい。

public def x
  $times_called += 1
end

def times_called
  $times_called = 0
  yield
  $times_called
end

# without `defined?`
times_called { x }           # => 1
times_called { -x }          # => 1
times_called { --x }         # => 1
times_called { ---x }        # => 1
times_called { x+0+0 }       # => 1
times_called { x.pred.pred } # => 1
times_called { x.x.x.x.x.x } # => 6

# with `defined?`
times_called { defined? x }           # => 0
times_called { defined? -x }          # => 1
times_called { defined? --x }         # => 2
times_called { defined? ---x }        # => 3
times_called { defined? x+0+0 }       # => 2
times_called { defined? x.pred.pred } # => 2
times_called { defined? x.x.x.x.x.x } # => 15

これは defined? a.b.c.d を呼び出した時に a a.b a.b.c a.b.c.d が個別に呼び出されしまっているからのようです。

puts RubyVM::InstructionSequence.new("defined? a.b.c.d").disasm
__END__
output:
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,16)> (catch: TRUE)
== catch table
| catch type: rescue st: 0001 ed: 0039 sp: 0000 cont: 0041
| == disasm: #<ISeq:defined guard in <compiled>@<compiled>:0 (0,0)-(-1,-1)> (catch: FALSE)
| local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| [ 1] $!@0
| 0000 putnil
| 0001 leave
|------------------------------------------------------------------------
0000 putnil                                                           (   1)[Li]
0001 putself
0002 defined                                func, :a, false
0006 branchunless                           41
0008 putself
0009 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0011 defined                                method, :b, false
0015 branchunless                           41
0017 putself
0018 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0020 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0022 defined                                method, :c, false
0026 branchunless                           41
0028 putself
0029 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0031 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0033 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
0035 defined                                method, :d, true
0039 swap
0040 pop
0041 leave

この問題は Ruby 3.1 で修正済みです。

public def x
  $times_called += 1
end

def times_called
  $times_called = 0
  yield
  $times_called
end

p times_called { defined? x.x.x.x.x.x }
# Ruby 3.0 => 15
# Ruby 3.1 => 5

[Bug #17590] M.prepend M has hidden side effect

M.prepend M を呼び出すとエラーになるが副作用があるというバグ報告です。
以下のように M.prepend M を呼んだ場合とそうでない場合で差異があります。

module M; end
class C; end
C.prepend M
C.include M

module M2; end
M2.prepend M
C.include M2

# M.prepend M を呼んでない場合
p C.ancestors # => [M, C, M2, Object, Kernel, BasicObject]


M.prepend M rescue nil
module M3; end
M3.prepend M
C.include M3

# M.prepend M を呼んだ場合
# M が複数追加されている…
p C.ancestors # => [M, C, M3, M, M3, M2, Object, Kernel, BasicObject]

これは Ruby 3.1 で修正済みです。

module M; end
class C; end
C.prepend M
C.include M

M.prepend M rescue nil
module M2; end
M2.prepend M
C.include M2

p C.ancestors
# Ruby 3.0 => [M, C, M2, M, M2, Object, Kernel, BasicObject]
# Ruby 3.1 => [M, C, M2, Object, Kernel, BasicObject]

また、上記とは別に以下のように M.prepend M すると M.ancestors # => [M, M] となる問題もあってこれも Ruby 3.1 では修正済みです。

module M
end

# 継承リストは自身だけ
pp M.ancestors
# => [M]

begin
  # 自身を prepend するとエラーになる
  # これは Ruby 2.7 でも 3.0 でも同じ
  M.prepend M
rescue => e
  puts "error : #{e.message}"
  # => error : cyclic prepend detected
end

# M.prepend M はエラーになるが Ruby 3.0 では副作用がある
# Ruby 3.1 では修正済み
pp M.ancestors
# Ruby 2.7 => [M]
# Ruby 3.0 => [M, M]
# Ruby 3.1 => [M]

[Bug #17554] [PATCH] Fix ObjectSpace.dump to include singleton class name

次のように ObjectSpace.dump に特異クラスを渡した場合に Ruby 3.0 だと "name" が含まれなくなっているというバグ報告です。

require "objspace"

puts ObjectSpace.dump(Object.new.singleton_class)
# 2.7 => {"address":"0x55adae76b630", "type":"CLASS", "class":"0x55adae7a76d0", "name":"Object", "references":["0x55adae7a9250", "0x55adae76b720"], "memsize":464, "flags":{"wb_protected":true}}
# 3.0 => {"address":"0x55d9048e80e0", "type":"CLASS", "class":"0x55d90476d738", "references":["0x55d90476e8b8", "0x55d9048e8158"], "memsize":472, "flags":{"wb_protected":true}}

これは Ruby 3.1 でまた別の情報を返すようにして対応されたようです。

require "objspace"

puts ObjectSpace.dump(Object.new.singleton_class)
# Ruby 2.7 => {"address":"0x55d5a0f891e0", "type":"CLASS", "class":"0x55d5a0ffb6f0", "name":"Object", "references":["0x55d5a1001258", "0x55d5a0f89348"], "memsize":464, "flags":{"wb_protected":true}}
# Ruby 3.0 => {"address":"0x55cfdc251690", "type":"CLASS", "class":"0x55cfdbf99718", "references":["0x55cfdbf9a898", "0x55cfdc251708"], "memsize":472, "flags":{"wb_protected":true}}
# Ruby 3.1 => {"address":"0x7f97065096b0", "type":"CLASS", "class":"0x7f9709a1a6b0", "superclass":"0x7f9709a1a868", "real_class_name":"Object", "singleton":true, "references":["0x7f9709a1a868", "0x7f9706509728"], "memsize":480, "flags":{"wb_protected":true}}

[Bug #17519] set_visibility fails when a prepended module and a refinement both exist

以下のように refine 後のメソッドを特異クラスを経由して private 化しようとするとエラーになるというバグ報告です。

module Nothing; end

class X
  # prepend しなかったらエラーにはならない
  prepend Nothing

  def hoge
  end
end

# これは OK
X.new.singleton_class.class_eval { private :hoge }

module NeverUsed
  refine X do
    def hoge(*keys)
    end
  end
end

# `private': undefined method `hoge' for class `#<Class:#<X:0x0000558fa95b7d70>>' (NameError)
# Refinements で拡張したあとに呼ぶとエラーになる
X.new.singleton_class.class_eval { private :hoge }

これは Ruby 3.0.1 で修正済みです。

[Bug #17488] Regression in Ruby 3: Hash#key? is non-deterministic when argument uses DelegateClass

次のように Ruby 3.0 で Hask#key?DelegateClass を渡すと意図しない結果が返ってくるというバグ報告です。

puts "Running on Ruby: #{RUBY_DESCRIPTION}"

program = <<~EOS
  require "delegate"
  TypeName = DelegateClass(String)

  hash = {
  "Int" => true,
  "Float" => true,
  "String" => true,
  "Boolean" => true,
  "WidgetFilter" => true,
  "WidgetAggregation" => true,
  "WidgetEdge" => true,
  "WidgetSortOrder" => true,
  "WidgetGrouping" => true,
  }

  puts hash.key?(TypeName.new("WidgetAggregation"))
EOS

iterations = 20
results = iterations.times.map { `ruby -e '#{program}'`.chomp }.tally

# Ruby 3.0 で実行すると false が返ってくることがある
puts "Results of checking `Hash#key?` #{iterations} times: #{results.inspect}"
# Ruby 2.7 => Results of checking `Hash#key?` 20 times: {"true"=>20}
# Ruby 3.0 => Results of checking `Hash#key?` 20 times: {"false"=>12, "true"=>8}

この問題は Ruby 3.0.1 で修正済みです。

[Bug #17481] Keyword arguments change value after calling super without arguments in Ruby 3.0

次のように super を呼び出す前と後でキーワード引数の値が変わってしまうというバグ報告です。

class BaseTest
  def call(a:, b:, **)
  end
end

class Test < BaseTest
  def call(a:, b:, **options)
  p options  # =>  {:c=>{}}
  super
  # super を呼び出した後で options の値が変わってしまっている…
  p options  # =>  {:c=>{}, :a=>1, :b=>2}
  end
end

Test.new.call(a: 1, b: 2, c: {})

この問題は Ruby 3.1 で修正済みです。
これ、Ruby 3.0 系にバックポートしなくても大丈夫なんだろうか…。

おわりに

と、言うことで25日続けた 一人 bugs.ruby Advent Calendar 2021 もこれにて完走です。
最初はやろうかどうしようか迷っていたんですがなんだかんだ今年の Ruby を振り返る事ができて楽しかったです。
開発者の皆様、今年も1年間お疲れ様でした&ありがとうございました。