-
Notifications
You must be signed in to change notification settings - Fork 13.6k
[clang][analyzer] Change modeling of 'fileno' in checkers. #81842
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -304,7 +304,8 @@ class StreamChecker : public Checker<check::PreCall, eval::Call, | |
{&StreamChecker::preDefault, | ||
std::bind(&StreamChecker::evalFeofFerror, _1, _2, _3, _4, ErrorFError), | ||
0}}, | ||
{{{"fileno"}, 1}, {&StreamChecker::preDefault, nullptr, 0}}, | ||
{{{"fileno"}, 1}, | ||
{&StreamChecker::preDefault, &StreamChecker::evalFileno, 0}}, | ||
}; | ||
|
||
CallDescriptionMap<FnDescription> FnTestDescriptions = { | ||
|
@@ -400,6 +401,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call, | |
void evalFflush(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const; | ||
|
||
void evalFileno(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const; | ||
|
||
/// Check that the stream (in StreamVal) is not NULL. | ||
/// If it can only be NULL a fatal error is emitted and nullptr returned. | ||
/// Otherwise the return value is a new state where the stream is constrained | ||
|
@@ -1343,6 +1347,84 @@ void StreamChecker::evalRewind(const FnDescription *Desc, const CallEvent &Call, | |
C.addTransition(State); | ||
} | ||
|
||
void StreamChecker::preFflush(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
SVal StreamVal = getStreamArg(Desc, Call); | ||
std::optional<DefinedSVal> Stream = StreamVal.getAs<DefinedSVal>(); | ||
if (!Stream) | ||
return; | ||
|
||
ProgramStateRef StateNotNull, StateNull; | ||
std::tie(StateNotNull, StateNull) = | ||
C.getConstraintManager().assumeDual(State, *Stream); | ||
if (StateNotNull && !StateNull) | ||
ensureStreamOpened(StreamVal, C, StateNotNull); | ||
} | ||
|
||
void StreamChecker::evalFflush(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
SVal StreamVal = getStreamArg(Desc, Call); | ||
std::optional<DefinedSVal> Stream = StreamVal.getAs<DefinedSVal>(); | ||
if (!Stream) | ||
return; | ||
|
||
// Skip if the stream can be both NULL and non-NULL. | ||
ProgramStateRef StateNotNull, StateNull; | ||
std::tie(StateNotNull, StateNull) = | ||
C.getConstraintManager().assumeDual(State, *Stream); | ||
if (StateNotNull && StateNull) | ||
return; | ||
if (StateNotNull && !StateNull) | ||
State = StateNotNull; | ||
else | ||
State = StateNull; | ||
|
||
const CallExpr *CE = dyn_cast_or_null<CallExpr>(Call.getOriginExpr()); | ||
if (!CE) | ||
return; | ||
|
||
// `fflush` returns EOF on failure, otherwise returns 0. | ||
ProgramStateRef StateFailed = bindInt(*EofVal, State, C, CE); | ||
ProgramStateRef StateNotFailed = bindInt(0, State, C, CE); | ||
|
||
// Clear error states if `fflush` returns 0, but retain their EOF flags. | ||
auto ClearErrorInNotFailed = [&StateNotFailed, Desc](SymbolRef Sym, | ||
const StreamState *SS) { | ||
if (SS->ErrorState & ErrorFError) { | ||
StreamErrorState NewES = | ||
(SS->ErrorState & ErrorFEof) ? ErrorFEof : ErrorNone; | ||
StreamState NewSS = StreamState::getOpened(Desc, NewES, false); | ||
StateNotFailed = StateNotFailed->set<StreamMap>(Sym, NewSS); | ||
} | ||
}; | ||
|
||
if (StateNotNull && !StateNull) { | ||
// Skip if the input stream's state is unknown, open-failed or closed. | ||
if (SymbolRef StreamSym = StreamVal.getAsSymbol()) { | ||
const StreamState *SS = State->get<StreamMap>(StreamSym); | ||
if (SS) { | ||
assert(SS->isOpened() && "Stream is expected to be opened"); | ||
ClearErrorInNotFailed(StreamSym, SS); | ||
} else | ||
return; | ||
} | ||
} else { | ||
// Clear error states for all streams. | ||
const StreamMapTy &Map = StateNotFailed->get<StreamMap>(); | ||
for (const auto &I : Map) { | ||
SymbolRef Sym = I.first; | ||
const StreamState &SS = I.second; | ||
if (SS.isOpened()) | ||
ClearErrorInNotFailed(Sym, &SS); | ||
} | ||
} | ||
|
||
C.addTransition(StateNotFailed); | ||
C.addTransition(StateFailed); | ||
} | ||
|
||
void StreamChecker::evalClearerr(const FnDescription *Desc, | ||
const CallEvent &Call, | ||
CheckerContext &C) const { | ||
|
@@ -1404,6 +1486,47 @@ void StreamChecker::evalFeofFerror(const FnDescription *Desc, | |
} | ||
} | ||
|
||
void StreamChecker::evalFileno(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
// Fileno should fail only if the passed pointer is invalid. | ||
// Some of the preconditions are checked already in preDefault. | ||
// Here we can assume that the operation does not fail. | ||
// An added failure case causes many unexpected warnings because a file number | ||
// becomes -1 that is not expected by the program. | ||
// The stream error states are not modified by 'fileno', and not the 'errno'. | ||
// (To ensure that errno is not changed, this evalCall is needed to not | ||
// invalidate 'errno' like in a default case.) | ||
ProgramStateRef State = C.getState(); | ||
SymbolRef StreamSym = getStreamArg(Desc, Call).getAsSymbol(); | ||
if (!StreamSym) | ||
return; | ||
|
||
const CallExpr *CE = dyn_cast_or_null<CallExpr>(Call.getOriginExpr()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we change to |
||
if (!CE) | ||
return; | ||
|
||
const StreamState *SS = State->get<StreamMap>(StreamSym); | ||
if (!SS) | ||
return; | ||
|
||
assertStreamStateOpened(SS); | ||
|
||
SValBuilder &SVB = C.getSValBuilder(); | ||
NonLoc RetVal = makeRetVal(C, CE).castAs<NonLoc>(); | ||
State = State->BindExpr(CE, C.getLocationContext(), RetVal); | ||
auto Cond = | ||
SVB.evalBinOp(State, BO_GE, RetVal, SVB.makeZeroVal(Call.getResultType()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a nice trick that you're creating the zero in the type of the other side to avoid surprising type conversion. There's a chance that I can borrow this solution to handle tricky issues in ArrayBoundCheckerV2 😄 |
||
SVB.getConditionType()) | ||
.getAs<DefinedOrUnknownSVal>(); | ||
if (!Cond) | ||
return; | ||
State = State->assume(*Cond, true); | ||
if (!State) | ||
return; | ||
|
||
C.addTransition(State); | ||
} | ||
|
||
void StreamChecker::preDefault(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
|
@@ -1432,84 +1555,6 @@ void StreamChecker::evalSetFeofFerror(const FnDescription *Desc, | |
C.addTransition(State); | ||
} | ||
|
||
void StreamChecker::preFflush(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
SVal StreamVal = getStreamArg(Desc, Call); | ||
std::optional<DefinedSVal> Stream = StreamVal.getAs<DefinedSVal>(); | ||
if (!Stream) | ||
return; | ||
|
||
ProgramStateRef StateNotNull, StateNull; | ||
std::tie(StateNotNull, StateNull) = | ||
C.getConstraintManager().assumeDual(State, *Stream); | ||
if (StateNotNull && !StateNull) | ||
ensureStreamOpened(StreamVal, C, StateNotNull); | ||
} | ||
|
||
void StreamChecker::evalFflush(const FnDescription *Desc, const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
SVal StreamVal = getStreamArg(Desc, Call); | ||
std::optional<DefinedSVal> Stream = StreamVal.getAs<DefinedSVal>(); | ||
if (!Stream) | ||
return; | ||
|
||
// Skip if the stream can be both NULL and non-NULL. | ||
ProgramStateRef StateNotNull, StateNull; | ||
std::tie(StateNotNull, StateNull) = | ||
C.getConstraintManager().assumeDual(State, *Stream); | ||
if (StateNotNull && StateNull) | ||
return; | ||
if (StateNotNull && !StateNull) | ||
State = StateNotNull; | ||
else | ||
State = StateNull; | ||
|
||
const CallExpr *CE = dyn_cast_or_null<CallExpr>(Call.getOriginExpr()); | ||
if (!CE) | ||
return; | ||
|
||
// `fflush` returns EOF on failure, otherwise returns 0. | ||
ProgramStateRef StateFailed = bindInt(*EofVal, State, C, CE); | ||
ProgramStateRef StateNotFailed = bindInt(0, State, C, CE); | ||
|
||
// Clear error states if `fflush` returns 0, but retain their EOF flags. | ||
auto ClearErrorInNotFailed = [&StateNotFailed, Desc](SymbolRef Sym, | ||
const StreamState *SS) { | ||
if (SS->ErrorState & ErrorFError) { | ||
StreamErrorState NewES = | ||
(SS->ErrorState & ErrorFEof) ? ErrorFEof : ErrorNone; | ||
StreamState NewSS = StreamState::getOpened(Desc, NewES, false); | ||
StateNotFailed = StateNotFailed->set<StreamMap>(Sym, NewSS); | ||
} | ||
}; | ||
|
||
if (StateNotNull && !StateNull) { | ||
// Skip if the input stream's state is unknown, open-failed or closed. | ||
if (SymbolRef StreamSym = StreamVal.getAsSymbol()) { | ||
const StreamState *SS = State->get<StreamMap>(StreamSym); | ||
if (SS) { | ||
assert(SS->isOpened() && "Stream is expected to be opened"); | ||
ClearErrorInNotFailed(StreamSym, SS); | ||
} else | ||
return; | ||
} | ||
} else { | ||
// Clear error states for all streams. | ||
const StreamMapTy &Map = StateNotFailed->get<StreamMap>(); | ||
for (const auto &I : Map) { | ||
SymbolRef Sym = I.first; | ||
const StreamState &SS = I.second; | ||
if (SS.isOpened()) | ||
ClearErrorInNotFailed(Sym, &SS); | ||
} | ||
} | ||
|
||
C.addTransition(StateNotFailed); | ||
C.addTransition(StateFailed); | ||
} | ||
|
||
ProgramStateRef | ||
StreamChecker::ensureStreamNonNull(SVal StreamVal, const Expr *StreamE, | ||
CheckerContext &C, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -268,6 +268,16 @@ void test_clearerr(FILE *F) { | |
// expected-warning@-1{{FALSE}} | ||
} | ||
|
||
void test_fileno(FILE *F) { | ||
errno = 0; | ||
int A = fileno(F); | ||
clang_analyzer_eval(F != NULL); // expected-warning{{TRUE}} | ||
clang_analyzer_eval(A >= 0); // expected-warning{{TRUE}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks better to making the comment lines begin from the same column. |
||
if (errno) {} // no-warning | ||
clang_analyzer_eval(errno == 0); // expected-warning{{TRUE}} | ||
// expected-warning@-1{{FALSE}} | ||
} | ||
|
||
void freadwrite_zerosize(FILE *F) { | ||
fwrite(WBuf, 1, 0, F); | ||
clang_analyzer_eval(feof(F)); // expected-warning {{UNKNOWN}} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt that this comment is a bit difficult to understand and composed a reworded alternative. Of course, this is a very subjective matter and English isn't my first language, so feel free to bikeshed this and/or override my suggestions.