Skip to content

Commit 47df664

Browse files
authored
[clang][analyzer] Support fgets in the SteamChecker (#73638)
1 parent 1bfb84b commit 47df664

File tree

4 files changed

+89
-36
lines changed

4 files changed

+89
-36
lines changed

clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
252252
std::bind(&StreamChecker::evalFreadFwrite, _1, _2, _3, _4, false), 3}},
253253
{{{"fgetc"}, 1},
254254
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, true),
255-
std::bind(&StreamChecker::evalFgetc, _1, _2, _3, _4), 0}},
255+
std::bind(&StreamChecker::evalFgetx, _1, _2, _3, _4, true), 0}},
256+
{{{"fgets"}, 3},
257+
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, true),
258+
std::bind(&StreamChecker::evalFgetx, _1, _2, _3, _4, false), 2}},
256259
{{{"fputc"}, 2},
257260
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, false),
258261
std::bind(&StreamChecker::evalFputx, _1, _2, _3, _4, true), 1}},
@@ -320,8 +323,8 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
320323
void evalFreadFwrite(const FnDescription *Desc, const CallEvent &Call,
321324
CheckerContext &C, bool IsFread) const;
322325

323-
void evalFgetc(const FnDescription *Desc, const CallEvent &Call,
324-
CheckerContext &C) const;
326+
void evalFgetx(const FnDescription *Desc, const CallEvent &Call,
327+
CheckerContext &C, bool SingleChar) const;
325328

326329
void evalFputx(const FnDescription *Desc, const CallEvent &Call,
327330
CheckerContext &C, bool IsSingleChar) const;
@@ -760,8 +763,8 @@ void StreamChecker::evalFreadFwrite(const FnDescription *Desc,
760763
C.addTransition(StateFailed);
761764
}
762765

