Skip to content

Commit 13d39cb

Browse files
authored
[analyzer] Fix crash of StreamChecker when eval calling 'fopen' (#100990)
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
1 parent f7491f5 commit 13d39cb

File tree

3 files changed

+49
-33
lines changed

3 files changed

+49
-33
lines changed

clang/docs/analyzer/checkers.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,13 @@ are detected:
17031703
* Invalid 3rd ("``whence``") argument to ``fseek``.
17041704
17051705
The stream operations are by this checker usually split into two cases, a success
1706-
and a failure case. However, in the case of write operations (like ``fwrite``,
1706+
and a failure case.
1707+
On the success case it also assumes that the current value of ``stdout``,
1708+
``stderr``, or ``stdin`` can't be equal to the file pointer returned by ``fopen``.
1709+
Operations performed on ``stdout``, ``stderr``, or ``stdin`` are not checked by
1710+
this checker in contrast to the streams opened by ``fopen``.
1711+
1712+
In the case of write operations (like ``fwrite``,
17071713
``fprintf`` and even ``fsetpos``) this behavior could produce a large amount of
17081714
unwanted reports on projects that don't have error checks around the write
17091715
operations, so by default the checker assumes that write operations always succeed.
@@ -1769,9 +1775,7 @@ are assumed to succeed.)
17691775
**Limitations**
17701776
17711777
The checker does not track the correspondence between integer file descriptors
1772-
and ``FILE *`` pointers. Operations on standard streams like ``stdin`` are not
1773-
treated specially and are therefore often not recognized (because these streams
1774-
are usually not opened explicitly by the program, and are global variables).
1778+
and ``FILE *`` pointers.
17751779
17761780
.. _osx-checkers:
17771781

clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -615,30 +615,22 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
615615
});
616616
}
617617

