integer overflow 流行りな昨今 (3)

現実的な解決策 in C++


でも、クイズに書いたようなオーバーフローって、なかなか防ぎきれないんですよね。

  • 技術力を向上する
  • がんばってレビューする
  • 静的なコード検査ツールを使う(QAC++とか、PolySpaceとか、これとか)

などが王道かとは思いますが、学習時間や手間やお金がかかります。で、なんとか自動的・透過的にどうにかする方法はないのかという話になると、「MicrosoftのDavid LeBlancさん作成の"SafeInt"クラスを使え」っていうのが巷のデフォルトの結論のようで、それ以外の方法はあまり提案されていません。


でもねー、全部の int hoge; を SafeInt hoge; に書き換えてくれっていうのはなかなか難しいものです。たとえば、ぱっと思いつくだけでも、

  1. 書くのが面倒
  2. パフォーマンスにかなり影響する(8-12%程度悪化するとのこと)
  3. 機械的な置換じゃオーバーロードが曖昧だと言われまくってしまうので手で調整する必要がある

などの問題がありまして。そもそもSafeIntの(v1はともかく)v2Windows専用みたいなものですので、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されても、それだけでは実害ナシだからいいじゃないかって?…そうですね。たまにはインラインアセンブラを書いてみたかっただけです。

*1:libsafeのようなもの

*2:符号付き

*3:クイズ(9)を参照のこと。ssize_tのほうの関数が呼ばれなくなるです