ClangのCastToStructCheckerを試してみる。

ClangのCastToStructCheckerを試してみます。

 

1. 実行方法

$ clang -cc1 -analyzer-checker=alpha.core.CastToStruct \
  -analyze <target>.c

2. CWE-588

脆弱性カタログによれば、構造体でない型を構造体の型にキャストし、構造体 の変数にアクセスするとアクセスエラーやデータ破壊が起きる問題です。 CastToStructCheckerはCWE-588を検出するCheckerです。

3. CastToStructCheckerの実装

check::PreStmt<CastExpr>を継承しています。

class CastToStructChecker : public Checker< check::PreStmt<CastExpr> > {
<snip>
public:
  void checkPreStmt(const CastExpr *CE, CheckerContext &C) const;
};

3.1. CastExpr

c++のdynamic_cast用のCXXDynamicCastExprや(int)f用のCStyleCastExpr等は CastExprの派生クラスになっています。

LValueからRValue、floatからint等のキャストはImplicitCastExprとなります。

checkPreStmt(const CastExpr *,...)内でCastExprをdump()した結果と checkPostStmt(const CastExpr*,...)内でCastExprをdump()した結果を比較し てみます。debug_stmtはソースコードの範囲を表示する独自関数です。

static void debug(const Stmt *S, CheckerContext &C)
{
  debug_stmt(S, C.getSourceManager());
  S->dump();
}
<snip>
  void checkPreStmt(const CastExpr *S, CheckerContext &C) const { debug(S, C); }
<snip>
  void checkPostStmt(const CastExpr *S, CheckerContext &C) const { debug(S, C); }

3.1.1. doubleからintへキャストするコード

以下の対象コードでdump()してみます。

#include <stdio.h>
 
int main(void)
{
  int a;
  double b;
  a = b;
  return 0;
}

3.1.2. checkPreStmt(const CastExpr *,...)の実行結果

ソースコードの範囲を表示した後、CastExprがdump()されます。 LValueToRValueを根にしたCastExprとFloatingToIntegralを根にしたCastExpr がそれぞれ2回呼ばれてます。

  a = b;
      ~
ImplicitCastExpr 0x7fef9c828550 'double' <LValueToRValue>
`-DeclRefExpr 0x7fef9c828528 'double' lvalue Var 0x7fef9c828490 'b' 'double'
  a = b;
      ~
ImplicitCastExpr 0x7fef9c828550 'double' <LValueToRValue>
`-DeclRefExpr 0x7fef9c828528 'double' lvalue Var 0x7fef9c828490 'b' 'double'
  a = b;
      ~
ImplicitCastExpr 0x7fef9c828568 'int' <FloatingToIntegral>
`-ImplicitCastExpr 0x7fef9c828550 'double' <LValueToRValue>
  `-DeclRefExpr 0x7fef9c828528 'double' lvalue Var 0x7fef9c828490 'b' 'double'
  a = b;
      ~
ImplicitCastExpr 0x7fef9c828568 'int' <FloatingToIntegral>
`-ImplicitCastExpr 0x7fef9c828550 'double' <LValueToRValue>
  `-DeclRefExpr 0x7fef9c828528 'double' lvalue Var 0x7fef9c828490 'b' 'double'

3.1.3. checkPostStmt(const CastExpr *,...)の実行結果

checkPreStmt(const CastExpr *,...)と比べて1回だけ呼ばれます。

  a = b;
      ~
