ClangのReturnUndefCheckerを試してみる

ClangのReturnUndefCheckerを試してみます。

 

1. 実行方法

$ clang -cc1 -analyzer-checker=core.uninitialized.UndefReturn \
  -analyze <target>.c

2. ReturnUndefCheckerの実装

PreStmt<ReturnStmt>を継承し、checkPreStmt(const ReturnStmt *,...)を実 装しています。

class ReturnUndefChecker : public Checker< check::PreStmt<ReturnStmt> > {
<snip>
  void emitUndef(CheckerContext &C, const Expr *RetE) const;
  void checkReference(CheckerContext &C, const Expr *RetE,
                      DefinedOrUnknownSVal RetVal) const;
public:
  void checkPreStmt(const ReturnStmt *RS, CheckerContext &C) const;
};

2.1. checkPreStmt(const ReturnStmt *,...)

return ;等のステートメントはRS->getValue()がNULLになるようです。

void ReturnUndefChecker::checkPreStmt(const ReturnStmt *RS,
                                      CheckerContext &C) const {
  const Expr *RetE = RS->getRetValue();
  if (!RetE)
    return;

getStackFrame()->getDecl()で現在いる関数を取得し、その戻り値の型をRTに 設定します。

  SVal RetVal = C.getSVal(RetE);
 
  const StackFrameContext *SFC = C.getStackFrame();
  QualType RT = CallEvent::getDeclaredResultType(SFC->getDecl());
 

戻り値が不定かどうかを確認します。

  if (RetVal.isUndef()) {

戻り値が不定な場合に、!RT.isNull() && RT->isVoidType()を判定します。 RT->isVoidTypeは戻り値がvoidである場合にtrueになります。void foo() { return; }をvoid test() {return foo(); }という呼び出しをしている場合に getRetValue()がNULLではなく、RT->isVoidType()がtrueになるようです。 RT.isNull()は何者なんですかね・・・。

    // "return;" is modeled to evaluate to an UndefinedVal. Allow UndefinedVal
    // to be returned in functions returning void to support this pattern:
    //   void foo() {
    //     return;
    //   }
    //   void test() {
    //     return foo();
    //   }
    if (!RT.isNull() && RT->isVoidType())
      return;

BlockDeclはObjective-Cの文法のようです。よく分かってません。

    // Not all blocks have explicitly-specified return types; if the return type
    // is not available, but the return value expression has 'void' type, assume
    // Sema already checked it.
    if (RT.isNull() && isa<BlockDecl>(SFC->getDecl()) &&
        RetE->getType()->isVoidType())
      return;

warningを出力します。

    emitUndef(C, RetE);
    return;
  }

戻り値が不定でない場合に、RT.isNull()を判定します。いつisNull()がtrue になるんですかね・・・。

  if (RT.isNull())
    return;

戻り値がリファレンスの場合はcheckReferenceを呼びます。

  if (RT->isReferenceType()) {
    checkReference(C, RetE, RetVal.castAs<DefinedOrUnknownSVal>());
    return;
  }
}

2.2. checkReference

参照であるgetRetValue()がNULLでないかどうかを判定します。

void ReturnUndefChecker::checkReference(CheckerContext &C, const Expr *RetE,
                                        DefinedOrUnknownSVal RetVal) const {
  ProgramStateRef StNonNull, StNull;
  llvm::tie(StNonNull, StNull) = C.getState()->assume(RetVal);

参照がNULLでない場合はCheckerContextにNULLでない経路を加えます。

  if (StNonNull) {
    // Going forward, assume the location is non-null.
    C.addTransition(StNonNull);
    return;
  }

warningを出力します。

  // The return value is known to be null. Emit a bug report.
  if (!BT_NullReference)
    BT_NullReference.reset(new BuiltinBug("Returning null reference"));
 
  emitBug(C, *BT_NullReference, RetE, bugreporter::getDerefExpr(RetE));
}

3. ReturnUndefCheckerの実行結果

3.1. 未初期化の変数を返す対象コード

