Safe level 4 環境と -d オプションの喧嘩

Ruby の Safe level 4 環境に関するバッドノウハウ!例えば以下のようなスクリプト(test.rb)を考えます。

thread = Thread.new do
  $SAFE = 4
  @aaa
end

thread.join

puts "finished"

これを実行します。

% ruby test.rb
finished

無事に終了しました。ところが -d オプションを付けてデバッグモードで実行すると次のようになります。

% ruby -d test.rb
Exception `SecurityError' at test.rb:6 - Insecure operation `write' at level 4
test.rb:3:in `write': Insecure operation `write' at level 4 (SecurityError)
	from test.rb:6:in `join'
	from test.rb:6

なぜ "write" に関するセキュリティーエラーが起こるのでしょうか。この理由は次のようにセーフレベルを下げればすぐに分かります。

thread = Thread.new do
  # $SAFE = 4 # コメントアウトしたから safe level 0
  @aaa
end

thread.join

puts "finished"

で、これ(test2.rb)を実行します。

% ruby -d test2.rb
test.rb:3: warning: instance variable @aaa not initialized
finished

というわけで、この警告文を $stderr に書き出そうとして、safe level 4 の制限にひっかかってしまっているのでした。なんという残念なことでしょうか。知っていれば対応できるので問題ありませんが、インスタンス変数を初期化しない場合というのは多々ありますから、困ったものです。

さて、ここで検討するべきは、safe level 4 とデバッグモードとの関係です。正しいのは以下のうちどれでしょう。

  1. safe level 4 環境ではデバッグモードを使用すべきではない。デバッグモードが使えなくても大した問題ではない。
  2. safe level 4 環境に入る前にデバッグモードを無効化($DEBUG = false)するべきだ。
  3. safe level 4 環境では全てのインスタンス変数を事前に初期化しておくべきだ。
  4. safe level 4 環境では $stderr の値を適当なものに変えておくべきだ。

1 だったら嫌です。2はある程度正しそうな気がしますが、それだったら safe level 4 環境になった時自動的に無効化して欲しい気がします。3 は自分で書く範囲ならともかく、ライブラリを使った場合などを考えれば実質的に無理です。4 も正しそうな気がしますが、これもそう気軽ではありません。例えば$stderrをStringIOインスタンスにしちゃう手は次のようになります。

require "stringio"

$stderr = StringIO.new
$stderr.string.taint # ここ重要

thread = Thread.new do
  $SAFE = 4
  @aaa
end

thread.join

puts $stderr.string
puts "finished"

というように、StringIO#string に対して taint しなきゃいけなかったりして、とっても素敵な罠でいっぱいな気がします。これを間違えたり忘れたりするとエラーが一切出力されずに停止するのでワケがわからないことになります。

追記

なんと 1.9 の trunk では解決されているみたいです。二ヶ月くらい前にビルドしたruby1.9では同様のエラーが出てたから、きっとダメなんだろうと思ってたけど、ひょっとして、と今リビルドしてみたら解決されていました。すごいよ、さすがだ!

thread = Thread.new do
  $SAFE = 4
  @aaa
end

thread.join

puts "finished"

を実行すると次のようになります。

% ruby1.9 -d test.rb
test.rb:3: warning: instance variable @aaa not initialized
finished

素晴しいと思います。そうそう、こうでなくちゃ。