Skip to content

Commit bc76018

Browse files
committed
Misc. false positive and false negative updates. as a response to reviewing the unit tests and conversations about how to handle some of the fp/fn cases observed. Updated the unit tests to use InlineExpectationsTestQuery.ql so it is easier to detect FP/FNs.
1 parent 9446a6b commit bc76018

5 files changed

Lines changed: 285 additions & 128 deletions

File tree

cpp/ql/src/Likely Bugs/Leap Year/LeapYear.qll

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,6 @@ class DateFebruary29Check extends Operation {
395395
exists(DateCheckMonthFebruary checkFeb, DateCheckDay29 check29 |
396396
checkFeb = this.getAnOperand() and
397397
check29 = this.getAnOperand()
398-
// and
399-
// hashCons(checkFeb.getDateQualifier()) = hashCons(check29.getDateQualifier())
400398
)
401399
}
402400

@@ -536,7 +534,7 @@ class TimeConversionFunction extends Function {
536534
"FileTimeToSystemTime", "SystemTimeToFileTime", "SystemTimeToTzSpecificLocalTime",
537535
"SystemTimeToTzSpecificLocalTimeEx", "TzSpecificLocalTimeToSystemTime",
538536
"TzSpecificLocalTimeToSystemTimeEx", "RtlLocalTimeToSystemTime",
539-
"RtlTimeToSecondsSince1970", "_mkgmtime"
537+
"RtlTimeToSecondsSince1970", "_mkgmtime", "SetSystemTime"
540538
]
541539
}
542540
}

cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql

Lines changed: 180 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import semmle.code.cpp.controlflow.IRGuards
1515

