コンピュータは数値さえ正確に扱えない

(Last Updated On: 2018年8月13日)

コンピュータで数値を正確に扱うのは「実は結構難しい」です。つまり「コンピューターは数値を正確に扱えない」という事です。「コンピューターが数値を正確に扱えない?!何を言ってるんだ?!」と思った方は是非読んでみてください。

コンピューターは数値を正確に取り扱えない

コンピューターは数値を正しく取り扱えるから便利なのでは?と思うかも知れません。「コンピューターは数値を正確に取り扱えない」これは動かし難い事実/制限です。

コンピューターが数値を正しく取り扱える条件が決まっています。条件の範囲外であれば正確に取り扱えません。

この問題によりロケットが爆発するといった事例もあります。

整数

最も解りやすいのは整数です。通常、整数は固定の記憶領域を持つ整数型で表現されます。32ビット整数、64ビット整数、という言葉はITシステムの開発者でなくても聞いた事があると思います。これらの整数には符号付きと符号無し整数があります。正確に表現できる整数は以下の通りです。

  • 符号付き32ビット整数: -2^31 〜 2^31 – 1
  • 符号無し32ビット整数: 0 〜2^32
  • 符号付き64ビット整数: -2^63 〜 2^63 – 1
  • 符号無し64ビット整数: 0 〜2^64

範囲を超えた場合、数値は正しく保存できません。下限を超えた場合はアンダーフロー、上限を超えた場合はオーバーフローが発生します。オーバー/アンダーフローした値は正確に表現できません。

例えばPHPの場合、整数がオーバーフローすると自動的により表現可能な範囲が広い浮動小数点型に変換します。

$ php -r 'echo PHP_INT_MAX + 1;'
9.2233720368548E+18

しかし、64ビット環境ではこの動作による数値の値は正確ではありません。

※ 任意精度整数を利用していても整数値に注意が必要です。ライブラリやデータベースの数値は64bit整数、32bit整数、倍精度浮動小数点数の場合がほとんどです。データ型と表現可能範囲に注意していないと問題の原因になり得ます。

 

実数(浮動小数点)

コンピューターによる浮動小数点型の数値演算で最初の方に学ぶことは「浮動小数点演算は正確でない」ことです。一般に浮動小数点演算を行う場合、IEEE 754の倍精度浮動小数点型(いわゆるdouble型)を利用します。

倍精度浮動小数点型が正確に数値を表現できる数値の有効数字の範囲は符号付き53ビット整数分しかありません。符号付き64ビット整数を浮動小数点型に変換した場合、12ビット分の情報が失われます。つまり、正確ではない、のです。

浮動小数点型は 1/3 (= 0.3333333333333333….) などの数値は正確に表現できません。有効数字の範囲内で”できるだけ正確”に保存します。”できるだけ正確”に保存しても、実際の正確な数値は保存されません。このような数値は必ず”丸められる”ことになります。

※ IEEE 754の浮動小数点数値は10進数で保存されません。”近似値”が保存されます。このため、割り切れる浮動小数点数値でも”近似値”です。詳しくはIEEE 754-1985の仕様を参考にしてください。

丸められた数値には”誤差”あります。丸められた数値を使って演算すると”誤差”がどんどん積み重なります。倍精度浮動小数点型の有効数字は符号付き2^52の範囲ですが、演算をすると有効桁数がたった数桁になってしまう(正確に表現できている演算結果の桁数が数桁になる)といったことがごく普通にあります。

 

実際のアプリケーションにおける数値の取り扱い

コンピューターが数値を正確に取り扱えないことは解りました。浮動小数点型はどうしようもないです。各自(開発者)ができるだけ演算が正確になるよう努力するしかありません。

整数が正確に取り扱えないならエラーにすれば良い、と考えるでしょう。データを正確に保存する役割をもつRDBMSのデータ型のキャスト(文字リテラルから整数型)でどのような動作になるか確認してみます。

PostgreSQLとMariaDB/MySQLの整数の取り扱い

PostgreSQLのint8型のオーバーフローはエラーになります。

yohgaki@[local] ~=> SELECT '9999999999999999999'::int8;
ERROR:  22003: value "9999999999999999999" is out of range for type bigint
行 1: SELECT '9999999999999999999'::int8;
             ^
LOCATION:  scanint8, int8.c:115
時間: 0.164 ms

MariaDBのint型のキャストはエラーとならず、オーバーフローした値になります。データを保存した場合、オーバーフローした値が保存されます。1

MariaDB [(none)]> SELECT CAST('9999999999999999999' AS INT);
+------------------------------------+
| CAST('9999999999999999999' AS INT) |
+------------------------------------+
| -8446744073709551617 |
+------------------------------------+
1 row in set, 1 warning (0.00 sec)

 

整数オーバー/アンダーフローをどう取り扱うべきか?

RDBMSの場合、整数のオーバー/アンダーフローがエラーとなって実行できなくてもあまり大きな問題ではありません2。通常のアプリケーション(Webアプリなどコード)で発生した場合にエラーになってプログラムが停止しては困ります。

例えば、

$result = $a * $b;

