Skip to main content

Command Palette

Search for a command to run...

Klee Rtfsc

Read the Friendly Source Code

Published
•8 min read•View as Markdown
Klee Rtfsc
B

bear with us, while we think.

Basic Architecture

In the "3.1 Basic Architecture" of KLEE Paper, the author states the basic function of KLEE, which is the symbolic execution engine they implemented.

However, because of the concise nature of the papers, there won't be lots of source code in the paper. In this blog, I am going to state the core source code.

Select and Execute

💡
The core of KLEE is an interpreter loop that selects a state to run and then symbolically executes a single instruction in the context of that state. This loop continues until no states are remaining, or a user-defined timeout is reached.

According to the source code of KLEE, the interpreter loop is implemented in the Executor::run in the file lib\Core\Executor.cpp. This function takes a reference to the ExecutionState as a parameter and iterates over the states until they are exhausted or the time expires.

 // in the /lib/Core/Executor.cpp of the KLEE source
 // main interpreter loop
  while (!states.empty() && !haltExecution) {
    ExecutionState &state = searcher->selectState();
    KInstruction *ki = state.pc;
    stepInstruction(state);

    executeInstruction(state, ki);
    timers.invoke();
    if (::dumpStates) dumpStates();
    if (::dumpPTree) dumpPTree();

    updateStates(&state);

    if (!checkMemoryUsage()) {
      // update searchers when states were terminated early due to memory pressure
      updateStates(nullptr);
    }
  }

By the way, there is a "usingSeeds" keyword in the "Executor::run" function, whose meaning is that it is a boolean variable that indicates whether the symbolic execution engine is running in the seeding mode or not. Seeding mode is the feature of KLEE that allows it to use concrete value inputs as seeds to guide the exploration of the state space. (similar to the concept "concolic" of DART). Seeding mode can improve the coverage and efficiency of symbolic execution, especially for programs that have complex or unknown input formats.

