Skip to content

Commit 1246b64

Browse files
authored
[clang][analyzer] Change modeling of 'fileno' in checkers. (#81842)
Function 'fileno' fails only if invalid pointer is passed, this is a case that is often ignored in source code. The failure case leads to many "false positive" reports when `fileno` returns -1 and this is not checked in the program. Because this, the function is now assumed to not fail (this is assumption that the passed file pointer is correct). The change affects `StdCLibraryFunctionsChecker` and `StreamChecker`.
1 parent 7ce1a11 commit 1246b64

File tree

7 files changed

+159
-121
lines changed

7 files changed

+159
-121
lines changed

clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2388,12 +2388,15 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
23882388
.ArgConstraint(NotNull(ArgNo(0))));
23892389

23902390
// int fileno(FILE *stream);
2391+
// According to POSIX 'fileno' may fail and set 'errno'.
2392+
// But in Linux it may fail only if the specified file pointer is invalid.
2393+
// At many places 'fileno' is used without check for failure and a failure
2394+
// case here would produce a large amount of likely false positive warnings.
2395+
// To avoid this, we assume here that it does not fail.
23912396
addToFunctionSummaryMap(
23922397
"fileno", Signature(ArgTypes{FilePtrTy}, RetType{IntTy}),
23932398
Summary(NoEvalCall)
2394-
.Case(ReturnsValidFileDescriptor, ErrnoMustNotBeChecked,
2395-
GenericSuccessMsg)
2396-
.Case(ReturnsMinusOne, ErrnoNEZeroIrrelevant, GenericFailureMsg)
2399+
.Case(ReturnsValidFileDescriptor, ErrnoUnchanged, GenericSuccessMsg)
23972400
.ArgConstraint(NotNull(ArgNo(0))));
23982401

23992402
// void rewind(FILE *stream);

clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp

Lines changed: 111 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ struct StreamOperationEvaluator {
249249

250250
bool isStreamEof() const { return SS->ErrorState == ErrorFEof; }
251251

252+
NonLoc getZeroVal(const CallEvent &Call) {
253+
return *SVB.makeZeroVal(Call.getResultType()).getAs<NonLoc>();
254+
}
255+
252256
ProgramStateRef setStreamState(ProgramStateRef State,
253257
const StreamState &NewSS) {
254258
return State->set<StreamMap>(StreamSym, NewSS);
@@ -390,7 +394,8 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
390394
{&StreamChecker::preDefault,
391395
std::bind(&StreamChecker::evalFeofFerror, _1, _2, _3, _4, ErrorFError),
392396
0}},
393-
{{{"fileno"}, 1}, {&StreamChecker::preDefault, nullptr, 0}},
397+
{{{"fileno"}, 1},
398+
{&StreamChecker::preDefault, &StreamChecker::evalFileno, 0}},
394399
};
395400

396401
CallDescriptionMap<FnDescription> FnTestDescriptions = {
@@ -486,6 +491,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
486491
void evalFflush(const FnDescription *Desc, const CallEvent &Call,
487492
CheckerContext &C) const;
488493

494+
void evalFileno(const FnDescription *Desc, const CallEvent &Call,
495+
CheckerContext &C) const;
496+
489497
/// Check that the stream (in StreamVal) is not NULL.
490498
/// If it can only be NULL a fatal error is emitted and nullptr returned.
491499
/// Otherwise the return value is a new state where the stream is constrained
@@ -929,8 +937,7 @@ void StreamChecker::evalFputx(const FnDescription *Desc, const CallEvent &Call,
929937
ProgramStateRef StateNotFailed =
930938
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
931939
StateNotFailed =
932-
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal,
933-
*E.SVB.makeZeroVal(E.ACtx.IntTy).getAs<NonLoc>());
940+
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
934941
if (!StateNotFailed)
935942
return;
936943
StateNotFailed =
@@ -1003,8 +1010,7 @@ void StreamChecker::evalFscanf(const FnDescription *Desc, const CallEvent &Call,
10031010
ProgramStateRef StateNotFailed =
10041011
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
10051012
StateNotFailed =
1006-
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal,
1007-
*E.SVB.makeZeroVal(E.ACtx.IntTy).getAs<NonLoc>());
1013+
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
10081014
if (StateNotFailed)
10091015
C.addTransition(StateNotFailed);
10101016
}
@@ -1073,8 +1079,7 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
10731079
ProgramStateRef StateNotFailed =
10741080
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
10751081
StateNotFailed =
1076-
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal,
1077-
*E.SVB.makeZeroVal(E.CE->getType()).getAs<NonLoc>());
1082+
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
10781083
if (!StateNotFailed)
10791084
return;
10801085
C.addTransition(StateNotFailed);
@@ -1200,8 +1205,7 @@ void StreamChecker::evalFtell(const FnDescription *Desc, const CallEvent &Call,
12001205
ProgramStateRef StateNotFailed =
12011206
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
12021207
StateNotFailed =
1203-
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal,
1204-
*E.SVB.makeZeroVal(Call.getResultType()).getAs<NonLoc>());
1208+
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
12051209
if (!StateNotFailed)
12061210
return;
12071211

