Skip to content

[analyzer] Avoid crashes in the stream checker #100901

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

Closed
wants to merge 1 commit into from

Conversation

vabridgers
Copy link
Contributor

This change avoids crashes in the stream checker when bifurcating the analysis state produces a non-null return value for opening a stream and does not produces a null return value because of constraints found during an analysis path.

A snippet of the crash stack seen is shown here.

...
#6 __restore_rt sigaction.c:0:0
#7 clang::ento::ProgramState::getStateManager() const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:148:13
#8 llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>
clang::ento::ProgramState::set<(anonymous namespace)::StreamMap>(clang::ento::ProgramStateTrait<(
anonymous namespace)::StreamMap>::key_type,clang::ento::ProgramStateTrait<(anonymous namespace)::StreamMap>::value_type) const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:864:10
#9 (anonymous namespace)::StreamChecker::evalFopen((anonymous namespace)::FnDescription const*,
clang::ento::CallEvent const&, clang::ento::CheckerContext&) const
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp:951:13
...

This change avoids crashes in the stream checker when bifurcating the
analysis state produces a non-null return value for opening a stream and
does not produces a null return value because of constraints found
during an analysis path.

A snippet of the crash stack seen is shown here.

...
 llvm#6 __restore_rt sigaction.c:0:0
 llvm#7 clang::ento::ProgramState::getStateManager() const
     clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:148:13
 llvm#8 llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>
     clang::ento::ProgramState::set<(anonymous namespace)::StreamMap>(clang::ento::ProgramStateTrait<(
     anonymous namespace)::StreamMap>::key_type,clang::ento::ProgramStateTrait<(anonymous namespace)::StreamMap>::value_type) const
     clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:864:10
 llvm#9 (anonymous namespace)::StreamChecker::evalFopen((anonymous namespace)::FnDescription const*,
    clang::ento::CallEvent const&, clang::ento::CheckerContext&) const
     clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp:951:13
...
@vabridgers vabridgers requested a review from steakhal July 27, 2024 22:06
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Jul 27, 2024
@llvmbot
Copy link
Member

llvmbot commented Jul 27, 2024

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

Author: None (vabridgers)

Changes

This change avoids crashes in the stream checker when bifurcating the analysis state produces a non-null return value for opening a stream and does not produces a null return value because of constraints found during an analysis path.

A snippet of the crash stack seen is shown here.