/// in the lib/Core/Executor.cpp of the KLEE source.
class Executor : public Interpreter {
    ...
public:
    ...
private:
    ...
/// When non-null a list of "seed" inputs which will be used to
/// drive execution.
    const std::vector<struct KTest *> *usingSeeds
    ...

The state of symbolic execution is represented by the class ExecutionState is defined in the source file lib/Core/ExecutionState.h.

class ExecutionState {
    ...
public:
  using stack_ty = std::vector<StackFrame>;
  /// @brief Pointer to instruction to be executed after the current
  /// instruction
  KInstIterator pc;
  /// @brief Pointer to instruction which is currently executed
  KInstIterator prevPC;
  /// @brief Stack representing the current instruction stream
  stack_ty stack;
  /// @brief Remember from which Basic Block control flow arrived
  /// (i.e. to select the right phi values)
  std::uint32_t incomingBBIndex;
  // Overall state of the state - Data specific
  /// @brief Exploration depth, i.e., number of times KLEE branched for this state
  std::uint32_t depth = 0;
  /// @brief Address space used by this state (e.g. Global and Heap)
  AddressSpace addressSpace;
  /// @brief Stack allocator (used with deterministic allocation)
  kdalloc::StackAllocator stackAllocator;
  /// @brief Heap allocator (used with deterministic allocation)
  kdalloc::Allocator heapAllocator;
  /// @brief Constraints collected so far
  ConstraintSet constraints;
  /// Statistics and information
   ... ....
}

Check whether concrete

💡
Check whether operands are concrete when executing.

Symbolic execution of the majority of instructions is straightforward. For example, to symbolically execute an LLVM add instruction:

%dst = add i32 %src0, %src1

KLEE retrieves the addends from the %src0 and %src1 registers and writes a new expression Add(%src0, %src1) to the %dst register. For efficiency, the code that builds expressions checks if all given operands are concrete (i.e., constants) and if so, operates natively, returning a constant expression.

The method AddExpr::create is invoked to check for concrete operands and return either a constant expression or a non-constant expression accordingly. For example, the add expression kind is defined as follows:

ref<Expr> AddExpr::createActual(ref<Expr> l, ref<Expr> r) {
  Expr::Width type = l->getWidth();

  assert(l->getWidth() == r->getWidth() && "type mismatch");

  if (ConstantExpr *cl = dyn_cast<ConstantExpr>(l)) {
    if (ConstantExpr *cr = dyn_cast<ConstantExpr>(r))
      return cl->Add(cr);
    else if (cl->isZero())
      return r;
  } else if (ConstantExpr *cr = dyn_cast<ConstantExpr>(r)) {
    if (cr->isZero())
      return l;
  }

  return AddExpr::alloc(l, r);
}

As you can see, this method checks if both operands are constant expressions (ConstantExpr) and, if so, performs the addition natively using the Add the method of the ConstantExpr class. Otherwise, it checks if either operand is zero and returns the other operand as a shortcut. Finally, if none of these cases apply, it allocates a new AddExpr object using the alloc method of the AddExpr class. Similar checks are performed for other expression kinds, such as Sub, Mul, Div, etc.

Fork Branch

💡
Conditional branches take a boolean expression (branch condition) and alter the instruction pointer of the state based on whether the condition is true or false.KLEE queries the constraint solver to determine if the branch condition is either provably true or provably false along the current path; if so, the instruction pointer is updated to the appropriate location. Otherwise, both branches are possible: KLEE clones the state so that it can explore both paths, updating the instruction pointer and path condition on each path appropriately.
void Executor::executeInstruction(ExecutionState &state, KInstruction *ki) {
  Instruction *i = ki->inst;
  switch (i->getOpcode()) {
    ...
    case Instruction::Br: {
        BranchInst *bi = cast<BranchInst>(i);
        if (bi->isUnconditional()) {
          transferToBasicBlock(bi->getSuccessor(0), bi->getParent(), state);
        } else {
          // FIXME: Find a way that we don't have this hidden dependency.
          assert(bi->getCondition() == bi->getOperand(0) &&
                 "Wrong operand index!");
          ref<Expr> cond = eval(ki, 0, state).value;

          cond = optimizer.optimizeExpr(cond, false);
          Executor::StatePair branches = fork(state, cond, false, BranchType::Conditional);

          // NOTE: There is a hidden dependency here, markBranchVisited
          // requires that we still be in the context of the branch
          // instruction (it reuses its statistic id). Should be cleaned
          // up with convenient instruction specific data.
          if (statsTracker && state.stack.back().kf->trackCoverage)
            statsTracker->markBranchVisited(branches.first, branches.second);

          if (branches.first)
            transferToBasicBlock(bi->getSuccessor(0), bi->getParent(), *branches.first);
          if (branches.second)
            transferToBasicBlock(bi->getSuccessor(1), bi->getParent(), *branches.second);
        }
        break;
      }
      ...
    }
    ...
}

As you can see, this method checks if the branch instruction is unconditional or conditional. If it is unconditional, it simply transfers the execution state to the successor basic block using the transferToBasicBlock method. If it is conditional, it evaluates the branch condition using the eval method and then fork the execution state into two possible branches using the fork method returning a pair of execution states, one for a true branch and one for a false branch.

The method then transfers each execution state to the corresponding successor basic block using the transferToBasicBlock method. This way, the instruction pointer of each state is altered based on the branch condition.

void Executor::executeGetValue(ExecutionState &state,
                               ref<Expr> e,
                               KInstruction *target) {
  e = ConstraintManager::simplifyExpr(state.constraints, e);
  std::map< ExecutionState*, std::vector<SeedInfo> >::iterator it = 
    seedMap.find(&state);
  if (it == seedMap.end() || isa<ConstantExpr>(e)) {
    ref<ConstantExpr> value;
    e = optimizer.optimizeExpr(e, true);
    bool success =
        solver->getValue(state.constraints, e, value, state.queryMetaData);
    assert(success && "FIXME: Unhandled solver failure");
    (void) success;
    bindLocal(target, state, value);
  } else {
    std::set< ref<Expr> > values;
    for (std::vector<SeedInfo>::iterator siit = it->second.begin(), 
           siie = it->second.end(); siit != siie; ++siit) {
      ref<Expr> cond = siit->assignment.evaluate(e);
      cond = optimizer.optimizeExpr(cond, true);
      ref<ConstantExpr> value;
      bool success =
          solver->getValue(state.constraints, cond, value, state.queryMetaData);
      assert(success && "FIXME: Unhandled solver failure");
      (void) success;
      values.insert(value);
    }

    std::vector< ref<Expr> > conditions;
    for (std::set< ref<Expr> >::iterator vit = values.begin(), 
           vie = values.end(); vit != vie; ++vit)
      conditions.push_back(EqExpr::create(e, *vit));

    std::vector<ExecutionState*> branches;
    branch(state, conditions, branches, BranchType::GetVal);

    std::vector<ExecutionState*>::iterator bit = branches.begin();
    for (std::set< ref<Expr> >::iterator vit = values.begin(), 
           vie = values.end(); vit != vie; ++vit) {
      ExecutionState *es = *bit;
      if (es)
        bindLocal(target, *es, *vit);
      ++bit;
    }
  }
}

As you can see, the Executor::executeGetValue in the lib\Core\Executor.cpp first checks if the current state is seed or the expression being executed is concrete, if so, invoke the solver to get the final value and bind it locally. Otherwise, deal with the seed condition and the branch condition.

Deal with Dangerous Operation

💡
Potentially dangerous operations implicitly generate branches that check if any input value exists that could cause an error. For example, a division instruction generates a branch that checks for a zero divisor. Such branches work identically to normal branches. Thus, even when the check succeeds (i.e., an error is detected), execution continues on the false path, which adds the negation of the check as a constraint (e.g., making the divisor not zero). If an error is detected, KLEE generates a test case to trigger the error and terminates the state.
💡
KLEE maps every memory object in the checked code to a distinct STP array (in a sense, mapping a flat address space to a segmented one). This representation dramatically improves performance since it lets STP ignore all arrays not referenced by a given expression", In what part of the code implemented this function?

The part of the code that implements the function of mapping every memory object in the checked code to a distinct STP array is the Executor::getInitialArray method in the file lib/Core/Executor.cpp1. This method takes two parameters: a reference to the execution state and a pointer to the memory object. The method performs the following steps:

Query optimization

In the KLEE paper, the author describes lots of optimizations of queries such as expression rewriting, constraint set simplification and so on. The KLEE implements the function in lib/Solver/Solver.cpp.

Random Path Selection and Coverage-Optimized Search

The KLEE source code contains two different search heuristics for selecting the next state to explore: Random Path Selection and Coverage-Optimized Search.

// lib\Core\Searcher.cpp
// RP Selection
ExecutionState &RandomSearcher::selectState() {
  return *states[theRNG.getInt32() % states.size()];
}
// CO Selection
ExecutionState &WeightedRandomSearcher::selectState() {
  return *states->choose(theRNG.getDoubleL());
}

REFERENCE

KLEE Source Code

KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs

Multi-solver Support in Symbolic Execution