Skip to content

Conversation

@flovent
Copy link
Contributor

@flovent flovent commented Jun 19, 2025

For lambdas that are converted to C function pointers,

int (*ret_zero)() = []() { return 0; };

clang will generate conversion method like:

CXXConversionDecl implicit used constexpr operator int (*)() 'int (*() const noexcept)()' inline
 -CompoundStmt
   -ReturnStmt
    -ImplicitCastExpr 'int (*)()' <FunctionToPointerDecay>
     -DeclRefExpr 'int ()' lvalue CXXMethod 0x5ddb6fe35b18 '__invoke' 'int ()'
-CXXMethodDecl implicit used __invoke 'int ()' static inline
 -CompoundStmt (empty)

Based on comment in Sema, __invoke's function body is left empty because it's will be filled in CodeGen, so in AST analysis phase we should get lambda's operator() directly instead of calling __invoke itself.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Jun 19, 2025
@llvmbot
Copy link
Member

llvmbot commented Jun 19, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-static-analyzer-1

Author: None (flovent)

Changes

For lambdas that are converted to C function pointers,

int (*ret_zero)() = []() { return 0; };

clang will generate conversion method like:

CXXConversionDecl implicit used constexpr operator int (*)() 'int (*() const noexcept)()' inline
 -CompoundStmt
   -ReturnStmt
    -ImplicitCastExpr 'int (*)()' &lt;FunctionToPointerDecay&gt;
     -DeclRefExpr 'int ()' lvalue CXXMethod 0x5ddb6fe35b18 '__invoke' 'int ()'
-CXXMethodDecl implicit used __invoke 'int ()' static inline
 -CompoundStmt (empty)

Based on comment in Sema, __invoke's function body is left empty because it's will be filled in CodeGen, so in AST analysis phase we should get lambda's operator() directly instead of calling __invoke itself.


Full diff: https://github.com/llvm/llvm-project/pull/144906.diff