1616
/**
1717
* Functions whose operations should never be considered a
18-
* source of a dangerous leap year operation.
18+
* source or sink of a dangerous leap year operation.
1919
*/
2020
class IgnorableFunction extends Function {
2121
IgnorableFunction() {
@@ -33,6 +33,18 @@ class IgnorableFunction extends Function {
3333
or
3434
// Windows APIs that do time conversions
3535
this.getName().matches("%SpecificLocalTimeToSystemTime%")
36+
or
37+
// postgres function for diffing timestamps, date for leap year
38+
// is not applicable.
39+
this.getName().toLowerCase().matches("%timestamp%age%")
40+
or
41+
// Reading byte streams often involves operations of some base, but that's
42+
// not a real source of leap year issues.
43+
this.getName().toLowerCase().matches("%read%bytes%")
44+
or
45+
// A postgres function for local time conversions
46+
// conversion operations (from one time structure to another) are generally ignorable
47+
this.getName() = "localsub"
3648
}
3749
}
3850

@@ -83,15 +95,32 @@ class IgnorableCharLiteralArithmetic extends IgnorableOperation {
8395
/**
8496
* Constants often used in date conversions (from one date data type to another)
8597
* Numerous examples exist, like 1900 or 2000 that convert years from one
86-
* representation to another. All examples found tend to be 100 or greater,
87-
* so just using this as a heuristic for detecting such conversion constants.
98+
* representation to another.
8899
* Also '0' is sometimes observed as an atoi style conversion.
89100
*/
90101
bindingset[c]
91102
predicate isLikelyConversionConstant(int c) {
92-
exists(int i | i = c.abs() | i >= 100)
93-
or
94-
c = 0
103+
exists(int i | i = c.abs() |
104+
//| i >= 100)
105+
i = 146097 or // days in 400-year Gregorian cycle
106+
i = 36524 or // days in 100-year Gregorian subcycle
107+
i = 1461 or // days in 4-year cycle (incl. 1 leap)
108+
i = 32044 or // Fliegel–van Flandern JDN epoch shift
109+
i = 1721425 or // JDN of 0001‑01‑01 (Gregorian)
110+
i = 1721119 or // alt epoch offset
111+
i = 2400000 or // MJD → JDN conversion
112+
i = 2400001 or // alt MJD → JDN conversion
113+
i = 2141 or // fixed‑point month/day extraction
114+
i = 2000 or // observed in some conversions
115+
i = 65536 or // observed in some conversions
116+
i = 7834 or // observed in some conversions
117+
i = 256 or // observed in some conversions
118+
i = 1900 or // struct tm base‑year offset; harmless
119+
i = 1899 or // Observed in uses with 1900 to address off by one scenarios
120+
i = 292275056 or // qdatetime.h Qt Core year range first year constant
121+
i = 292278994 or // qdatetime.h Qt Core year range last year constant
122+
i = 0
123+
)
95124
}
96125

97126
/**
@@ -112,7 +141,35 @@ class IgnorableConstantArithmetic extends IgnorableOperation {
112141
}
113142

114143
// If a unary minus assume it is some sort of conversion
115-
class IgnorableUnaryMinus extends IgnorableOperation instanceof UnaryMinusExpr { }
144+
class IgnorableUnaryMinus extends IgnorableOperation {
145+
IgnorableUnaryMinus() {
146+
this instanceof UnaryMinusExpr
147+
or
148+
this.(Operation).getAnOperand() instanceof UnaryMinusExpr
149+
}
150+
}
151+
152+
class OperationAsArgToIgnorableFunction extends IgnorableOperation {
153+
OperationAsArgToIgnorableFunction() {
154+
exists(Call c |
155+
c.getAnArgument().getAChild*() = this and
156+
c.getTarget() instanceof IgnorableFunction
157+
)
158+
}
159+
}
160+
161+
/**
162+
* Literal OP literal means the result is constant/known
163+
* and the operation is basically ignorable (it's not a real operation but
164+
* probably one visual simplicity what it means).
165+
*/
166+
class ConstantBinaryArithmeticOperation extends IgnorableOperation {
167+
ConstantBinaryArithmeticOperation() {
168+
this instanceof BinaryArithmeticOperation and
169+
this.(BinaryArithmeticOperation).getLeftOperand() instanceof Literal and
170+
this.(BinaryArithmeticOperation).getRightOperand() instanceof Literal
171+
}
172+
}
116173

117174
class IgnorableBinaryBitwiseOperation extends IgnorableOperation instanceof BinaryBitwiseOperation {
118175
}
@@ -132,12 +189,31 @@ class IgnorablePointerOrCharArithmetic extends IgnorableOperation {
132189
this.(BinaryArithmeticOperation).getAnOperand().getUnspecifiedType() instanceof PointerType
133190
or
134191
this.(BinaryArithmeticOperation).getAnOperand().getUnspecifiedType() instanceof CharType
192+
or
193+
// Operations on calls to functions that accept char or char*
194+
this.(BinaryArithmeticOperation)
195+
.getAnOperand()
196+
.(Call)
197+
.getAnArgument()
198+
.getUnspecifiedType()
199+
.stripType() instanceof CharType
200+
or
201+
// Operations on calls to functions named like "strlen", "wcslen", etc
202+
// NOTE: workaround for cases where the wchar_t type is not a char, but an unsigned short
203+
// unclear if there is a best way to filter cases like these out based on type info.
204+
this.(BinaryArithmeticOperation).getAnOperand().(Call).getTarget().getName().matches("%len%")
135205
)
136206
or
137207
exists(AssignArithmeticOperation a | a.getRValue() = this |
138208
a.getAnOperand().getUnspecifiedType() instanceof PointerType
139209
or
140210
a.getAnOperand().getUnspecifiedType() instanceof CharType
211+
or
212+
// Operations on calls to functions that accept char or char*
213+
a.getAnOperand().(Call).getAnArgument().getUnspecifiedType().stripType() instanceof CharType
214+
or
215+
// Operations on calls to functions named like "strlen", "wcslen", etc
216+
this.(BinaryArithmeticOperation).getAnOperand().(Call).getTarget().getName().matches("%len%")
141217
)
142218
}
143219
}
@@ -161,25 +237,6 @@ predicate isOperationSourceCandidate(Expr e) {
161237
)
162238
}
163239

