integer overflow 流行りな昨今 (3)
現実的な解決策 in C++
でも、クイズに書いたようなオーバーフローって、なかなか防ぎきれないんですよね。
- 技術力を向上する
- がんばってレビューする
- 静的なコード検査ツールを使う(QAC++とか、PolySpaceとか、これとか)
などが王道かとは思いますが、学習時間や手間やお金がかかります。で、なんとか自動的・透過的にどうにかする方法はないのかという話になると、「MicrosoftのDavid LeBlancさん作成の"SafeInt"クラスを使え」っていうのが巷のデフォルトの結論のようで、それ以外の方法はあまり提案されていません。
でもねー、全部の int hoge; を SafeInt
などの問題がありまして。そもそもSafeIntの(v1はともかく)v2はWindows専用みたいなものですので、POSIXばかりいじっている僕は使えませんし。
ではどうするか。
解決案: size_tをとる標準ライブラリ関数を、ssize_t版でオーバーロードする
整数オーバーフローに起因するスタックベースのバッファオーバーフローを無害化するために、g++ -DFORTIFY_SOURCE=2*1とか ProPolice は常に使ってもらうことにします。
それに加えて、スタック以外のオーバーフローをなるべく検出するために、次のような方法をとるのはどうでしょうかね?なんでもかんでも防げるわけではないですけど、多少はマシになりませんかねえ。
- safe_memcpy() のような関数を用意して、memcpy()のかわりに使う
- safe_memcpy() は、size_t と ssize_t*2 にこっそりオーバーロードしておく。で、ssize_t版にコピー長として負数が与えられたら、整数オーバーフローの脆弱性を突かれたっぽいので、例外を投げてコピーはしない。
- memcpy() 以外の、size_tを引数に取る危険な関数も同様の処置をする
- memmove, memset, memcmp, strncpy, ...
実装は単純で、
void* safe_memcpy(void* dest, const void* src, std::size_t n) __THROW { return std::memcpy(dest, src, n); } void* safe_memcpy(void* dest, const void* src, ssize_t n) { if(n < 0) { throw std::runtime_error("safe_memcpy: integer overflow detected."); } return safe_memcpy(dest, src, (std::size_t)n); }
こんな感じ。こうしておくと、変数 int x; に -1 などの負数が代入された状態で safe_memcpy(x); されたときに、コピーが行われる前に例外が飛びます。もっと簡単に、直接次のような関数
namespace std { void* memcpy(void* dest, const void* src, ssize_t n); }
を作ってしまう手もありますけど、標準では一応、名前空間stdに自作関数を突っ込むのは禁止されている(ISO/IEC 14882:2003 §17.4.3.1)ので行儀は良くないですね。
(蛇足) mallocも対処してみる
負数でmallocされるのも気分が悪いので、同様に対処してみます。ただ、mallocの場合、
void foo(int n) { char* p = (char*)std::malloc(n * sizeof(int));
のようなidiomがあるじゃないですか。これって、sizeofの戻りを掛け算したとたんに式全体としてはunsigned型になってしまうんですよねー*3。なので、callocのように、1つのオブジェクトのサイズと、確保するオブジェクトの個数を別々に指定させるI/Fにすると良いと思います。
void* safe_malloc(std::size_t nmemb, std::size_t size) __THROW { int overflow; std::size_t w = checked_multiply_(nmemb, size, overflow); return (overflow ? 0 : std::malloc(w)); } void* safe_malloc(ssize_t nmemb, ssize_t size) __THROW { if(nmemb <= 0 || size <= 0) { return 0; } return safe_malloc((std::size_t)nmemb, (std::size_t)size); } void* safe_malloc(ssize_t nmemb, std::size_t size) __THROW { if(nmemb <= 0) { return 0; } return safe_malloc((std::size_t)nmemb, size); } void* safe_malloc(std::size_t nmemb, ssize_t size) __THROW { if(size <= 0) { return 0; } return safe_malloc(nmemb, (std::size_t)size); }
のように。これで、safe_malloc(nmemb, sizeof(char)); でint型の変数nmembの値が負数のとき、確実にメモリ確保が失敗するようになります。
なお、checked_multiply_() は、safe_malloc() に渡した二つのsize_tの掛け算がsize_tの最大値を超えないかチェックする関数です。これは例えばx86だとインラインアセンブラで次のように簡単に書けます。
#include <cstddef> #include <boost/static_assert.hpp> namespace { // // x * y を戻す。ただし、x * y の結果がsize_tで表現できないなら、 // overflow を 1にセットする。表現できるなら 0にセットする。 // inline std::size_t checked_multiply_(std::size_t x, std::size_t y, int& overflow) __THROW { std::size_t ret; BOOST_STATIC_ASSERT(sizeof(std::size_t) == 4); // なくてもいい __asm__ __volatile__ ( "mull %3 \r\n\t" "seto %%cl " : "=a"(ret), "=c"(overflow) : "0"(x), "m"(y), "1"(0) : "edx" ); return ret; } }
アセンブリ言語による記述はイヤだという場合で、size_tが32bitだとわかっているなら、
if (x * (unsigned long long)y > UINT_MAX) { /* overflow */ }
でいいでしょうし、64bitだったり不明だったりするなら、
if (x > std::numeric_limits<std::size_t>::max() / y) { /* overflow! */ }
とか書けばいいんでしょうね、たぶん。
え、負数でmallocされても、それだけでは実害ナシだからいいじゃないかって?…そうですね。たまにはインラインアセンブラを書いてみたかっただけです。