3 Files Affected:

  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h (+2)
  • (modified) clang/lib/StaticAnalyzer/Core/CallEvent.cpp (+12)
  • (added) clang/test/Analysis/lambda-convert-to-func-ptr.cpp (+21)
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
index f6a43bf5f493b..5dcf03f7a4648 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
@@ -554,6 +554,8 @@ class SimpleFunctionCall : public AnyFunctionCall {
 
   const FunctionDecl *getDecl() const override;
 
+  RuntimeDefinition getRuntimeDefinition() const override;
+
   unsigned getNumArgs() const override { return getOriginExpr()->getNumArgs(); }
 
   const Expr *getArgExpr(unsigned Index) const override {
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index f78b1b84f9df6..34fcb9b64d555 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -688,6 +688,18 @@ const FunctionDecl *SimpleFunctionCall::getDecl() const {
   return getSVal(getOriginExpr()->getCallee()).getAsFunctionDecl();
 }
 
+RuntimeDefinition SimpleFunctionCall::getRuntimeDefinition() const {
+  // Clang converts lambdas to function pointers using an implicit conversion
+  // operator, which returns the lambda's '__invoke' method. However, Sema
+  // leaves the body of '__invoke' empty (it is generated later in CodeGen), so
+  // we need to skip '__invoke' and access the lambda's operator() directly.
+  if (const auto *CMD = dyn_cast_if_present<CXXMethodDecl>(getDecl());
+      CMD && CMD->isLambdaStaticInvoker())
+    return RuntimeDefinition{CMD->getParent()->getLambdaCallOperator()};
+
+  return AnyFunctionCall::getRuntimeDefinition();
+}
+
 const FunctionDecl *CXXInstanceCall::getDecl() const {
   const auto *CE = cast_or_null<CallExpr>(getOriginExpr());
   if (!CE)
diff --git a/clang/test/Analysis/lambda-convert-to-func-ptr.cpp b/clang/test/Analysis/lambda-convert-to-func-ptr.cpp
new file mode 100644
index 0000000000000..c2ad7cd2de34a
--- /dev/null
+++ b/clang/test/Analysis/lambda-convert-to-func-ptr.cpp
@@ -0,0 +1,21 @@
+// RUN: %clang_analyze_cc1 -std=c++11 -analyzer-checker=core,debug.ExprInspection -analyzer-config inline-lambdas=true -verify %s
+
+void clang_analyzer_eval(bool);
+
+void basic() {
+  int (*ret_zero)() = []() { return 0; };
+  clang_analyzer_eval(ret_zero() == 0); // expected-warning{{TRUE}}
+}
+
+void withParam() {
+  int (*add_ten)(int) = [](int b) { return b + 10; };
+  clang_analyzer_eval(add_ten(1) == 11); // expected-warning{{TRUE}}
+}
+
+int callBack(int (*fp)(int), int x) {
+  return fp(x);
+}
+
+void passWithFunc() {
+  clang_analyzer_eval(callBack([](int x) { return x; }, 5) == 5); // expected-warning{{TRUE}}
+}

@steakhal
Copy link
Contributor

Hey, looks like a nice patch!

I only have one question. Have you considered overriding some other getRuntimeDefinition too? When I'm looking at the CallEvent inheritance graph, there could be a couple other options too.
Don't get me wrong, the SimpleFunctionCall::getRuntimeDefinition looks like the right place. I'm just curious.

@firewave
Copy link

The same occurs with assigning to auto: https://godbolt.org/z/nofG6cehf

Will this also be handled by this change?

@flovent
Copy link
Contributor Author

flovent commented Jun 20, 2025

I only have one question. Have you considered overriding some other getRuntimeDefinition too? When I'm looking at the CallEvent inheritance graph, there could be a couple other options too. Don't get me wrong, the SimpleFunctionCall::getRuntimeDefinition looks like the right place. I'm just curious.

As far as i know, __invoke will always be a normal static member method for lambda, and static member method will be modeled as SimpleFunctionCall in analyzer, other derived classes of same level seems to for memory-allocation related (CXXAllocatorCall, CXXDeallocatorCall), or class-instance related (AnyCXXConstructorCall, CXXInstanceCall)

@flovent
Copy link
Contributor Author

flovent commented Jun 20, 2025

The same occurs with assigning to auto: https://godbolt.org/z/nofG6cehf

Will this also be handled by this change?

If you mean directly calling this lambda, analyzer can already analyze operator() correctly before this patch, because neither CXXConversionDecl and __invoke is used here.

This patch fix the situation like this: https://godbolt.org/z/4YWPP6939

void f()
{
    auto f = []() { return 0; };
    int (*ptr)() = f;
    1 / ptr();
}

lambda f should be called indirecly from function pointer ptr and produce divzero.

@steakhal steakhal merged commit b8bda9d into llvm:main Jun 25, 2025
10 checks passed
@llvm-ci
Copy link
Collaborator

llvm-ci commented Jun 25, 2025

LLVM Buildbot has detected a new failure on builder clangd-ubuntu-tsan running on clangd-ubuntu-clang while building clang at step 6 "test-build-clangd-clangd-index-server-clangd-in...".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/134/builds/21264

Here is the relevant piece of the build log for the reference
Step 6 (test-build-clangd-clangd-index-server-clangd-in...) failure: test (failure)
******************** TEST 'Clangd :: target_info.test' FAILED ********************
Exit Code: 66

Command Output (stderr):
--
rm -rf /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir && mkdir -p /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir # RUN: at line 5
+ rm -rf /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir
+ mkdir -p /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir
echo '[{"directory": "/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir", "command": "/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir/armv7-clang -x c++ the-file.cpp -v", "file": "the-file.cpp"}]' > /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir/compile_commands.json # RUN: at line 7
+ echo '[{"directory": "/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir", "command": "/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir/armv7-clang -x c++ the-file.cpp -v", "file": "the-file.cpp"}]'
sed -e "s|INPUT_DIR|/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir|g" /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/llvm-project/clang-tools-extra/clangd/test/target_info.test > /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test.1 # RUN: at line 9
+ sed -e 's|INPUT_DIR|/vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.dir|g' /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/llvm-project/clang-tools-extra/clangd/test/target_info.test
sed -E -e 's|"file://([A-Z]):/|"file:///\1:/|g' /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test.1 > /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test # RUN: at line 12
+ sed -E -e 's|"file://([A-Z]):/|"file:///\1:/|g' /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test.1
clangd -lit-test < /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test 2>&1 | /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/bin/FileCheck -strict-whitespace /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test # RUN: at line 14
+ clangd -lit-test
+ /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/bin/FileCheck -strict-whitespace /vol/worker/clangd-ubuntu-clang/clangd-ubuntu-tsan/build/tools/clang/tools/extra/clangd/test/Output/target_info.test.tmp.test

--

********************


@flovent flovent deleted the csa-lambda-func-ptr-invoke branch June 26, 2025 11:57
anthonyhatran pushed a commit to anthonyhatran/llvm-project that referenced this pull request Jun 26, 2025
…llvm#144906)

For lambdas that are converted to C function pointers, 
```
int (*ret_zero)() = []() { return 0; };
```
clang will generate conversion method like:
```
CXXConversionDecl implicit used constexpr operator int (*)() 'int (*() const noexcept)()' inline
 -CompoundStmt
   -ReturnStmt
    -ImplicitCastExpr 'int (*)()' <FunctionToPointerDecay>
     -DeclRefExpr 'int ()' lvalue CXXMethod 0x5ddb6fe35b18 '__invoke' 'int ()'
-CXXMethodDecl implicit used __invoke 'int ()' static inline
 -CompoundStmt (empty)
```
Based on comment in Sema, `__invoke`'s function body is left empty
because it's will be filled in CodeGen, so in AST analysis phase we
should get lambda's `operator()` directly instead of calling `__invoke`
itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:static analyzer clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants