Skip to content

[analyzer] Accept C library functions from the std namespace #84469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 12, 2024

Conversation

NagyDonat
Copy link
Contributor

Previously, the function isCLibraryFunction() and logic relying on it only accepted functions that are declared directly within a TU (i.e. not in a namespace or a class). However C++ headers like declare many C standard library functions within the namespace std, so this commit ensures that functions within the namespace std are also accepted.

After this commit it will be possible to match functions like malloc or free with CallDescription::Mode::CLibrary.

Previously, the function `isCLibraryFunction()` and logic relying on it
only accepted functions that are declared directly within a TU (i.e. not
in a namespace or a class). However C++ headers like <cstdlib> declare
many C standard library functions within the namespace `std`, so this
commit ensures that functions within the namespace `std` are also
accepted.

After this commit it will be possible to match functions like `malloc` or
`free` with `CallDescription::Mode::CLibrary`.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Mar 8, 2024
@llvmbot
Copy link
Member

llvmbot commented Mar 8, 2024

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

@llvm/pr-subscribers-clang

Author: None (NagyDonat)

Changes

Previously, the function isCLibraryFunction() and logic relying on it only accepted functions that are declared directly within a TU (i.e. not in a namespace or a class). However C++ headers like <cstdlib> declare many C standard library functions within the namespace std, so this commit ensures that functions within the namespace std are also accepted.

After this commit it will be possible to match functions like malloc or free with CallDescription::Mode::CLibrary.


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

