JPCERT コーディネーションセンター

FLP32-C. 数学関数における定義域エラーおよび値域エラーを防止または検出する

FLP32-C. 数学関数における定義域エラーおよび値域エラーを防止または検出する

C 標準 [ISO/IEC 9899:2011]、7.12.1 節、第2パラグラフでは <math.h> の数学関数について、3種類のエラーが規定されている。

入力実引数の値が、数学関数が定義される定義域の外側に存在する場合、定義域エラー (domain error) が発生する。

第3パラグラフには次のように規定されている。

極エラー (pole error) (特異点 (singularity) や 無限項的 (infinitary) としても知られている)) が発生するのは、有限の入力引数がその極限に近づくことで、結果が無限大そのものとなる場合である。

第4パラグラフには次のように規定されている。

関数の数学的な結果の絶対値が大きすぎるため、指定された型のオブジェクトで表現できない場合、値域エラー (range error) が発生する。

定義域エラーの例として、たとえば sqrt(-1.0) のような負の数の平方根が考えられる。これは実数の世界では意味をなさない。 一方、10 の 100 万乗、pow(10., 1e6) のような計算は、多くの処理系において、double 型が持つ範囲の限界ゆえに浮動小数点表現で表現することができず値域エラーとなる。いずれの場合も、関数は何らかの値を返すが、値は正しい計算結果ではない。極エラーの一例は log(0.0) である。結果は負の無限大となる。

定義域エラーや極エラーを防ぐ方法は、数学関数を呼び出す前に引数の範囲検査を慎重に行い、範囲を超える場合にはそれに対処することである。

値域エラーは通常、未然に防ぐことができない。なぜならば浮動小数点数や使用する関数の実装に依存するからである。 値域エラーを未然に防ぐよりもその発生の検知に努め、値域エラーが発生した際には別の対応を行うべきである。

次に示す表では、double 型の標準数学関数を列挙し、引数が適切な入力範囲であることを確認するために行うべき検査を示すとともに、演算結果が閾値エラーあるいは極エラーになりうるか否かを、C標準の規定に従い示している。floatlong double 型の同様の関数も存在するが、表の簡略化のために省略する。関数に特定の定義域が規定されている場合、引数の値を検査すべきである。また、閾値エラーが発生しうるのであれば、これもまた検査すべきである。

関数

定義域

値域

極 

acos(x)

-1 <= x && x <= 1

No

No
asin(x) -1 <= x && x <= 1 Yes No
atan(x) None Yes No

atan2(y, x)

x != 0 && y != 0

No

No

acosh(x)

x >= 1

Yes

No
asinh(x) None Yes No

atanh(x)

-1 < x && x < 1

Yes

Yes

cosh(x), sinh(x)

None

Yes

No

exp(x), exp2(x), expm1(x)

None

Yes

No

ldexp(x, exp)

None

Yes

No

log(x), log10(x), log2(x)

x >= 0

No

Yes

log1p(x)

x >= -1

No

Yes

ilogb(x)

x != 0 && !isinf(x) && !isnan(x)

Yes

No
logb(x) x != 0 Yes  Yes

scalbn(x, n), scalbln(x, n)

None

Yes

No

hypot(x, y)

None

Yes

No

pow(x,y)

x > 0 || (x == 0 && y > 0) ||
(x < 0 && y is an integer)

Yes

Yes

sqrt(x)

x >= 0

No

No
erf(x) None Yes No

erfc(x)

None

Yes

No

lgamma(x), tgamma(x)

x != 0 && ! (x < 0 && x is an integer)

Yes

Yes

lrint(x), lround(x)

None

Yes

No

fmod(x, y), remainder(x, y),
remquo(x, y, quo)

y != 0

Yes

No

nextafter(x, y),
nexttoward(x, y)

None

Yes

No

fdim(x,y)

None

Yes

No 

fma(x,y,z)

None

Yes

No
定義域と極の検査

