CodeQL’s C++ data flow library gets a precision upgrade

CodeQL’s data flow and taint tracking libraries for C++ have been updated with a new analysis model that improves precision in standard queries and simplifies custom query authoring. The new libraries are active in CodeQL’s standard queries, but remain opt-in for custom queries. Here’s what changed and how to enable it.

Def-use gives way to use-use

The key architectural change is a shift from a “def-use” to a “use-use” pattern for modeling variable reads. In the old def-use model, reads from a variable were modeled as edges from a definition (an assignment) to a use (a read), with no direct relationship between two consecutive reads of the same definition. In the new use-use model, flow moves from the previous read of a variable directly to the next one (or from the definition if there’s no prior read).

This distinction matters most for queries that need to account for conditional sanitizers. Consider a check that happens sometime between the definition of a value and its dangerous use:

char *str = source();
if (isSafe(str)) {
    sink(str)
}

Under the old model, filtering out such a result required a separate control-flow analysis to prove that the sanitizing check occurs on every path leading to the dangerous use. In the use-use model, that reasoning is already encoded in the data flow graph, so queries can express the intent more directly.

Pointers and their pointees are treated separately

The updated library also distinguishes between a pointer’s value and the value it points at, and can model multiple levels of indirection between a function boundary and the actual tainted data. This is especially useful for string flows.

int main (int argc, char **argv) {
    if(argc >= 2) {
        fopen(argv[1])
    }
}

In this example, the tainted data isn’t argv itself — it’s what argv points to after two dereferences. In older analyses, queries that flag command-line input would mark argv as tainted, and any read or dereference of it would also be considered unsafe. With the new model, the analysis understands that only the double-dereferenced value is dangerous, so the first dereference of the pointer is no longer flagged.

Adopting the new library in custom queries

The new libraries are already enabled for standard queries, but custom queries must opt in. Spacing of the migration is relatively simple: replace import semmle.code.cpp.dataflow.DataFlow with import semmle.code.cpp.dataflow.new.DataFlow.

Once you’ve done that, review your isSource() and isSink() definitions. The semantic of node.asExpr() and node.asParameter() has shifted: they now refer to the value of the node itself rather than any dereference of it. If your query was using them to describe the data that flows indirectly through a pointer or reference, you’ll want to switch to the new predicates:

  • node.asIndirectArgument(int)
  • node.asIndirectExpr(int)
  • node.asDefiningArgument(int)
  • node.asParameter(int)

To see how this plays out in practice, consider finding a flow from argv to the first argument of fopen. Previously, the query would look like this:

import cpp
import semmle.code.cpp.dataflow.TaintTracking

class ArgvTaintedFopenConfig extends TaintTracking::Configuration {
  ArgvTaintedFopenConfig() { this = "ArgvTaintedFopenConfig" }

  override predicate isSource(DataFlow::Node node) {
    exists(Parameter argv |
      node.asParameter() = argv and
      argv.hasName("argv") and
      argv.getFunction().hasGlobalName("main")
    )
  }

  override predicate isSink(DataFlow::Node node) {
    exists(FunctionCall fopenCall |
      node.asExpr() = fopenCall.getArgument(0) and
      fopenCall.getTarget().hasGlobalOrStdName("fopen")
    )
  }
}

With the new library, you can be more precise. node.asParameter(2) identifies the character series that argv points to after two dereferences, and node.asIndirectArgument(1) describes the value reachable after one dereference of the fopen argument:

import cpp
import semmle.code.cpp.dataflow.new.TaintTracking

class ArgvTaintedFopenConfig extends TaintTracking::Configuration {
  ArgvTaintedFopenConfig() { this = "ArgvTaintedFopenConfig" }

  override predicate isSource(DataFlow::Node node) {
    exists(Parameter argv |
      node.asParameter(2) = argv and
      argv.hasName("argv") and
      argv.getFunction().hasGlobalName("main")
    )
  }

  override predicate isSink(DataFlow::Node node) {
    exists(FunctionCall fopenCall |
      node.asIndirectArgument(1) = fopenCall.getArgument(0) and
      fopenCall.getTarget().hasGlobalOrStdName("fopen")
    )
  }
}

For broader cases, the more general node.asIndirectExpr(int) predicate can describe the value of an indirection at any expression of pointer type in the program.