Skip to content

Commit e485d98

Browse files
committed
Created a new leap year check guard condition. Found that the prior definitions had gaps resulting in false positives and inconsistencies (inconsistent as to what is a guard and what is a function that does a leap year check).
1 parent f507239 commit e485d98

3 files changed

Lines changed: 218 additions & 17 deletions

File tree

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

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -385,22 +385,11 @@ module OperationToYearAssignmentConfig implements DataFlow::ConfigSig {
385385
module OperationToYearAssignmentFlow = TaintTracking::Global<OperationToYearAssignmentConfig>;
386386

387387
predicate isLeapYearCheckSink(DataFlow::Node sink) {
388-
exists(ExprCheckLeapYear e | sink.asExpr() = e.getAChild*())
389-
or
390-
// Local leap year check
391-
sink.asExpr().(LeapYearFieldAccess).isUsedInCorrectLeapYearCheck()
392-
or
393-
// passed to function that seems to check for leap year
394-
exists(ChecksForLeapYearFunctionCall fc |
395-
sink.asExpr() = fc.getAnArgument()
396-
or
397-
sink.asExpr() = fc.getAnArgument().(FieldAccess).getQualifier()
398-
or
399-
exists(AddressOfExpr aoe |
400-
fc.getAnArgument() = aoe and
401-
aoe.getOperand() = sink.asExpr()
402-
)
388+
exists(LeapYearGuardCondition lgc |
389+
lgc.checkedYearAccess() = [sink.asExpr(), sink.asIndirectExpr()]
403390
)
391+
or
392+
isLeapYearCheckCall(_, [sink.asExpr(), sink.asIndirectExpr()])
404393
}
405394

406395
/**
@@ -513,6 +502,147 @@ module TimeStructToCheckedTimeConversionConfig implements DataFlow::StateConfigS
513502
module TimeStructToCheckedTimeConversionFlow =
514503
DataFlow::GlobalWithState<TimeStructToCheckedTimeConversionConfig>;
515504

505+
/**
506+
* Finds flow from a parameter of a function to a leap year check.
507+
* This is necessary to handle for scenarios like this:
508+
*
509+
* year = DANGEROUS_OP // source
510+
* isLeap = isLeapYear(year);
511+
* // logic based on isLeap
512+
* struct.year = year; // sink
513+
*
514+
* In this case, we may flow a dangerous op to a year assignment, failing
515+
* to barrier the flow through a leap year check, as the leap year check
516+
* is nested, and dataflow does not progress down into the check and out.
517+
* Instead, the point of this flow is to detect isLeapYear's argument
518+
* is checked for leap year, making the isLeapYear call a barrier for
519+
* the dangerous flow if we flow through the parameter identified to
520+
* be checked.
521+
*/
522+
module ParameterToLeapYearCheckConfig implements DataFlow::ConfigSig {
523+
predicate isSource(DataFlow::Node source) { exists(source.asParameter()) }
524+
525+
predicate isSink(DataFlow::Node sink) {
526+
exists(LeapYearGuardCondition lgc |
527+
lgc.checkedYearAccess() = [sink.asExpr(), sink.asIndirectExpr()]
528+
)
529+
}
530+
531+
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
532+
// flow from a YearFieldAccess to the qualifier
533+
node2.asExpr() = node1.asExpr().(YearFieldAccess).getQualifier()
534+
or
535+
// flow from a year access qualifier to a year field
536+
exists(YearFieldAccess yfa | node2.asExpr() = yfa and node1.asExpr() = yfa.getQualifier())
537+
}
538+
539+
/**
540+
* Enforcing the check must occur in the same call context as the source,
541+
* i.e., do not return from the source function and check in a caller.
542+
*/
543+
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
544+
}
545+
546+
// NOTE: I do not believe taint flow is necessary here as we should
547+
// be flowing directyly from some parameter to a leap year check.
548+
module ParameterToLeapYearCheckFlow = DataFlow::Global<ParameterToLeapYearCheckConfig>;
549+
550+
predicate isLeapYearCheckCall(Call c, Expr arg) {
551+
exists(ParameterToLeapYearCheckFlow::PathNode src, Function f, int i |
552+
src.isSource() and
553+
f.getParameter(i) = src.getNode().asParameter() and
554+
c.getTarget() = f and
555+
c.getArgument(i) = arg
556+
)
557+
}
558+
559+
class LeapYearGuardCondition extends GuardCondition {
560+
Expr yearSinkDiv4;
561+
Expr yearSinkDiv100;
562+
Expr yearSinkDiv400;
563+
564+
LeapYearGuardCondition() {
565+
exists(
566+
LogicalAndExpr andExpr, LogicalOrExpr orExpr, GuardCondition div4Check,
567+
GuardCondition div100Check, GuardCondition div400Check, GuardValue gv
568+
|
569+
// Cannonical case:
570+
// form: `(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)`
571+
// or : `!(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)`
572+
this = andExpr and
573+
andExpr.hasOperands(div4Check, orExpr) and
574+
orExpr.hasOperands(div100Check, div400Check) and
575+
exists(RemExpr e |
576+
div4Check.comparesEq(e, 0, true, gv) and
577+
e.getRightOperand().getValue().toInt() = 4 and
578+
yearSinkDiv4 = e.getLeftOperand()
579+
) and
580+
exists(RemExpr e |
581+
div100Check.comparesEq(e, 0, false, gv) and
582+
e.getRightOperand().getValue().toInt() = 100 and
583+
yearSinkDiv100 = e.getLeftOperand()
584+
) and
585+
exists(RemExpr e |
586+
div400Check.comparesEq(e, 0, true, gv) and
587+
e.getRightOperand().getValue().toInt() = 400 and
588+
yearSinkDiv400 = e.getLeftOperand()
589+
)
590+
or
591+
// Inverted logic case:
592+
// `year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)`
593+
this = orExpr and
594+
orExpr.hasOperands(div4Check, andExpr) and
595+
andExpr.hasOperands(div100Check, div400Check) and
596+
exists(RemExpr e |
597+
div4Check.comparesEq(e, 0, false, gv) and
598+
e.getRightOperand().getValue().toInt() = 4 and
599+
yearSinkDiv4 = e.getLeftOperand()
600+
) and
601+
exists(RemExpr e |
602+
div100Check.comparesEq(e, 0, true, gv) and
603+
e.getRightOperand().getValue().toInt() = 100 and
604+
yearSinkDiv100 = e.getLeftOperand()
605+
) and
606+
exists(RemExpr e |
607+
div400Check.comparesEq(e, 0, false, gv) and
608+
e.getRightOperand().getValue().toInt() = 400 and
609+
yearSinkDiv400 = e.getLeftOperand()
610+
)
611+
)
612+
}
613+
614+
Expr getYearSinkDiv4() { result = yearSinkDiv4 }
615+
616+
Expr getYearSinkDiv100() { result = yearSinkDiv100 }
617+
618+
Expr getYearSinkDiv400() { result = yearSinkDiv400 }
619+
620+
/**
621+
* The variable access that is used in all 3 components of the leap year check
622+
* e.g., see getYearSinkDiv4/100/400..
623+
* If a field access is used, the qualifier and the field access are both returned
624+
* in checked condition.
625+
* NOTE: if the year is not checked using the same access in all 3 components, no result is returned.
626+
* The typical case observed is a consistent variable access is used. If not, this may indicate a bug.
627+
* We could check more accurately with a dataflow analysis, but this is likely sufficient for now.
628+
*/
629+
VariableAccess checkedYearAccess() {
630+
exists(Variable var |
631+
(
632+
this.getYearSinkDiv4().getAChild*() = var.getAnAccess() and
633+
this.getYearSinkDiv100().getAChild*() = var.getAnAccess() and
634+
this.getYearSinkDiv400().getAChild*() = var.getAnAccess() and
635+
result = var.getAnAccess() and
636+
(
637+
result = this.getYearSinkDiv4().getAChild*() or
638+
result = this.getYearSinkDiv100().getAChild*() or
639+
result = this.getYearSinkDiv400().getAChild*()
640+
)
641+
)
642+
)
643+
}
644+
}
645+
516646
import OperationToYearAssignmentFlow::PathGraph
517647

518648
from OperationToYearAssignmentFlow::PathNode src, OperationToYearAssignmentFlow::PathNode sink

cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.expected

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
| test.cpp:1095:16:1095:23 | increment_arg output argument | test.cpp:1083:2:1083:4 | ... ++ | test.cpp:1095:16:1095:23 | increment_arg output argument | Year field has been modified, but no appropriate check for LeapYear was found. |
1515
| test.cpp:1099:27:1099:35 | increment_arg_by_pointer output argument | test.cpp:1087:2:1087:7 | ... ++ | test.cpp:1099:27:1099:35 | increment_arg_by_pointer output argument | Year field has been modified, but no appropriate check for LeapYear was found. |
1616
| test.cpp:1153:14:1153:26 | ... - ... | test.cpp:1153:14:1153:26 | ... - ... | test.cpp:1153:14:1153:26 | ... - ... | Year field has been modified, but no appropriate check for LeapYear was found. |
17+
| test.cpp:1204:16:1204:19 | year | test.cpp:1202:10:1202:15 | offset | test.cpp:1204:16:1204:19 | year | Year field has been modified, but no appropriate check for LeapYear was found. |
18+
| test.cpp:1243:16:1243:28 | ... + ... | test.cpp:1243:16:1243:28 | ... + ... | test.cpp:1243:16:1243:28 | ... + ... | Year field has been modified, but no appropriate check for LeapYear was found. |
19+
| test.cpp:1257:16:1257:28 | ... + ... | test.cpp:1257:16:1257:28 | ... + ... | test.cpp:1257:16:1257:28 | ... + ... | Year field has been modified, but no appropriate check for LeapYear was found. |
1720
edges
1821
| test.cpp:854:7:854:31 | ... = ... | test.cpp:857:33:857:36 | year | provenance | |
1922
| test.cpp:854:14:854:31 | ... + ... | test.cpp:854:7:854:31 | ... = ... | provenance | |
@@ -24,6 +27,8 @@ edges
2427
| test.cpp:1087:2:1087:7 | ... ++ | test.cpp:1086:37:1086:37 | *x | provenance | |
2528
| test.cpp:1183:2:1183:15 | ... += ... | test.cpp:1185:16:1185:19 | year | provenance | |
2629
| test.cpp:1183:10:1183:15 | offset | test.cpp:1183:2:1183:15 | ... += ... | provenance | |
30+
| test.cpp:1202:2:1202:15 | ... += ... | test.cpp:1204:16:1204:19 | year | provenance | |
31+
| test.cpp:1202:10:1202:15 | offset | test.cpp:1202:2:1202:15 | ... += ... | provenance | |
2732
nodes
2833
| test.cpp:422:14:422:14 | 2 | semmle.label | 2 |
2934
| test.cpp:440:2:440:11 | ... ++ | semmle.label | ... ++ |
@@ -61,6 +66,15 @@ nodes
6166
| test.cpp:1183:2:1183:15 | ... += ... | semmle.label | ... += ... |
6267
| test.cpp:1183:10:1183:15 | offset | semmle.label | offset |
6368
| test.cpp:1185:16:1185:19 | year | semmle.label | year |
69+
| test.cpp:1202:2:1202:15 | ... += ... | semmle.label | ... += ... |
70+
| test.cpp:1202:10:1202:15 | offset | semmle.label | offset |
71+
| test.cpp:1204:16:1204:19 | year | semmle.label | year |
72+
| test.cpp:1223:16:1223:28 | ... + ... | semmle.label | ... + ... |
73+
| test.cpp:1229:16:1229:28 | ... + ... | semmle.label | ... + ... |
74+
| test.cpp:1236:16:1236:28 | ... + ... | semmle.label | ... + ... |
75+
| test.cpp:1243:16:1243:28 | ... + ... | semmle.label | ... + ... |
76+
| test.cpp:1250:16:1250:28 | ... + ... | semmle.label | ... + ... |
77+
| test.cpp:1257:16:1257:28 | ... + ... | semmle.label | ... + ... |
6478
subpaths
6579
testFailures
6680
| test.cpp:1075:2:1075:11 | ... ++ | Unexpected result: Alert |

cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/test.cpp

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,5 +1198,62 @@ void leap_year_checked_raw_false_positive2(WORD year, WORD offset, WORD day){
11981198
}
11991199

12001200
tmp.tm_mday = day;
1201-
1202-
}
1201+
1202+
year += offset; // $ Source
1203+
1204+
tmp.tm_year = year; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification]
1205+
1206+
}
1207+
1208+
1209+
bool isNotLeapYear(struct tm tm)
1210+
{
1211+
return !(tm.tm_year % 4 == 0 && (tm.tm_year % 100 != 0 || tm.tm_year % 400 == 0));
1212+
}
1213+
1214+
bool isNotLeapYear2(struct tm tm)
1215+
{
1216+
return (tm.tm_year % 4 != 0 || (tm.tm_year % 100 == 0 && tm.tm_year % 400 != 0));
1217+
}
1218+
1219+
1220+
void inverted_leap_year_check(WORD year, WORD offset, WORD day){
1221+
struct tm tmp;
1222+
1223+
tmp.tm_year = year + offset;
1224+
1225+
if (isNotLeapYear(tmp)){
1226+
day = 28;
1227+
}
1228+
1229+
tmp.tm_year = year + offset;
1230+
1231+
if(isNotLeapYear2(tmp)){
1232+
day = 28;
1233+
}
1234+
1235+
1236+
tmp.tm_year = year + offset;
1237+
bool isNotLeapYear = (tmp.tm_year % 4 != 0 || (tmp.tm_year % 100 == 0 && tmp.tm_year % 400 != 0));
1238+
1239+
if(isNotLeapYear){
1240+
day = 28;
1241+
}
1242+
1243+
tmp.tm_year = year + offset; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification]
1244+
}
1245+
1246+
1247+
void simplified_leap_year_check(WORD year, WORD offset){
1248+
struct tm tmp;
1249+
1250+
tmp.tm_year = year + offset;
1251+
1252+
bool isLeap = !(tmp.tm_year % 4) && (tmp.tm_year % 100 || !(tmp.tm_year % 400));
1253+
if(isLeap){
1254+
// do something
1255+
}
1256+
1257+
tmp.tm_year = year + offset; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification]
1258+
}
1259+

0 commit comments

Comments
 (0)