の実行結果が整数オーバー/アンダーフローしたからといって、ゼロ除算エラーのようにいきなりプログラムの実行が停止すると困ります。Webアプリなら真っ白なページになったりします。Excel/Wordのようなアプリの場合、セーブする間も無くクラッシュすることになります。

「ゼロ除算エラーのようにいきなりプログラムの実行が停止すると困る」ということは「言語レベルで整数オーバー/アンダーフローを致命的エラーにできない」ということです。3

 

整数オーバー/アンダーフローを防ぐのはアプリ開発者の責任

「MariaDB/MySQLがデフォルトで整数オーバーフローをエラーにしないのはMariaDB/MySQLが悪い!」と思うかも知れません。しかし、これは半分当たりですが半分外れです。

RDBMSに限らず、整数オーバーフローはどこにでも発生し得ます。外部システムやライブラリの中でなく、アプリ開発者が書いているコードにも発生し得ます。

誰の責任に於て整数オーバー/アンダーフローを防止/検出すべきか?

整数オーバー/アンダーフローはアプリ開発者が書いているコードにも発生し得えます。任意精度整数をサポートしている場合でも、ライブラリ/データベースのパラメーターが任意精度整数とは限りません。RDBMSやライブラリなどが整数オーバー/アンダーフローを検出してくれるかも知れませんが、それでは手遅れの場合もあります。

エラーになった時はエラーになったで、いい加減に(プログラマが予期しない場所で)プログラムが停止するようでは問題の原因を作っているようなモノです。4

従って、基本的にはアプリ開発者が責任を持つべきであることになります。5

 

アプリ開発者はどう責任を果すべきか?

一般的な数値型のデータの場合、普通に数値範囲や数値桁数を入力バリデーションしていればオーバー/アンダーフローは起こりえないケースが多数あります。

入力バリデーションだけではオーバー/アンダーフローを防止できない場合、プログラマはプログラムのロジックの中で、オーバー/アンダーフローを検出しなければなりません。

参考: PHPで整数オーバーフロー/アンダーフローをチェックする方法

 

使っている言語(Ruby/Python3など)は任意精度の整数だから大丈夫?

いいえ、大丈夫でないことは既に説明した通り明らかです。言語の整数型は任意精度整数でも、外部システムやライブラリの整数型が任意精度であることの方が少ないです。計算結果が出鱈目だったり、データベースに不正な情報が保存されても構わない、というシステムは多くないでしょう。例外の発生も問題の原因になります。

言語が任意精度整数型をサポートしていても、整数の表現範囲の制限を気にせずに信頼できるシステムを作ることは出来ません。

まとめ

コンピューターが最も得意とする分野が数値の取り扱いですが、その最も得意とする分野でさえ「正確」に取り扱えません。

しかも、ライブラリや基盤となるソフトウェアにはオーバーフローを敢えてチェックしていないモノも多数あります。

さらに、単純におかしなデータになるだけでなく整数演算のバグがセキュリティ脆弱性の原因になっているモノも多数あります。

Webアプリに限らず、数値パラメーターが「数値である」ことだけを入力バリデーションしている「弱い入力バリデーション」であること6が多いです。ISO 27000等のセキュリティ標準が求める入力バリデーションはこのような「弱い入力バリデーション」ではありません

コンピューターは数値を正確に扱えません。数値の入力データが正しく取り扱えるよう「強いバリデーション」を使うようにしましょう。

たった一文字でも余計な文字が入っているとインジェクションが可能になる場合も少なくありません。

入力バリデーションで許可した文字で発生するリスク

最近は安易にデータベースにNoSQLを使っているアプリが増えています。RDBMSを使い、参照整合性を定義していれば”不正なID”でアプリが使い物にならなくなる(DoSに脆弱になる)ことはありませんが、安易にNoSQLを使っているアプリの場合、極端にDoSに脆弱になることがあります

仕組み的にコンピューターは数値を正確に扱えません。基本的なことですが忘れがちなので注意しましょう。

正しく動作するソフトウェアの作り方

参考

 


  1. MariaDB/MySQLの場合、SQLモードをTRADITIONALにすると、PostgreSQLと同様にエラーにできます。ただし、SQLモードをTRADITIONALにするとデフォルト状態のMariaDB/MySQL用に書かれたコードが多数動作しなくなります。 
  2. データベースのクエリ/トランザクションは、そもそも失敗すること、を前提として利用するものです。しかし、データベースエラーで処理を中止しなければならない場合、既に実行済みの操作(メール送信など)の取り消しができなかったり、難しい場合も少くありません。出来る限りエラーが起きないようにしなければなりません。 
  3. オーバー/アンダーフローを実行時の例外にするのはあり得ます。 
  4. iPhoneは特定の文字でクラッシュする問題が頻繁に起きています。これは「不正な文字」があったらあったで、いい加減に(プログラマが予期しない場所で)プログラムが停止することが原因のようです。エラーが起きるから出鱈目なデータでもOK、だと様々な問題を作ります。 
  5.  RDBMSやライブラリが整数オーバー/アンダーフローを検出してエラーにすることは無意味ではありません。多層的にバグや脆弱性を防止するのは必要なことです。 
  6. もっと酷いケースは数値範囲が明白にも関わらず、バリデーションせず数値に強制キャストするだけの場合もあります。 

投稿者: yohgaki