Skip to content

[analyzer] Remove inaccurate legacy handling of bad bitwise shifts #66647

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 1 commit into from
Sep 29, 2023

Conversation

NagyDonat
Copy link
Contributor

Previously, bitwise shifts with constant operands were validated by the checker core.UndefinedBinaryOperatorResult. However, this logic was unreliable, and commit 25b9696 added the dedicated checker core.BitwiseShift which validated the preconditions of all bitwise shifts with a more accurate logic (that uses the real types from the AST instead of the unreliable type information encoded in APSInt objects).

This commit disables the inaccurate logic that could mark bitwise shifts as 'undefined' and removes the redundant shift-related warning messages from core.UndefinedBinaryOperatorResult. The tests that were validating this logic are also deleted by this commit; but I verified that those testcases trigger the expected bug reports from core.BitwiseShift. (I didn't convert them to tests of core.BitwiseShift, because that checker already has its own extensive test suite with many analogous testcases.)

I hope that there will be a time when the constant folding will be reliable, but until then we need hacky solutions like this improve the quality of results.

Previously, bitwise shifts with constant operands were validated by
the checker `core.UndefinedBinaryOperatorResult`. However, this logic was
unreliable, and commit 25b9696 added
the dedicated checker `core.BitwiseShift` which validated the
preconditions of all bitwise shifts with a more accurate logic (that
uses the real types from the AST instead of the unreliable type
information encoded in `APSInt` objects).

This commit disables the inaccurate logic that could mark bitwise shifts
as 'undefined' and removes the redundant shift-related warning messages
from core.UndefinedBinaryOperatorResult. The tests that were validating
this logic are also deleted by this commit; but I verified that those
testcases trigger the expected bug reports from `core.BitwiseShift`.
(I didn't convert them to tests of `core.BitwiseShift`, because that
checker already has its own extensive test suite with many analogous
testcases.)

I hope that there will be a time when the constant folding will be
reliable, but until then we need hacky solutions like this improve the
quality of results.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer clang:dataflow Clang Dataflow Analysis framework - https://clang.llvm.org/docs/DataFlowAnalysisIntro.html labels Sep 18, 2023
@llvmbot
Copy link
Member

llvmbot commented Sep 18, 2023

@llvm/pr-subscribers-clang

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

Changes

Previously, bitwise shifts with constant operands were validated by the checker core.UndefinedBinaryOperatorResult. However, this logic was unreliable, and commit 25b9696 added the dedicated checker core.BitwiseShift which validated the preconditions of all bitwise shifts with a more accurate logic (that uses the real types from the AST instead of the unreliable type information encoded in APSInt objects).

This commit disables the inaccurate logic that could mark bitwise shifts as 'undefined' and removes the redundant shift-related warning messages from core.UndefinedBinaryOperatorResult. The tests that were validating this logic are also deleted by this commit; but I verified that those testcases trigger the expected bug reports from core.BitwiseShift. (I didn't convert them to tests of core.BitwiseShift, because that checker already has its own extensive test suite with many analogous testcases.)

I hope that there will be a time when the constant folding will be reliable, but until then we need hacky solutions like this improve the quality of results.

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

8 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/UndefResultChecker.cpp (+3-53)
  • (modified) clang/lib/StaticAnalyzer/Core/BasicValueFactory.cpp (-8)
  • (modified) clang/lib/StaticAnalyzer/Core/SimpleSValBuilder.cpp (+14-1)
  • (removed) clang/test/Analysis/bitwise-ops-nocrash.c (-28)
  • (removed) clang/test/Analysis/bitwise-ops.c (-60)
  • (modified) clang/test/Analysis/bitwise-shift-sanity-checks.c (+19-3)
  • (removed) clang/test/Analysis/left-shift-cxx2a.cpp (-22)
  • (modified) clang/test/Analysis/svalbuilder-simplify-no-crash.c (+3-1)
diff --git a/clang/lib/StaticAnalyzer/Checkers/UndefResultChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/UndefResultChecker.cpp
index a2b5e2987db5959..5f37b915deb8a02 100644
--- a/clang/lib/StaticAnalyzer/Checkers/UndefResultChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/UndefResultChecker.cpp
@@ -115,59 +115,9 @@ void UndefResultChecker::checkPostStmt(const BinaryOperator *B,
         OS << " due to array index out of bounds";
     } else {
       // Neither operand was undefined, but the result is undefined.
-      if ((B->getOpcode() == BinaryOperatorKind::BO_Shl ||
-           B->getOpcode() == BinaryOperatorKind::BO_Shr) &&
-          C.isNegative(B->getRHS())) {
-        OS << "The result of the "
-           << ((B->getOpcode() == BinaryOperatorKind::BO_Shl) ? "left"
-                                                              : "right")
-           << " shift is undefined because the right operand is negative";
-        Ex = B->getRHS();
-      } else if ((B->getOpcode() == BinaryOperatorKind::BO_Shl ||
-                  B->getOpcode() == BinaryOperatorKind::BO_Shr) &&
-                 isShiftOverflow(B, C)) {
-
-        OS << "The result of the "
-           << ((B->getOpcode() == BinaryOperatorKind::BO_Shl) ? "left"
-                                                              : "right")
-           << " shift is undefined due to shifting by ";
-        Ex = B->getRHS();
-
-        SValBuilder &SB = C.getSValBuilder();
-        const llvm::APSInt *I =
-            SB.getKnownValue(C.getState(), C.getSVal(B->getRHS()));
-        if (!I)
-          OS << "a value that is";
-        else if (I->isUnsigned())
-          OS << '\'' << I->getZExtValue() << "\', which is";
-        else
-          OS << '\'' << I->getSExtValue() << "\', which is";
-
-        OS << " greater or equal to the width of type '"
-           << B->getLHS()->getType() << "'.";
-      } else if (B->getOpcode() == BinaryOperatorKind::BO_Shl &&
-                 C.isNegative(B->getLHS())) {
-        OS << "The result of the left shift is undefined because the left "
-              "operand is negative";
-        Ex = B->getLHS();
-      } else if (B->getOpcode() == BinaryOperatorKind::BO_Shl &&
-                 isLeftShiftResultUnrepresentable(B, C)) {
-        ProgramStateRef State = C.getState();
-        SValBuilder &SB = C.getSValBuilder();
-        const llvm::APSInt *LHS =
-            SB.getKnownValue(State, C.getSVal(B->getLHS()));
-        const llvm::APSInt *RHS =
-            SB.getKnownValue(State, C.getSVal(B->getRHS()));
-        OS << "The result of the left shift is undefined due to shifting \'"
-           << LHS->getSExtValue() << "\' by \'" << RHS->getZExtValue()
-           << "\', which is unrepresentable in the unsigned version of "
-           << "the return type \'" << B->getLHS()->getType() << "\'";
-        Ex = B->getLHS();
-      } else {
-        OS << "The result of the '"
-           << BinaryOperator::getOpcodeStr(B->getOpcode())
-           << "' expression is undefined";
-      }
+      OS << "The result of the '"
+         << BinaryOperator::getOpcodeStr(B->getOpcode())
+         << "' expression is undefined";
     }
     auto report = std::make_unique<PathSensitiveBugReport>(*BT, OS.str(), N);
     if (Ex) {
diff --git a/clang/lib/StaticAnalyzer/Core/BasicValueFactory.cpp b/clang/lib/StaticAnalyzer/Core/BasicValueFactory.cpp
index 5924f6a671c2ac1..e8d74b40c6fd846 100644
--- a/clang/lib/StaticAnalyzer/Core/BasicValueFactory.cpp
+++ b/clang/lib/StaticAnalyzer/Core/BasicValueFactory.cpp
@@ -280,14 +280,6 @@ BasicValueFactory::evalAPSInt(BinaryOperator::Opcode Op,
       if (Amt >= V1.getBitWidth())
         return nullptr;
 
-      if (!Ctx.getLangOpts().CPlusPlus20) {
-        if (V1.isSigned() && V1.isNegative())
-          return nullptr;
-
-        if (V1.isSigned() && Amt > V1.countl_zero())
-          return nullptr;
-      }
-
       return &getValue( V1.operator<<( (unsigned) Amt ));
     }
 
diff --git a/clang/lib/StaticAnalyzer/Core/SimpleSValBuilder.cpp b/clang/lib/StaticAnalyzer/Core/SimpleSValBuilder.cpp
index 58d360a2e2dbdf1..aa2c8bbd15d4206 100644
--- a/clang/lib/StaticAnalyzer/Core/SimpleSValBuilder.cpp
+++ b/clang/lib/StaticAnalyzer/Core/SimpleSValBuilder.cpp
@@ -529,8 +529,21 @@ SVal SimpleSValBuilder::evalBinOpNN(ProgramStateRef state,
 
         const llvm::APSInt *Result =
           BasicVals.evalAPSInt(op, LHSValue, RHSValue);
-        if (!Result)
+        if (!Result) {
+          if (op == BO_Shl || op == BO_Shr) {
+            // FIXME: At this point the constant folding claims that the result
+            // of a bitwise shift is undefined. However, constant folding
+            // relies on the inaccurate type information that is stored in the
+            // bit size of APSInt objects, and if we reached this point, then
+            // the checker core.BitwiseShift already determined that the shift
+            // is valid (in a PreStmt callback, by querying the real type from
+            // the AST node).
+            // To avoid embarrassing false positives, let's just say that we
+            // don't know anything about the result of the shift.
+            return UnknownVal();
+          }
           return UndefinedVal();
+        }
 
         return nonloc::ConcreteInt(*Result);
       }
diff --git a/clang/test/Analysis/bitwise-ops-nocrash.c b/clang/test/Analysis/bitwise-ops-nocrash.c
deleted file mode 100644
index 4c4900bc96a1855..000000000000000
--- a/clang/test/Analysis/bitwise-ops-nocrash.c
+++ /dev/null
@@ -1,28 +0,0 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core -analyzer-disable-checker=core.BitwiseShift -analyzer-output=text -triple x86_64-linux-gnu -Wno-shift-count-overflow -verify %s
-
-// NOTE: This test file disables the checker core.BitwiseShift (which would
-// report all undefined behavior connected to bitwise shifts) to verify the
-// behavior of core.UndefinedBinaryOperatorResult (which resports cases when
-// the constant folding in BasicValueFactory produces an "undefined" result
-// from a shift or any other binary operator).
-
-#define offsetof(type,memb) ((unsigned long)&((type*)0)->memb)
-
-typedef struct {
-  unsigned long guest_counter;
-  unsigned int guest_fpc;
-} S;
-
-// no crash
-int left_shift_overflow_no_crash(unsigned int i) {
-  unsigned shift = 323U; // expected-note{{'shift' initialized to 323}}
-  switch (i) { // expected-note{{Control jumps to 'case 8:'  at line 20}}
-  case offsetof(S, guest_fpc):
-    return 3 << shift; // expected-warning{{The result of the left shift is undefined due to shifting by '323', which is greater or equal to the width of type 'int'}}
-    // expected-note@-1{{The result of the left shift is undefined due to shifting by '323', which is greater or equal to the width of type 'int'}}
-  default:
-    break;
-  }
-
-  return 0;
-}
diff --git a/clang/test/Analysis/bitwise-ops.c b/clang/test/Analysis/bitwise-ops.c
deleted file mode 100644
index 7ff7490ba80cf63..000000000000000
--- a/clang/test/Analysis/bitwise-ops.c
+++ /dev/null
@@ -1,60 +0,0 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -analyzer-disable-checker=core.BitwiseShift -triple x86_64-apple-darwin13 -Wno-shift-count-overflow -verify %s
-
-// NOTE: This test file disables the checker core.BitwiseShift (which would
-// report all undefined behavior connected to bitwise shifts) to verify the
-// behavior of core.UndefinedBinaryOperatorResult (which resports cases when
-// the constant folding in BasicValueFactory produces an "undefined" result
-// from a shift or any other binary operator).
-
-void clang_analyzer_eval(int);
-#define CHECK(expr) if (!(expr)) return; clang_analyzer_eval(expr)
-
-void testPersistentConstraints(int x, int y) {
-  // Basic check
-  CHECK(x); // expected-warning{{TRUE}}
-  CHECK(x & 1); // expected-warning{{TRUE}}
-  
-  CHECK(1 - x); // expected-warning{{TRUE}}
-  CHECK(x & y); // expected-warning{{TRUE}}
-}
-
-int testConstantShifts_PR18073(int which) {
-  switch (which) {
-  case 1:
-    return 0ULL << 63; // no-warning
-  case 2:
-    return 0ULL << 64; // expected-warning{{The result of the left shift is undefined due to shifting by '64', which is greater or equal to the width of type 'unsigned long long'}}
-  case 3:
-    return 0ULL << 65; // expected-warning{{The result of the left shift is undefined due to shifting by '65', which is greater or equal to the width of type 'unsigned long long'}}
-
-  default:
-    return 0;
-  }
-}
-
-int testOverflowShift(int a) {
-  if (a == 323) {
-    return 1 << a; // expected-warning{{The result of the left shift is undefined due to shifting by '323', which is greater or equal to the width of type 'int'}}
-  }
-  return 0;
-}
-
-int testNegativeShift(int a) {
-  if (a == -5) {
-    return 1 << a; // expected-warning{{The result of the left shift is undefined because the right operand is negative}}
-  }
-  return 0;
-}
-
-int testNegativeLeftShift(int a) {
-  if (a == -3) {
-    return a << 1; // expected-warning{{The result of the left shift is undefined because the left operand is negative}}
-  }
-  return 0;
-}
-
-int testUnrepresentableLeftShift(int a) {
-  if (a == 8)
-    return a << 30; // expected-warning{{The result of the left shift is undefined due to shifting '8' by '30', which is unrepresentable in the unsigned version of the return type 'int'}}
-  return 0;
-}
diff --git a/clang/test/Analysis/bitwise-shift-sanity-checks.c b/clang/test/Analysis/bitwise-shift-sanity-checks.c
index 1e20d5df3567e44..a31424f18818c45 100644
--- a/clang/test/Analysis/bitwise-shift-sanity-checks.c
+++ b/clang/test/Analysis/bitwise-shift-sanity-checks.c
@@ -1,4 +1,4 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core.BitwiseShift \
+// RUN: %clang_analyze_cc1 -analyzer-checker=core \
 // RUN:    -analyzer-config core.BitwiseShift:Pedantic=true \
 // RUN:    -verify=expected,pedantic \
 // RUN:    -triple x86_64-pc-linux-gnu -x c %s \
@@ -6,7 +6,7 @@
 // RUN:    -Wno-shift-count-overflow -Wno-shift-overflow \
 // RUN:    -Wno-shift-sign-overflow
 //
-// RUN: %clang_analyze_cc1 -analyzer-checker=core.BitwiseShift \
+// RUN: %clang_analyze_cc1 -analyzer-checker=core \
 // RUN:    -analyzer-config core.BitwiseShift:Pedantic=true \
 // RUN:    -verify=expected,pedantic \
 // RUN:    -triple x86_64-pc-linux-gnu -x c++ -std=c++14 %s \
@@ -14,7 +14,7 @@
 // RUN:    -Wno-shift-count-overflow -Wno-shift-overflow \
 // RUN:    -Wno-shift-sign-overflow
 //
-// RUN: %clang_analyze_cc1 -analyzer-checker=core.BitwiseShift \
+// RUN: %clang_analyze_cc1 -analyzer-checker=core \
 // RUN:    -verify=expected \
 // RUN:    -triple x86_64-pc-linux-gnu -x c++ -std=c++20 %s \
 // RUN:    -Wno-shift-count-negative -Wno-shift-negative-value \
@@ -23,6 +23,8 @@
 
 // This test file verifies that the BitwiseShift checker does not crash or
 // report false positives (at least on the cases that are listed here...)
+// Other core checkers are also enabled to see interactions with e.g.
+// core.UndefinedBinaryOperatorResult.
 // For the sake of brevity, 'note' output is not checked in this file.
 
 // TEST OBVIOUSLY CORRECT CODE
@@ -116,3 +118,17 @@ void doubles_cast_to_integer(int *c) {
   *c = ((int)1.5) << 1;        // no-crash
   *c = ((int)1.5) << (int)1.5; // no-crash
 }
+
+// TEST CODE THAT WAS TRIGGERING BUGS IN EARLIER REVISIONS
+//===----------------------------------------------------------------------===//
+
+unsigned int strange_cast(unsigned short sh) {
+  // This testcase triggers a bug in the constant folding (it "forgets" the
+  // cast), which is silenced in SimpleSValBuilder::evalBinOpNN() with an ugly
+  // workaround, because otherwise it would lead to a false positive from
+  // core.UndefinedBinaryOperatorResult.
+  unsigned int i;
+  sh++;
+  for (i=0; i<sh; i++) {}
+  return (unsigned int) ( ((unsigned int) sh) << 16 ); // no-warning
+}
diff --git a/clang/test/Analysis/left-shift-cxx2a.cpp b/clang/test/Analysis/left-shift-cxx2a.cpp
deleted file mode 100644
index 006642e9f06a2cb..000000000000000
--- a/clang/test/Analysis/left-shift-cxx2a.cpp
+++ /dev/null
@@ -1,22 +0,0 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -analyzer-disable-checker core.BitwiseShift -triple x86_64-apple-darwin13 -Wno-shift-count-overflow -verify=expected,cxx17 -std=c++17 %s
-// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -analyzer-disable-checker core.BitwiseShift -triple x86_64-apple-darwin13 -Wno-shift-count-overflow -verify=expected,cxx2a -std=c++2a %s
-
-int testNegativeShift(int a) {
-  if (a == -5) {
-    return 1 << a; // expected-warning{{The result of the left shift is undefined because the right operand is negative}}
-  }
-  return 0;
-}
-
-int testNegativeLeftShift(int a) {
-  if (a == -3) {
-    return a << 1; // cxx17-warning{{The result of the left shift is undefined because the left operand is negative}}
-  }
-  return 0;
-}
-
-int testUnrepresentableLeftShift(int a) {
-  if (a == 8)
-    return a << 30; // cxx17-warning{{The result of the left shift is undefined due to shifting '8' by '30', which is unrepresentable in the unsigned version of the return type 'int'}}
-  return 0;
-}
diff --git a/clang/test/Analysis/svalbuilder-simplify-no-crash.c b/clang/test/Analysis/svalbuilder-simplify-no-crash.c
index b43ccbdbfd1002d..b3166413ecace87 100644
--- a/clang/test/Analysis/svalbuilder-simplify-no-crash.c
+++ b/clang/test/Analysis/svalbuilder-simplify-no-crash.c
@@ -6,8 +6,10 @@
 // Here, we test that svalbuilder simplification does not produce any
 // assertion failure.
 
+// expected-no-diagnostics
+
 void crashing(long a, _Bool b) {
   (void)(a & 1 && 0);
   b = a & 1;
-  (void)(b << 1); // expected-warning{{core.UndefinedBinaryOperatorResult}}
+  (void)(b << 1);
 }

@steakhal
Copy link
Contributor

What report diff's should we expect?

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Sep 19, 2023

The impact is limited, because core.BitwiseShift is a PreStmt callback, so when I merged it, it turned the logic tweaked/affected by this commit into "almost dead" code.

The most significant change is that some "pedantic-only" issues (i.e. code like int i=-1; i >> 3; that's undefined behavior according to the standard, but works in practice) are no longer reported after this commit. (Note that I ran the evaluation with the default mode core.BitwiseShift:Pedantic=false, it's clear that pedantic mode would've reported these situations.) The change also eliminates a few FPs, and sometimes new alpha.core.Conversion results appeared on branches that were previously stopped by a core.UndefinedBinaryOperatorResult report.

Full results:

Project New reports Lost reports Difference
memcached New reports Lost reports no effect
tmux New reports Lost reports no effect
curl New reports Lost reports no effect
twin New reports Lost reports no effect
vim New reports Lost reports one cryptic FP eliminated
openssl New reports Lost reports one pedantic-only issue lost
sqlite New reports Lost reports no effect
ffmpeg New reports Lost reports 2 new, 7 lost [1]
postgres New reports Lost reports 1 new, 2 lost [2]
tinyxml2 New reports Lost reports no effect
libwebm New reports Lost reports no effect
xerces New reports Lost reports no effect
bitcoin New reports Lost reports no effect
protobuf New reports Lost reports no effect

[1] one alpha.core.Conversion result lost for unclear reason, 4 pedantic-only results and 2 FPs eliminated, one of the pedantic-only results was replaced by two new alpha.core.Conversion results
[2] two pedantic-only issues not reported, one replaced by a new alpha.core.Conversion result

@steakhal
Copy link
Contributor

I think I'm fine with the numbers. Note that I could not inspect individual reports because the URL likely refers to some internal server on your side.
Anyway, I'd recommend keeping the test files for two reasons:

  • It's probably not a big deal, we are talking about ~150 lines if I'm not mistaken.
  • It gives an example of how the reports will change: how they looked before and how they will from now on. This is useful for folks rebasing internal forks and wondering what happened with these reports.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Sep 21, 2023

Sorry for the inaccessible reports, I forgot to specify that the results should go to the server that we use for open-source reviews.

I'd like to skip the conversion of the testcases that I deleted, because there won't be folks "wondering what happened with these reports": In the majority of situations, the report type already "switched over" to core.BitwiseShift when that checker was released.

This PR only affects the "undefined behavior, but works in practice" cases where core.BitwiseShift does nothing (in the default non-pedantic mode), so the analyzer was able to reach the "old" logic that produces the core.UndefinedBinaryOperatorResult reports (which are eliminated by this PR). Any reports that are eliminated by this commit are either FPs or "undefined behavior, but works in practice" issues reports that are "essentially" FPs for those who don't aim for standard compliance in this area.

Also, even if I wasted some time on converting those testcases, I'd have to delete them in a later commit, because they're completely redundant with the testsuite of core.BitwiseShift.

@vabridgers
Copy link
Contributor

LGTM if ok with everyone.

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.

Should be good. Feel free to merge this.
Sorry for it taking so long.

@NagyDonat NagyDonat merged commit 23b88e8 into llvm:main Sep 29, 2023
@NagyDonat NagyDonat deleted the remove_ubor_shifts branch September 29, 2023 18:02
@NagyDonat
Copy link
Contributor Author

This commit accidentally left behind two unused functions; thanks @kazutakahirata for quickly fixing my mistake!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:dataflow Clang Dataflow Analysis framework - https://clang.llvm.org/docs/DataFlowAnalysisIntro.html 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.

4 participants