ret_int_undef関数で未初期化の変数を返します。

$ cat ret.c
#include <stdio.h>
 
int ret_int(void)
{
  return 1;
}
 
void ret_none(void)
{
  return ;
}
 
void ret_void(void)
{
  return ret_none();
}
 
int ret_int_undef(void)
{
  int uninit;
  return uninit;
}
 
int main(void)
{
  ret_int();
  ret_void();
  ret_int_undef();
  return 0;
}

実行結果は以下の通りです。未初期化の関数が戻ることを補足してます。

ret.c:21:3: warning: Undefined or garbage value returned to caller
  return uninit;
  ^~~~~~~~~~~~~
1 warning generated.

3.2. NULLとなる参照を返す対象コード

ref関数の引数を指定しない場合にNULLとなる参照を返すようにします。

#include <iostream>
 
int &ref(int a = 0)
{
  int *p;
  if (!a)
        p = NULL;
  else
        p = new int();
  int &b = *p;
  return b;
}
 
int main()
{
  int &a = ref();
  return 0;
}

実行結果は以下の通りです。参照がNULLになることを補足してます。

ret.cpp:11:3: warning: Returning null reference
  return b;
  ^~~~~~~~
1 warning generated.

4. 参照がtaintedな領域を指す場合

ArrayBoundCheckerV2のように、外部から入力されるtaintedな領域を参照が指 す場合に、checkReferenceのassumeがtrue/falseの両方になり得る場合があり ます。

4.1. 参照がtaintedな領域を指す対象コード

argvで与えられた領域を引数に指定し、その参照を返すref関数を定義します。

#include <iostream>
 
char &ref(char *p)
{
  char &b = *p;
  return b;
}
 
int main(int argc, char *argv[])
{
  char &a = ref(argv[0]);
  return 0;
}

ReturnUndefCheckerを実行しても、参照がNULLでないというassumeが成立する 為、参照がNULLであるというassumeは判定されません。 これは本Checkerの仕様で、taintedなコードは他のCheckerに任せるという方 針なのかもしれませんが、taintedなコードの場合にもwarningが出力されるよ うに変更してみます。

4.2. 参照がtaintedな領域を指す場合にwarningを出力する変更

以下の変更を加えます。

Index: ReturnUndefChecker.cpp
===================================================================
--- ReturnUndefChecker.cpp      (revision 201037)
+++ ReturnUndefChecker.cpp      (working copy)
@@ -27,6 +27,7 @@
 class ReturnUndefChecker : public Checker< check::PreStmt<ReturnStmt> > {
   mutable OwningPtr<BuiltinBug> BT_Undef;
   mutable OwningPtr<BuiltinBug> BT_NullReference;
+  mutable OwningPtr<BuiltinBug> BT_TaintedReference;
 
   void emitUndef(CheckerContext &C, const Expr *RetE) const;
   void checkReference(CheckerContext &C, const Expr *RetE,
@@ -105,6 +106,13 @@
   ProgramStateRef StNonNull, StNull;
   llvm::tie(StNonNull, StNull) = C.getState()->assume(RetVal);
 
+  if (StNonNull && StNull) {
+    if (!BT_TaintedReference)
+      BT_TaintedReference.reset(new BuiltinBug("Returning tainted reference"));
+    emitBug(C, *BT_TaintedReference, RetE, bugreporter::getDerefExpr(RetE));
+    return;
+  }
+
   if (StNonNull) {
     // Going forward, assume the location is non-null.
     C.addTransition(StNonNull);

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

tainted.cpp:6:3: warning: Returning tainted reference
  return b;
  ^~~~~~~~
1 warning generated.

しかし、false positiveな結果になりがちなのかな・・・。クラスメソッドの 引数なんて他のファイルのコードから指定されるのが当たり前ですし・・・。 いまいちですね。

5. まとめ

ReturnUndefCheckerで未初期化の変数を返す関数を補足できました。また、 NULLとなる参照を返す処理を補足できました。

ダウンロード
ソースコード
対象コード
src.tar.gz
GNU tar 541 Bytes