ImplicitCastExpr 0x7fb44b055d50 'double' <LValueToRValue>
`-DeclRefExpr 0x7fb44b055d28 'double' lvalue Var 0x7fb44b055c90 'b' 'double'
  a = b;
      ~
ImplicitCastExpr 0x7fb44b055d68 'int' <FloatingToIntegral>
`-ImplicitCastExpr 0x7fb44b055d50 'double' <LValueToRValue>
  `-DeclRefExpr 0x7fb44b055d28 'double' lvalue Var 0x7fb44b055c90 'b' 'double'

checkPreStmt(const CastExpr *,...)とcheckPostStmt(const CastExpr*,...) の間でanalyzer coreが何かをしているからなんでしょうが、何かが分かって いません。
CastToStructCheckerとCastSizeCheckerはcheckPreStmt(const CastExpr*,...) を使用しているので、CastExprを扱う場合は前者を使えばよいのでしょう。

3.2. getCanonicalType

CE->getSubExpr()で入れ子のExprを取得しています。

void CastToStructChecker::checkPreStmt(const CastExpr *CE,
                                       CheckerContext &C) const {
  const Expr *E = CE->getSubExpr();

以下の入れ子のCastExprを補足しようとしています。

CStyleCastExpr 0x7fc5110a5030 'struct data *' <BitCast>
`-ImplicitCastExpr 0x7fc5110a5018 'char *' <ArrayToPointerDecay>
  `-DeclRefExpr 0x7fc5110a4fc8 'char [32]' lvalue Var 0x7fc5110a4e10 'buffer' 'char [3

それぞれのExprでQualTypeを取得します。  ImplicitCastExprが元々の型を表し、CStyleCastExprがキャストしようとして いる型を表します。

  ASTContext &Ctx = C.getASTContext();
  QualType OrigTy = Ctx.getCanonicalType(E->getType());
  QualType ToTy = Ctx.getCanonicalType(CE->getType());

ここで気になるのはASTContextがgetCanonicalTypeメソッドを持っている一方 で、QualType自身もgetCanonicalTypeメソッドを持っている点です。どちらを 使うべきなんですかね。
ざっとgrepした結果を見ると、少なくともStaticAnalyzerディレクトリ配下で はASTContextのgetCanonicalTypeを使用している場合が多いです。ASTContext の方を使う方が無難なのかな。

3.3. PointerType

QualType::getTypePtr()でType型を取得し、dyn_cast<PointerType>で PointerType型であるかどうかを確認します。PointerTypeはポインタ型を表す クラスです。

  const PointerType *OrigPTy = dyn_cast<PointerType>(OrigTy.getTypePtr());
  const PointerType *ToPTy = dyn_cast<PointerType>(ToTy.getTypePtr());
 
  if (!ToPTy || !OrigPTy)
    return;

PointerType::getPointeeType()でポインタが指す領域のQualTypeを取得しま す。キャスト後の型のポインタがさす領域が構造体型あるいはクラス型あるい はインターフェース型かどうかを確認します。キャスト前の型のポインタが指 す領域がvoid型かどうかを確認します。コメントにあるようにvoid *へのキャ ストは許可するようですね。

  QualType OrigPointeeTy = OrigPTy->getPointeeType();
  QualType ToPointeeTy = ToPTy->getPointeeType();
 
  if (!ToPointeeTy->isStructureOrClassType())
    return;
 
  // We allow cast from void*.
  if (OrigPointeeTy->isVoidType())
    return;

キャスト前の型のポインタがさす領域が構造体かどうかを確認します。

  if (!OrigPointeeTy->isRecordType()) {
<snip> #warning

4. CastToStructCheckerの実行結果

以下の対象コードを実行してみます。

#include <stdio.h>
#include <stdlib.h>
 
struct data {
  int a;
  char b[];
};
 
int main(void)
{
  struct data struct_array[2];
  char char_array[32];
  void *void_malloc;
  struct data *struct_ptr;
  unsigned char *char_ptr;
 
  void_malloc = malloc(sizeof(struct data));
  if (!void_malloc)
    return 1;
 
  struct_ptr = (struct data *) struct_array;
  struct_ptr = (struct data *) char_array;
  struct_ptr = (struct data *) void_malloc;
 
  char_ptr = (unsigned char *) struct_array;
  char_ptr = (unsigned char *) char_array;
  char_ptr = (unsigned char *) void_malloc;
 
  free(void_malloc);
  return 0;
}

以下のwarningが得られました。

cast.c:22:32: warning: Casting a non-structure type to a structure
type and accessing a field can lead to memory access errors or data
corruption
  struct_ptr = (struct data *) char_array;
               ~~~~~~~~~~~~~~~~^~~~~~~~~~
1 warning generated.

ちなみにcheckPreStmt(const CastExpr *,...)なので、2回同じ箇所でwarning 出力の条件が成立しますが、2回目以降のwarningは CheckerContext::emitReportではじいているようです。

5. まとめ

まとめると以下の条件でwarningが出力されます。

キャスト前とキャスト後の型がポインタ型であること。
キャスト後の型が構造体、クラス、インターフェースであること。
キャスト前の型がvoid型でないこと。
キャスト前の型が構造体でないこと。