定義域エラーと極エラーに対する最も確実な対応は、次の例のように、演算前に引数を検査してエラーの発生を防ぐことである。

double safe_sqrt(double x) {
  if (x < 0) {
    fprintf(stderr, "sqrt requires a nonnegative argument");
    /* 値域エラーや極エラーを処理する */
  }
  return sqrt (x);
}
値域の検査

値域エラーは通常未然に防ぐことはできない。したがって、最も確実な方法は、値域エラーの発生を検出し、適切に処理することである。

数学関数が返すエラー条件を直接取り扱おうとすると処理が煩雑になる。C 標準 [ISO/IEC 9899:2011]、7.12.1 節では、浮動小数点オーバーフローの動作について、次のように規定されている。

浮動小数点演算の結果は、数学的な結果の絶対値が有限だが大きすぎ、指定された型のオブジェクトにおいて、極めて大きい丸めエラーを発生させずには表現することができない場合に、オーバフローを発生する。 浮動小数点演算の結果がオーバーフローし既定の丸め動作が有効な場合、関数は返却値の型に応じ、関数の正しい値と同じ符号でマクロ HUGE_VALHUGE_VALF、または HUGE_VALL の値を返す。整数式 math_errhandling & MATH_ERRNO が 0 以外の場合、整数式 errno の値は ERANGE になる。整数式 math_errhandling & MATH_ERREXCEPT が 0 以外の値の場合、"オーバーフロー" 浮動小数点例外を生成する。

エラー検査を行う場合、関数の戻り値を HUGE_VAL あるいは 0 と比較するべきではない。理由は次の通り。

処理系によっては errno を設定しないものもあるため、数学関数のエラーを errno を使って検査する方法は確実ではない。実数関数については、math_errhandling & MATH_ERRNO がゼロでないことを確認することで、errno が設定されているかどうかを調べることができる。複素数関数については、C 標準、7.3.2 節に「処理系は、errno を設定してもよいが、必ずしも設定する必要はない」と規定されている [ISO/IEC 9899:2011]。

System V Interface Definition (SVID3) では数学ライブラリにおけるエラーの取り扱いをより柔軟に規定していた[UNIX 1992]。ユーザは数学関数にエラーが発生した際に呼び出される matherr() という関数を定義することができる。 この関数を使うことで、診断メッセージを出力したり、プログラムの実行を強制終了したり、望ましい戻り値を設定することができる。 matherr() 関数は C や POSIX には採用されていないため、一般に可搬性はない。

次に示すエラー処理テンプレートでは、C のマクロ math_errhandling が定義されている場合に、C 標準関数を使用して浮動小数点エラーを処理し、かつマクロが使用されるべきことを知らせる。それ以外の場合には、errno を検査する。

#include <math.h>
#include <fenv.h>
#include <errno.h>

/* ... */
/* 数学関数を呼び出しエラーチェック */
{
  #pragma STDC FENV_ACCESS ON

  if (math_errhandling & MATH_ERREXCEPT) {
    feclearexcept(FE_ALL_EXCEPT);
  }
  errno = 0;

  /* 数学関数の呼び出し */

  if ((math_errhandling & MATH_ERRNO) && errno != 0) {
    /* 値域エラーの処理 */
  } else if ((math_errhandling & MATH_ERREXCEPT) &&
             fetestexcept(FE_INVALID | FE_DIVBYZERO |
                          FE_OVERFLOW | FE_UNDERFLOW) != 0) {
    /* 値域エラーの処理 */
  }
}

浮動小数点エラーの検知方法の詳細については「FLP03-C. 浮動小数点エラーを検知して処理する」を参照。

非正規化数

