ClangのCheckSecuritySyntaxOnlyを試してみる

Clangの静的解析コードであるCheckerのひとつ、CheckSecuritySyntaxOnlyを試してみます。

 

1. Checkerの実行方法

CheckSecuritySyntaxOnlyが提供するCheckerは以下の通りです。

  security.FloatLoopCounter       Warn on using a floating point value as a loop counter (CERT: FLP30-C, FLP30-CPP)
  security.insecureAPI.UncheckedReturn
                                  Warn on uses of functions whose return values must be always checked
  security.insecureAPI.getpw      Warn on uses of the 'getpw' function
  security.insecureAPI.gets       Warn on uses of the 'gets' function
  security.insecureAPI.mkstemp    Warn when 'mkstemp' is passed fewer than 6 X's in the format string
  security.insecureAPI.mktemp     Warn on uses of the 'mktemp' function
  security.insecureAPI.rand       Warn on uses of the 'rand', 'random', and related functions
  security.insecureAPI.strcpy     Warn on uses of the 'strcpy' and 'strcat' functions
  security.insecureAPI.vfork      Warn on uses of the 'vfork' function

これらのCheckerはgets等の問題がある関数呼び出しを補足してwarningを出力 します。CheckSecuritySyntaxOnly.cppのヘッダ部分によれば、 flow-insensitive(順序を考慮しない)セキュリティチェックを定義している とのことです。

下記の通り、security.insecureAPIとsecurity.FloatLoopCounterを指定する ことで、CheckSecuritySyntaxOnlyのCheckerを全て実行することができます。

$ clang -cc1 -analyzer-checker=security.insecureAPI \
  -analyzer-checker=security.FloatLoopCounter \
  -analyze <target>.c

2. StmtVisitorとEvaluatedExprVisitor

CheckSecuritySyntaxOnlyではStmtVisitorを使用してます。以前の記事で EvaluatedExprVisitorを使用したMallocOverflowSecurityCheckerを試したこ とがあります。StmtVisitorとEvaluatedExprVisitorを用いたCheckerを作成し、 これらの違いを確認してみます。

2.1. Checkerの内容

StmtVisitorを継承したVCStmtVisitor、EvaluatedExprVisitorを継承した VCEvaluatedExprVisitorを作成し、それぞれVisitStmtをオーバライドします。 VCStmtVisitorのVisitStmtではchild_iteratorで得られるStmtに対して、 Visitを実行します。VCEvaluatedExprVisitorではEvaluatedExprVisitorの VisitStmtを呼び出します。その際、独自定義のdebug_stmtを用いてStmtのク ラス名と内容を表示します。 checkASTCodeBodyで関数のDeclをdumpで表示し、それぞれのVisitorのVisitを 実行します。

class VCStmtVisitor : public StmtVisitor<VCStmtVisitor> {
private:
  const SourceManager &mSMgr;
public:
  VCStmtVisitor(ASTContext &ctx) : mSMgr(ctx.getSourceManager()) {
    llvm::errs() << "[StmtVisitor]\n";
  }
 
  void VisitStmt(Stmt *S) {
    debug_stmt(S, mSMgr);
    for (Stmt::child_iterator I = S->child_begin(),
           E = S->child_end(); I!=E; ++I)
      if (*I)
        Visit(*I);
  }
};
 
class VCEvaluatedExprVisitor :
  public EvaluatedExprVisitor<VCEvaluatedExprVisitor> {
private:
  const SourceManager &mSMgr;
public:
  VCEvaluatedExprVisitor(ASTContext &ctx) : EvaluatedExprVisitor(ctx),
                                            mSMgr(ctx.getSourceManager()) {
    llvm::errs() << "[EvaluatedExprVisitor]\n";
  }
 
  void VisitStmt(Stmt *S) {
    debug_stmt(S, mSMgr);
    EvaluatedExprVisitor::VisitStmt(S);
  }
};
 
class VisitorChecker : public Checker<check::ASTCodeBody> {
public:
  void checkASTCodeBody(const Decl *D, AnalysisManager& mgr,
                        BugReporter &BR) const {
    llvm::errs() << "[D->dump()]\n";
    D->dump();
    VCStmtVisitor(mgr.getASTContext()).Visit(D->getBody());
    VCEvaluatedExprVisitor(mgr.getASTContext()).Visit(D->getBody());
  }
};
}
 