...
#6 __restore_rt sigaction.c:0:0
#7 clang::ento::ProgramState::getStateManager() const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:148:13
#8 llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>
clang::ento::ProgramState::set<(anonymous namespace)::StreamMap>(clang::ento::ProgramStateTrait<(
anonymous namespace)::StreamMap>::key_type,clang::ento::ProgramStateTrait<(anonymous namespace)::StreamMap>::value_type) const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:864:10
#9 (anonymous namespace)::StreamChecker::evalFopen((anonymous namespace)::FnDescription const*,
clang::ento::CallEvent const&, clang::ento::CheckerContext&) const
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp:951:13
...


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

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp (+12-8)
  • (added) clang/test/Analysis/stream-no-crash.c (+25)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 53770532609d5..c34c6b9fc6c0d 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -946,14 +946,18 @@ void StreamChecker::evalFopen(const FnDescription *Desc, const CallEvent &Call,
   std::tie(StateNotNull, StateNull) =
       C.getConstraintManager().assumeDual(State, RetVal);
 
-  StateNotNull =
-      StateNotNull->set<StreamMap>(RetSym, StreamState::getOpened(Desc));
-  StateNull =
-      StateNull->set<StreamMap>(RetSym, StreamState::getOpenFailed(Desc));
-
-  C.addTransition(StateNotNull,
-                  constructLeakNoteTag(C, RetSym, "Stream opened here"));
-  C.addTransition(StateNull);
+  if (StateNotNull)
+    StateNotNull =
+        StateNotNull->set<StreamMap>(RetSym, StreamState::getOpened(Desc));
+  if (StateNull)
+    StateNull =
+        StateNull->set<StreamMap>(RetSym, StreamState::getOpenFailed(Desc));
+
+  if (StateNotNull)
+    C.addTransition(StateNotNull,
+                    constructLeakNoteTag(C, RetSym, "Stream opened here"));
+  if (StateNull)
+    C.addTransition(StateNull);
 }
 
 void StreamChecker::preFreopen(const FnDescription *Desc, const CallEvent &Call,
diff --git a/clang/test/Analysis/stream-no-crash.c b/clang/test/Analysis/stream-no-crash.c
new file mode 100644
index 0000000000000..7a4922c8a35c2
--- /dev/null
+++ b/clang/test/Analysis/stream-no-crash.c
@@ -0,0 +1,25 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Stream -verify %s
+
+// This test is isolate since it uses line markers to repro the problem.
+// The test is expected to find the issues noted below without crashing.
+
+# 1 "" 1
+# 1 "" 1
+# 1 "" 1
+# 1 "" 1 3
+typedef FILE;
+extern *stdout;
+char a;
+*fopen();
+# 0 "" 2
+# 7 "" 2
+# 7 "" 2
+# 7 "" 2
+void b() {
+  fopen(&a, "");
+  int c = stdout && c;
+  b();
+}
+// expected-warning@-3{{Assigned value is garbage or undefined}}
+// expected-warning@-4{{Opened stream never closed. Potential resource leak}}
+

@llvmbot
Copy link
Member

llvmbot commented Jul 27, 2024

@llvm/pr-subscribers-clang

Author: None (vabridgers)

Changes

This change avoids crashes in the stream checker when bifurcating the analysis state produces a non-null return value for opening a stream and does not produces a null return value because of constraints found during an analysis path.

A snippet of the crash stack seen is shown here.

...
#6 __restore_rt sigaction.c:0:0
#7 clang::ento::ProgramState::getStateManager() const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:148:13
#8 llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>
clang::ento::ProgramState::set<(anonymous namespace)::StreamMap>(clang::ento::ProgramStateTrait<(
anonymous namespace)::StreamMap>::key_type,clang::ento::ProgramStateTrait<(anonymous namespace)::StreamMap>::value_type) const
clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h:864:10
#9 (anonymous namespace)::StreamChecker::evalFopen((anonymous namespace)::FnDescription const*,
clang::ento::CallEvent const&, clang::ento::CheckerContext&) const
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp:951:13
...


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

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp (+12-8)
  • (added) clang/test/Analysis/stream-no-crash.c (+25)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 53770532609d5..c34c6b9fc6c0d 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -946,14 +946,18 @@ void StreamChecker::evalFopen(const FnDescription *Desc, const CallEvent &Call,
   std::tie(StateNotNull, StateNull) =
       C.getConstraintManager().assumeDual(State, RetVal);
 
-  StateNotNull =
-      StateNotNull->set<StreamMap>(RetSym, StreamState::getOpened(Desc));
-  StateNull =
-      StateNull->set<StreamMap>(RetSym, StreamState::getOpenFailed(Desc));
-
-  C.addTransition(StateNotNull,
-                  constructLeakNoteTag(C, RetSym, "Stream opened here"));
-  C.addTransition(StateNull);
+  if (StateNotNull)
+    StateNotNull =
+        StateNotNull->set<StreamMap>(RetSym, StreamState::getOpened(Desc));
+  if (StateNull)
+    StateNull =
+        StateNull->set<StreamMap>(RetSym, StreamState::getOpenFailed(Desc));
+
+  if (StateNotNull)
+    C.addTransition(StateNotNull,
+                    constructLeakNoteTag(C, RetSym, "Stream opened here"));
+  if (StateNull)
+    C.addTransition(StateNull);
 }
 
 void StreamChecker::preFreopen(const FnDescription *Desc, const CallEvent &Call,
diff --git a/clang/test/Analysis/stream-no-crash.c b/clang/test/Analysis/stream-no-crash.c
new file mode 100644
index 0000000000000..7a4922c8a35c2
--- /dev/null
+++ b/clang/test/Analysis/stream-no-crash.c
@@ -0,0 +1,25 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Stream -verify %s
+
+// This test is isolate since it uses line markers to repro the problem.
+// The test is expected to find the issues noted below without crashing.
+
+# 1 "" 1
+# 1 "" 1
+# 1 "" 1
+# 1 "" 1 3
+typedef FILE;
+extern *stdout;
+char a;
+*fopen();
+# 0 "" 2
+# 7 "" 2
+# 7 "" 2
+# 7 "" 2
+void b() {
+  fopen(&a, "");
+  int c = stdout && c;
+  b();
+}
+// expected-warning@-3{{Assigned value is garbage or undefined}}
+// expected-warning@-4{{Opened stream never closed. Potential resource leak}}
+

@vabridgers
Copy link
Contributor Author

A little background on how this was found, and a few debugging notes.

This was found in a daily static analysis systems level test that we drive internally on our daily integrations, on the lz4 project, found here https://github.com/lz4/lz4.git. There are a number of open source projects that we drive these daily code analysis tests on, and in this case we seemed to have caught a regression because previous tests had been passing.

When I looked at state after a crash through gdb, the crash occurred because StateNull in method evalFopen was NULL, and StateNotNull was not NULL - so that seemed to indicate the assumeDual(State, RetVal) operation in method evalFopen had not returned expected non null states. That led me to look at RetSym and it's constraints in Program State. See below.

I suspect the change to add assumeNoAliasingWithStdStreams somehow is causing this since when I revert b60fec2 ( [analyzer] Assume the result of 'fopen' can't alias with 'std{in,out,err}' (#100085) ) I do not see this problem.

(gdb) p StateNull
$1 = {Obj = 0x0}
(gdb) p StateNotNull
$2 = {Obj = 0x5555695e79b0}

(gdb) p RetSym->dump()
conj_$3{FILE *, LC2, S801, #1}$4 = void

(gdb) p State->dump()
"program_state": {
"store": { "pointer": "0x5555695d40e0", "items": [
{ "cluster": "SymRegion{conj_$0{int &, LC1, no stmt, #0}}", "pointer": "0x5555695d3f60", "items": [
{ "kind": "Direct", "offset": 0, "value": "0 S32b" }
]}
]},
"environment": { "pointer": "0x5555695dc890", "items": [
{ "lctx_id": 2, "location_context": "#0 Call", "calling": "b", "location": { "line": 10, "column": 3, "file": "aa.c" }, "items": [
{ "stmt_id": 798, "kind": "ImplicitCastExpr", "pretty": "fopen", "value": "&code{fopen}" },
{ "stmt_id": 801, "kind": "CallExpr", "pretty": "fopen(&a, "")", "value": "&SymRegion{conj_$3{FILE *, LC2, S801, #1}}" },
{ "stmt_id": 807, "kind": "ImplicitCastExpr", "pretty": "&a", "value": "&a" },
{ "stmt_id": 813, "kind": "ImplicitCastExpr", "pretty": """", "value": "&Element{"",0 S64b,char}" }
]},
{ "lctx_id": 1, "location_context": "#1 Call", "calling": "b", "location": null, "items": [
{ "stmt_id": 864, "kind": "ImplicitCastExpr", "pretty": "b", "value": "&code{b}" }
]}
]},
"constraints": [
{ "symbol": "conj_$3{FILE *, LC2, S801, #1}", "range": "{ [1, 18446744073709551615] }" },
{ "symbol": "reg_$2<int * stdout>", "range": "{ [0, 0] }" }
],

@balazske
Copy link
Collaborator

In the state dump I see that stdout seems to be NULL (last line in "constraints"). This explains why the StateNull becomes NULL, because call to assumeNoAliasingWithStdStreams was called already. I think the better solution is to check NULL-ness of the std stream variable assumeNoAliasingWithStdStreams and do not assume if it is NULL. There is not a case when fopen returns non-null for sure, but at least not in the current situation, so the current fix is not as good. We could add an assert to check if both StateNull and StateNotNull are non-zero.

@steakhal
Copy link
Contributor

In the state dump I see that stdout seems to be NULL (last line in "constraints"). This explains why the StateNull becomes NULL, because call to assumeNoAliasingWithStdStreams was called already. I think the better solution is to check NULL-ness of the std stream variable assumeNoAliasingWithStdStreams and do not assume if it is NULL. There is not a case when fopen returns non-null for sure, but at least not in the current situation, so the current fix is not as good. We could add an assert to check if both StateNull and StateNotNull are non-zero.

Exactly. I didn't want to rush too much, but I can share that my current idea is to call assumeNoAliasingWithStdStreams only on the success path.

@balazske
Copy link
Collaborator

Exactly. I didn't want to rush too much, but I can share that my current idea is to call assumeNoAliasingWithStdStreams only on the success path.

This can be a better (and more simple) solution.

@vabridgers
Copy link
Contributor Author

Thanks for the comments. I do not mind abandoning this change in favor of a better solution. Please let me know when you have posted an improved PR and I'll abandon this one. Best

@steakhal
Copy link
Contributor

Thanks for the really valuable reduced case @vabridgers. I'll close this in favor of #100990 if you don't mind.
Thanks again for reporting the crash.

@steakhal steakhal closed this Jul 29, 2024
@vabridgers
Copy link
Contributor Author

Thanks @steakhal !

steakhal added a commit that referenced this pull request Jul 29, 2024
)

Actually, on the failure branch of `fopen`, the resulting pointer could
alias with `stdout` iff `stdout` is already known to be null.
We crashed in this case as the implementation assumed that the
state-split for creating the success and failure branches both should be
viable; thus dereferenced both of those states - leading to the crash.

To fix this, let's just only add this no-alias property for the success
path, and that's it :)

Fixes #100901
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.

4 participants