MSC01-C. 論理的な完全性を追求する
データの状態についてあらゆる可能性を考慮しておかないと、ソフトウェアの脆弱性を引き起こす可能性がある。
違反コード (if の連鎖)
以下のコード例は、a が b と c のいずれでもない条件を判定できない。これは、以下の例においては問題ないかもしれないが、a が取り得るすべての値を考慮しないと、a が予期せず異なる値を取った場合に論理エラーを引き起こしかねない。
if (a == b) { /* ... */ } else if (a == c) { /* ... */ }
適合コード (if の連鎖)
以下の解決法は、予期せぬ条件がないか明示的に検査して、そのような条件を適切に処理する。
if (a == b) { /* ... */ } else if (a == c) { /* ... */ } else { /* エラー条件の処理 */ }
違反コード (switch)
以下のコード例は、想定される場合をすべては考慮していない。以下の例においてはそれでも問題ないかもしれないが、widget_type の値をすべては考慮していないため、widget_type が予期しない値である場合に論理エラーを引き起こしかねない。
C では特にこの問題を起こしやすい。列挙定数として宣言される識別子の型は int であるため、プログラマはこの例に示すように任意の整数値を enum 型に誤って代入してしまうことがある。
enum WidgetEnum { WE_W, WE_X, WE_Y, WE_Z } widget_type; widget_type = 45; switch (widget_type) { case WE_X: /* ... */ break; case WE_Y: /* ... */ break; case WE_Z: /* ... */ break; }
処理系固有の詳細
Microsoft Visual C++ .NET は、/W4 が指定されていると、enum 型に整数値が代入されても警告せず、取り得るすべての値が switch 文に含まれていなくても警告しない。
適合コード (switch)
以下の解決法は、switch 文に default 句を追加することで、予期せぬ状態がないか明示的に検査している。
enum WidgetEnum { WE_W, WE_X, WE_Y, WE_Z } widget_type; widget_type = WE_X; switch (widget_type) { case WE_W: /* ... */ break; case WE_X: /* ... */ break; case WE_Y: /* ... */ break; case WE_Z: /* ... */ break; default: /* 予期せぬ状態 */ /* エラー条件の処理 */ break; }
switch 文の制御式が取り得る値がすべてラベルとして指定されていても、default ラベルを追加してよい。これは予防策として追加しているからであり、「MSC07-C. デッドコードを検出して削除する」の例外にあたる(MSC07-EX1)。
歴史的議論の経緯
このコーディング方法は長い間議論のテーマとなっていたが、ようやく方針が明らかになった。
当初、ベストプラクティスの策定者たちの間で合意されていたのは、switch 文ごとに必ず default ラベルを付けるという考え方だった。やがてコンパイラや静的解析ツールが登場し、enum 型の switch に対して列挙値ごとに case ラベルがあるかを検証できるようになった。しかし検証できるのは default ラベルがない場合に限られていたため、静的解析ができるように default ラベルを意図的に省略しようという方針に変わった。しかし、その結果得られたコードには、enum 値の範囲を超えた int 値が enum 変数に代入された場合に予期しない動作を引き起こすという脆弱性が生じてしまった。
現在はこの 2 つの考え方が組み合わせられている。enum 型の switch 文には、enum 値ごとに case ラベルを付けるだけでなく、安全性を考慮して default ラベルも付けるものとしている。これによって静的解析が難しくなることはない。
既存の処理系は移行段階にあり、default ラベルが付いている switch 文をまだ解析できないものもある。この新しい作法が一般的になるまで、開発者は switch 文を特に注意深く検査する必要がある。
違反コード (Zune 30)
以下のコード例は、不完全な日付の変換ロジックを示している。2008 年 12 月 30 日真夜中(PST)、Zune 30 メディアプレーヤーのプログラミングコードが原因で、多くのプレーヤーが動かなくなってしまった。以下のコード例は、MC13783 PMIC RTC 用のリアルタイムクロック(RTC)ルーチンの ConvertDays 関数から抜粋したものである。このコードは、1980 年 1 月 1 日からの日数を数え、正しい年を計算し、正しい年の 1 月 1 日からの日数を計算する。
days の値が 366 になるとループが終了しないため、欠陥が生じる。このバグは、このコードが作動した最初のうるう年である 2008 年の 366 日目に発覚した。
#define ORIGINYEAR 1980 UINT32 days = /* 1980 年 1 月 1 日以降の日数 */ int year = ORIGINYEAR; /* ... */ while (days > 365) { if (IsLeapYear(year)) { if (days > 366) { days -= 366; year += 1; } } else { days -= 365; year += 1; } }
適合コード(Zune 30)
以下の修正案は http://www.aeroxp.org/2009/01/lesson-on-infinite-loops で提供されている。while 条件が失敗してループが強制終了にならない限り、ループの反復のたびに days が減少するため、ループは確実に終了する。
#define ORIGINYEAR 1980 UINT32 days = /* 入力パラメータ */ int year = ORIGINYEAR; /* ... */ int daysThisYear = (IsLeapYear(year) ? 366 : 365); while (days > daysThisYear) { days -= daysThisYear; year += 1; daysThisYear = (IsLeapYear(year) ? 366 : 365); }
この解決策は説明のために示したものであり、Microsoft が採用した解決法であるとは限らない。
リスク評価
条件文や場合分けにおいてすべての可能性を考慮していないと、間違った実行状態につながったり、意図せぬ情報漏えいや異常終了を引き起こしたりするおそれがある。
レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
MSC01-C | 中 | 中 | 中 | P8 | L2 |
自動検出
LDRA tool suite Version 7.6.0 はこのレコメンデーションの違反を検出できる。
GCC コンパイラは、-Wswitch フラグと -Wswitch-default フラグが使用されているときに、このレコメンデーションのいくつかの違反を検出することができる。
Compass/ROSE はこのレコメンデーションのいくつかの違反を検出できる。特に、default 句のない switch 文を警告する。ROSE も「擬似 switch」、すなわち、同じ変数の値を検査する if 文の連鎖を検出できる。このような if 文は常に 'else' 句で終了するか、あらゆる可能性を数学的に網羅する必要がある。たとえば、
if (x > 0) { /* ... */ } else if (x < 0) { /* ... */ } else if (x == 0) { /* ... */ }
Klocwork Version 8.0.4.16 は、LA_UNUSED チェッカーを使ってこのレコメンデーションの違反を検出できる。
参考情報
- [Hatton 95] Section 2.7.2, "Errors of omission and addition"
- [ISO/IEC PDTR 24772] "CLL Switch statements and static analysis"
- [Viega 05] Section 5.2.17, "Failure to account for default case in switch"
- [http://www.aeroxp.org/2009/01/lesson-on-infinite-loops] for analysis on the Zune 30 bug
翻訳元
これは以下のページを翻訳したものです。