763-
void StreamChecker::evalFgetc(const FnDescription *Desc, const CallEvent &Call,
764-
CheckerContext &C) const {
766+
void StreamChecker::evalFgetx(const FnDescription *Desc, const CallEvent &Call,
767+
CheckerContext &C, bool SingleChar) const {
765768
ProgramStateRef State = C.getState();
766769
SymbolRef StreamSym = getStreamArg(Desc, Call).getAsSymbol();
767770
if (!StreamSym)
@@ -778,42 +781,61 @@ void StreamChecker::evalFgetc(const FnDescription *Desc, const CallEvent &Call,
778781
assertStreamStateOpened(OldSS);
779782

780783
// `fgetc` returns the read character on success, otherwise returns EOF.
784+
// `fgets` returns the read buffer address on success, otherwise returns NULL.
781785

782-
// Generate a transition for the success state of `fgetc`.
783-
// If we know the state to be FEOF at fgetc, do not add a success state.
784786
if (OldSS->ErrorState != ErrorFEof) {
785-
NonLoc RetVal = makeRetVal(C, CE).castAs<NonLoc>();
786-
ProgramStateRef StateNotFailed =
787-
State->BindExpr(CE, C.getLocationContext(), RetVal);
788-
SValBuilder &SVB = C.getSValBuilder();
789-
ASTContext &ASTC = C.getASTContext();
790-
// The returned 'unsigned char' of `fgetc` is converted to 'int',
791-
// so we need to check if it is in range [0, 255].
792-
auto CondLow =
793-
SVB.evalBinOp(State, BO_GE, RetVal, SVB.makeZeroVal(ASTC.IntTy),
794-
SVB.getConditionType())
795-
.getAs<DefinedOrUnknownSVal>();
796-
auto CondHigh =
797-
SVB.evalBinOp(State, BO_LE, RetVal,
798-
SVB.makeIntVal(SVB.getBasicValueFactory()
799-
.getMaxValue(ASTC.UnsignedCharTy)
800-
.getLimitedValue(),
801-
ASTC.IntTy),
802-
SVB.getConditionType())
803-
.getAs<DefinedOrUnknownSVal>();
804-
if (!CondLow || !CondHigh)
805-
return;
806-
StateNotFailed = StateNotFailed->assume(*CondLow, true);
807-
if (!StateNotFailed)
808-
return;
809-
StateNotFailed = StateNotFailed->assume(*CondHigh, true);
810-
if (!StateNotFailed)
811-
return;
812-
C.addTransition(StateNotFailed);
787+
if (SingleChar) {
788+
// Generate a transition for the success state of `fgetc`.
789+
NonLoc RetVal = makeRetVal(C, CE).castAs<NonLoc>();
790+
ProgramStateRef StateNotFailed =
791+
State->BindExpr(CE, C.getLocationContext(), RetVal);
792+
SValBuilder &SVB = C.getSValBuilder();
793+
ASTContext &ASTC = C.getASTContext();
794+
// The returned 'unsigned char' of `fgetc` is converted to 'int',
795+
// so we need to check if it is in range [0, 255].
796+
auto CondLow =
797+
SVB.evalBinOp(State, BO_GE, RetVal, SVB.makeZeroVal(ASTC.IntTy),
798+
SVB.getConditionType())
799+
.getAs<DefinedOrUnknownSVal>();
800+
auto CondHigh =
801+
SVB.evalBinOp(State, BO_LE, RetVal,
802+
SVB.makeIntVal(SVB.getBasicValueFactory()
803+
.getMaxValue(ASTC.UnsignedCharTy)
804+
.getLimitedValue(),
805+
ASTC.IntTy),
806+
SVB.getConditionType())
807+
.getAs<DefinedOrUnknownSVal>();
808+
if (!CondLow || !CondHigh)
809+
return;
810+
StateNotFailed = StateNotFailed->assume(*CondLow, true);
811+
if (!StateNotFailed)
812+
return;
813+
StateNotFailed = StateNotFailed->assume(*CondHigh, true);
814+
if (!StateNotFailed)
815+
return;
816+
C.addTransition(StateNotFailed);
817+
} else {
818+
// Generate a transition for the success state of `fgets`.
819+
std::optional<DefinedSVal> GetBuf =
820+
Call.getArgSVal(0).getAs<DefinedSVal>();
821+
if (!GetBuf)
822+
return;
823+
ProgramStateRef StateNotFailed =
824+
State->BindExpr(CE, C.getLocationContext(), *GetBuf);
825+
StateNotFailed = StateNotFailed->set<StreamMap>(
826+
StreamSym, StreamState::getOpened(Desc));
827+
C.addTransition(StateNotFailed);
828+
}
813829
}
814830

815831
// Add transition for the failed state.
816-
ProgramStateRef StateFailed = bindInt(*EofVal, State, C, CE);
832+
ProgramStateRef StateFailed;
833+
if (SingleChar)
834+
StateFailed = bindInt(*EofVal, State, C, CE);
835+
else
836+
StateFailed =
837+
State->BindExpr(CE, C.getLocationContext(),
838+
C.getSValBuilder().makeNullWithType(CE->getType()));
817839

818840
// If a (non-EOF) error occurs, the resulting value of the file position
819841
// indicator for the stream is indeterminate.

clang/test/Analysis/Inputs/system-header-simulator.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ int fclose(FILE *fp);
4949
size_t fread(void *restrict, size_t, size_t, FILE *restrict);
5050
size_t fwrite(const void *restrict, size_t, size_t, FILE *restrict);
5151
int fgetc(FILE *stream);
52+
char *fgets(char *restrict str, int count, FILE *restrict stream);
5253
int fputc(int ch, FILE *stream);
5354
int fputs(const char *restrict s, FILE *restrict stream);
5455
int fseek(FILE *__stream, long int __off, int __whence);

clang/test/Analysis/stream-error.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,29 @@ void error_fgetc(void) {
123123
fgetc(F); // expected-warning {{Stream might be already closed}}
124124
}
125125

126+
void error_fgets(void) {
127+
FILE *F = tmpfile();
128+
char Buf[256];
129+
if (!F)
130+
return;
131+
char *Ret = fgets(Buf, sizeof(Buf), F);
132+
if (Ret == Buf) {
133+
clang_analyzer_eval(feof(F) || ferror(F)); // expected-warning {{FALSE}}
134+
} else {
135+
clang_analyzer_eval(Ret == NULL); // expected-warning {{TRUE}}
136+
clang_analyzer_eval(feof(F) || ferror(F)); // expected-warning {{TRUE}}
137+
if (feof(F)) {
138+
clang_analyzer_eval(ferror(F)); // expected-warning {{FALSE}}
139+
fgets(Buf, sizeof(Buf), F); // expected-warning {{Read function called when stream is in EOF state}}
140+
} else {
141+
clang_analyzer_eval(ferror(F)); // expected-warning {{TRUE}}
142+
fgets(Buf, sizeof(Buf), F); // expected-warning {{might be 'indeterminate'}}
143+
}
144+
}
145+
fclose(F);
146+
fgets(Buf, sizeof(Buf), F); // expected-warning {{Stream might be already closed}}
147+
}
148+
126149
void error_fputc(void) {
127150
FILE *F = tmpfile();
128151
if (!F)

clang/test/Analysis/stream.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ void check_fgetc(void) {
2020
fclose(fp);
2121
}
2222

23+
void check_fgets(void) {
24+
FILE *fp = tmpfile();
25+
char buf[256];
26+
fgets(buf, sizeof(buf), fp); // expected-warning {{Stream pointer might be NULL}}
27+
fclose(fp);
28+
}
29+
2330
void check_fputc(void) {
2431
FILE *fp = tmpfile();
2532
fputc('A', fp); // expected-warning {{Stream pointer might be NULL}}

0 commit comments

Comments
 (0)