164-
/**
165-
* A Dataflow that identifies flows from an Operation (addition, subtraction, etc) to some ignorable operation (bitwise operations for example) that disqualify it
166-
*/
167-
module OperationSourceCandidateToIgnorableOperationConfig implements DataFlow::ConfigSig {
168-
predicate isSource(DataFlow::Node n) { isOperationSourceCandidate(n.asExpr()) }
169-
170-
predicate isSink(DataFlow::Node n) { n.asExpr() instanceof IgnorableOperation }
171-
172-
// looking for sources and sinks in the same function
173-
DataFlow::FlowFeature getAFeature() {
174-
result instanceof DataFlow::FeatureEqualSourceSinkCallContext
175-
}
176-
177-
predicate isBarrierOut(DataFlow::Node n) { isSink(n) }
178-
}
179-
180-
module OperationSourceCandidateToIgnorableOperationFlow =
181-
TaintTracking::Global<OperationSourceCandidateToIgnorableOperationConfig>;
182-
183240
/**
184241
* A dataflow that tracks an ignorable operation (eg. bitwise op) to a operation source, so we may disqualify it.
185242
*/
@@ -210,10 +267,10 @@ module IgnorableOperationToOperationSourceCandidateFlow =
210267
class OperationSource extends Expr {
211268
OperationSource() {
212269
isOperationSourceCandidate(this) and
213-
not exists(OperationSourceCandidateToIgnorableOperationFlow::PathNode src |
214-
src.getNode().asExpr() = this and
215-
src.isSource()
216-
) and
270+
// If the candidate came from an ignorable operation, ignore the candidate
271+
// NOTE: we cannot easily flow the candidate to an ignorable operation as that can
272+
// be tricky in practice, e.g., a mod operation on a year would be part of a leap year check
273+
// but a mod operation ending in a year is more indicative of something to ignore (a conversion)
217274
not exists(IgnorableOperationToOperationSourceCandidateFlow::PathNode sink |
218275
sink.getNode().asExpr() = this and
219276
sink.isSink()
@@ -223,28 +280,37 @@ class OperationSource extends Expr {
223280

224281
class YearFieldAssignmentNode extends DataFlow::Node {
225282
YearFieldAssignmentNode() {
226-
this.asExpr() instanceof YearFieldAssignment
227-
// or
228-
// this.asDefiningArgument() instanceof YearFieldAccess
229-
// or
230-
// // TODO: is there a better way to do this?
231-
// // i.e., without having to be cognizant of the addressof
232-
// // occurring, especially if this occurs on a dataflow
233-
// exists(AddressOfExpr aoe |
234-
// aoe = this.asDefiningArgument() and
235-
// aoe.getOperand() instanceof YearFieldAccess
236-
// )
283+
not this.getEnclosingCallable().getUnderlyingCallable() instanceof IgnorableFunction and
284+
(
285+
this.asExpr() instanceof YearFieldAssignment or
286+
this.asDefiningArgument() instanceof YearFieldOutArgAssignment
287+
)
237288
}
238289
}
239290

240291
/**
241292
* An assignment to a year field, which will be a sink
242-
* NOTE: could not make this abstract, it was binding to all expressions
243293
*/
244294
abstract class YearFieldAssignment extends Expr {
245295
abstract YearFieldAccess getYearFieldAccess();
246296
}
247297

298+
class YearFieldOutArgAssignment extends YearFieldAssignment {
299+
YearFieldAccess access;
300+
301+
YearFieldOutArgAssignment() {
302+
exists(Call c | c.getAnArgument() = access and this = access)
303+
or
304+
exists(Call c, AddressOfExpr aoe |
305+
c.getAnArgument() = aoe and
306+
aoe.getOperand() = access and
307+
this = aoe
308+
)
309+
}
310+
311+
override YearFieldAccess getYearFieldAccess() { result = access }
312+
}
313+
248314
/**
249315
* An assignment to x ie `x = a`.
250316
*/
@@ -298,7 +364,6 @@ module OperationToYearAssignmentConfig implements DataFlow::ConfigSig {
298364
predicate isBarrierIn(DataFlow::Node n) { isSource(n) }
299365

300366
predicate isBarrierOut(DataFlow::Node n) { isSink(n) }
301-
// DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
302367
}
303368

304369
module OperationToYearAssignmentFlow = TaintTracking::Global<OperationToYearAssignmentConfig>;
@@ -335,11 +400,6 @@ module YearFieldAccessToLeapYearCheckConfig implements DataFlow::ConfigSig {
335400
node2.asExpr() = node1.asExpr().(YearFieldAccess).getQualifier()
336401
}
337402

338-
// Use this to make the sink for leap year check intra-proc only
339-
// i.e., the sink must be in the same scope (function) as the source.
340-
// DataFlow::FlowFeature getAFeature() {
341-
// result instanceof DataFlow::FeatureEqualSourceSinkCallContext
342-
// }
343403
/**
344404
* Enforcing the check must occur in the same call context as the source,
345405
* i.e., do not return from the source function and check in a caller.
@@ -357,20 +417,26 @@ predicate isYearModifiedWithCheck(YearFieldAccess fa) {
357417
src.getNode().asExpr().(YearFieldAssignment).getYearFieldAccess() = fa
358418
)
359419
or
360-
isUsedInFeb29Check(fa)
361-
}
362-
363-
/**
364-
* If there is a flow from a date struct year field access/assignment to a Feb 29 check
365-
*/
366-
predicate isUsedInFeb29Check(YearFieldAccess fa) {
367-
exists(DateFebruary29Check check |
368-
DataFlow::localExprFlow(fa.getQualifier(), check.getDateQualifier())
369-
or
370-
DataFlow::localExprFlow(check.getDateQualifier(), fa.getQualifier())
420+
// If the time flows to a time conversion whose value/result is checked,
421+
// assume the leap year is being handled.
422+
exists(TimeStructToCheckedTimeConversionFlow::PathNode timeQualSrc |
423+
timeQualSrc.isSource() and
424+
timeQualSrc.getNode().asExpr() = fa.getQualifier()
371425
)
426+
// or
427+
// isUsedInFeb29Check(fa)
372428
}
373429

430+
// /**
431+
// * If there is a flow from a date struct year field access/assignment to a Feb 29 check
432+
// */
433+
// predicate isUsedInFeb29Check(YearFieldAccess fa) {
434+
// exists(DateFebruary29Check check |
435+
// DataFlow::localExprFlow(fa.getQualifier(), check.getDateQualifier())
436+
// or
437+
// DataFlow::localExprFlow(check.getDateQualifier(), fa.getQualifier())
438+
// )
439+
// }
374440
/**
375441
* An expression which checks the value of a Month field `a->month == 1`.
376442
*/
@@ -400,12 +466,66 @@ predicate isControlledByMonthEqualityCheckNonFebruary(Expr e) {
400466
)
401467
}
402468

469+
module TimeStructToCheckedTimeConversionConfig implements DataFlow::StateConfigSig {
470+
class FlowState = boolean;
471+
472+
predicate isSource(DataFlow::Node source, FlowState state) {
473+
exists(YearFieldAccess yfa | source.asExpr() = yfa.getQualifier()) and
474+
state = false
475+
}
476+
477+
predicate isSink(DataFlow::Node sink, FlowState state) {
478+
state = true and
479+
exists(IfStmt ifs | ifs.getCondition().getAChild*() = sink.asExpr())
480+
}
481+
482+
predicate isAdditionalFlowStep(
483+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
484+
) {
485+
state1 in [true, false] and
486+
state2 = true and
487+
exists(Call c |
488+
c.getTarget() instanceof TimeConversionFunction and
489+
c.getAnArgument().getAChild*() = node1.asExpr() and
490+
node2.asExpr() = c
491+
)
492+
}
493+
494+
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
495+
}
496+
497+
module TimeStructToCheckedTimeConversionFlow =
498+
DataFlow::GlobalWithState<TimeStructToCheckedTimeConversionConfig>;
499+
403500
import OperationToYearAssignmentFlow::PathGraph
404501

405502
from OperationToYearAssignmentFlow::PathNode src, OperationToYearAssignmentFlow::PathNode sink
406503
where
407504
OperationToYearAssignmentFlow::flowPath(src, sink) and
408505
not isYearModifiedWithCheck(sink.getNode().asExpr().(YearFieldAssignment).getYearFieldAccess()) and
409-
not isControlledByMonthEqualityCheckNonFebruary(sink.getNode().asExpr())
506+
not isControlledByMonthEqualityCheckNonFebruary(sink.getNode().asExpr()) and
507+
// TODO: this is an older heuristic that should be reassessed.
508+
// The heuristic stipulates that if a month or day is set to a constant in the local flow up to or after
509+
// a modified year's sink, then assume the user is knowingly handling the conversion correctly.
510+
// There is no interpretation of the assignment of the month/day or consideration for what branch the assignment is on.
511+
// Merely if the assignment of a constant day/month occurs anywhere, the year modification can likely
512+
// be ignored.
513+
not exists(FieldAccess mfa, AssignExpr ae, YearFieldAccess yfa, int val, Variable var |
514+
mfa instanceof MonthFieldAccess or mfa instanceof DayFieldAccess
515+
|
516+
yfa = sink.getNode().asExpr().(YearFieldAssignment).getYearFieldAccess() and
517+
var.getAnAccess() = yfa.getQualifier() and
518+
mfa.getQualifier() = var.getAnAccess() and
519+
mfa.isModified() and
520+
(
521+
mfa.getBasicBlock() = yfa.getBasicBlock().getASuccessor*() or
522+
yfa.getBasicBlock() = mfa.getBasicBlock().getASuccessor+()
523+
) and
524+
ae = mfa.getEnclosingElement() and
525+
ae.getAnOperand().getValue().toInt() = val and
526+
// If the constant looks like it relates to february, then there still might be an error
527+
// so only look for any other constant.
528+
not val in [2, 28, 29]
529+
)
410530
select sink, src, sink,
411531
"Year field has been modified, but no appropriate check for LeapYear was found."

0 commit comments

Comments
 (0)