void ento::registerVisitorChecker(CheckerManager &mgr) {
  mgr.registerChecker<VisitorChecker>();
}

child_iteratorの説明は以下の通りです。substatements/subexpressionsにア クセスするインターフェースとなっております。

Child Iterators: All subclasses must implement 'children' to permit
easy iteration over the substatements/subexpessions of an AST
node. This permits easy iteration over all nodes in the AST.

2.2. 実行結果

以下のコードに対してCheckerを実行します。

#include <stdio.h>
 
int main(void)
{
  char buffer[128];
  printf("%s\n", gets(buffer));
  return 0;
}

D->dump()の実行結果は以下の通りです。関数定義に本体の内容がくっついて いることが分かります。

[D->dump()]
FunctionDecl 0x7fd1cc839520 <gets.c:3:1, line:8:1> main 'int (void)'
`-CompoundStmt 0x7fd1cc8398e8 <line:4:1, line:8:1>
  |-DeclStmt 0x7fd1cc839668 <line:5:3, col:19>
  | `-VarDecl 0x7fd1cc839610 <col:3, col:18> buffer 'char [128]'
  |-CallExpr 0x7fd1cc839840 <line:6:3, col:30> 'int'
  | |-ImplicitCastExpr 0x7fd1cc839828 <col:3> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
  | | `-DeclRefExpr 0x7fd1cc839680 <col:3> 'int (const char *, ...)' Function 0x7fd1cc82e460 'printf' 'int (const char *, ...)'
  | |-ImplicitCastExpr 0x7fd1cc839890 <col:10> 'const char *' <BitCast>
  | | `-ImplicitCastExpr 0x7fd1cc839878 <col:10> 'char *' <ArrayToPointerDecay>
  | |   `-StringLiteral 0x7fd1cc8396e8 <col:10> 'char [4]' lvalue "%s\n"
  | `-CallExpr 0x7fd1cc8397b0 <col:18, col:29> 'char *'
  |   |-ImplicitCastExpr 0x7fd1cc839798 <col:18> 'char *(*)(char *)' <FunctionToPointerDecay>
  |   | `-DeclRefExpr 0x7fd1cc839718 <col:18> 'char *(char *)' Function 0x7fd1cc82d750 'gets' 'char *(char *)'
  |   `-ImplicitCastExpr 0x7fd1cc8397e0 <col:23> 'char *' <ArrayToPointerDecay>
  |     `-DeclRefExpr 0x7fd1cc839740 <col:23> 'char [128]' lvalue Var 0x7fd1cc839610 'buffer' 'char [128]'
  `-ReturnStmt 0x7fd1cc8398c8 <line:7:3, col:10>
    `-IntegerLiteral 0x7fd1cc8398a8 <col:10> 'int' 0

VCStmtVisitorの実行結果は以下の通りです。DeclStmtの下にあるVarDeclが表 示されてませんが、これはD->dump()ではDeclStmtの場合に decl_iteratorとchild_iteratorを辿るのに対し、VCStmtVisitorでは child_iteratorのみ辿る為です。VCStmtVisitorにてVisitDeclStmtをオーバラ イドすればこの限りではありません。

[StmtVisitor]
CompoundStmt
{
~~
DeclStmt
  char buffer[128];
  ~~~~~~~~~~~~~~~~~
CallExpr
  printf("%s\n", gets(buffer));
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
  ~
DeclRefExpr
  printf("%s\n", gets(buffer));
  ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
         ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
         ~
StringLiteral
  printf("%s\n", gets(buffer));
         ~
CallExpr
  printf("%s\n", gets(buffer));
                 ~~~~~~~~~~~~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
                 ~
DeclRefExpr
  printf("%s\n", gets(buffer));
                 ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
                      ~
DeclRefExpr
  printf("%s\n", gets(buffer));
                      ~
ReturnStmt
  return 0;
  ~~~~~~~~
IntegerLiteral
  return 0;
         ~

VCEvaluatedExprVisitorの実行結果は以下の通りです。VCStmtVisitorに比べ てDeclRefExprが表示されません。

[EvaluatedExprVisitor]
CompoundStmt
{
~~
DeclStmt
  char buffer[128];
  ~~~~~~~~~~~~~~~~~
CallExpr
  printf("%s\n", gets(buffer));
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
  ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
         ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
         ~
StringLiteral
  printf("%s\n", gets(buffer));
         ~
CallExpr
  printf("%s\n", gets(buffer));
                 ~~~~~~~~~~~~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
                 ~
ImplicitCastExpr
  printf("%s\n", gets(buffer));
                      ~
ReturnStmt
  return 0;
  ~~~~~~~~
IntegerLiteral
  return 0;
         ~

これはEvaluatedExprVisitorでVisitDeclRefExprをオーバライドして、空の処 理にしている為です。またEvaluatedExprVisitorのVisitStmtにて、 VCStmtVisitorのVisitStmtと同様にchild_iteratorを辿っております。

template<typename ImplClass>
class EvaluatedExprVisitor : public StmtVisitor<ImplClass> {
<snip>  
  // Expressions that have no potentially-evaluated subexpressions (but may have
  // other sub-expressions).
  void VisitDeclRefExpr(DeclRefExpr *E) { }
<snip>
  /// rief The basis case walks all of the children of the statement or
  /// expression, assuming they are all potentially evaluated.
  void VisitStmt(Stmt *S) {
    for (Stmt::child_range C = S->children(); C; ++C)
      if (*C)
        this->Visit(*C);
  }
};

EvaluatedExprVisitorが継承しているStmtVisitor、がさらに継承している StmtVisitorBaseは以下の通りです(一部抜粋)。Visitメソッド内にて getStmtClass()の値を判定し、Visit##NAMEメソッドを呼び出します。なお、 StmtVisitorではメソッドはオーバライドされていません。

template<template <typename> class Ptr, typename ImplClass, typename RetTy=void>
class StmtVisitorBase {
<snip>
#define PTR(CLASS) typename Ptr<CLASS>::type
#define DISPATCH(NAME, CLASS) \
 return static_cast<ImplClass*>(this)->Visit ## NAME(static_cast<PTR(CLASS)>(S))
 
  RetTy Visit(PTR(Stmt) S) {
<snip>
    // Top switch stmt: dispatch to VisitFooStmt for each FooStmt.
    // Top switch stmt: dispatch to VisitFooStmt for each FooStmt.
    switch (S->getStmtClass()) {
    default: llvm_unreachable("Unknown stmt kind!");
#define ABSTRACT_STMT(STMT)
#define STMT(CLASS, PARENT)                              \
    case Stmt::CLASS ## Class: DISPATCH(CLASS, CLASS);
#include "clang/AST/StmtNodes.inc"
    }
  }
 
  // If the implementation chooses not to implement a certain visit method, fall
  // back on VisitExpr or whatever else is the superclass.
#define STMT(CLASS, PARENT)                                   \
  RetTy Visit ## CLASS(PTR(CLASS) S) { DISPATCH(PARENT, PARENT); }
#include "clang/AST/StmtNodes.inc"
<snip>
  // Base case, ignore it. :)
  RetTy VisitStmt(PTR(Stmt) Node) { return RetTy(); }
 
#undef PTR
#undef DISPATCH
};

StmtNodes.incの内容は以下の通りです(llvmをビルドしたディレクトリ配下 にあります)。これによりAsmStmtVisitメソッドとExprVisitメソッドは内部 でVisitStmtを呼び出すようになります。Stmtのリファレンスに記載されてい るクラス構造の親クラスのVisit##NAMEを呼び出すようになっているようです。 

<snip>
#ifndef ASMSTMT
#  define ASMSTMT(Type, Base) STMT(Type, Base)
#endif
ABSTRACT_STMT(ASMSTMT(AsmStmt, Stmt))
<snip>
#ifndef EXPR
#  define EXPR(Type, Base) STMT(Type, Base)
#endif
ABSTRACT_STMT(EXPR(Expr, Stmt))
<snip>

つまり、StmtVisitorはVisitメソッド内でVisit##NAMEメソッドを呼び出しま すが、呼び出したVisit##NAME内で親クラスのVisit##NAMEを呼び出していき、 VisitStmtが最後に呼ばれるようになります。StmtVisitorのVisitStmtでは return RetTy()をするだけで、child_iteratorを辿るような処理はしません。

その為、もしVisitStmtを継承したクラスでVisit##NAMEをオーバライドしない ままVisitを実行した場合、Visitの呼び出し、Visit##NAMEの呼び出し、親ク ラスのVisit##NAMEの呼び出し、・・・、VisitStmtで処理は終了します。

3. CheckSecuritySyntaxOnlyの実装

getsに関する項目に絞って見ていきます。

3.1. checkASTCodeBody

ASTCodeBodyを継承し、checkASTCodeBodyを実装してます。flow-insensitive の通り、checkASTCodeBodyはpass sensitiveでないコールバック関数です。 checkASTCodeBodyの中でStmtVisitorを継承したWalkASTを用いて関数本体を再 帰的に辿っていきます。

namespace {
class SecuritySyntaxChecker : public Checker<check::ASTCodeBody> {
public:
  ChecksFilter filter;
  
  void checkASTCodeBody(const Decl *D, AnalysisManager& mgr,
                        BugReporter &BR) const {
    WalkAST walker(BR, mgr.getAnalysisDeclContext(D), filter);
    walker.Visit(D->getBody());
  }
};
}

3.2. ChecksFilter

チェックする項目の真偽値を管理する構造体です。

struct ChecksFilter {
  DefaultBool check_gets;
  DefaultBool check_getpw;
  DefaultBool check_mktemp;
  DefaultBool check_mkstemp;
  DefaultBool check_strcpy;
  DefaultBool check_rand;
  DefaultBool check_vfork;
  DefaultBool check_FloatLoopCounter;
  DefaultBool check_UncheckedReturn;
};

これらの真偽値は下記のREGISTER_CHECKERマクロでtrueに設定されます。

#define REGISTER_CHECKER(name) \
void ento::register##name(CheckerManager &mgr) {\
  mgr.registerChecker<SecuritySyntaxChecker>()->filter.check_##name = true;\
}
 
REGISTER_CHECKER(gets)
REGISTER_CHECKER(getpw)
REGISTER_CHECKER(mkstemp)
REGISTER_CHECKER(mktemp)
REGISTER_CHECKER(strcpy)
REGISTER_CHECKER(rand)
REGISTER_CHECKER(vfork)
REGISTER_CHECKER(FloatLoopCounter)
REGISTER_CHECKER(UncheckedReturn)

registerCheckerメソッドは既に登録済みのCheckerに対しては、そのポインタ を返す動作をします。

  /// rief Used to register checkers.
  ///
  /// eturns a pointer to the checker object.
  template <typename CHECKER>
  CHECKER *registerChecker() {
    CheckerTag tag = getTag<CHECKER>();
    CheckerRef &ref = CheckerTags[tag];
    if (ref)
      return static_cast<CHECKER *>(ref); // already registered.
 
    CHECKER *checker = new CHECKER();
    CheckerDtors.push_back(CheckerDtor(checker, destruct<CHECKER>));
    CHECKER::_register(checker, *this);
    ref = checker;
    return checker;
  }

よって、初めに呼ばれたREGISTER_CHECKERによってCheckerの登録と真偽値を trueに設定され、その後のREGISTER_CHECKERでは真偽値をtrueに設定するのみ となります。REGISTER_CHECKERは-analyzer-checkerオプション経由で呼ばれ ます。

3.3. WalkAST

WalkASTではVisitCallExpr、VisitForStmt、VisitCompoundStmt、VisitStmtを オーバライドしています。

class WalkAST : public StmtVisitor<WalkAST> {
<snip>
  // Statement visitor methods.
  void VisitCallExpr(CallExpr *CE);
  void VisitForStmt(ForStmt *S);
  void VisitCompoundStmt (CompoundStmt *S);
  void VisitStmt(Stmt *S) { VisitChildren(S); }
 
  void VisitChildren(Stmt *S);
 
  // Helpers.
  bool checkCall_strCommon(const CallExpr *CE, const FunctionDecl *FD);
 
  typedef void (WalkAST::*FnCheck)(const CallExpr *,
                                   const FunctionDecl *);
 
  // Checker-specific methods.
  void checkLoopConditionForFloat(const ForStmt *FS);
  void checkCall_gets(const CallExpr *CE, const FunctionDecl *FD);
<snip>
};

3.4. VisitChildren

child_iteratorを辿ってVisitを呼びます。

void WalkAST::VisitChildren(Stmt *S) {
  for (Stmt::child_iterator I = S->child_begin(), E = S->child_end(); I!=E; ++I)
    if (Stmt *child = *I)
      Visit(child);
}

3.5. VisitCallExpr

呼び出している関数の名前を確認します。該当する関数の場合はより詳細に確 認します。

void WalkAST::VisitCallExpr(CallExpr *CE) {
  # IdentifierInfoで呼び出している関数の名前を取得します。
  // Get the callee.  
  const FunctionDecl *FD = CE->getDirectCallee();
 
  if (!FD)
    return;
 
  // Get the name of the callee. If it's a builtin, strip off the prefix.
  IdentifierInfo *II = FD->getIdentifier();
  if (!II)   // if no identifier, not a simple C function
    return;
  # __builtin_な関数も対象としているようです。
  StringRef Name = II->getName();
  if (Name.startswith("__builtin_"))
    Name = Name.substr(10);
 
  # NameがCaseの第一引数に等しい場合は第二引数の処理をevalFunctionに設
  # 定します。Casesの場合は第一引数あるいは第二引数に等しい場合に第三
  # 引数の処理をevalFunctionに設定します。
  // Set the evaluation function by switching on the callee name.
  FnCheck evalFunction = llvm::StringSwitch<FnCheck>(Name)
    .Case("gets", &WalkAST::checkCall_gets)
    .Case("getpw", &WalkAST::checkCall_getpw)
    .Case("mktemp", &WalkAST::checkCall_mktemp)
    .Case("mkstemp", &WalkAST::checkCall_mkstemp)
    .Case("mkdtemp", &WalkAST::checkCall_mkstemp)
    .Case("mkstemps", &WalkAST::checkCall_mkstemp)
    .Cases("strcpy", "__strcpy_chk", &WalkAST::checkCall_strcpy)
    .Cases("strcat", "__strcat_chk", &WalkAST::checkCall_strcat)
    .Case("drand48", &WalkAST::checkCall_rand)
    .Case("erand48", &WalkAST::checkCall_rand)
    .Case("jrand48", &WalkAST::checkCall_rand)
    .Case("lrand48", &WalkAST::checkCall_rand)
    .Case("mrand48", &WalkAST::checkCall_rand)
    .Case("nrand48", &WalkAST::checkCall_rand)
    .Case("lcong48", &WalkAST::checkCall_rand)
    .Case("rand", &WalkAST::checkCall_rand)
    .Case("rand_r", &WalkAST::checkCall_rand)
    .Case("random", &WalkAST::checkCall_random)
    .Case("vfork", &WalkAST::checkCall_vfork)
    .Default(NULL);
 
  # evalFunctionに設定された処理を実行します。
  // If the callee isn't defined, it is not of security concern.
  // Check and evaluate the call.
  if (evalFunction)
    (this->*evalFunction)(CE, FD);
 
  // Recurse and check children.
  VisitChildren(CE);
}

3.5.1. StringSwitch

コンストラクタで比較対象文字列のStrを設定し、判定結果のResultをNULLで 初期化します。
CaseメソッドでResultがNULLか、文字列が等しいかを判定し、両方なりたつ場 合はResultにValueを設定します。最後に*thisを返します。Caseを多段で呼べ るのは*thisを返す為です。Casesメソッドについても同様です。Defaultメソッ ドはResultがNULLの場合にValueを設定します。

template<typename T, typename R = T>
class StringSwitch {
  /// rief The string we are matching.
  StringRef Str;
 
  /// rief The pointer to the result of this switch statement, once known,
  /// null before that.
  const T *Result;
 
public:
  explicit StringSwitch(StringRef S)
  : Str(S), Result(0) { }
 
  template<unsigned N>
  StringSwitch& Case(const char (&S)[N], const T& Value) {
    if (!Result && N-1 == Str.size() &&
        (std::memcmp(S, Str.data(), N-1) == 0)) {
      Result = &Value;
    }
 
    return *this;
  }
<snip>
  template<unsigned N0, unsigned N1>
  StringSwitch& Cases(const char (&S0)[N0], const char (&S1)[N1],
                      const T& Value) {
    return Case(S0, Value).Case(S1, Value);
  }
<snip>
  R Default(const T& Value) const {
    if (Result)
      return *Result;
 
    return Value;
  }
<snip>
}

コードの内容はすっきりしているような印象ですが、実際はifが最後まで実行 されることになります。最初にマッチした箇所でValueが適用されます。

3.6. checkCall_gets

getsを判定する処理です。文字列比較だけでなく、型の確認等もしています。

void WalkAST::checkCall_gets(const CallExpr *CE, const FunctionDecl*FD) {
  # 真偽値を確認します。-analyzer-chekcerで指定されていない場合はfalse
  # です。
  if (!filter.check_gets)
    return;
 
  # K&Rスタイルのプロトタイプ宣言でないことを確認します。  
  const FunctionProtoType *FPT = FD->getType()->getAs<FunctionProtoType>();
  if (!FPT)
    return;
 
  # 引数の数を確認します。
  // Verify that the function takes a single argument.
  if (FPT->getNumParams() != 1)
    return;
 
  # 戻り値を確認します。
  // Is the argument a 'char*'?
  const PointerType *PT = FPT->getParamType(0)->getAs<PointerType>();
  if (!PT)
    return;
 
  # 戻り値がchar *であるかどうかを確認します。
  if (PT->getPointeeType().getUnqualifiedType() != BR.getContext().CharTy)
    return;
 
  # warningを出力します。
  // Issue a warning.
  PathDiagnosticLocation CELoc =
    PathDiagnosticLocation::createBegin(CE, BR.getSourceManager(), AC);
  BR.EmitBasicReport(AC->getDecl(),
                     "Potential buffer overflow in call to 'gets'",
                     "Security",
                     "Call to function 'gets' is extremely insecure as it can "
                     "always result in a buffer overflow",
                     CELoc, CE->getCallee()->getSourceRange());
}

4. CheckSecuritySyntaxOnlyの実行結果

4.1. libcで定義されているgetsの場合

以下のコードに対してChekcerを実行します。

$ cat gets.c 
#include <stdio.h>
 
int main(void)
{
  char buffer[128];
  printf("%s\n", gets(buffer));
  return 0;
}

warningが出力されます。

gets.c:6:18: warning: Call to function 'gets' is extremely insecure as it can always result in a buffer overflow
  printf("%s\n", gets(buffer));
                 ^~~~
1 warning generated.

4.2. 独自定義のgetsの場合

libcのものとインターフェースが同じgetsを定義します。

$ cat mygets.c 
#include <stdlib.h>
 
char *gets(char *buffer)
{
  return NULL;
}
 
int main(void)
{
  char buffer[128];
  gets(buffer);
  return 0;
}

warningが出力されます。

mygets.c:11:3: warning: Call to function 'gets' is extremely insecure as it can always result in a buffer overflow
  gets(buffer);
  ^~~~
1 warning generated.

4.3. 独自定義のK&Rなプロトタイプ宣言を持つgets

K&Rなプロトタイプ宣言では引数情報を記載しません。

#include <stdlib.h>
 
char *gets();
 
int main(void)
{
  char buffer[128];
  gets(buffer);
  return 0;
}
 
char *gets(buffer)
  char *buffer;
{
  return NULL;
}

warningが出力されません。

(何も表示されない)

5. まとめ

今回はgetsに絞って処理を見ていきました。FloatLoopCounterの為のForStmt の扱いも参考になると思います。StmtVisitorの使い方も参考になります。

ダウンロード
ソースコード
Checkerを実行したコードとVisitorの動作を確認するCheckerのコード
src.tar.gz
GNU tar 1.2 KB