Skip to content

Commit

Permalink
[clang][analyzer] Support fgetc in StreamChecker (#72627)
Browse files Browse the repository at this point in the history
  • Loading branch information
benshi001 committed Nov 23, 2023
1 parent 0bc7cd4 commit 53578e5
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 35 deletions.
84 changes: 66 additions & 18 deletions clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,12 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
{{{"fwrite"}, 4},
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, false),
std::bind(&StreamChecker::evalFreadFwrite, _1, _2, _3, _4, false), 3}},
{{{"fgetc"}, 1},
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, true),
std::bind(&StreamChecker::evalFgetcFputc, _1, _2, _3, _4, true), 0}},
{{{"fputc"}, 2},
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, false),
&StreamChecker::evalFputc, 1}},
std::bind(&StreamChecker::evalFgetcFputc, _1, _2, _3, _4, false), 1}},
{{{"fseek"}, 3},
{&StreamChecker::preFseek, &StreamChecker::evalFseek, 0}},
{{{"ftell"}, 1},
Expand Down Expand Up @@ -314,8 +317,8 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
void evalFreadFwrite(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C, bool IsFread) const;

void evalFputc(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C) const;
void evalFgetcFputc(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C, bool IsRead) const;

void preFseek(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C) const;
Expand Down Expand Up @@ -751,8 +754,9 @@ void StreamChecker::evalFreadFwrite(const FnDescription *Desc,
C.addTransition(StateFailed);
}

void StreamChecker::evalFputc(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C) const {
void StreamChecker::evalFgetcFputc(const FnDescription *Desc,
const CallEvent &Call, CheckerContext &C,
bool IsRead) const {
ProgramStateRef State = C.getState();
SymbolRef StreamSym = getStreamArg(Desc, Call).getAsSymbol();
if (!StreamSym)
Expand All @@ -768,26 +772,70 @@ void StreamChecker::evalFputc(const FnDescription *Desc, const CallEvent &Call,

assertStreamStateOpened(OldSS);

// `fgetc` returns the read character on success, otherwise returns EOF.
// `fputc` returns the written character on success, otherwise returns EOF.

// Generate a transition for the success state.
std::optional<NonLoc> PutVal = Call.getArgSVal(0).getAs<NonLoc>();
if (!PutVal)
return;
ProgramStateRef StateNotFailed =
State->BindExpr(CE, C.getLocationContext(), *PutVal);
StateNotFailed =
StateNotFailed->set<StreamMap>(StreamSym, StreamState::getOpened(Desc));
C.addTransition(StateNotFailed);
// Generate a transition for the success state of fputc.
if (!IsRead) {
std::optional<NonLoc> PutVal = Call.getArgSVal(0).getAs<NonLoc>();
if (!PutVal)
return;
ProgramStateRef StateNotFailed =
State->BindExpr(CE, C.getLocationContext(), *PutVal);
StateNotFailed =
StateNotFailed->set<StreamMap>(StreamSym, StreamState::getOpened(Desc));
C.addTransition(StateNotFailed);
}
// Generate a transition for the success state of fgetc.
// If we know the state to be FEOF at fgetc, do not add a success state.
else if (OldSS->ErrorState != ErrorFEof) {
NonLoc RetVal = makeRetVal(C, CE).castAs<NonLoc>();
ProgramStateRef StateNotFailed =
State->BindExpr(CE, C.getLocationContext(), RetVal);
SValBuilder &SVB = C.getSValBuilder();
auto &ASTC = C.getASTContext();
// The returned 'unsigned char' of `fgetc` is converted to 'int',
// so we need to check if it is in range [0, 255].
auto CondLow =
SVB.evalBinOp(State, BO_GE, RetVal, SVB.makeZeroVal(ASTC.IntTy),
SVB.getConditionType())
.getAs<DefinedOrUnknownSVal>();
auto CondHigh =
SVB.evalBinOp(State, BO_LE, RetVal,
SVB.makeIntVal(SVB.getBasicValueFactory()
.getMaxValue(ASTC.UnsignedCharTy)
.getLimitedValue(),
ASTC.IntTy),
SVB.getConditionType())
.getAs<DefinedOrUnknownSVal>();
if (!CondLow || !CondHigh)
return;
StateNotFailed = StateNotFailed->assume(*CondLow, true);
if (!StateNotFailed)
return;
StateNotFailed = StateNotFailed->assume(*CondHigh, true);
if (!StateNotFailed)
return;
C.addTransition(StateNotFailed);
}

// Add transition for the failed state.
ProgramStateRef StateFailed = bindInt(*EofVal, State, C, CE);

// If a (non-EOF) error occurs, the resulting value of the file position
// indicator for the stream is indeterminate.
ProgramStateRef StateFailed = bindInt(*EofVal, State, C, CE);
StreamState NewSS = StreamState::getOpened(
Desc, ErrorFError, /*IsFilePositionIndeterminate*/ true);
StreamErrorState NewES;
if (IsRead)
NewES =
OldSS->ErrorState == ErrorFEof ? ErrorFEof : ErrorFEof | ErrorFError;
else
NewES = ErrorFError;
StreamState NewSS = StreamState::getOpened(Desc, NewES, !NewES.isFEof());
StateFailed = StateFailed->set<StreamMap>(StreamSym, NewSS);
C.addTransition(StateFailed);
if (IsRead && OldSS->ErrorState != ErrorFEof)
C.addTransition(StateFailed, constructSetEofNoteTag(C, StreamSym));
else
C.addTransition(StateFailed);
}

void StreamChecker::preFseek(const FnDescription *Desc, const CallEvent &Call,
Expand Down
1 change: 1 addition & 0 deletions clang/test/Analysis/Inputs/system-header-simulator.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ FILE *freopen(const char *restrict pathname, const char *restrict mode, FILE *re
int fclose(FILE *fp);
size_t fread(void *restrict, size_t, size_t, FILE *restrict);
size_t fwrite(const void *restrict, size_t, size_t, FILE *restrict);
int fgetc(FILE *stream);
int fputc(int ch, FILE *stream);
int fseek(FILE *__stream, long int __off, int __whence);
long int ftell(FILE *__stream);
Expand Down
39 changes: 22 additions & 17 deletions clang/test/Analysis/stream-error.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ void error_fwrite(void) {
Ret = fwrite(0, 1, 10, F); // expected-warning {{Stream might be already closed}}
}

void error_fgetc(void) {
FILE *F = tmpfile();
if (!F)
return;
int Ret = fgetc(F);
if (0 <= Ret && Ret <= 255) {
clang_analyzer_eval(feof(F) || ferror(F)); // expected-warning {{FALSE}}
} else {
clang_analyzer_eval(Ret == EOF); // expected-warning {{TRUE}}
clang_analyzer_eval(feof(F) || ferror(F)); // expected-warning {{TRUE}}
if (feof(F)) {
clang_analyzer_eval(ferror(F)); // expected-warning {{FALSE}}
fgetc(F); // expected-warning {{Read function called when stream is in EOF state}}
} else {
clang_analyzer_eval(ferror(F)); // expected-warning {{TRUE}}
fgetc(F); // expected-warning {{might be 'indeterminate'}}
}
}
fclose(F);
fgetc(F); // expected-warning {{Stream might be already closed}}
}

void error_fputc(void) {
FILE *F = tmpfile();
if (!F)
Expand Down Expand Up @@ -259,23 +281,6 @@ void error_indeterminate_clearerr(void) {
fclose(F);
}

void error_indeterminate_fputc(void) {
FILE *F = fopen("file", "r+");
if (!F)
return;
int rc = fseek(F, 0, SEEK_SET);
if (rc) {
if (feof(F)) {
fputc('X', F); // no warning
} else if (ferror(F)) {
fputc('C', F); // expected-warning {{might be 'indeterminate'}}
} else {
fputc('E', F); // expected-warning {{might be 'indeterminate'}}
}
}
fclose(F);
}

void error_indeterminate_feof1(void) {
FILE *F = fopen("file", "r+");
if (!F)
Expand Down
6 changes: 6 additions & 0 deletions clang/test/Analysis/stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ void check_fwrite(void) {
fclose(fp);
}

void check_fgetc(void) {
FILE *fp = tmpfile();
fgetc(fp); // expected-warning {{Stream pointer might be NULL}}
fclose(fp);
}

void check_fputc(void) {
FILE *fp = tmpfile();
fputc('A', fp); // expected-warning {{Stream pointer might be NULL}}
Expand Down

0 comments on commit 53578e5

Please sign in to comment.