ClangのUndefBranchCheckerを試してみます。
1. 実行方法
$ clang -cc1 -analyzer-checker=core.uninitialized.Branch \ -analyze <target>.c
core.uninitializedで何となく未初期化の変数を用いたコードを検出する Checkerと分かります。
2. UndefBranchCheckerの実装
BranchConditionを継承し、checkBranchConditionを実装しています。引数 Stmt *から得られるSValのisUndef()がtrueの場合にwarning出力します。
class UndefBranchChecker : public Checker<check::BranchCondition> { <snip> public: void checkBranchCondition(const Stmt *Condition, CheckerContext &Ctx) const; }; <snip> void UndefBranchChecker::checkBranchCondition(const Stmt *Condition, CheckerContext &Ctx) const { SVal X = Ctx.getState()->getSVal(Condition, Ctx.getLocationContext()); if (X.isUndef()) { // Generate a sink node, which implicitly marks both outgoing branches as // infeasible. ExplodedNode *N = Ctx.generateSink(); <snip> }
3. checkBranchConditionを用いたChecker
checkBranchConditionの挙動を確かめる為にcheckBranchConditionを用いた Checkerを作成します。
3.1. Checkerの内容
checkBranchConditionにて、debug_stmtでStmtのクラスとソースコードを表示 します。
debug_svalでSValのBaseKind、クラス名、isUndef()の値を表示します。SVal がnonloc::ConcreteIntあるいはloc::ConcreteIntの場合はgetValue()の値も 表示します。
class BranchConditionChecker : public Checker<check::BranchCondition> { public: void checkBranchCondition(const Stmt *S, CheckerContext &C) const; }; <snip> static void debug_sval(const nonloc::ConcreteInt *sval) { llvm::errs() << "Value = " << sval->getValue() << "\n"; } static void debug_sval(const loc::ConcreteInt *sval) { llvm::errs() << "Value = " << sval->getValue() << "\n"; } static void debug_sval(const SVal *sval) { llvm::errs() << "BaseKind = " << getBaseKind(sval) << ", " << "Class = " << getClass(sval) << ", " << "isUndef() = " << sval->isUndef() << "\n"; if (sval->getAs<nonloc::ConcreteInt>() != None) { const nonloc::ConcreteInt __sval = (*sval).castAs<nonloc::ConcreteInt>(); debug_sval(&__sval); } else if (sval->getAs<loc::ConcreteInt>() != None) { const loc::ConcreteInt __sval = (*sval).castAs<loc::ConcreteInt>(); debug_sval(&__sval); } } static void debug_sval(const Stmt *S, CheckerContext &C) { SVal Val = C.getState()->getSVal(S, C.getLocationContext()); debug_sval(&Val); } void BranchConditionChecker::checkBranchCondition(const Stmt *S, CheckerContext &C) const { debug_stmt(S, C.getSourceManager()); debug_sval(S, C); }
3.2. 対象コード
chの値によって未初期化の変数を返す関数ret、getchar()の値によってpの値 が未初期化になり得るmain関数を定義します。
#include <stdio.h> int ret(int ch) { int uninit; if (ch == 1) uninit = 1; return uninit; } int main(void) { char array[] = { 1 }; char *p; int ch; ch = getchar(); if (ch == 0) p = NULL; else if (ret(ch)) /** maybe uninit */ p = array; if (p) /** maybe uninit */ return 1; else if (p == array) /** maybe uninit */ return 2; return 0; }
3.3. 実行結果
checkBranchConditionはfStmtのgetCond/getThen/getElse等のStmtを補足する ようです。
ch == 0なのでBinaryOperatorとなります。 ここでSValがSymbolValになってます。 SymbolValはSymbolic expressionを表すものだそうです。BinaryOperatorは木 構造になっており、Symbolic expressionと評価されるのは納得なのですが、 SymbolValになる場合とnonloc::ConcreteIntになる場合があることが少し難し いです。
どうやらBinaryOperatorの評価値が確定している場合はnonloc::ConcreteInt となり、確定していない場合にSymbolValとなるような気がします。 本ステートメントではchがgetchar()によって決まる為、SymbolValとなってい るようです。
BinaryOperator if (ch == 0) ~~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::SymbolVal, isUndef() = 0
ch == 0がfalseになった経路で、ch == 1が評価されています。else if (ret(ch))を見て、ret関数内部まで追跡していますね。評価順は checkPostStmtに近いと思います。
BinaryOperator if (ch == 1) ~~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::SymbolVal, isUndef() = 0
次にret関数の戻り値が評価されます。isUndef() = 1となっているので、未初 期化の値が返った場合の経路のようです。
CallExpr else if (ret(ch)) /** maybe uninit */ ~~~~~~~ BaseKind = SVal::UndefinedKind, Class = UndefinedVal, isUndef() = 1
未初期化の変数を返したret(ch)がfalseでpが未初期化な経路を辿り、 p == NULLが評価されます。isUndef()が1となります。
BinaryOperator if (p == NULL) /** maybe uninit */ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BaseKind = SVal::UndefinedKind, Class = UndefinedVal, isUndef() = 1
p == NULLがfalseでpが未初期化な経路を辿り、p == arrayが評価されます。
BinaryOperator else if (p == array) /** maybe uninit */ ~~~~~~ BaseKind = SVal::UndefinedKind, Class = UndefinedVal, isUndef() = 1
未初期化の変数を返したret(ch)がtrueでp = arrayな経路を辿り、p == NULL が評価されます。isUndef()が0でValueが0となります。このValueは真偽値の falseを表します。
BinaryOperator if (p == NULL) /** maybe uninit */ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::ConcreteInt, isUndef() = 0 Value = 0
p == NULLがfalseでp = arrayな経路を辿り、p == arrayが評価されます。 isUndef()が0でValueが1となります。Valueはtrueですね。
BinaryOperator else if (p == array) /** maybe uninit */ ~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::ConcreteInt, isUndef() = 0 Value = 1
retが未初期化の変数を返さなかった場合の経路で、ret(ch)が評価されます。 isUndef()が0でValueが1となります。
CallExpr else if (ret(ch)) /** maybe uninit */ ~~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::ConcreteInt, isUndef() = 0 Value = 1
getchar()の戻り値が0でch == 0がtrueな経路で、p == NULLが評価されます。 isUndef()が0でValueが1となります。
BinaryOperator if (p == NULL) /** maybe uninit */ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BaseKind = SVal::NonLocKind, Class = nonloc::ConcreteInt, isUndef() = 0 Value = 1
3.4. 対象コードのCFG
CFGをdottyで見てみます。
$ clang -cc1 -analyzer-checker=debug.ViewCFG -analyze if.c
chの経路で分岐しますが、pの経路で一度合流していることが確認できます。 checkBranchConditionは、経路が合流したものについてはそれ以降追わないよ うです(以前に一度追っている為)。
4. UndefBranchCheckerの実行結果
ret(ch)の戻り値が未初期化となるケースを補足しております。
if.c:20:12: warning: Branch condition evaluates to a garbage value else if (ret(ch)) /** maybe uninit */ ^~~~~~~ 1 warning generated.
では、その後のpが未初期化となるケースは補足しないのでしょうか。
4.1. generateSink
isUndef()がtrueの場合にgenerateSinkを呼び出しております。
ExplodedNode *N = Ctx.generateSink();
本メソッドはsink nodeを作成すると同時に、それ以降の評価を実行しないよ うにするようです。
よってret(ch)を評価した際にgenerateSinkを呼び出すので、評価はそこで止 まります。
5. まとめ
checkBranchConditionが判定文を補足できることが確認できました。 判定文がBinaryOperatorの場合、判定結果が確定している場合は nonloc::ConcreteInt、確定していない場合はnonloc::SymbolValとなります。 未初期化の変数が使われている場合はisUndef()がtrueとなります。 UndefBranchCheckerでは未初期化の変数を用いた判定文をwarning出力した後、 その後の評価は停止します。