unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
refactoringtesting-strategy

[clang][analyzer] Model function addresses in constant initializers

Project: llvm/llvm-project | Language: C++ | Concepts: refactoring, testing-strategy | Difficulty: mid
Link: https://github.com/llvm/llvm-project/pull/217608

The problem

The Clang static analyzer could not properly handle constant initializers that are function addresses. When a const function pointer was initialized directly with a function (e.g., static Callback const ConstPointer = pointerTarget;), the analyzer would treat its value as UnknownVal. This prevented the analyzer from resolving and inlining calls through such pointers, reducing the accuracy of analysis and missing possible inlining and checks.

Why it's non-trivial

A naive fix might only look for literal constants or simple expressions, missing the fact that function names in C/C++ can be used as constant addresses in initializers (via decay, address-of, or casting). Handling all forms of function address expressions and mapping them to the correct symbolic value for the analyzer requires awareness of language semantics and AST structures.

The approach

The main change is in SValBuilder::getConstantVal, which is responsible for evaluating constant expressions for the analyzer. The patch adds logic to recognize when an expression is a function pointer or a direct function reference, and to return the correct symbolic value for the function address.

+  if (E->getType()->isFunctionPointerType()) {
+    if (const auto *FD =
+            dyn_cast_or_null<FunctionDecl>(E->getReferencedDeclOfCallee()))
+      return getFunctionPointer(FD);
+  }

What this does:

  • Checks if the expression's type is a function pointer.
  • If so, tries to get the referenced function declaration.
  • If successful, returns a symbolic value representing the function pointer.

This logic is placed before the main switch on statement class, ensuring that function addresses are handled early and correctly.

The patch also adds a comprehensive test file (clang/test/Analysis/constant-function-pointer.c) that exercises various ways to initialize function pointers (decay, address-of, cast) and confirms that the analyzer now recognizes and inlines through these constants:

static callback const decayCallback = decay_target;
static callback const addressCallback = &address_target;
static callback const castCallback = (callback)cast_target;

void test_function_pointer_forms(void) {
  clang_analyzer_eval(decayCallback == decay_target); // forms-warning{{TRUE}}
  // ...
}

What this does:

  • Declares constant function pointers initialized in different ways.
  • Verifies with clang_analyzer_eval that the analyzer can now resolve and compare the function addresses correctly.

Another test checks that mutable function pointers are still treated as unknown:

static callback mutableCallback = decay_target;

void test_mutable_pointer(void) {
  clang_analyzer_eval(mutableCallback == decay_target); // mutable-warning{{UNKNOWN}}
}

What this does:

  • Ensures that only truly constant initializers are resolved, not mutable ones.

From the review thread

No notable review discussion on this PR.

Takeaways

  • When modeling language semantics in static analysis, handle all forms of constant expressions, including function addresses, not just literals.
  • Tests should cover all initialization forms (decay, address-of, cast) to ensure robust handling of language features.
  • Only constant (immutable) initializers should be resolved to specific values during static analysis; mutable values must remain unknown to avoid unsoundness.