618-
void initMacroValues(CheckerContext &C) const {
618+
void initMacroValues(const Preprocessor &PP) const {
619619
if (EofVal)
620620
return;
621621

622-
if (const std::optional<int> OptInt =
623-
tryExpandAsInteger("EOF", C.getPreprocessor()))
622+
if (const std::optional<int> OptInt = tryExpandAsInteger("EOF", PP))
624623
EofVal = *OptInt;
625624
else
626625
EofVal = -1;
627-
if (const std::optional<int> OptInt =
628-
tryExpandAsInteger("SEEK_SET", C.getPreprocessor()))
626+
if (const std::optional<int> OptInt = tryExpandAsInteger("SEEK_SET", PP))
629627
SeekSetVal = *OptInt;
630-
if (const std::optional<int> OptInt =
631-
tryExpandAsInteger("SEEK_END", C.getPreprocessor()))
628+
if (const std::optional<int> OptInt = tryExpandAsInteger("SEEK_END", PP))
632629
SeekEndVal = *OptInt;
633-
if (const std::optional<int> OptInt =
634-
tryExpandAsInteger("SEEK_CUR", C.getPreprocessor()))
630+
if (const std::optional<int> OptInt = tryExpandAsInteger("SEEK_CUR", PP))
635631
SeekCurVal = *OptInt;
636632
}
637633

638-
void initVaListType(CheckerContext &C) const {
639-
VaListType = C.getASTContext().getBuiltinVaListType().getCanonicalType();
640-
}
641-
642634
/// Searches for the ExplodedNode where the file descriptor was acquired for
643635
/// StreamSym.
644636
static const ExplodedNode *getAcquisitionSite(const ExplodedNode *N,
@@ -880,9 +872,6 @@ static ProgramStateRef escapeArgs(ProgramStateRef State, CheckerContext &C,
880872

881873
void StreamChecker::checkPreCall(const CallEvent &Call,
882874
CheckerContext &C) const {
883-
initMacroValues(C);
884-
initVaListType(C);
885-
886875
const FnDescription *Desc = lookupFn(Call);
887876
if (!Desc || !Desc->PreFn)
888877
return;
@@ -938,7 +927,6 @@ void StreamChecker::evalFopen(const FnDescription *Desc, const CallEvent &Call,
938927
assert(RetSym && "RetVal must be a symbol here.");
939928

940929
State = State->BindExpr(CE, C.getLocationContext(), RetVal);
941-
State = assumeNoAliasingWithStdStreams(State, RetVal, C);
942930

943931
// Bifurcate the state into two: one with a valid FILE* pointer, the other
944932
// with a NULL.
@@ -951,6 +939,8 @@ void StreamChecker::evalFopen(const FnDescription *Desc, const CallEvent &Call,
951939
StateNull =
952940
StateNull->set<StreamMap>(RetSym, StreamState::getOpenFailed(Desc));
953941

942+
StateNotNull = assumeNoAliasingWithStdStreams(StateNotNull, RetVal, C);
943+
954944
C.addTransition(StateNotNull,
955945
constructLeakNoteTag(C, RetSym, "Stream opened here"));
956946
C.addTransition(StateNull);
@@ -2081,10 +2071,12 @@ getGlobalStreamPointerByName(const TranslationUnitDecl *TU, StringRef VarName) {
20812071
}
20822072

20832073
void StreamChecker::checkASTDecl(const TranslationUnitDecl *TU,
2084-
AnalysisManager &, BugReporter &) const {
2074+
AnalysisManager &Mgr, BugReporter &) const {
20852075
StdinDecl = getGlobalStreamPointerByName(TU, "stdin");
20862076
StdoutDecl = getGlobalStreamPointerByName(TU, "stdout");
20872077
StderrDecl = getGlobalStreamPointerByName(TU, "stderr");
2078+
VaListType = TU->getASTContext().getBuiltinVaListType().getCanonicalType();
2079+
initMacroValues(Mgr.getPreprocessor());
20882080
}
20892081

20902082
//===----------------------------------------------------------------------===//

clang/test/Analysis/stream.c

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// RUN: %clang_analyze_cc1 -triple=x86_64-pc-linux-gnu -analyzer-checker=core,unix.Stream,debug.ExprInspection \
2-
// RUN: -analyzer-config unix.Stream:Pedantic=true -verify %s
2+
// RUN: -analyzer-config eagerly-assume=false,unix.Stream:Pedantic=true -verify %s
33
// RUN: %clang_analyze_cc1 -triple=armv8-none-linux-eabi -analyzer-checker=core,unix.Stream,debug.ExprInspection \
4-
// RUN: -analyzer-config unix.Stream:Pedantic=true -verify %s
4+
// RUN: -analyzer-config eagerly-assume=false,unix.Stream:Pedantic=true -verify %s
55
// RUN: %clang_analyze_cc1 -triple=aarch64-linux-gnu -analyzer-checker=core,unix.Stream,debug.ExprInspection \
6-
// RUN: -analyzer-config unix.Stream:Pedantic=true -verify %s
6+
// RUN: -analyzer-config eagerly-assume=false,unix.Stream:Pedantic=true -verify %s
77
// RUN: %clang_analyze_cc1 -triple=hexagon -analyzer-checker=core,unix.Stream,debug.ExprInspection \
8-
// RUN: -analyzer-config unix.Stream:Pedantic=true -verify %s
8+
// RUN: -analyzer-config eagerly-assume=false,unix.Stream:Pedantic=true -verify %s
99

1010
#include "Inputs/system-header-simulator.h"
1111
#include "Inputs/system-header-simulator-for-malloc.h"
@@ -499,14 +499,34 @@ void gh_93408_regression_ZeroSized(struct ZeroSized *buffer) {
499499
fclose(f);
500500
}
501501

502-
extern FILE *stdout_like_ptr;
503-
void no_aliasing(void) {
502+
extern FILE *non_standard_stream_ptr;
503+
void test_fopen_does_not_alias_with_standard_streams(void) {
504504
FILE *f = fopen("file", "r");
505-
clang_analyzer_eval(f == stdin); // expected-warning {{FALSE}} no-TRUE
506-
clang_analyzer_eval(f == stdout); // expected-warning {{FALSE}} no-TRUE
507-
clang_analyzer_eval(f == stderr); // expected-warning {{FALSE}} no-TRUE
508-
clang_analyzer_eval(f == stdout_like_ptr); // expected-warning {{FALSE}} expected-warning {{TRUE}}
509-
if (f && f != stdout) {
505+
if (!f) return;
506+
clang_analyzer_eval(f == stdin); // expected-warning {{FALSE}} no-TRUE
507+
clang_analyzer_eval(f == stdout); // expected-warning {{FALSE}} no-TRUE
508+
clang_analyzer_eval(f == stderr); // expected-warning {{FALSE}} no-TRUE
509+
clang_analyzer_eval(f == non_standard_stream_ptr); // expected-warning {{UNKNOWN}}
510+
if (f != stdout) {
510511
fclose(f);
511512
}
512513
} // no-leak: 'fclose()' is always called because 'f' cannot be 'stdout'.
514+
515+
void reopen_std_stream(void) {
516+
FILE *oldStdout = stdout;
517+
fclose(stdout);
518+
FILE *fp = fopen("blah", "w");
519+
if (!fp) return;
520+
521+
stdout = fp; // Let's make them alias.
522+
clang_analyzer_eval(fp == oldStdout); // expected-warning {{UNKNOWN}}
523+
clang_analyzer_eval(fp == stdout); // expected-warning {{TRUE}} no-FALSE
524+
clang_analyzer_eval(oldStdout == stdout); // expected-warning {{UNKNOWN}}
525+
}
526+
527+
void only_success_path_does_not_alias_with_stdout(void) {
528+
if (stdout) return;
529+
FILE *f = fopen("/tmp/foof", "r"); // no-crash
530+
if (!f) return;
531+
fclose(f);
532+
}

0 commit comments

Comments
 (0)