a-sanの日記

 | 

2011-02-25(Fri)

Double.parseDouble("2.2250738585072012e-308")

実際のソースを見て、解析してみました。

	// 問題のコード
	Double.parseDouble("2.2250738585072012e-308");

これが問題のコードですが、Double.parseDouble()の中身は以下のようになっています

import sun.misc.FloatingDecimal;

public final class Double extends Number implements Comparable<Double> {
    ...
    public static double parseDouble(String s) throws NumberFormatException {
	    return FloatingDecimal.readJavaFormatString(s).doubleValue();
    }
    ...
}

実際にはsun.misc.FloatingDecimal が呼ばれています

今回問題となるのは、後者のdoubleValue()になります


まず前者のreadJavaFormatString() は文字列を解析し、符号指数部、仮数部、指数部、異常な値かどうかを判断しています

このメソッドが返すのは、FloatingDecimalのオブジェクトであり、問題の文字列では、以下のコンストラクタの値と同じ物を返します。

    new FloatingDecimal(false, -307, "22250738585072012******", 17, false)

個々の引数は以下の内容です。

  マイナスか?   false
  指数部         -307
  仮数部の文字列 "22250738585072012******" ここで'*'は初期化されたままの文字'\u0000'
  仮数部の文字数  17
  異常な値か?    false

すなわち、0.22250738585072012e-307 という数値をあらわしています


次の doubleValue()は上記のオブジェクトからdoubleの値に変換しています

変換は大きく分けて2段階で行っています

前半は、上記の値からおおよその数値を求めています

後半は、より精度をあげるため、丸め幅(ulp:Units in the Last Place)の1/2になるまで、小さな値を足したり引いたりしています

通常なら一度で誤差が1/2になり、ループから抜けます(多分)。

ところが、その判断の仕方が1ケタ間違っているため、無限ループを回っています

問題の値では、以下のように足したり引いたりしながらいつまでも続きます

 2.2250738585072014E-308
 2.225073858507201E-308
 2.2250738585072014E-308
 2.225073858507201E-308
 2.2250738585072014E-308
 2.225073858507201E-308
 2.2250738585072014E-308
 2.225073858507201E-308
 ...

現象としては無限ループであり、CPU負荷があがり、処理が戻らなくなります

そのプロセスが異常終了したり、メモリを食いつぶすことはないでしょう。

他のプロセスが誤動作することもないでしょう(CPU負荷を除けば)。

また、JavaVM上で動いているので、セキュリティ上の問題となることもないでしょう。

保証はできませんが、ソースでの確認、および動作確認した感じではそうでした。)


コードを読んだ感じでは、他の値でも起こりうるのではないかと思います

少なくとも、以下のパターンでも同様の現象が起こります


パターン1:前後に空白文字がある場合

最初trim()しているので、前後に空白文字がある場合でも同様の現象が起きます

例 " 2.2250738585072012e-308"


パターン2:指数部の文字がE/eの大文字小文字が異なる場合

例 "2.2250738585072012E-308"


パターン3:末尾にD,d,F,fがある場合

末尾にD,d,F,fがあっても読み飛ばしているだけなので、同様の現象が起きます

例 "2.2250738585072012e-308D"


パターン4:符号が異なる場合。ついている場合

例 "-2.2250738585072012e-308"

例 "+2.2250738585072012e-308"


パターン5:仮数部と指数部が異なっても同じ数値を表すとき。

例 "0.22250738585072012e-307"

例 "22.250738585072012e-309"


パターン6:仮数部に有効桁数以上の数値がある場合

例 "2.225073858507201200000000e-308"

例 "2.225073858507201244444444e-308"

(2011/03/02追記)KINさんのコメントにあるように、数字が続く場合なら常に起こるわけではなく、2進数にしたとき有効桁数に影響を及ぼさない程度の端数があるときです。



他にも同じ現象を起こすパターンがあるかもしれません。

unibonunibon 2011/02/25 02:50 世間ではこのようなことが話題になっていたのですね。
しかし、Sun はテストしないで出荷してたのでしょうかね。コアな部分ですからもっと厳密なテストをして出荷されているものかと思っていましたが、案外抜けがあるものなのですね。

a-sana-san 2011/02/25 03:34 メジャーなAPIだし、再現性があるし、発生すれば無限ループなので気づくハズですし、何年も潜んでいたということは、極めて頻度が低いのでしょう。
しかし低いとはいえ、影響範囲は広いので、悪用されないか心配してます。

KINKIN 2011/03/02 15:15 この無限ループに関する、詳細かつ日本語の情報がすくなかったため、とても参考になりました。
ありがとうございます。

私も試しに再現させてみたのですが、パターン6については認識が違っていました。
例えば、以下の数値だと無限ループは発生しません。
"2.225073858507201245555555e-308"

私が色々と試行した結果、以下のような推測をしています。
・四捨五入して"2.2250738585072012e-308"になる範囲の値の場合に、無限ループが発生する。
(要するに、"2.22507385850720115e-308"以上、"2.22507385850720125e-308"未満の範囲の数値の場合に無限ループが発生する。)
パターン6の例は有効桁数の問題ではなくて、この数値の範囲に該当する数値だったため、無限ループが発生しているのではないかと思います。
もしお時間があれば、お試しください。

KINKIN 2011/03/02 15:30 すみません、先ほどのコメントを訂正させていただきます・・・

再度確認したところ、以下の範囲の値の場合に無限ループが発生することを確認しました。
"2.22507385850720114e-308"以上、"2.22507385850720126e-308"未満。

ですので、「四捨五入して〜」の下りは間違っています。
失礼いたしました。

a-sana-san 2011/03/02 23:53 お役に立ててうれしいです。
パターン6については、再現する条件の記述が不十分でした。ご指摘ありがとうございます。
これについては2進数に変換した後の処理になるので、四捨五入ではなく、2進数にしたときの端数だと思います。

 |