@@ -1226,79 +1230,6 @@ void StreamChecker::evalRewind(const FnDescription *Desc, const CallEvent &Call,
12261230
C.addTransition(State);
12271231
}
12281232

1229-
void StreamChecker::evalClearerr(const FnDescription *Desc,
1230-
const CallEvent &Call,
1231-
CheckerContext &C) const {
1232-
ProgramStateRef State = C.getState();
1233-
StreamOperationEvaluator E(C);
1234-
if (!E.Init(Desc, Call, C, State))
1235-
return;
1236-
1237-
// FilePositionIndeterminate is not cleared.
1238-
State = E.setStreamState(
1239-
State,
1240-
StreamState::getOpened(Desc, ErrorNone, E.SS->FilePositionIndeterminate));
1241-
C.addTransition(State);
1242-
}
1243-
1244-
void StreamChecker::evalFeofFerror(const FnDescription *Desc,
1245-
const CallEvent &Call, CheckerContext &C,
1246-
const StreamErrorState &ErrorKind) const {
1247-
ProgramStateRef State = C.getState();
1248-
StreamOperationEvaluator E(C);
1249-
if (!E.Init(Desc, Call, C, State))
1250-
return;
1251-
1252-
if (E.SS->ErrorState & ErrorKind) {
1253-
// Execution path with error of ErrorKind.
1254-
// Function returns true.
1255-
// From now on it is the only one error state.
1256-
ProgramStateRef TrueState = bindAndAssumeTrue(State, C, E.CE);
1257-
C.addTransition(E.setStreamState(
1258-
TrueState, StreamState::getOpened(Desc, ErrorKind,
1259-
E.SS->FilePositionIndeterminate &&
1260-
!ErrorKind.isFEof())));
1261-
}
1262-
if (StreamErrorState NewES = E.SS->ErrorState & (~ErrorKind)) {
1263-
// Execution path(s) with ErrorKind not set.
1264-
// Function returns false.
1265-
// New error state is everything before minus ErrorKind.
1266-
ProgramStateRef FalseState = E.bindReturnValue(State, C, 0);
1267-
C.addTransition(E.setStreamState(
1268-
FalseState,
1269-
StreamState::getOpened(
1270-
Desc, NewES, E.SS->FilePositionIndeterminate && !NewES.isFEof())));
1271-
}
1272-
}
1273-
1274-
void StreamChecker::preDefault(const FnDescription *Desc, const CallEvent &Call,
1275-
CheckerContext &C) const {
1276-
ProgramStateRef State = C.getState();
1277-
SVal StreamVal = getStreamArg(Desc, Call);
1278-
State = ensureStreamNonNull(StreamVal, Call.getArgExpr(Desc->StreamArgNo), C,
1279-
State);
1280-
if (!State)
1281-
return;
1282-
State = ensureStreamOpened(StreamVal, C, State);
1283-
if (!State)
1284-
return;
1285-
1286-
C.addTransition(State);
1287-
}
1288-
1289-
void StreamChecker::evalSetFeofFerror(const FnDescription *Desc,
1290-
const CallEvent &Call, CheckerContext &C,
1291-
const StreamErrorState &ErrorKind) const {
1292-
ProgramStateRef State = C.getState();
1293-
SymbolRef StreamSym = getStreamArg(Desc, Call).getAsSymbol();
1294-
assert(StreamSym && "Operation not permitted on non-symbolic stream value.");
1295-
const StreamState *SS = State->get<StreamMap>(StreamSym);
1296-
assert(SS && "Stream should be tracked by the checker.");
1297-
State = State->set<StreamMap>(
1298-
StreamSym, StreamState::getOpened(SS->LastOperation, ErrorKind));
1299-
C.addTransition(State);
1300-
}
1301-
13021233
void StreamChecker::preFflush(const FnDescription *Desc, const CallEvent &Call,
13031234
CheckerContext &C) const {
13041235
ProgramStateRef State = C.getState();
@@ -1377,6 +1308,104 @@ void StreamChecker::evalFflush(const FnDescription *Desc, const CallEvent &Call,
13771308
C.addTransition(StateFailed);
13781309
}
13791310

1311+
void StreamChecker::evalClearerr(const FnDescription *Desc,
1312+
const CallEvent &Call,
1313+
CheckerContext &C) const {
1314+
ProgramStateRef State = C.getState();
1315+
StreamOperationEvaluator E(C);
1316+
if (!E.Init(Desc, Call, C, State))
1317+
return;
1318+
1319+
// FilePositionIndeterminate is not cleared.
1320+
State = E.setStreamState(
1321+
State,
1322+
StreamState::getOpened(Desc, ErrorNone, E.SS->FilePositionIndeterminate));
1323+
C.addTransition(State);
1324+
}
1325+
1326+
void StreamChecker::evalFeofFerror(const FnDescription *Desc,
1327+
const CallEvent &Call, CheckerContext &C,
1328+
const StreamErrorState &ErrorKind) const {
1329+
ProgramStateRef State = C.getState();
1330+
StreamOperationEvaluator E(C);
1331+
if (!E.Init(Desc, Call, C, State))
1332+
return;
1333+
1334+
if (E.SS->ErrorState & ErrorKind) {
1335+
// Execution path with error of ErrorKind.
1336+
// Function returns true.
1337+
// From now on it is the only one error state.
1338+
ProgramStateRef TrueState = bindAndAssumeTrue(State, C, E.CE);
1339+
C.addTransition(E.setStreamState(
1340+
TrueState, StreamState::getOpened(Desc, ErrorKind,
1341+
E.SS->FilePositionIndeterminate &&
1342+
!ErrorKind.isFEof())));
1343+
}
1344+
if (StreamErrorState NewES = E.SS->ErrorState & (~ErrorKind)) {
1345+
// Execution path(s) with ErrorKind not set.
1346+
// Function returns false.
1347+
// New error state is everything before minus ErrorKind.
1348+
ProgramStateRef FalseState = E.bindReturnValue(State, C, 0);
1349+
C.addTransition(E.setStreamState(
1350+
FalseState,
1351+
StreamState::getOpened(
1352+
Desc, NewES, E.SS->FilePositionIndeterminate && !NewES.isFEof())));
1353+
}
1354+
}
1355+
1356+
void StreamChecker::evalFileno(const FnDescription *Desc, const CallEvent &Call,
1357+
CheckerContext &C) const {
1358+
// Fileno should fail only if the passed pointer is invalid.
1359+
// Some of the preconditions are checked already in preDefault.
1360+
// Here we can assume that the operation does not fail, because if we
1361+
// introduced a separate branch where fileno() returns -1, then it would cause
1362+
// many unexpected and unwanted warnings in situations where fileno() is
1363+
// called on valid streams.
1364+
// The stream error states are not modified by 'fileno', and 'errno' is also
1365+
// left unchanged (so this evalCall does not invalidate it, but we have a
1366+
// custom evalCall instead of the default that would invalidate it).
1367+
ProgramStateRef State = C.getState();
1368+
StreamOperationEvaluator E(C);
1369+
if (!E.Init(Desc, Call, C, State))
1370+
return;
1371+
1372+
NonLoc RetVal = makeRetVal(C, E.CE).castAs<NonLoc>();
1373+
State = State->BindExpr(E.CE, C.getLocationContext(), RetVal);
1374+
State = E.assumeBinOpNN(State, BO_GE, RetVal, E.getZeroVal(Call));
1375+
if (!State)
1376+
return;
1377+
1378+
C.addTransition(State);
1379+
}
1380+
1381+
void StreamChecker::preDefault(const FnDescription *Desc, const CallEvent &Call,
1382+
CheckerContext &C) const {
1383+
ProgramStateRef State = C.getState();
1384+
SVal StreamVal = getStreamArg(Desc, Call);
1385+
State = ensureStreamNonNull(StreamVal, Call.getArgExpr(Desc->StreamArgNo), C,
1386+
State);
1387+
if (!State)
1388+
return;
1389+
State = ensureStreamOpened(StreamVal, C, State);
1390+
if (!State)
1391+
return;
1392+
1393+
C.addTransition(State);
1394+
}
1395+
1396+
void StreamChecker::evalSetFeofFerror(const FnDescription *Desc,
1397+
const CallEvent &Call, CheckerContext &C,
1398+
const StreamErrorState &ErrorKind) const {
1399+
ProgramStateRef State = C.getState();
1400+
SymbolRef StreamSym = getStreamArg(Desc, Call).getAsSymbol();
1401+
assert(StreamSym && "Operation not permitted on non-symbolic stream value.");
1402+
const StreamState *SS = State->get<StreamMap>(StreamSym);
1403+
assert(SS && "Stream should be tracked by the checker.");
1404+
State = State->set<StreamMap>(
1405+
StreamSym, StreamState::getOpened(SS->LastOperation, ErrorKind));
1406+
C.addTransition(State);
1407+
}
1408+
13801409
ProgramStateRef
13811410
StreamChecker::ensureStreamNonNull(SVal StreamVal, const Expr *StreamE,
13821411
CheckerContext &C,

clang/test/Analysis/std-c-library-functions-path-notes.c

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,22 @@ int test_islower(int *x) {
6161
}
6262

6363
int test_bugpath_notes(FILE *f1, char c, FILE *f2) {
64-
int f = fileno(f2);
65-
if (f == -1) // \
64+
// This test has the purpose of checking that notes appear at correct place.
65+
long a = ftell(f2); // no note
66+
if (a == -1) // \
6667
// expected-note{{Taking false branch}}
67-
return 0;
68-
int l = islower(c);
69-
f = fileno(f1); // \
70-
// expected-note{{Value assigned to 'f'}} \
71-
// expected-note{{Assuming that 'fileno' fails}}
72-
return dup(f); // \
68+
return -1;
69+
int l = islower(c); // no note
70+
a = ftell(f1); // \
71+
// expected-note{{Value assigned to 'a'}} \
72+
// expected-note{{Assuming that 'ftell' fails}}
73+
return dup(a); // \
7374
// expected-warning{{The 1st argument to 'dup' is -1 but should be >= 0}} \
7475
// expected-note{{The 1st argument to 'dup' is -1 but should be >= 0}}
7576
}
7677

7778
int test_fileno_arg_note(FILE *f1) {
78-
return dup(fileno(f1)); // \
79-
// expected-warning{{The 1st argument to 'dup' is < 0 but should be >= 0}} \
80-
// expected-note{{The 1st argument to 'dup' is < 0 but should be >= 0}} \
81-
// expected-note{{Assuming that 'fileno' fails}}
79+
return dup(fileno(f1)); // no warning
8280
}
8381

8482
int test_readlink_bufsize_zero(char *Buf, size_t Bufsize) {

clang/test/Analysis/stream-errno-note.c

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,8 @@ void check_rewind_errnocheck(void) {
141141
}
142142

143143
void check_fileno(void) {
144-
FILE *F = tmpfile();
145-
// expected-note@+2{{'F' is non-null}}
146-
// expected-note@+1{{Taking false branch}}
147-
if (!F)
148-
return;
149-
fileno(F);
150-
// expected-note@-1{{Assuming that 'fileno' is successful; 'errno' becomes undefined after the call}}
151-
if (errno) {} // expected-warning{{An undefined value may be read from 'errno'}}
152-
// expected-note@-1{{An undefined value may be read from 'errno'}}
153-
(void)fclose(F);
144+
// nothing to check: checker assumes that 'fileno' is always successful
145+
// (and does not change 'errno')
154146
}
155147

156148
void check_fwrite_zeroarg(size_t Siz) {

clang/test/Analysis/stream-errno.c

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ void check_no_errno_change(void) {
173173
if (errno) {} // no-warning
174174
ferror(F);
175175
if (errno) {} // no-warning
176+
fileno(F);
177+
if (errno) {} // no-warning
176178
clang_analyzer_eval(errno == 1); // expected-warning{{TRUE}}
177179
fclose(F);
178180
}
@@ -250,20 +252,6 @@ void check_rewind(void) {
250252
fclose(F);
251253
}
252254

253-
void check_fileno(void) {
254-
FILE *F = tmpfile();
255-
if (!F)
256-
return;
257-
int N = fileno(F);
258-
if (N == -1) {
259-
clang_analyzer_eval(errno != 0); // expected-warning{{TRUE}}
260-
if (errno) {} // no-warning
261-
fclose(F);
262-
return;
263-
}
264-
if (errno) {} // expected-warning{{An undefined value may be read from 'errno'}}
265-
}
266-
267255
void check_fflush_opened_file(void) {
268256
FILE *F = tmpfile();
269257
if (!F)

clang/test/Analysis/stream-error.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,24 @@ void error_ftello(void) {
491491
fclose(F);
492492
}
493493

494+
void error_fileno(void) {
495+
FILE *F = fopen("file", "r");
496+
if (!F)
497+
return;
498+
int N = fileno(F);
499+
clang_analyzer_eval(N >= 0); // expected-warning {{TRUE}}
500+
clang_analyzer_eval(feof(F) && ferror(F)); // expected-warning {{FALSE}}
501+
StreamTesterChecker_make_feof_stream(F);
502+
N = fileno(F);
503+
clang_analyzer_eval(feof(F)); // expected-warning {{TRUE}}
504+
clang_analyzer_eval(ferror(F)); // expected-warning {{FALSE}}
505+
StreamTesterChecker_make_ferror_stream(F);
506+
N = fileno(F);
507+
clang_analyzer_eval(feof(F)); // expected-warning {{FALSE}}
508+
clang_analyzer_eval(ferror(F)); // expected-warning {{TRUE}}
509+
fclose(F);
510+
}
511+
494512
void error_fflush_on_non_null_stream_clear_error_states(void) {
495513
FILE *F0 = tmpfile(), *F1 = tmpfile();
496514
// `fflush` clears a non-EOF stream's error state.

0 commit comments

Comments
 (0)