2 Files Affected:

  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h (+2-6)
  • (modified) clang/lib/StaticAnalyzer/Core/CheckerContext.cpp (+6-3)
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h
index 3432d2648633c2..7da65734a44cf3 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h
@@ -41,12 +41,8 @@ class CallDescription {
     ///  - We also accept calls where the number of arguments or parameters is
     ///    greater than the specified value.
     /// For the exact heuristics, see CheckerContext::isCLibraryFunction().
-    /// Note that functions whose declaration context is not a TU (e.g.
-    /// methods, functions in namespaces) are not accepted as C library
-    /// functions.
-    /// FIXME: If I understand it correctly, this discards calls where C++ code
-    /// refers a C library function through the namespace `std::` via headers
-    /// like <cstdlib>.
+    /// (This mode only matches functions that are declared either directly
+    /// within a TU or in the `std::` namespace.)
     CLibrary,
 
     /// Matches "simple" functions that are not methods. (Static methods are
diff --git a/clang/lib/StaticAnalyzer/Core/CheckerContext.cpp b/clang/lib/StaticAnalyzer/Core/CheckerContext.cpp
index d6d4cec9dd3d4d..c1ae9b441d98d1 100644
--- a/clang/lib/StaticAnalyzer/Core/CheckerContext.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CheckerContext.cpp
@@ -12,6 +12,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
+#include "clang/Analysis/AnalysisDeclContext.h"
 #include "clang/Basic/Builtins.h"
 #include "clang/Lex/Lexer.h"
 #include "llvm/ADT/StringExtras.h"
@@ -87,9 +88,11 @@ bool CheckerContext::isCLibraryFunction(const FunctionDecl *FD,
   if (!II)
     return false;
 
-  // Look through 'extern "C"' and anything similar invented in the future.
-  // If this function is not in TU directly, it is not a C library function.
-  if (!FD->getDeclContext()->getRedeclContext()->isTranslationUnit())
+  // C library functions are either declared directly within a TU (the common
+  // case) or they are accessed through the namespace `std::` (when they are
+  // used in C++ via headers like <cstdlib>).
+  if (!FD->getDeclContext()->getRedeclContext()->isTranslationUnit() &&
+          !AnalysisDeclContext::isInStdNamespace(FD))
     return false;
 
   // If this function is not externally visible, it is not a C library function.

Copy link

github-actions bot commented Mar 8, 2024

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff 6a0618a0289cb0c23ef3e5c820418650cc1d0fdc 4e91ad4be0a9055e0b86e27dbe22a18b8a08a8c5 -- clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h clang/lib/StaticAnalyzer/Core/CheckerContext.cpp
View the diff from clang-format here.
diff --git a/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp b/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp
index 19c66cc6be..88f056ebe0 100644
--- a/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp
+++ b/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp
@@ -48,7 +48,8 @@ TEST(IsCLibraryFunctionTest, AcceptsExternCGlobal) {
 }
 
 TEST(IsCLibraryFunctionTest, RejectsNoInlineNoExternalLinkage) {
-  // Functions that are neither inlined nor externally visible cannot be C library functions.
+  // Functions that are neither inlined nor externally visible cannot be C
+  // library functions.
   const FunctionDecl *Result;
   ASSERT_TRUE(extractFunctionDecl(R"cpp(static void fun();)cpp", Result));
   EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));

Only accept those functions whose declaration is _directly_ within the
namespace `std` (that is, not within a class or a sub-namespace).
Transparent declaration contexts (e.g. `extern "C"`) are still allowed,
but this prevents matching methods.
Copy link
Contributor

@Szelethus Szelethus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes perfect sense to me. Can you add a testcase for std::malloc or something similar?

/// refers a C library function through the namespace `std::` via headers
/// like <cstdlib>.
/// (This mode only matches functions that are declared either directly
/// within a TU or in the namespace `std`.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well go the extra mile and mention an example, like std::malloc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think saying "functions from the C standard library" (at the beginning of this comment block) is clear enough, mentioning malloc is redundant after it. Also, technically, we are not using this mode for malloc yet.

if (!FD->getDeclContext()->getRedeclContext()->isTranslationUnit())
// C library functions are either declared directly within a TU (the common
// case) or they are accessed through the namespace `std` (when they are used
// in C++ via headers like <cstdlib>).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@NagyDonat
Copy link
Contributor Author

Currently MallocChecker doesn't use this CDM::CLibrary mode in the CallDescription for malloc -- because before this commit it would've been incorrect to use it. I'm planning to ensure that functions like malloc are matched in this mode; but I would like to do it in separate commits.

This means that I cannot create a simple testcase that demonstrates the advantage of this commit: the limitations of isCLibraryFunction meant that many checkers just didn't use it even if they were handling C library functions; but I don't know about a case where it was used and incorrect.

However, my followup commits will soon add testcases that demonstrate the effects of this change, so I think it's a forgivable sin to merge this without a TC.

@NagyDonat NagyDonat requested a review from Szelethus March 8, 2024 12:47
Copy link
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, but I'd be more eased if had seen some unittests for this matter.
You could use AST-matchers selecting different FunctionDecls and query them using this free-function. I think this deserves a test, thus I vote weak reject.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Mar 8, 2024

I feel that I'm getting bogged down in this area with changes that are more general than what's strictly necessary and only tangentially related to my real goals...

I'm not familiar with the unittest framework and if unittests were necessary for this change, then I'd sooner abandon it, because it's not worth the effort.

(Disclaimer: this comment reflects my current annoyance, and is not necessarily aligned with my long-term opinion.)

@steakhal
Copy link
Contributor

steakhal commented Mar 8, 2024

I feel that I'm getting bogged down in this area with changes that are more general than what's strictly necessary and only tangentially related to my real goals...

I'm not familiar with the unittest framework and if unittests were necessary for this change, then I'd sooner abandon it, because it's not worth the effort.

(Disclaimer: this comment reflects my current annoyance, and is not necessarily aligned with my long-term opinion.)

No worries!
Note the IsCLibraryFunctionTest.AcceptsStdNamespace test case which would fail on llvm/main, but would pass with the PR.

Here is what I had in mind:
(I could not push to your branch, neither upload the patch file, so here is the verbatim content instead:

commit 1cfbeec724d758b136d36966696df29cf850ca5b
Author: Balazs Benics <[email protected]>
Date:   Fri Mar 8 17:26:49 2024 +0100

    Add unittests
    
    FYI IsCLibraryFunctionTest.AcceptsStdNamespace would have been FALSE
    prior this PR.

diff --git a/clang/unittests/StaticAnalyzer/CMakeLists.txt b/clang/unittests/StaticAnalyzer/CMakeLists.txt
index 775f0f8486b8..db56e77331b8 100644
--- a/clang/unittests/StaticAnalyzer/CMakeLists.txt
+++ b/clang/unittests/StaticAnalyzer/CMakeLists.txt
@@ -11,6 +11,7 @@ add_clang_unittest(StaticAnalysisTests
   CallEventTest.cpp
   ConflictingEvalCallsTest.cpp
   FalsePositiveRefutationBRVisitorTest.cpp
+  IsCLibraryFunctionTest.cpp
   NoStateChangeFuncVisitorTest.cpp
   ParamRegionTest.cpp
   RangeSetTest.cpp
diff --git a/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp b/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp
new file mode 100644
index 000000000000..e1dea3458bc1
--- /dev/null
+++ b/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp
@@ -0,0 +1,89 @@
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/AnalysisDeclContext.h"
+#include "clang/Frontend/ASTUnit.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
+#include "clang/Tooling/Tooling.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+
+using namespace clang;
+using namespace ento;
+using namespace ast_matchers;
+
+testing::AssertionResult extractFunctionDecl(StringRef Code,
+                                             const FunctionDecl *&Result) {
+  auto ASTUnit = tooling::buildASTFromCode(Code);
+  if (!ASTUnit)
+    return testing::AssertionFailure() << "AST construction failed";
+
+  ASTContext &Context = ASTUnit->getASTContext();
+  if (Context.getDiagnostics().hasErrorOccurred())
+    return testing::AssertionFailure() << "Compilation error";
+
+  auto Matches = ast_matchers::match(functionDecl().bind("fn"), Context);
+  if (Matches.empty())
+    return testing::AssertionFailure() << "No function declaration found";
+
+  if (Matches.size() > 1)
+    return testing::AssertionFailure()
+           << "Multiple function declarations found";
+
+  Result = Matches[0].getNodeAs<FunctionDecl>("fn");
+  return testing::AssertionSuccess();
+}
+
+TEST(IsCLibraryFunctionTest, AcceptsGlobal) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(extractFunctionDecl(R"cpp(void fun();)cpp", Result));
+  EXPECT_TRUE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, AcceptsExternCGlobal) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(
+      extractFunctionDecl(R"cpp(extern "C" { void fun(); })cpp", Result));
+  EXPECT_TRUE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, AcceptsStaticGlobal) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(extractFunctionDecl(R"cpp(static void fun();)cpp", Result));
+  EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
+  // FIXME: Shouldn't this be TRUE?
+}
+
+TEST(IsCLibraryFunctionTest, RejectsAnonymousNamespace) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(
+      extractFunctionDecl(R"cpp(namespace { void fun(); })cpp", Result));
+  EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, AcceptsStdNamespace) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(
+      extractFunctionDecl(R"cpp(namespace std { void fun(); })cpp", Result));
+  EXPECT_TRUE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, RejectsOtherNamespaces) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(
+      extractFunctionDecl(R"cpp(namespace stdx { void fun(); })cpp", Result));
+  EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, RejectsClassStatic) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(
+      extractFunctionDecl(R"cpp(class A { static void fun(); };)cpp", Result));
+  EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
+}
+
+TEST(IsCLibraryFunctionTest, RejectsClassMember) {
+  const FunctionDecl *Result;
+  ASSERT_TRUE(extractFunctionDecl(R"cpp(class A { void fun(); };)cpp", Result));
+  EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
+}
diff --git a/llvm/utils/gn/secondary/clang/unittests/StaticAnalyzer/BUILD.gn b/llvm/utils/gn/secondary/clang/unittests/StaticAnalyzer/BUILD.gn
index 01c2b6ced336..9c240cff1816 100644
--- a/llvm/utils/gn/secondary/clang/unittests/StaticAnalyzer/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/unittests/StaticAnalyzer/BUILD.gn
@@ -19,6 +19,7 @@ unittest("StaticAnalysisTests") {
     "CallEventTest.cpp",
     "ConflictingEvalCallsTest.cpp",
     "FalsePositiveRefutationBRVisitorTest.cpp",
+    "IsCLibraryFunctionTest.cpp",
     "NoStateChangeFuncVisitorTest.cpp",
     "ParamRegionTest.cpp",
     "RangeSetTest.cpp",

@Xazax-hun
Copy link
Collaborator

Once the other reviewers are happy, this looks good to me!

NagyDonat added a commit to Ericsson/llvm-project that referenced this pull request Mar 11, 2024
This reverts commit df6a67f, which was
accidentally pushed here instead of the branch corresponding to
llvm#84469
@NagyDonat
Copy link
Contributor Author

@steakhal Thanks for contributing the unit tests, I applied your commit.

Copy link
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good now.

Comment on lines 50 to 55
TEST(IsCLibraryFunctionTest, AcceptsStaticGlobal) {
const FunctionDecl *Result;
ASSERT_TRUE(extractFunctionDecl(R"cpp(static void fun();)cpp", Result));
EXPECT_FALSE(CheckerContext::isCLibraryFunction(Result));
// FIXME: Shouldn't this be TRUE?
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you all think about this case?
I thought static means the same for C in this context, and as such it should be accepted by isCLibraryFunction - at least that's what I'd expect.
This behavior was likely broken before, thus I don't expect direct actions on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method isCLibraryFunction() applies a heuristic that C library functions must be externally visible (unless they're inlined):

  // If this function is not externally visible, it is not a C library function.
  // Note that we make an exception for inline functions, which may be
  // declared in header files without external linkage.
  if (!FD->isInlined() && !FD->isExternallyVisible())
return false;

I think this is a reasonable heuristic that should not cause false negatives (I don't think that we can have a library function that isn't inlined and isn't externally visible) and might prevent a few false positives.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, stdlib functions should be "extern".

@NagyDonat NagyDonat merged commit 80ab823 into llvm:main Mar 12, 2024
@NagyDonat NagyDonat deleted the allow_clibrary_from_namespace_std branch March 12, 2024 12:51
NagyDonat added a commit that referenced this pull request Mar 12, 2024
NagyDonat added a commit that referenced this pull request Mar 12, 2024
…e" (#84926)

Reverts #84469 because it causes buildbot failures.
I'll examine them and re-submit the change.
@NagyDonat
Copy link
Contributor Author

@steakhal It seems that the unit tests that you suggested fail on many different buildbots:

Most of these reports are either crashes or memory sanitizer reports with messages like

[----------] 1 test from IsCLibraryFunctionTest
[ RUN ] IsCLibraryFunctionTest.AcceptsGlobal
==3810031==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x5599a8127045 in getAttrclang::ArmBuiltinAliasAttr /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/clang/include/clang/AST/DeclBase.h:579:12
#1 0x5599a8127045 in clang::FunctionDecl::getBuiltinID(bool) const /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/clang/lib/AST/Decl.cpp:3597:26
#2 0x5599a97495d6 in clang::ento::CheckerContext::isCLibraryFunction(clang::FunctionDecl const*, llvm::StringRef) /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/clang/lib/StaticAnalyzer/Core/CheckerContext.cpp:54:22
#3 0x5599a776b8a1 in IsCLibraryFunctionTest_AcceptsGlobal_Test::TestBody() /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp:40:3
#4 0x5599a7ae1a3a in HandleExceptionsInMethodIfSupported<testing::Test, void> /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/third-party/unittest/googletest/src/gtest.cc
#5 0x5599a7ae1a3a in testing::Test::Run() /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/third-party/unittest/googletest/src/gtest.cc:2687:5
[rest of stacktrace omitted]
SUMMARY: MemorySanitizer: use-of-uninitialized-value /b/sanitizer-x86_64-linux-bootstrap-msan/build/llvm-project/clang/include/clang/AST/DeclBase.h:579:12 in getAttrclang::ArmBuiltinAliasAttr

but there are also some "managed to execute it, but the test failed" situations.

As similar issues did not appear when the method isCLibraryFunction was used in "real" invocations, I suspect that these issues might be caused by imperfections of the unit test environment (e.g. perhaps it doesn't initialize the builtin ID table, but the tests still happen to pass on linux).

@steakhal
Copy link
Contributor

Thanks!
Indeed, it looks like valgrind also complains:

[ RUN      ] IsCLibraryFunctionTest.AcceptsGlobal
==999215== Invalid read of size 1
==999215==    at 0x15420D0: hasAttrs (DeclBase.h:523)
==999215==    by 0x15420D0: getAttr<clang::ArmBuiltinAliasAttr> (DeclBase.h:579)
==999215==    by 0x15420D0: clang::FunctionDecl::getBuiltinID(bool) const (???:3597)
==999215==    by 0x1D20BE0: clang::ento::CheckerContext::isCLibraryFunction(clang::FunctionDecl const*, llvm::StringRef) (../../clang/lib/StaticAnalyzer/Core/CheckerContext.cpp:54)
==999215==    by 0x117DACC: IsCLibraryFunctionTest_AcceptsGlobal_Test::TestBody() (../../clang/unittests/StaticAnalyzer/IsCLibraryFunctionTest.cpp:40)
==999215==    by 0x12D9778: HandleExceptionsInMethodIfSupported<testing::Test, void> (gtest.cc:0)
==999215==    by 0x12D9778: testing::Test::Run() (gtest.cc:2687)
==999215==    by 0x12DAB6D: testing::TestInfo::Run() (gtest.cc:2836)
==999215==    by 0x12DBE84: testing::TestSuite::Run() (gtest.cc:3015)
==999215==    by 0x12EAA3D: testing::internal::UnitTestImpl::RunAllTests() (gtest.cc:5920)
==999215==    by 0x12E9E6E: HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool> (gtest.cc:0)
==999215==    by 0x12E9E6E: testing::UnitTest::Run() (gtest.cc:5484)
==999215==    by 0x12C71EC: RUN_ALL_TESTS (gtest.h:2317)
==999215==    by 0x12C71EC: main (???:55)

I'll look into this.

@steakhal
Copy link
Contributor

I proposed the PR #84963 for relanding this, including the fix for the use-after-free of the AST of the unittest.

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