非正規化数 (subnormal number) とは全ての精度ビットを使用しない非ゼロの数である[IEEE 754 2006]。非正規化数を使うことで、正規化数(全ての精度ビットを使う)よりもゼロに近い値を表現することができる。asin()asinh()atan()atanh()erf() 関数は非正規化数を渡されると値域エラーを生成する場合がある。実引数に非正規化数を渡されたときこれらの関数は正確でない非正規化数を生成し、アンダーフローエラーとなる。C 標準、7.12.1 節、第6パラグラフには、浮動小数点アンダーフローについて次のように規定している[ISO/IEC 9899:2011]。

浮動小数点演算では、数学的な結果の絶対値が小さすぎ、指定された型のオブジェクトにおいて、極めて大きい丸めエラーを発生させずには表現することができない場合に、アンダフローが発生する。演算結果がアンダフローした場合、関数は処理系定義の値を返す。この値の絶対値は高々、指定された型の最小の正の正規化数とする。整数式 math_errhandling & MATH_ERRNO が 0 以外の値の場合、整数式 errno の値が ERANGE となるかどうかは、処理系定義とする。整数式 math_errhandling & MATH_ERREXCEPT が 0 以外の値の場合、"アンダフロー"浮動小数点例外を生成するかどうかは、処理系定義とする。

浮動小数点数演算をサポートしていても非正規化数をサポートしない処理系、たとえば16進(16を基数とする)浮動小数点表現を採用している IBM S/360 や非正規化数をスキップする(あるいは非正規化数をゼロとして扱う) IEEE-754 非準拠の処理系では、次に挙げる引数で関数を呼び出すと閾値エラーを返す。

min は与えられた浮動小数点数型における最小値を表し、subnorm は非正規化数を表す。

付属書Fと非正規化数がサポートされている場合、戻り値は正確であり、かつ値域エラーは発生しない。C 標準、F.10.7.1 には fmod()remainder()remquo() 関数について次のように規定している [ISO/IEC 9899:2011]。

非正規化数の結果がサポートされている場合、戻り値は正確であり、現在の丸め方向モードに依存しない。

C 標準、付属書F の F.10.7.2節、第2パラグラフおよび F.10.7.3節、第2パラグラフには非正規化数がサポートされる場合について規定している。

違反コード (sqrt())

次の違反コードは x の平方根を求めている。

#include <math.h>

void func(double x) {
  double result;
  result = sqrt(x);
}

しかし、x の値が負の場合、このコードでは定義域エラーとなる。

適合コード (sqrt())

この関数には定義域エラーは存在するが値域エラーは存在しないため、範囲検査を行うことで定義域エラーを防止することができる。

#include <math.h>

void func(double x) {
  double result;

  if (isless(x, 0.0)) {
    /* 定義域エラーの処理 */
  }

  result = sqrt(x);
}
違反コード (sinh()、値域エラー)

次の違反コードでは x を引数とする双曲線正弦関数の値を求めている。

#include <math.h>

void func(double x) {
  double result;
  result = sinh(x);
}

このコードでは、x の絶対値が非常に大きい場合に値域エラーが発生する。

適合コード (sinh()、値域エラー)

この関数には定義域エラーはないが値域エラーが発生する可能性があるため、値域エラーを検知して適切に処理しなくてはならない。

#include <math.h>
#include <fenv.h>
#include <errno.h>

void func(double x) {
  double result;
  {
    #pragma STDC FENV_ACCESS ON
    if (math_errhandling & MATH_ERREXCEPT) {
      feclearexcept(FE_ALL_EXCEPT);
    }
    errno = 0;

    result = sinh(x);

    if ((math_errhandling & MATH_ERRNO) && errno != 0) {
      /* 値域エラーの処理 */
    } else if ((math_errhandling & MATH_ERREXCEPT) &&
               fetestexcept(FE_INVALID | FE_DIVBYZERO |
                            FE_OVERFLOW | FE_UNDERFLOW) != 0) {
      /* 値域エラーの処理 */
    }
  }

  /* 演算結果を使った処理... */
}
違反コード (pow())

次の違反コードは xy 乗を求めている。

#include <math.h>

void func(double x, double y) {
  double result;
  result = pow(x, y);
}

このコードでは、x の値が負で y の値が整数でない場合、あるいは xy もゼロの場合に定義域エラーが発生する。x の値がゼロで y の値が負の場合には定義域エラーあるいは極エラーが発生する。また、結果の値が double で表現できない場合には値域エラーが発生する。

適合コード (pow())

pow() 関数は定義域エラー、極エラー、および値域エラーを生成する可能性があるため、まずはじめに xy の値が適切な定義域の範囲にあり極エラーを生成しないことを確認し、値域エラーの発生時にはこれを検出して適切に処理しなくてはならない。

#include <math.h>
#include <fenv.h>
#include <errno.h>

void func(double x, double y) {
  double result;

  if (((x == 0.0f) && islessequal(y, 0.0)) || isless(x, 0.0)) {
    /* 定義域エラーと極エラーの処理 */
  }

  {
    #pragma STDC FENV_ACCESS ON
    if (math_errhandling & MATH_ERREXCEPT) {
      feclearexcept(FE_ALL_EXCEPT);
    }
    errno = 0;

    result = pow(x, y);

    if ((math_errhandling & MATH_ERRNO) && errno != 0) {
      /* 値域エラーの処理 */
    } else if ((math_errhandling & MATH_ERREXCEPT) &&
               fetestexcept(FE_INVALID | FE_DIVBYZERO |
                            FE_OVERFLOW | FE_UNDERFLOW) != 0) {
      /* 値域エラーの処理 */
    }
  }

  /* 計算結果を使った処理... */
}
違反コード (asin()、非正規化数)

次の違反コード例では x を引数とする逆正弦関数の値を求めている。

#include <math.h>
 
void func(float x) {
  float result = asin(x);
  /* ... */
}
適合コード (asin()、非正規化数)

この関数には定義域エラーはないが値域エラーが生じる可能性がある。そのため、値域エラーを検出し、適切に処理しなくてはならない。

#include <math.h>
#include <fenv.h>
#include <errno.h>
void func(float x) { 
  float result;

  {
    #pragma STDC FENV_ACCESS ON
    if (math_errhandling & MATH_ERREXCEPT) {
      feclearexcept(FE_ALL_EXCEPT);
    }
    errno = 0;

    result = asin(x);

    if ((math_errhandling & MATH_ERRNO) && errno != 0) {
      /* 値域エラーの処理 */
    } else if ((math_errhandling & MATH_ERREXCEPT) &&
               fetestexcept(FE_INVALID | FE_DIVBYZERO |
                            FE_OVERFLOW | FE_UNDERFLOW) != 0) {
      /* 値域エラーの処理 */
    }
  }

  /* 演算結果を使った処理... */
}
リスク評価

数学関数呼出しにおいて定義域エラー防止と値域エラーの検査を行っていない場合、予期せぬ結果を引き起こす可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

FLP32-C

P8

L2

自動検出(最新の情報はこちら

ツール

バージョン

チェッカー

説明

Fortify SCA

5.0

 

CERT C Rule Packを使ってこのルールの違反を検出できる

Polyspace Bug Finder R2016a Invalid use of standard library floating point routine

Wrong arguments to standard library function

関連するガイドライン
CERT C コーディングスタンダード FLP03-C. 浮動小数点エラーを検知して処理する
MITRE CWE CWE-682, Incorrect Calculation
参考資料
[ISO/IEC 9899:2011]

7.3.2, "Conventions"
7.12.1, "Treatment of Error Conditions"
F.10.7, "Remainder Functions" 

[IEEE 754 2006]  
[Plum 1985] Rule 2-2
[Plum 1989] Topic 2.10, "conv—Conversions and Overflow"
[UNIX 1992] System V Interface Definition (SVID3)
翻訳元

これは以下のページを翻訳したものです。

FLP32-C. Prevent or detect domain and range errors in math functions (revision 158)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter