diff --git a/cpp/ql/lib/semmle/code/cpp/commons/DateTime.qll b/cpp/ql/lib/semmle/code/cpp/commons/DateTime.qll index c67bf7cf96e3..b01c775e5535 100644 --- a/cpp/ql/lib/semmle/code/cpp/commons/DateTime.qll +++ b/cpp/ql/lib/semmle/code/cpp/commons/DateTime.qll @@ -14,7 +14,7 @@ class PackedTimeType extends Type { } } -private predicate timeType(string typeName) { typeName = ["_SYSTEMTIME", "SYSTEMTIME", "tm"] } +private predicate timeType(string typeName) { typeName = ["_SYSTEMTIME", "SYSTEMTIME", "tm", "TIME_FIELDS", "PTIME_FIELDS"] } /** * A type that is used to represent times and dates in an 'unpacked' form, that is, @@ -95,3 +95,24 @@ class StructTmMonthFieldAccess extends MonthFieldAccess { class StructTmYearFieldAccess extends YearFieldAccess { StructTmYearFieldAccess() { this.getTarget().getName() = "tm_year" } } + +/** + * A `DayFieldAccess` for the `TIME_FIELDS` struct. + */ +class TimeFieldsDayFieldAccess extends DayFieldAccess { + TimeFieldsDayFieldAccess() { this.getTarget().getName() = "Day" } +} + +/** + * A `MonthFieldAccess` for the `TIME_FIELDS` struct. + */ +class TimeFieldsMonthFieldAccess extends MonthFieldAccess { + TimeFieldsMonthFieldAccess() { this.getTarget().getName() = "Month" } +} + +/** + * A `YearFieldAccess` for the `TIME_FIELDS` struct. + */ +class TimeFieldsYearFieldAccess extends YearFieldAccess { + TimeFieldsYearFieldAccess() { this.getTarget().getName() = "Year" } +} \ No newline at end of file diff --git a/cpp/ql/src/Likely Bugs/Leap Year/LeapYear.qll b/cpp/ql/src/Likely Bugs/Leap Year/LeapYear.qll index 06b6aff66abd..2325fd339182 100644 --- a/cpp/ql/src/Likely Bugs/Leap Year/LeapYear.qll +++ b/cpp/ql/src/Likely Bugs/Leap Year/LeapYear.qll @@ -41,7 +41,6 @@ class CheckForLeapYearOperation extends Expr { } } -bindingset[modVal] Expr moduloCheckEQ_0(EQExpr eq, int modVal) { exists(RemExpr rem | rem = eq.getLeftOperand() | result = rem.getLeftOperand() and @@ -50,7 +49,6 @@ Expr moduloCheckEQ_0(EQExpr eq, int modVal) { eq.getRightOperand().getValue().toInt() = 0 } -bindingset[modVal] Expr moduloCheckNEQ_0(NEExpr neq, int modVal) { exists(RemExpr rem | rem = neq.getLeftOperand() | result = rem.getLeftOperand() and @@ -59,48 +57,6 @@ Expr moduloCheckNEQ_0(NEExpr neq, int modVal) { neq.getRightOperand().getValue().toInt() = 0 } -/** - * Returns if the two expressions resolve to the same value, albeit it is a fuzzy attempt. - * SSA is not fit for purpose here as calls break SSA equivalence. - */ -predicate exprEq_propertyPermissive(Expr e1, Expr e2) { - not e1 = e2 and - ( - DataFlow::localFlow(DataFlow::exprNode(e1), DataFlow::exprNode(e2)) - or - if e1 instanceof ThisExpr and e2 instanceof ThisExpr - then any() - else - /* If it's a direct Access, check that the target is the same. */ - if e1 instanceof Access - then e1.(Access).getTarget() = e2.(Access).getTarget() - else - /* If it's a Call, compare qualifiers and only permit no-argument Calls. */ - if e1 instanceof Call - then - e1.(Call).getTarget() = e2.(Call).getTarget() and - e1.(Call).getNumberOfArguments() = 0 and - e2.(Call).getNumberOfArguments() = 0 and - if e1.(Call).hasQualifier() - then exprEq_propertyPermissive(e1.(Call).getQualifier(), e2.(Call).getQualifier()) - else any() - else - /* If it's a binaryOperation, compare op and recruse */ - if e1 instanceof BinaryOperation - then - e1.(BinaryOperation).getOperator() = e2.(BinaryOperation).getOperator() and - exprEq_propertyPermissive(e1.(BinaryOperation).getLeftOperand(), - e2.(BinaryOperation).getLeftOperand()) and - exprEq_propertyPermissive(e1.(BinaryOperation).getRightOperand(), - e2.(BinaryOperation).getRightOperand()) - else - // Otherwise fail (and permit the raising of a finding) - if e1 instanceof Literal - then e1.(Literal).getValue() = e2.(Literal).getValue() - else none() - ) -} - /** * An expression that is the subject of a mod-4 check. * ie `expr % 4 == 0` @@ -226,8 +182,7 @@ class ExprCheckCenturyComponent extends LogicalOrExpr { ExprCheckCenturyComponent() { exists(ExprCheckCenturyComponentDiv400 exprDiv400, ExprCheckCenturyComponentDiv100 exprDiv100 | this.getAnOperand() = exprDiv100 and - this.getAnOperand() = exprDiv400 and - exprEq_propertyPermissive(exprDiv100.getYearExpr(), exprDiv400.getYearExpr()) + this.getAnOperand() = exprDiv400 ) } @@ -252,8 +207,7 @@ final class ExprCheckLeapYearFormA extends ExprCheckLeapYear, LogicalAndExpr { ExprCheckLeapYearFormA() { exists(Expr e, ExprCheckCenturyComponent centuryCheck | e = moduloCheckEQ_0(this.getLeftOperand(), 4) and - centuryCheck = this.getAnOperand().getAChild*() and - exprEq_propertyPermissive(e, centuryCheck.getYearExpr()) + centuryCheck = this.getAnOperand().getAChild*() ) } } @@ -268,12 +222,7 @@ final class ExprCheckLeapYearFormB extends ExprCheckLeapYear, LogicalOrExpr { exists(VariableAccess va1, VariableAccess va2, VariableAccess va3 | va1 = moduloCheckEQ_0(this.getAnOperand(), 400) and va2 = moduloCheckNEQ_0(this.getAnOperand().(LogicalAndExpr).getAnOperand(), 100) and - va3 = moduloCheckEQ_0(this.getAnOperand().(LogicalAndExpr).getAnOperand(), 4) and - // The 400-leap year check may be offset by [1900,1970,2000]. - exists(Expr va1_subExpr | va1_subExpr = va1.getAChild*() | - exprEq_propertyPermissive(va1_subExpr, va2) and - exprEq_propertyPermissive(va2, va3) - ) + va3 = moduloCheckEQ_0(this.getAnOperand().(LogicalAndExpr).getAnOperand(), 4) ) } } @@ -410,6 +359,50 @@ class StructTmLeapYearFieldAccess extends LeapYearFieldAccess { } } +/** + * `stDate.wMonth == 2` + */ +class DateCheckMonthFebruary extends Operation { + DateCheckMonthFebruary() { + this.getOperator() = "==" and + this.getAnOperand() instanceof MonthFieldAccess and + this.getAnOperand().(Literal).getValue() = "2" + } + + Expr getDateQualifier() { result = this.getAnOperand().(MonthFieldAccess).getQualifier() } +} + +/** + * `stDate.wDay == 29` + */ +class DateCheckDay29 extends Operation { + DateCheckDay29() { + this.getOperator() = "==" and + this.getAnOperand() instanceof DayFieldAccess and + this.getAnOperand().(Literal).getValue() = "29" + } + + Expr getDateQualifier() { result = this.getAnOperand().(DayFieldAccess).getQualifier() } +} + +/** + * The combination of a February and Day 29 verification + * `stDate.wMonth == 2 && stDate.wDay == 29` + */ +class DateFebruary29Check extends Operation { + DateFebruary29Check() { + this.getOperator() = "&&" and + exists(DateCheckMonthFebruary checkFeb, DateCheckDay29 check29 | + checkFeb = this.getAnOperand() and + check29 = this.getAnOperand() + ) + } + + Expr getDateQualifier() { + result = this.getAnOperand().(DateCheckMonthFebruary).getDateQualifier() + } +} + /** * `Function` that includes an operation that is checking for leap year. */ @@ -425,7 +418,7 @@ class ChecksForLeapYearFunctionCall extends FunctionCall { } /** - * A `DataFlow` configuraiton for finding a variable access that would flow into + * A `DataFlow` configuration for finding a variable access that would flow into * a function call that includes an operation to check for leap year. */ private module LeapYearCheckFlowConfig implements DataFlow::ConfigSig { @@ -541,7 +534,12 @@ class TimeConversionFunction extends Function { "FileTimeToSystemTime", "SystemTimeToFileTime", "SystemTimeToTzSpecificLocalTime", "SystemTimeToTzSpecificLocalTimeEx", "TzSpecificLocalTimeToSystemTime", "TzSpecificLocalTimeToSystemTimeEx", "RtlLocalTimeToSystemTime", - "RtlTimeToSecondsSince1970", "_mkgmtime" + "RtlTimeToSecondsSince1970", "_mkgmtime", "SetSystemTime", "SystemTimeToVariantTime", + "VariantTimeToSystemTime", + // NOTE: mktime will normalize a Feb 29 on a non-leap year to Mar 1 silently, + "mktime", "_mktime32", "_mktime64", "VarUdateFromDate" ] + or + this.getQualifiedName().matches("GetDateFormat%") } } diff --git a/cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql b/cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql index 18ad003eb71f..0c8c8b3ce7d4 100644 --- a/cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql +++ b/cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql @@ -1,7 +1,7 @@ /** * @name Year field changed using an arithmetic operation without checking for leap year (AntiPattern 1) * @description A field that represents a year is being modified by an arithmetic operation, but no proper check for leap years can be detected afterwards. - * @kind problem + * @kind path-problem * @problem.severity warning * @id cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification * @precision medium @@ -11,98 +11,328 @@ import cpp import LeapYear +import semmle.code.cpp.controlflow.IRGuards +import semmle.code.cpp.dataflow.new.TaintTracking +import semmle.code.cpp.commons.DateTime /** - * Holds if there is no known leap-year verification for the given `YearWriteOp`. - * Binds the `var` argument to the qualifier of the `ywo` argument. - */ -bindingset[ywo] -predicate isYearModifedWithoutExplicitLeapYearCheck(Variable var, YearWriteOp ywo) { - exists(VariableAccess va, YearFieldAccess yfa | - yfa = ywo.getYearAccess() and - yfa.getQualifier() = va and - var.getAnAccess() = va and - // Avoid false positives - not ( - // If there is a local check for leap year after the modification - exists(LeapYearFieldAccess yfacheck | - yfacheck.getQualifier() = var.getAnAccess() and - yfacheck.isUsedInCorrectLeapYearCheck() and - yfacheck.getBasicBlock() = yfa.getBasicBlock().getASuccessor*() - ) + * Functions whose operations should never be considered a + * source or sink of a dangerous leap year operation. + * The general concept is to add conversion functions + * that convert one time type to another. Often + * other ignorable operation heuristics will filter these, + * but some cases, the simplest approach is to simply filter + * the function entirely. + * Note that flow through these functions should still be allowed + * we just cannot start or end flow from an operation to a + * year assignment in one of these functions. + */ +class IgnorableFunction extends Function { + IgnorableFunction() { + this instanceof TimeConversionFunction + or + // Helper utility in postgres with string time conversions + this.getName() = "DecodeISO8601Interval" + or + // helper utility for date conversions in qtbase + this.getName() = "adjacentDay" + or + // Windows API function that does timezone conversions + this.getName().matches("%SystemTimeToTzSpecificLocalTime%") + or + // Windows APIs that do time conversions + this.getName().matches("%localtime%\\_s%") + or + // Windows APIs that do time conversions + this.getName().matches("%SpecificLocalTimeToSystemTime%") + or + // postgres function for diffing timestamps, date for leap year + // is not applicable. + this.getName().toLowerCase().matches("%timestamp%age%") + or + // Reading byte streams often involves operations of some base, but that's + // not a real source of leap year issues. + this.getName().toLowerCase().matches("%read%bytes%") + or + // A postgres function for local time conversions + // conversion operations (from one time structure to another) are generally ignorable + this.getName() = "localsub" + or + // Hijri years are not applicable for gregorian leap year checks + this.getName().toLowerCase().matches("%hijri%") + or + this.getFile().getBaseName().toLowerCase().matches("%hijri%") + or + // Persian calendar conversions are not applicable for gregorian leap year checks + this.getName().toLowerCase().matches("%persian%") + or + this.getFile().getBaseName().toLowerCase().matches("%persian%") + or + // misc. from string/char converters heuristic + this.getName() + .toLowerCase() + .matches(["%char%to%", "%string%to%", "%from%char%", "%from%string%"]) + } +} + +/** + * The set of expressions which are ignorable; either because they seem to not be part of a year mutation, + * or because they seem to be a conversion pattern of mapping date scalars. + */ +abstract class IgnorableOperation extends Expr { } + +class IgnorableExprRem extends IgnorableOperation instanceof RemExpr { } + +/** + * Anything involving an operation with 10, 100, 1000, 10000 is often a sign of conversion + * or atoi. + */ +class IgnorableExpr10MulipleComponent extends IgnorableOperation { + IgnorableExpr10MulipleComponent() { + this.(Operation).getAnOperand().getValue().toInt() in [10, 100, 1000, 10000] + or + exists(AssignOperation a | a.getRValue() = this | + a.getRValue().getValue().toInt() in [10, 100, 1000, 10000] + ) + } +} + +/** + * Anything involving a sub expression with char literal 48, ignore as a likely string conversion + * e.g., X - '0' + */ +class IgnorableExpr48Mapping extends IgnorableOperation { + IgnorableExpr48Mapping() { + this.(SubExpr).getRightOperand().getValue().toInt() = 48 + or + exists(AssignSubExpr e | e.getRValue() = this | e.getRValue().getValue().toInt() = 48) + } +} + +class IgnorableCharLiteralArithmetic extends IgnorableOperation { + IgnorableCharLiteralArithmetic() { + exists(this.(BinaryArithmeticOperation).getAnOperand().(TextLiteral).getValue()) + or + exists(AssignArithmeticOperation e | e.getRValue() = this | + exists(this.(TextLiteral).getValue()) + ) + } +} + +/** + * Constants often used in date conversions (from one date data type to another) + * Numerous examples exist, like 1900 or 2000 that convert years from one + * representation to another. + * Also '0' is sometimes observed as an atoi style conversion. + */ +bindingset[c] +predicate isLikelyConversionConstant(int c) { + exists(int i | i = c.abs() | + //| i >= 100) + i = 146097 or // days in 400-year Gregorian cycle + i = 36524 or // days in 100-year Gregorian subcycle + i = 1461 or // days in 4-year cycle (incl. 1 leap) + i = 32044 or // Fliegel–van Flandern JDN epoch shift + i = 1721425 or // JDN of 0001‑01‑01 (Gregorian) + i = 1721119 or // alt epoch offset + i = 2400000 or // MJD → JDN conversion + i = 2400001 or // alt MJD → JDN conversion + i = 2141 or // fixed‑point month/day extraction + i = 65536 or // observed in some conversions + i = 7834 or // observed in some conversions + i = 256 or // observed in some conversions + i = 292275056 or // qdatetime.h Qt Core year range first year constant + i = 292278994 or // qdatetime.h Qt Core year range last year constant + i = 1601 or // Windows FILETIME epoch start year + i = 1970 or // Unix epoch start year + i = 70 or // Unix epoch start year short form + i = 1899 or // Observed in uses with 1900 to address off by one scenarios + i = 1900 or // Used when converting a 2 digit year + i = 2000 or // Used when converting a 2 digit year + i = 1400 or // Hijri base year, used when converting a 2 digit year + i = 1980 or // FAT filesystem epoch start year + i = 227013 or // constant observed for Hirji year conversion, and Hirji years are not applicable for gregorian leap year + i = 10631 or // constant observed for Hirji year conversion, and Hirji years are not applicable for gregorian leap year + i = 0 + ) +} + +/** + * Some constants indicate conversion that are ignorable, e.g., + * julian to gregorian conversion or conversions from linux time structs + * that start at 1900, etc. + */ +class IgnorableConstantArithmetic extends IgnorableOperation { + IgnorableConstantArithmetic() { + exists(int i | isLikelyConversionConstant(i) | + this.(Operation).getAnOperand().getValue().toInt() = i or - // If there is a data flow from the variable that was modified to a function that seems to check for leap year - exists(VariableAccess source, ChecksForLeapYearFunctionCall fc | - source = var.getAnAccess() and - LeapYearCheckFlow::flow(DataFlow::exprNode(source), DataFlow::exprNode(fc.getAnArgument())) + exists(AssignArithmeticOperation a | this = a.getRValue() | + a.getRValue().getValue().toInt() = i ) + ) + } +} + +// If a unary minus assume it is some sort of conversion +class IgnorableUnaryMinus extends IgnorableOperation { + IgnorableUnaryMinus() { + this instanceof UnaryMinusExpr + or + this.(Operation).getAnOperand() instanceof UnaryMinusExpr + } +} + +class OperationAsArgToIgnorableFunction extends IgnorableOperation { + OperationAsArgToIgnorableFunction() { + exists(Call c | + c.getAnArgument().getAChild*() = this and + c.getTarget() instanceof IgnorableFunction + ) + } +} + +/** + * Literal OP literal means the result is constant/known + * and the operation is basically ignorable (it's not a real operation but + * probably one visual simplicity what it means). + */ +class ConstantBinaryArithmeticOperation extends IgnorableOperation { + ConstantBinaryArithmeticOperation() { + this instanceof BinaryArithmeticOperation and + this.(BinaryArithmeticOperation).getLeftOperand() instanceof Literal and + this.(BinaryArithmeticOperation).getRightOperand() instanceof Literal + } +} + +class IgnorableBinaryBitwiseOperation extends IgnorableOperation instanceof BinaryBitwiseOperation { +} + +class IgnorableUnaryBitwiseOperation extends IgnorableOperation instanceof UnaryBitwiseOperation { } + +class IgnorableAssignmentBitwiseOperation extends IgnorableOperation instanceof AssignBitwiseOperation +{ } + +/** + * Any arithmetic operation where one of the operands is a pointer or char type, ignore it + */ +class IgnorablePointerOrCharArithmetic extends IgnorableOperation { + IgnorablePointerOrCharArithmetic() { + this instanceof BinaryArithmeticOperation and + ( + this.(BinaryArithmeticOperation).getAnOperand().getUnspecifiedType() instanceof PointerType or - // If there is a data flow from the field that was modified to a function that seems to check for leap year - exists(VariableAccess vacheck, YearFieldAccess yfacheck, ChecksForLeapYearFunctionCall fc | - vacheck = var.getAnAccess() and - yfacheck.getQualifier() = vacheck and - LeapYearCheckFlow::flow(DataFlow::exprNode(yfacheck), DataFlow::exprNode(fc.getAnArgument())) - ) + this.(BinaryArithmeticOperation).getAnOperand().getUnspecifiedType() instanceof CharType or - // If there is a successor or predecessor that sets the month or day to a fixed value - exists(FieldAccess mfa, AssignExpr ae, int val | - mfa instanceof MonthFieldAccess or mfa instanceof DayFieldAccess - | - mfa.getQualifier() = var.getAnAccess() and - mfa.isModified() and - ( - mfa.getBasicBlock() = yfa.getBasicBlock().getASuccessor*() or - yfa.getBasicBlock() = mfa.getBasicBlock().getASuccessor+() - ) and - ae = mfa.getEnclosingElement() and - ae.getAnOperand().getValue().toInt() = val - ) + // Operations on calls to functions that accept char or char* + this.(BinaryArithmeticOperation) + .getAnOperand() + .(Call) + .getAnArgument() + .getUnspecifiedType() + .stripType() instanceof CharType + or + // Operations on calls to functions named like "strlen", "wcslen", etc + // NOTE: workaround for cases where the wchar_t type is not a char, but an unsigned short + // unclear if there is a best way to filter cases like these out based on type info. + this.(BinaryArithmeticOperation).getAnOperand().(Call).getTarget().getName().matches("%len%") ) - ) + or + exists(AssignArithmeticOperation a | a.getRValue() = this | + a.getAnOperand().getUnspecifiedType() instanceof PointerType + or + a.getAnOperand().getUnspecifiedType() instanceof CharType + or + // Operations on calls to functions that accept char or char* + a.getAnOperand().(Call).getAnArgument().getUnspecifiedType().stripType() instanceof CharType + or + // Operations on calls to functions named like "strlen", "wcslen", etc + this.(BinaryArithmeticOperation).getAnOperand().(Call).getTarget().getName().matches("%len%") + ) + } } /** - * The set of all write operations to the Year field of a date struct. + * An expression that is a candidate source for an dataflow configuration for an Operation that could flow to a Year field. */ -abstract class YearWriteOp extends Operation { - /** Extracts the access to the Year field */ - abstract YearFieldAccess getYearAccess(); - - /** Get the expression which represents the new value. */ - abstract Expr getMutationExpr(); +predicate isOperationSourceCandidate(Expr e) { + not e instanceof IgnorableOperation and + not e.getEnclosingFunction() instanceof IgnorableFunction and + ( + e instanceof SubExpr + or + e instanceof AddExpr + or + e instanceof CrementOperation + or + e instanceof AssignSubExpr + or + e instanceof AssignAddExpr + ) } /** - * A unary operation (Crement) performed on a Year field. + * A dataflow that tracks an ignorable operation (eg. bitwise op) to a operation source, so we may disqualify it. */ -class YearWriteOpUnary extends YearWriteOp, UnaryOperation { - YearWriteOpUnary() { this.getOperand() instanceof YearFieldAccess } +module IgnorableOperationToOperationSourceCandidateConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node n) { n.asExpr() instanceof IgnorableOperation } - override YearFieldAccess getYearAccess() { result = this.getOperand() } + predicate isSink(DataFlow::Node n) { isOperationSourceCandidate(n.asExpr()) } - override Expr getMutationExpr() { result = this } + // looking for sources and sinks in the same function + DataFlow::FlowFeature getAFeature() { + result instanceof DataFlow::FeatureEqualSourceSinkCallContext + } } +module IgnorableOperationToOperationSourceCandidateFlow = + TaintTracking::Global; + /** - * An assignment operation or mutation on the Year field of a date object. + * The set of all expressions which is a candidate expression and also does not flow from to to some ignorable expression (eg. bitwise op) + * ``` + * a = something <<< 2; + * myDate.year = a + 1; // invalid + * ... + * a = someDate.year + 1; + * myDate.year = a; // valid + * ``` */ -class YearWriteOpAssignment extends YearWriteOp, Assignment { - YearWriteOpAssignment() { this.getLValue() instanceof YearFieldAccess } +class OperationSource extends Expr { + OperationSource() { + isOperationSourceCandidate(this) and + // If the candidate came from an ignorable operation, ignore the candidate + // NOTE: we cannot easily flow the candidate to an ignorable operation as that can + // be tricky in practice, e.g., a mod operation on a year would be part of a leap year check + // but a mod operation ending in a year is more indicative of something to ignore (a conversion) + not exists(IgnorableOperationToOperationSourceCandidateFlow::PathNode sink | + sink.getNode().asExpr() = this and + sink.isSink() + ) + } +} - override YearFieldAccess getYearAccess() { result = this.getLValue() } +class YearFieldAssignmentNode extends DataFlow::Node { + YearFieldAccess access; - override Expr getMutationExpr() { - // Note: may need to use DF analysis to pull out the original value, - // if there is excessive false positives. - if this.getOperator() = "=" - then - exists(DataFlow::Node source, DataFlow::Node sink | - sink.asExpr() = this.getRValue() and - OperationToYearAssignmentFlow::flow(source, sink) and - result = source.asExpr() + YearFieldAssignmentNode() { + not this.getEnclosingCallable().getUnderlyingCallable() instanceof IgnorableFunction and + ( + this.asDefinition().(Assignment).getLValue() = access + or + this.asDefinition().(CrementOperation).getOperand() = access + or + exists(Call c | c.getAnArgument() = access and this.asDefiningArgument() = access) + or + exists(Call c, AddressOfExpr aoe | + c.getAnArgument() = aoe and + aoe.getOperand() = access and + this.asDefiningArgument() = aoe ) - else result = this + ) } + + YearFieldAccess getYearFieldAccess() { result = access } } /** @@ -110,32 +340,419 @@ class YearWriteOpAssignment extends YearWriteOp, Assignment { * to the Year field of a date object. */ module OperationToYearAssignmentConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node n) { - not n.asExpr() instanceof Access and - not n.asExpr() instanceof Literal + predicate isSource(DataFlow::Node n) { n.asExpr() instanceof OperationSource } + + predicate isSink(DataFlow::Node n) { n instanceof YearFieldAssignmentNode } + + predicate isBarrier(DataFlow::Node n) { + exists(ArrayExpr arr | arr.getArrayOffset() = n.asExpr()) + or + n.getType().getUnspecifiedType() instanceof PointerType + or + n.getType().getUnspecifiedType() instanceof CharType + or + // If a type resembles "string" ignore flow (likely string conversion, currently ignored) + n.getType().getUnspecifiedType().stripType().getName().toLowerCase().matches("%string%") + or + n.asExpr() instanceof IgnorableOperation + or + // Flowing into variables that indicate likely non-gregorian years are barriers + // e.g., names similar to hijri, persian, lunar, chinese, etc. + exists(Variable v | + v.getName().toLowerCase().matches(["%hijri%", "%persian%", "%lunar%", "%chinese%"]) and + v.getAnAccess() = [n.asIndirectExpr(), n.asExpr()] + ) + or + isLeapYearCheckSink(n) } - predicate isSink(DataFlow::Node n) { + /** Block flow out of an operation source to get the "closest" operation to the sink */ + predicate isBarrierIn(DataFlow::Node n) { isSource(n) } + + predicate isBarrierOut(DataFlow::Node n) { isSink(n) } +} + +module OperationToYearAssignmentFlow = TaintTracking::Global; + +predicate isLeapYearCheckSink(DataFlow::Node sink) { + exists(LeapYearGuardCondition lgc | + lgc.checkedYearAccess() = [sink.asExpr(), sink.asIndirectExpr()] + ) + or + isLeapYearCheckCall(_, [sink.asExpr(), sink.asIndirectExpr()]) +} + +/** + * A flow configuration from a Year field access to some Leap year check or guard + */ +module YearAssignmentToLeapYearCheckConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source instanceof YearFieldAssignmentNode } + + predicate isSink(DataFlow::Node sink) { isLeapYearCheckSink(sink) } + + predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { + // flow from a YearFieldAccess to the qualifier + node2.asExpr() = node1.asExpr().(YearFieldAccess).getQualifier*() + or + // Pass through any intermediate struct + exists(Assignment a, DataFlow::PostUpdateNode pun | + a.getLValue().(YearFieldAccess).getQualifier*() = pun.getPreUpdateNode().asExpr() and + a.getRValue() = node1.asExpr() and + node2.asExpr() = a.getLValue().(YearFieldAccess).getQualifier*() + ) + or + // flow from a year access qualifier to a year field + exists(YearFieldAccess yfa | node2.asExpr() = yfa and node1.asExpr() = yfa.getQualifier()) + or + // in cases of x.year = x and the x is checked, but the year x.year isn't directly + // flow from a year assignment node to an RHS if it is an assignment + exists(YearFieldAssignmentNode yfan | + node1 = yfan and + node2.asExpr() = yfan.asDefinition().(Assignment).getRValue() + ) + } + + /** + * Enforcing the check must occur in the same call context as the source, + * i.e., do not return from the source function and check in a caller. + */ + DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext } +} + +module YearAssignmentToLeapYearCheckFlow = + TaintTracking::Global; + +/** Does there exist a flow from the given YearFieldAccess to a Leap Year check or guard? */ +predicate isYearModifiedWithCheck(YearFieldAssignmentNode n) { + exists(YearAssignmentToLeapYearCheckFlow::PathNode src | + src.isSource() and + src.getNode() = n + ) + or + // If the time flows to a time conversion whose value/result is checked, + // assume the leap year is being handled. + exists(YearAssignmentToCheckedTimeConversionFlow::PathNode timeQualSrc | + timeQualSrc.isSource() and + timeQualSrc.getNode() = n + ) +} + +/** + * An expression which checks the value of a Month field `a->month == 1`. + */ +class MonthEqualityCheck extends EqualityOperation { + MonthEqualityCheck() { this.getAnOperand() instanceof MonthFieldAccess } + + Expr getExprCompared() { + exists(Expr e | + e = this.getAnOperand() and + not e instanceof MonthFieldAccess and + result = e + ) + } +} + +class MonthEqualityCheckGuard extends GuardCondition instanceof MonthEqualityCheck { } + +/** + * Verifies if the expression is guarded by a check on the Month property of a date struct, that is NOT February. + */ +bindingset[e] +pragma[inline_late] +predicate isControlledByMonthEqualityCheckNonFebruary(Expr e) { + exists(MonthEqualityCheckGuard monthGuard | + monthGuard.controls(e.getBasicBlock(), true) and + not monthGuard.(MonthEqualityCheck).getExprCompared().getValueText() = "2" + ) +} + +/** + * Flow from a year field access through a time conversion function + * where the call's result is used to check error. The result must + * be used as a guard for an if or ternary operator. If so, + * assume some sort of error handling is occurring that could be used + * to detect bad dates due to leap year. + */ +module YearAssignmentToCheckedTimeConversionConfig implements DataFlow::StateConfigSig { + class FlowState = boolean; + + predicate isSource(DataFlow::Node source, FlowState state) { + source instanceof YearFieldAssignmentNode and + state = false + } + + predicate isSink(DataFlow::Node sink, FlowState state) { + state = true and + ( + exists(IfStmt ifs | ifs.getCondition().getAChild*() = [sink.asExpr(), sink.asIndirectExpr()]) + or + exists(ConditionalExpr ce | + ce.getCondition().getAChild*() = [sink.asExpr(), sink.asIndirectExpr()] + ) + or + exists(Loop l | l.getCondition().getAChild*() = [sink.asExpr(), sink.asIndirectExpr()]) + ) + } + + predicate isAdditionalFlowStep( + DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2 + ) { + state1 in [true, false] and + state2 = true and + exists(Call c | + c.getTarget() instanceof TimeConversionFunction and + c.getAnArgument().getAChild*() = [node1.asExpr(), node1.asIndirectExpr()] and + node2.asExpr() = c + ) + } + + predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { + // flow from a YearFieldAccess to the qualifier + node2.asExpr() = node1.asExpr().(YearFieldAccess).getQualifier*() + or + node1.(YearFieldAssignmentNode).getYearFieldAccess().getQualifier() = node2.asExpr() + or + // Pass through any intermediate struct + exists(Assignment a, DataFlow::PostUpdateNode pun | + a.getLValue().(YearFieldAccess).getQualifier*() = pun.getPreUpdateNode().asExpr() and + a.getRValue() = node1.asExpr() and + node2.asExpr() = a.getLValue().(YearFieldAccess).getQualifier*() + ) + or + // flow from a year access qualifier to a year field + exists(YearFieldAccess yfa | node2.asExpr() = yfa and node1.asExpr() = yfa.getQualifier()) + } + + DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext } +} + +module YearAssignmentToCheckedTimeConversionFlow = + DataFlow::GlobalWithState; + +/** + * Finds flow from a parameter of a function to a leap year check. + * This is necessary to handle for scenarios like this: + * + * year = DANGEROUS_OP // source + * isLeap = isLeapYear(year); + * // logic based on isLeap + * struct.year = year; // sink + * + * In this case, we may flow a dangerous op to a year assignment, failing + * to barrier the flow through a leap year check, as the leap year check + * is nested, and dataflow does not progress down into the check and out. + * Instead, the point of this flow is to detect isLeapYear's argument + * is checked for leap year, making the isLeapYear call a barrier for + * the dangerous flow if we flow through the parameter identified to + * be checked. + */ +module ParameterToLeapYearCheckConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { exists(source.asParameter()) } + + predicate isSink(DataFlow::Node sink) { + exists(LeapYearGuardCondition lgc | + lgc.checkedYearAccess() = [sink.asExpr(), sink.asIndirectExpr()] + ) + } + + predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { + // flow from a YearFieldAccess to the qualifier + node2.asExpr() = node1.asExpr().(YearFieldAccess).getQualifier*() + or + // flow from a year access qualifier to a year field + exists(YearFieldAccess yfa | node2.asExpr() = yfa and node1.asExpr() = yfa.getQualifier()) + } + + /** + * Enforcing the check must occur in the same call context as the source, + * i.e., do not return from the source function and check in a caller. + */ + DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext } +} + +// NOTE: I do not believe taint flow is necessary here as we should +// be flowing directyly from some parameter to a leap year check. +module ParameterToLeapYearCheckFlow = DataFlow::Global; + +predicate isLeapYearCheckCall(Call c, Expr arg) { + exists(ParameterToLeapYearCheckFlow::PathNode src, Function f, int i | + src.isSource() and + f.getParameter(i) = src.getNode().asParameter() and + c.getTarget() = f and + c.getArgument(i) = arg + ) +} + +class LeapYearGuardCondition extends GuardCondition { + Expr yearSinkDiv4; + Expr yearSinkDiv100; + Expr yearSinkDiv400; + + LeapYearGuardCondition() { + exists( + LogicalAndExpr andExpr, LogicalOrExpr orExpr, GuardCondition div4Check, + GuardCondition div100Check, GuardCondition div400Check, GuardValue gv + | + // Cannonical case: + // form: `(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)` + // or : `!((year % 4 == 0) && (year % 100 != 0 || year % 400 == 0))` + // Also accepting `((year & 3) == 0) && (year % 100 != 0 || year % 400 == 0)` + this = andExpr and + andExpr.hasOperands(div4Check, orExpr) and + orExpr.hasOperands(div100Check, div400Check) and + ( + exists(RemExpr e | + div4Check.comparesEq(e, 0, true, gv) and + e.getRightOperand().getValue().toInt() = 4 and + yearSinkDiv4 = e.getLeftOperand() + ) + or + exists(BitwiseAndExpr e | + div4Check.comparesEq(e, 0, true, gv) and + e.getRightOperand().getValue().toInt() = 3 and + yearSinkDiv4 = e.getLeftOperand() + ) + ) and + exists(RemExpr e | + div100Check.comparesEq(e, 0, false, gv) and + e.getRightOperand().getValue().toInt() = 100 and + yearSinkDiv100 = e.getLeftOperand() + ) and + exists(RemExpr e | + div400Check.comparesEq(e, 0, true, gv) and + e.getRightOperand().getValue().toInt() = 400 and + yearSinkDiv400 = e.getLeftOperand() + ) + or + // Inverted logic case: + // `year % 4 != 0 || (year % 100 == 0 && year % 400 != 0)` + // or `year & 3 != 0 || (year % 100 == 0 && year % 400 != 0)` + this = orExpr and + orExpr.hasOperands(div4Check, andExpr) and + andExpr.hasOperands(div100Check, div400Check) and + ( + exists(RemExpr e | + div4Check.comparesEq(e, 0, false, gv) and + e.getRightOperand().getValue().toInt() = 4 and + yearSinkDiv4 = e.getLeftOperand() + ) + or + exists(BitwiseAndExpr e | + div4Check.comparesEq(e, 0, false, gv) and + e.getRightOperand().getValue().toInt() = 3 and + yearSinkDiv4 = e.getLeftOperand() + ) + ) and + exists(RemExpr e | + div100Check.comparesEq(e, 0, true, gv) and + e.getRightOperand().getValue().toInt() = 100 and + yearSinkDiv100 = e.getLeftOperand() + ) and + exists(RemExpr e | + div400Check.comparesEq(e, 0, false, gv) and + e.getRightOperand().getValue().toInt() = 400 and + yearSinkDiv400 = e.getLeftOperand() + ) + ) + } + + Expr getYearSinkDiv4() { result = yearSinkDiv4 } + + Expr getYearSinkDiv100() { result = yearSinkDiv100 } + + Expr getYearSinkDiv400() { result = yearSinkDiv400 } + + /** + * The variable access that is used in all 3 components of the leap year check + * e.g., see getYearSinkDiv4/100/400.. + * If a field access is used, the qualifier and the field access are both returned + * in checked condition. + * NOTE: if the year is not checked using the same access in all 3 components, no result is returned. + * The typical case observed is a consistent variable access is used. If not, this may indicate a bug. + * We could check more accurately with a dataflow analysis, but this is likely sufficient for now. + */ + VariableAccess checkedYearAccess() { + exists(Variable var | + ( + this.getYearSinkDiv4().getAChild*() = var.getAnAccess() and + this.getYearSinkDiv100().getAChild*() = var.getAnAccess() and + this.getYearSinkDiv400().getAChild*() = var.getAnAccess() and + result = var.getAnAccess() and + ( + result = this.getYearSinkDiv4().getAChild*() or + result = this.getYearSinkDiv100().getAChild*() or + result = this.getYearSinkDiv400().getAChild*() + ) + ) + ) + } +} + +/** + * A difficult case to detect is if a year modification is tied to a month or day modification + * and the month or day is safe for leap year. + * e.g., + * year++; + * month = 1; + * // alternative: day = 15; + * ... values eventually used in the same time struct + * If this is even more challenging if the struct the values end up in are not + * local (set inter-procedurally). + * This flow flows constants 1-31 to a month or day assignment. + * It is assumed a user of this flow will check if the month/day source and month/day sink + * are in the same basic blocks as a year modification source and a year modification sink. + * It is also assumed a user will check if the constant source is a value that is ignorable + * e.g., if it is 2 and the sink is a month assignment, then it isn't ignorable or + * if the value is < 27 and is a day assignment, it is likely ignorable + * + * Obviously this does not handle all conditions (e.g., the month set in another block). + * It is meant to capture the most common cases of false positives. + */ +module CandidateConstantToDayOrMonthAssignmentConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source.asExpr().getValue().toInt() in [1 .. 31] } + + predicate isSink(DataFlow::Node sink) { exists(Assignment a | - a.getLValue() instanceof YearFieldAccess and - a.getRValue() = n.asExpr() + (a.getLValue() instanceof MonthFieldAccess or a.getLValue() instanceof DayFieldAccess) and + a.getRValue() = sink.asExpr() ) } } -module OperationToYearAssignmentFlow = DataFlow::Global; +// NOTE: only data flow here (no taint tracking) as we want the exact +// constant flowing to the month assignment +module CandidateConstantToDayOrMonthAssignmentFlow = + DataFlow::Global; + +import OperationToYearAssignmentFlow::PathGraph -from Variable var, YearWriteOp ywo, Expr mutationExpr +from OperationToYearAssignmentFlow::PathNode src, OperationToYearAssignmentFlow::PathNode sink where - mutationExpr = ywo.getMutationExpr() and - isYearModifedWithoutExplicitLeapYearCheck(var, ywo) and - not isNormalizationOperation(mutationExpr) and - not ywo instanceof AddressOfExpr and - not exists(Call c, TimeConversionFunction f | f.getACallToThisFunction() = c | - c.getAnArgument().getAChild*() = var.getAnAccess() and - ywo.getASuccessor*() = c + OperationToYearAssignmentFlow::flowPath(src, sink) and + not isYearModifiedWithCheck(sink.getNode()) and + not isControlledByMonthEqualityCheckNonFebruary(sink.getNode().asExpr()) and + // Check if a month is set in the same block as the year operation source + // and the month value would indicate its set to any other month than february. + // Finds if the source year node is in the same block as a source month block + // and if the same for the sinks. + not exists(DataFlow::Node dayOrMonthValSrc, DataFlow::Node dayOrMonthValSink, Assignment a | + CandidateConstantToDayOrMonthAssignmentFlow::flow(dayOrMonthValSrc, dayOrMonthValSink) and + a.getRValue() = dayOrMonthValSink.asExpr() and + ( + // The source of the day is set in the same block as the source for the year + // or the source for the day is set in the same block as the sink for the year + dayOrMonthValSrc.getBasicBlock() = src.getNode().getBasicBlock() or + dayOrMonthValSrc.getBasicBlock() = sink.getNode().getBasicBlock() + ) and + dayOrMonthValSink.getBasicBlock() = sink.getNode().getBasicBlock() and + ( + a.getLValue() instanceof MonthFieldAccess and + dayOrMonthValSrc.asExpr().getValue().toInt() != 2 + or + a.getLValue() instanceof DayFieldAccess and + dayOrMonthValSrc.asExpr().getValue().toInt() <= 27 + ) ) -select ywo, - "$@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found.", - ywo.getEnclosingFunction(), ywo.getEnclosingFunction().toString(), - ywo.getYearAccess().getTarget(), ywo.getYearAccess().getTarget().toString(), var, var.toString() +// TODO: all days to sink are safe? +select sink, src, sink, + "Year field has been modified, but no appropriate check for LeapYear was found." diff --git a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.expected b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.expected index 555002b40867..d4393c4d8eda 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.expected +++ b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.expected @@ -1,8 +1,155 @@ -| test.cpp:617:2:617:11 | ... ++ | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:611:6:611:32 | AntiPattern_1_year_addition | AntiPattern_1_year_addition | test.cpp:12:7:12:11 | wYear | wYear | test.cpp:613:13:613:14 | st | st | -| test.cpp:634:2:634:25 | ... += ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:627:6:627:32 | AntiPattern_simple_addition | AntiPattern_simple_addition | test.cpp:12:7:12:11 | wYear | wYear | test.cpp:629:13:629:14 | st | st | -| test.cpp:763:2:763:19 | ... ++ | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:756:6:756:40 | AntiPattern_year_addition_struct_tm | AntiPattern_year_addition_struct_tm | test.cpp:56:6:56:12 | tm_year | tm_year | test.cpp:759:12:759:19 | timeinfo | timeinfo | -| test.cpp:800:2:800:40 | ... = ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:791:6:791:23 | FalseNegativeTests | FalseNegativeTests | test.cpp:56:6:56:12 | tm_year | tm_year | test.cpp:793:12:793:19 | timeinfo | timeinfo | -| test.cpp:803:2:803:43 | ... = ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:791:6:791:23 | FalseNegativeTests | FalseNegativeTests | test.cpp:56:6:56:12 | tm_year | tm_year | test.cpp:793:12:793:19 | timeinfo | timeinfo | -| test.cpp:808:2:808:24 | ... = ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:791:6:791:23 | FalseNegativeTests | FalseNegativeTests | test.cpp:12:7:12:11 | wYear | wYear | test.cpp:794:13:794:14 | st | st | -| test.cpp:811:2:811:33 | ... = ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:791:6:791:23 | FalseNegativeTests | FalseNegativeTests | test.cpp:12:7:12:11 | wYear | wYear | test.cpp:794:13:794:14 | st | st | -| test.cpp:850:3:850:36 | ... = ... | $@: Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found. | test.cpp:818:6:818:23 | tp_intermediaryVar | tp_intermediaryVar | test.cpp:56:6:56:12 | tm_year | tm_year | test.cpp:70:18:70:19 | tm | tm | +#select +| test.cpp:422:2:422:14 | ... += ... | test.cpp:422:2:422:14 | ... += ... | test.cpp:422:2:422:14 | ... += ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:440:2:440:11 | ... ++ | test.cpp:440:2:440:11 | ... ++ | test.cpp:440:2:440:11 | ... ++ | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:456:2:456:12 | ... ++ | test.cpp:456:2:456:12 | ... ++ | test.cpp:456:2:456:12 | ... ++ | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:681:2:681:23 | ... += ... | test.cpp:681:2:681:23 | ... += ... | test.cpp:681:2:681:23 | ... += ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:769:2:769:23 | ... -= ... | test.cpp:769:2:769:23 | ... -= ... | test.cpp:769:2:769:23 | ... -= ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:813:2:813:40 | ... = ... | test.cpp:813:21:813:40 | ... + ... | test.cpp:813:2:813:40 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:818:2:818:24 | ... = ... | test.cpp:818:13:818:24 | ... + ... | test.cpp:818:2:818:24 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:954:3:954:25 | ... = ... | test.cpp:954:14:954:25 | ... + ... | test.cpp:954:3:954:25 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:972:3:972:12 | ... ++ | test.cpp:972:3:972:12 | ... ++ | test.cpp:972:3:972:12 | ... ++ | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1077:2:1077:11 | ... ++ | test.cpp:1077:2:1077:11 | ... ++ | test.cpp:1077:2:1077:11 | ... ++ | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1097:16:1097:23 | increment_arg output argument | test.cpp:1085:2:1085:4 | ... ++ | test.cpp:1097:16:1097:23 | increment_arg output argument | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1101:27:1101:35 | increment_arg_by_pointer output argument | test.cpp:1089:2:1089:7 | ... ++ | test.cpp:1101:27:1101:35 | increment_arg_by_pointer output argument | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1155:2:1155:26 | ... = ... | test.cpp:1155:14:1155:26 | ... - ... | test.cpp:1155:2:1155:26 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1206:2:1206:19 | ... = ... | test.cpp:1204:2:1204:15 | ... += ... | test.cpp:1206:2:1206:19 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1245:2:1245:28 | ... = ... | test.cpp:1245:16:1245:28 | ... + ... | test.cpp:1245:2:1245:28 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1259:2:1259:28 | ... = ... | test.cpp:1259:16:1259:28 | ... + ... | test.cpp:1259:2:1259:28 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1293:2:1293:17 | ... = ... | test.cpp:1384:12:1384:17 | ... + ... | test.cpp:1293:2:1293:17 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1293:2:1293:17 | ... = ... | test.cpp:1398:9:1398:16 | ... + ... | test.cpp:1293:2:1293:17 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1293:2:1293:17 | ... = ... | test.cpp:1410:9:1410:16 | ... + ... | test.cpp:1293:2:1293:17 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1467:2:1467:15 | ... = ... | test.cpp:1464:2:1464:10 | ... += ... | test.cpp:1467:2:1467:15 | ... = ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1497:2:1497:22 | ... += ... | test.cpp:1497:2:1497:22 | ... += ... | test.cpp:1497:2:1497:22 | ... += ... | Year field has been modified, but no appropriate check for LeapYear was found. | +| test.cpp:1505:2:1505:22 | ... += ... | test.cpp:1505:2:1505:22 | ... += ... | test.cpp:1505:2:1505:22 | ... += ... | Year field has been modified, but no appropriate check for LeapYear was found. | +edges +| test.cpp:813:21:813:40 | ... + ... | test.cpp:813:2:813:40 | ... = ... | provenance | | +| test.cpp:818:13:818:24 | ... + ... | test.cpp:818:2:818:24 | ... = ... | provenance | | +| test.cpp:854:7:854:31 | ... = ... | test.cpp:857:3:857:36 | ... = ... | provenance | | +| test.cpp:854:14:854:31 | ... + ... | test.cpp:854:7:854:31 | ... = ... | provenance | | +| test.cpp:854:35:854:40 | -- ... | test.cpp:857:3:857:36 | ... = ... | provenance | | +| test.cpp:954:14:954:25 | ... + ... | test.cpp:954:3:954:25 | ... = ... | provenance | | +| test.cpp:1084:26:1084:26 | *x | test.cpp:1097:16:1097:23 | increment_arg output argument | provenance | | +| test.cpp:1085:2:1085:4 | ... ++ | test.cpp:1084:26:1084:26 | *x | provenance | | +| test.cpp:1088:37:1088:37 | *x | test.cpp:1101:27:1101:35 | increment_arg_by_pointer output argument | provenance | | +| test.cpp:1089:2:1089:7 | ... ++ | test.cpp:1088:37:1088:37 | *x | provenance | | +| test.cpp:1155:14:1155:26 | ... - ... | test.cpp:1155:2:1155:26 | ... = ... | provenance | | +| test.cpp:1185:2:1185:15 | ... += ... | test.cpp:1187:2:1187:19 | ... = ... | provenance | | +| test.cpp:1204:2:1204:15 | ... += ... | test.cpp:1206:2:1206:19 | ... = ... | provenance | | +| test.cpp:1225:16:1225:28 | ... + ... | test.cpp:1225:2:1225:28 | ... = ... | provenance | | +| test.cpp:1231:16:1231:28 | ... + ... | test.cpp:1231:2:1231:28 | ... = ... | provenance | | +| test.cpp:1238:16:1238:28 | ... + ... | test.cpp:1238:2:1238:28 | ... = ... | provenance | | +| test.cpp:1245:16:1245:28 | ... + ... | test.cpp:1245:2:1245:28 | ... = ... | provenance | | +| test.cpp:1252:16:1252:28 | ... + ... | test.cpp:1252:2:1252:28 | ... = ... | provenance | | +| test.cpp:1259:16:1259:28 | ... + ... | test.cpp:1259:2:1259:28 | ... = ... | provenance | | +| test.cpp:1265:16:1265:28 | ... + ... | test.cpp:1265:2:1265:28 | ... = ... | provenance | | +| test.cpp:1279:14:1279:26 | ... + ... | test.cpp:1279:2:1279:26 | ... = ... | provenance | | +| test.cpp:1290:20:1290:23 | year | test.cpp:1293:2:1293:17 | ... = ... | provenance | | +| test.cpp:1303:15:1303:22 | ... + ... | test.cpp:1303:3:1303:22 | ... = ... | provenance | | +| test.cpp:1308:12:1308:17 | ... + ... | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1317:15:1317:22 | ... + ... | test.cpp:1317:3:1317:22 | ... = ... | provenance | | +| test.cpp:1327:3:1327:20 | ... = ... | test.cpp:1329:12:1329:18 | yeartmp | provenance | | +| test.cpp:1327:13:1327:20 | ... + ... | test.cpp:1327:3:1327:20 | ... = ... | provenance | | +| test.cpp:1329:12:1329:18 | yeartmp | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1342:15:1342:27 | ... + ... | test.cpp:1342:2:1342:27 | ... = ... | provenance | | +| test.cpp:1355:13:1355:25 | ... + ... | test.cpp:1355:2:1355:25 | ... = ... | provenance | | +| test.cpp:1372:15:1372:22 | ... + ... | test.cpp:1372:3:1372:22 | ... = ... | provenance | | +| test.cpp:1377:12:1377:17 | ... + ... | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1384:12:1384:17 | ... + ... | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1398:2:1398:16 | ... = ... | test.cpp:1402:3:1402:18 | ... = ... | provenance | | +| test.cpp:1398:2:1398:16 | ... = ... | test.cpp:1407:12:1407:15 | year | provenance | | +| test.cpp:1398:9:1398:16 | ... + ... | test.cpp:1398:2:1398:16 | ... = ... | provenance | | +| test.cpp:1407:12:1407:15 | year | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1410:2:1410:16 | ... = ... | test.cpp:1416:12:1416:15 | year | provenance | | +| test.cpp:1410:9:1410:16 | ... + ... | test.cpp:1410:2:1410:16 | ... = ... | provenance | | +| test.cpp:1416:12:1416:15 | year | test.cpp:1290:20:1290:23 | year | provenance | | +| test.cpp:1464:2:1464:10 | ... += ... | test.cpp:1467:2:1467:15 | ... = ... | provenance | | +nodes +| test.cpp:422:2:422:14 | ... += ... | semmle.label | ... += ... | +| test.cpp:440:2:440:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:456:2:456:12 | ... ++ | semmle.label | ... ++ | +| test.cpp:482:3:482:12 | ... ++ | semmle.label | ... ++ | +| test.cpp:500:2:500:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:526:2:526:23 | ... += ... | semmle.label | ... += ... | +| test.cpp:553:2:553:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:592:2:592:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:647:2:647:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:665:2:665:25 | ... += ... | semmle.label | ... += ... | +| test.cpp:681:2:681:23 | ... += ... | semmle.label | ... += ... | +| test.cpp:744:2:744:19 | ... ++ | semmle.label | ... ++ | +| test.cpp:769:2:769:23 | ... -= ... | semmle.label | ... -= ... | +| test.cpp:792:2:792:19 | ... ++ | semmle.label | ... ++ | +| test.cpp:813:2:813:40 | ... = ... | semmle.label | ... = ... | +| test.cpp:813:21:813:40 | ... + ... | semmle.label | ... + ... | +| test.cpp:818:2:818:24 | ... = ... | semmle.label | ... = ... | +| test.cpp:818:13:818:24 | ... + ... | semmle.label | ... + ... | +| test.cpp:854:7:854:31 | ... = ... | semmle.label | ... = ... | +| test.cpp:854:14:854:31 | ... + ... | semmle.label | ... + ... | +| test.cpp:854:35:854:40 | -- ... | semmle.label | -- ... | +| test.cpp:857:3:857:36 | ... = ... | semmle.label | ... = ... | +| test.cpp:875:4:875:15 | ... ++ | semmle.label | ... ++ | +| test.cpp:954:3:954:25 | ... = ... | semmle.label | ... = ... | +| test.cpp:954:14:954:25 | ... + ... | semmle.label | ... + ... | +| test.cpp:972:3:972:12 | ... ++ | semmle.label | ... ++ | +| test.cpp:990:3:990:12 | ... ++ | semmle.label | ... ++ | +| test.cpp:1077:2:1077:11 | ... ++ | semmle.label | ... ++ | +| test.cpp:1084:26:1084:26 | *x | semmle.label | *x | +| test.cpp:1085:2:1085:4 | ... ++ | semmle.label | ... ++ | +| test.cpp:1088:37:1088:37 | *x | semmle.label | *x | +| test.cpp:1089:2:1089:7 | ... ++ | semmle.label | ... ++ | +| test.cpp:1097:16:1097:23 | increment_arg output argument | semmle.label | increment_arg output argument | +| test.cpp:1101:27:1101:35 | increment_arg_by_pointer output argument | semmle.label | increment_arg_by_pointer output argument | +| test.cpp:1135:3:1135:15 | -- ... | semmle.label | -- ... | +| test.cpp:1155:2:1155:26 | ... = ... | semmle.label | ... = ... | +| test.cpp:1155:14:1155:26 | ... - ... | semmle.label | ... - ... | +| test.cpp:1185:2:1185:15 | ... += ... | semmle.label | ... += ... | +| test.cpp:1187:2:1187:19 | ... = ... | semmle.label | ... = ... | +| test.cpp:1204:2:1204:15 | ... += ... | semmle.label | ... += ... | +| test.cpp:1206:2:1206:19 | ... = ... | semmle.label | ... = ... | +| test.cpp:1225:2:1225:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1225:16:1225:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1231:2:1231:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1231:16:1231:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1238:2:1238:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1238:16:1238:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1245:2:1245:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1245:16:1245:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1252:2:1252:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1252:16:1252:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1259:2:1259:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1259:16:1259:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1265:2:1265:28 | ... = ... | semmle.label | ... = ... | +| test.cpp:1265:16:1265:28 | ... + ... | semmle.label | ... + ... | +| test.cpp:1279:2:1279:26 | ... = ... | semmle.label | ... = ... | +| test.cpp:1279:14:1279:26 | ... + ... | semmle.label | ... + ... | +| test.cpp:1290:20:1290:23 | year | semmle.label | year | +| test.cpp:1293:2:1293:17 | ... = ... | semmle.label | ... = ... | +| test.cpp:1303:3:1303:22 | ... = ... | semmle.label | ... = ... | +| test.cpp:1303:15:1303:22 | ... + ... | semmle.label | ... + ... | +| test.cpp:1308:12:1308:17 | ... + ... | semmle.label | ... + ... | +| test.cpp:1317:3:1317:22 | ... = ... | semmle.label | ... = ... | +| test.cpp:1317:15:1317:22 | ... + ... | semmle.label | ... + ... | +| test.cpp:1327:3:1327:20 | ... = ... | semmle.label | ... = ... | +| test.cpp:1327:13:1327:20 | ... + ... | semmle.label | ... + ... | +| test.cpp:1329:12:1329:18 | yeartmp | semmle.label | yeartmp | +| test.cpp:1342:2:1342:27 | ... = ... | semmle.label | ... = ... | +| test.cpp:1342:15:1342:27 | ... + ... | semmle.label | ... + ... | +| test.cpp:1355:2:1355:25 | ... = ... | semmle.label | ... = ... | +| test.cpp:1355:13:1355:25 | ... + ... | semmle.label | ... + ... | +| test.cpp:1372:3:1372:22 | ... = ... | semmle.label | ... = ... | +| test.cpp:1372:15:1372:22 | ... + ... | semmle.label | ... + ... | +| test.cpp:1377:12:1377:17 | ... + ... | semmle.label | ... + ... | +| test.cpp:1384:12:1384:17 | ... + ... | semmle.label | ... + ... | +| test.cpp:1398:2:1398:16 | ... = ... | semmle.label | ... = ... | +| test.cpp:1398:9:1398:16 | ... + ... | semmle.label | ... + ... | +| test.cpp:1402:3:1402:18 | ... = ... | semmle.label | ... = ... | +| test.cpp:1407:12:1407:15 | year | semmle.label | year | +| test.cpp:1410:2:1410:16 | ... = ... | semmle.label | ... = ... | +| test.cpp:1410:9:1410:16 | ... + ... | semmle.label | ... + ... | +| test.cpp:1416:12:1416:15 | year | semmle.label | year | +| test.cpp:1464:2:1464:10 | ... += ... | semmle.label | ... += ... | +| test.cpp:1467:2:1467:15 | ... = ... | semmle.label | ... = ... | +| test.cpp:1497:2:1497:22 | ... += ... | semmle.label | ... += ... | +| test.cpp:1505:2:1505:22 | ... += ... | semmle.label | ... += ... | +| test.cpp:1551:2:1551:22 | ... += ... | semmle.label | ... += ... | +| test.cpp:1562:2:1562:22 | ... += ... | semmle.label | ... += ... | +subpaths diff --git a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.qlref b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.qlref index 22a30d72b689..12bb5eb1e69b 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.qlref +++ b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedLeapYearAfterYearModification.qlref @@ -1 +1,2 @@ -Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql +query: Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedReturnValueForTimeFunctions.expected b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedReturnValueForTimeFunctions.expected index ae8a55449daf..d5681a722739 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedReturnValueForTimeFunctions.expected +++ b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/UncheckedReturnValueForTimeFunctions.expected @@ -1,5 +1,6 @@ -| test.cpp:395:2:395:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:385:6:385:48 | AntiPattern_unchecked_filetime_conversion2a | AntiPattern_unchecked_filetime_conversion2a | test.cpp:75:1:75:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:387:13:387:14 | st | st | -| test.cpp:413:2:413:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:403:6:403:48 | AntiPattern_unchecked_filetime_conversion2b | AntiPattern_unchecked_filetime_conversion2b | test.cpp:75:1:75:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:405:13:405:14 | st | st | -| test.cpp:429:2:429:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:421:6:421:48 | AntiPattern_unchecked_filetime_conversion2b | AntiPattern_unchecked_filetime_conversion2b | test.cpp:75:1:75:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:421:62:421:63 | st | st | -| test.cpp:948:3:948:22 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:938:7:938:15 | modified3 | modified3 | test.cpp:75:1:75:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:940:14:940:15 | st | st | -| test.cpp:965:3:965:22 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:955:7:955:15 | modified4 | modified4 | test.cpp:75:1:75:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:957:14:957:15 | st | st | +| test.cpp:425:2:425:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:415:6:415:48 | AntiPattern_unchecked_filetime_conversion2a | AntiPattern_unchecked_filetime_conversion2a | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:417:13:417:14 | st | st | +| test.cpp:443:2:443:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:433:6:433:48 | AntiPattern_unchecked_filetime_conversion2b | AntiPattern_unchecked_filetime_conversion2b | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:435:13:435:14 | st | st | +| test.cpp:459:2:459:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:451:6:451:48 | AntiPattern_unchecked_filetime_conversion2b | AntiPattern_unchecked_filetime_conversion2b | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:451:62:451:63 | st | st | +| test.cpp:956:3:956:22 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:945:7:945:15 | modified3 | modified3 | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:947:14:947:15 | st | st | +| test.cpp:974:3:974:22 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:963:7:963:15 | modified4 | modified4 | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:965:14:965:15 | st | st | +| test.cpp:1081:2:1081:21 | call to SystemTimeToFileTime | $@: Return value of $@ function should be verified to check for any error because variable $@ is not guaranteed to be safe. | test.cpp:1070:6:1070:22 | fp_daymonth_guard | fp_daymonth_guard | test.cpp:101:1:101:20 | SystemTimeToFileTime | SystemTimeToFileTime | test.cpp:1071:13:1071:14 | st | st | diff --git a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/test.cpp index beb2c4061496..86c49d5cd357 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/test.cpp +++ b/cpp/ql/test/query-tests/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification/test.cpp @@ -46,6 +46,8 @@ typedef struct _TIME_DYNAMIC_ZONE_INFORMATION { BOOLEAN DynamicDaylightTimeDisabled; } DYNAMIC_TIME_ZONE_INFORMATION, *PDYNAMIC_TIME_ZONE_INFORMATION; +typedef const wchar_t* LPCWSTR; + struct tm { int tm_sec; // seconds after the minute - [0, 60] including leap second @@ -71,6 +73,30 @@ struct logtime { long usec; }; +/* + * Data structure representing a broken-down timestamp. + * + * CAUTION: the IANA timezone library (src/timezone/) follows the POSIX + * convention that tm_mon counts from 0 and tm_year is relative to 1900. + * However, Postgres' datetime functions generally treat tm_mon as counting + * from 1 and tm_year as relative to 1 BC. Be sure to make the appropriate + * adjustments when moving from one code domain to the other. + */ +struct pg_tm +{ + int tm_sec; + int tm_min; + int tm_hour; + int tm_mday; + int tm_mon; /* see above */ + int tm_year; /* see above */ + int tm_wday; + int tm_yday; + int tm_isdst; + long int tm_gmtoff; + const char *tm_zone; +}; + BOOL SystemTimeToFileTime( const SYSTEMTIME* lpSystemTime, @@ -111,6 +137,10 @@ TzSpecificLocalTimeToSystemTimeEx( LPSYSTEMTIME lpUniversalTime ); +int _wtoi( + const wchar_t *str +); + void GetSystemTime( LPSYSTEMTIME lpSystemTime ); @@ -389,7 +419,7 @@ void AntiPattern_unchecked_filetime_conversion2a() GetSystemTime(&st); // BUG - UncheckedLeapYearAfterYearModification - st.wYear += 2; + st.wYear += 2; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] // BUG - UncheckedReturnValueForTimeFunctions SystemTimeToFileTime(&st, &ft); @@ -407,7 +437,7 @@ void AntiPattern_unchecked_filetime_conversion2b() GetSystemTime(&st); // BUG - UncheckedLeapYearAfterYearModification - st.wYear++; + st.wYear++; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] // BUG - UncheckedReturnValueForTimeFunctions SystemTimeToFileTime(&st, &ft); @@ -423,7 +453,7 @@ void AntiPattern_unchecked_filetime_conversion2b(SYSTEMTIME* st) FILETIME ft; // BUG - UncheckedLeapYearAfterYearModification - st->wYear++; + st->wYear++; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] // BUG - UncheckedReturnValueForTimeFunctions SystemTimeToFileTime(st, &ft); @@ -605,24 +635,26 @@ void CorrectPattern_NotManipulated_DateFromAPI_1(HANDLE hWatchdog) ///////////////////////////////////////////////////////////////// /** - * Positive Case - Anti-pattern 1: [year ±n, month, day] - * Years is incremented by some integer but a leap year is not handled. + * Negative Case - Anti-pattern 1: [year ±n, month, day] + * Year is incremented by some integer and checked through a conversion through an inter procedural function check */ void AntiPattern_1_year_addition() { SYSTEMTIME st; GetSystemTime(&st); - // BUG - UncheckedLeapYearAfterYearModification - st.wYear++; // BUg V2 + // Safe, checked interprocedurally through Correct_filetime_conversion_check + st.wYear++; // Usage of potentially invalid date Correct_filetime_conversion_check(st); } + + /** - * Positive Case - Anti-pattern 1: [year ±n, month, day] - * Years is incremented by some integer but a leap year is not handled. + * Negative Case - Anti-pattern 1: [year ±n, month, day] + * Years is incremented by some integer and checked through a conversion through an inter procedural function check */ void AntiPattern_simple_addition(int yearAddition) { @@ -630,8 +662,7 @@ void AntiPattern_simple_addition(int yearAddition) GetSystemTime(&st); - // BUG - UncheckedLeapYearAfterYearModification - st.wYear += yearAddition; // Bug V2 + st.wYear += yearAddition; // Usage of potentially invalid date Correct_filetime_conversion_check(st); @@ -647,7 +678,7 @@ void AntiPattern_IncorrectGuard(int yearsToAdd) GetSystemTime(&st); // BUG - UncheckedLeapYearAfterYearModification - st.wYear += yearsToAdd; + st.wYear += yearsToAdd; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] // Incorrect Guard if (st.wMonth == 2 && st.wDay == 29) @@ -660,9 +691,6 @@ void AntiPattern_IncorrectGuard(int yearsToAdd) st.wDay = 28; } } - - // Potentially Unsafe to use - Correct_filetime_conversion_check(st); } /************************************************* @@ -725,10 +753,10 @@ void Correct_year_addition_struct_tm() } /** - * Negative Case - Anti-pattern 1: [year ±n, month, day] + * Positive Case - Anti-pattern 1: [year ±n, month, day] * Years is incremented by some integer and leap year is not handled correctly. */ -void Correct_LinuxPattern() +void Incorrect_LinuxPattern() { time_t rawtime; struct tm timeinfo; @@ -737,7 +765,8 @@ void Correct_LinuxPattern() errno_t err = gmtime_s(&timeinfo, &rawtime); /* from 1900 -> from 1980 */ - timeinfo.tm_year -= 80; + // BUG - UncheckedLeapYearAfterYearModification + timeinfo.tm_year -= 80; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] /* 0~11 -> 1~12 */ timeinfo.tm_mon++; /* 0~59 -> 0~29(2sec counts) */ @@ -751,7 +780,8 @@ void Correct_LinuxPattern() /** * Negative Case - Anti-pattern 1: [year ±n, month, day] - * Years is incremented by some integer and leap year is not handled correctly. + * Years is incremented by some integer and leap year is assumed checked through + * check of a conversion functions return value. */ void AntiPattern_year_addition_struct_tm() { @@ -759,36 +789,19 @@ void AntiPattern_year_addition_struct_tm() struct tm timeinfo; time(&rawtime); gmtime_s(&timeinfo, &rawtime); - // BUG - UncheckedLeapYearAfterYearModification - timeinfo.tm_year++; // Bug V2 + timeinfo.tm_year++; - // Usage of potentially invalid date + // mkgmtime result checked in nested call here, assume leap year conversion is potentially handled CorrectUsageOf_mkgmtime(timeinfo); } ///////////////////////////////////////////////////////// -/** - * Negative Case - Anti-pattern 1: [year ±n, month, day] - * False positive: Years is initialized to or incremented by some integer (but never used). -*/ -void FalsePositiveTests(int x) -{ - struct tm timeinfo; - SYSTEMTIME st; - - timeinfo.tm_year = x; - timeinfo.tm_year = 1970; - - st.wYear = x; - st.wYear = 1900 + x; -} /** * Positive Case - Anti-pattern 1: [year ±n, month, day] - * False positive: Years is initialized to or incremented by some integer (but never used). */ -void FalseNegativeTests(int x) +void test(int x) { struct tm timeinfo; SYSTEMTIME st; @@ -797,22 +810,16 @@ void FalseNegativeTests(int x) // BUG - UncheckedLeapYearAfterYearModification // Positive Case - Anti-pattern 1: [year ±n, month, day] - timeinfo.tm_year = x + timeinfo.tm_year; // Bug V2 - // BUG - UncheckedLeapYearAfterYearModification - // Positive Case - Anti-pattern 1: [year ±n, month, day] - timeinfo.tm_year = 1970 + timeinfo.tm_year; // Bug V2 + timeinfo.tm_year = x + timeinfo.tm_year; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] st.wYear = x; // BUG - UncheckedLeapYearAfterYearModification // Positive Case - Anti-pattern 1: [year ±n, month, day] - st.wYear = x + st.wYear; // Bug V2 - // BUG - UncheckedLeapYearAfterYearModification - // Positive Case - Anti-pattern 1: [year ±n, month, day] - st.wYear = (1986 + st.wYear) - 1; // Bug V2 + st.wYear = x + st.wYear; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] } /** - * Positive AntiPattern 1 + * Positive AntiPattern 1 NOTE: historically considered positive but mktime checks year validity, needs re-assessment * Year field is modified but via an intermediary variable. */ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) @@ -844,10 +851,10 @@ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) timestamp_remote.tm = tm_parsed; timestamp_remote.tm.tm_isdst = -1; timestamp_remote.usec = now.tv_nsec * 0.001; - for (year = tm_now.tm_year + 1;; --year) + for (year = tm_now.tm_year + 1;; --year) { // assert(year >= tm_now.tm_year - 1); - timestamp_remote.tm.tm_year = year; + timestamp_remote.tm.tm_year = year; if (mktime(×tamp_remote.tm) < t_now + 7 * 24 * 60 * 60) break; } @@ -943,7 +950,8 @@ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) GetSystemTime(&st); - st.wYear = st.wYear + 1; // BAD + // BUG - UncheckedLeapYearAfterYearModification + st.wYear = st.wYear + 1; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] SystemTimeToFileTime(&st, &ft); } @@ -960,14 +968,16 @@ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) GetSystemTime(&st); - st.wYear++; // BAD Positive Case - Anti-pattern 1: [year ±n, month, day] + // BUG - UncheckedLeapYearAfterYearModification + st.wYear++; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] SystemTimeToFileTime(&st, &ft); } /** * Negative Case - Anti-pattern 1: [year ±n, month, day] - * Modification of SYSTEMTIME struct adding to year but no leap year guard is conducted. + * Modification of SYSTEMTIME struct adding to year but value passed to a + * conversion function that can be checked for success, and the result is checked. */ void modified5() { @@ -977,8 +987,13 @@ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) GetSystemTime(&st); - st.wYear++; // Negative Case - Anti-pattern 1: [year ±n, month, day], guard condition below. + st.wYear++; + // Presumed safe usage, as if the conversion is incorrect, a user can handle the error. + // NOTE: it doesn't mean the user actually does the correct conversion and it it also + // doesn't mean it will error our in all cases that may be invalid. + // For example, if a leap year and the date is 28, we may want 29 if the time is meant + // to capture the end of the month, but 28 is still valid and will not error out. if (SystemTimeToFileTime(&st, &ft)) { ///... @@ -1004,3 +1019,589 @@ bool tp_intermediaryVar(struct timespec now, struct logtime ×tamp_remote) tm.tm_mday = tm.tm_mon == 2 && tm.tm_mday == 29 && !isLeapYear ? 28 : tm.tm_mday; return tm; } + +/** +* Negative Case - Anti-pattern 1: [year ±n, month, day] +* Modification of SYSTEMTIME struct by copying from another struct, but no arithmetic is performed. +*/ +bool +FMAPITimeToSysTimeW(LPCWSTR wszTime, SYSTEMTIME *psystime) +{ + // if (!wszTime || SafeIsBadReadPtr(wszTime, 1) || lstrlenW(wszTime) != cchMAPITime) + // return false; + // AssertTag(!SafeIsBadWritePtr(psystime, sizeof(SYSTEMTIME)), 0x0004289a /* tag_abc80 */); + // memset(psystime, 0, sizeof(SYSTEMTIME)); + + psystime->wYear = (WORD)_wtoi(wszTime); + psystime->wMonth = (WORD)_wtoi(wszTime+5); + psystime->wDay = (WORD)_wtoi(wszTime+8); + psystime->wHour = (WORD)_wtoi(wszTime+11); + psystime->wMinute = (WORD)_wtoi(wszTime+14); + return true; +} + +/** +* Negative Case - Anti-pattern 1: [year ±n, month, day] +* Modification of SYSTEMTIME struct by copying from another struct, but no arithmetic is performed. +*/ +bool +ATime_HrGetSysTime(SYSTEMTIME *pst) +{ + // if (!FValidSysTime()) + // { + // TrapSzTag("ATime cannot be converted to SYSTEMTIME", 0x1e14f5c3 /* tag_4fpxd */); + // CORgTag(E_FAIL, 0x6c373230 /* tag_l720 */); + // } + + // pst->wYear = static_cast(m_lYear); + // pst->wMonth = static_cast(m_lMonth); + // //pst->wDayOfWeek = ???; + // pst->wDay = static_cast(m_lDay); + // pst->wHour = static_cast(m_lHour); + // pst->wMinute = static_cast(m_lMinute); + // pst->wSecond = static_cast(m_lSecond); + // pst->wMilliseconds = 0; +} + +/** +* Negative Case - Anti-pattern 1: [year ±n, month, day] +* Modification of SYSTEMTIME struct by copying from another struct, but no arithmetic is performed. +*/ +void fp_daymonth_guard(){ + SYSTEMTIME st; + FILETIME ft; + GetSystemTime(&st); + // FALSE POSITIVE: year is incremented but month is checked and day corrected + // in a ternary operation. It may be possible to fix this with a more sophisticated + // data flow analysis. + st.wYear++; // $ SPURIOUS: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + + st.wDay = st.wMonth == 2 && st.wDay == 29 ? 28 : st.wDay; + + SystemTimeToFileTime(&st, &ft); +} + +void increment_arg(WORD &x){ + x++; // $ Source +} + +void increment_arg_by_pointer(WORD *x){ + (*x)++; // $ Source +} + + +void fn_year_set_through_out_arg(){ + SYSTEMTIME st; + GetSystemTime(&st); + // BAD, year incremented without check + increment_arg(st.wYear); // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + + // GetSystemTime(&st); + // Bad, year incremented without check + increment_arg_by_pointer(&st.wYear); // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + + +/* TODO: don't alert on simple copies from another struct where all three {year,month,day} are copied +void +GetEpochTime(struct pg_tm *tm) +{ + struct pg_tm *t0; + pg_time_t epoch = 0; + + t0 = pg_gmtime(&epoch); + + tm->tm_year = t0->tm_year; + tm->tm_mon = t0->tm_mon; + tm->tm_mday = t0->tm_mday; + tm->tm_hour = t0->tm_hour; + tm->tm_min = t0->tm_min; + tm->tm_sec = t0->tm_sec; + + tm->tm_year += 1900; + tm->tm_mon++; +} */ + +void +fp_guarded_by_month(struct pg_tm *tm){ + int woy = 52; + int MONTHS_PER_YEAR = 12; + /* + * If it is week 52/53 and the month is January, then the + * week must belong to the previous year. Also, some + * December dates belong to the next year. + */ + if (woy >= 52 && tm->tm_mon == 1) + --tm->tm_year; // Negative Test Case + if (woy <= 1 && tm->tm_mon == MONTHS_PER_YEAR) + ++tm->tm_year; // Negative Test Case +} + +typedef unsigned short CSHORT; + +typedef struct _TIME_FIELDS { + CSHORT Year; + CSHORT Month; + CSHORT Day; + CSHORT Hour; + CSHORT Minute; + CSHORT Second; + CSHORT Milliseconds; + CSHORT Weekday; +} TIME_FIELDS, *PTIME_FIELDS; + +void +tp_ptime(PTIME_FIELDS ptm){ + ptm->Year = ptm->Year - 1; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + + +bool isLeapYearRaw(WORD year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +void leap_year_checked_raw_false_positive1(WORD year, WORD offset, WORD day){ + struct tm tmp; + + year += offset; + + if (isLeapYearRaw(year)){ + // in this simplified example, assume the logic of this function + // can assume a day is 28 by default + // this check is more to establish the leap year guard is present + day += 1; + } + + // Assume the check handled leap year correctly + tmp.tm_year = year; // GOOD + tmp.tm_mday = day; +} + + +void leap_year_checked_raw_false_positive2(WORD year, WORD offset, WORD day){ + struct tm tmp; + + year += offset; + + tmp.tm_year = year; // GOOD, check performed immediately after on raw year + + // Adding some additional checks to resemble cases observed in the wild + if ( day > 0) + { + if (isLeapYearRaw(year)){ + // Assume logic that would adjust the day correctly + } + } + else{ + if (isLeapYearRaw(year)){ + // Assume logic that would adjust the day correctly + } + } + + tmp.tm_mday = day; + + year += offset; // $ Source + + tmp.tm_year = year; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + +} + + +bool isNotLeapYear(struct tm tm) +{ + return !(tm.tm_year % 4 == 0 && (tm.tm_year % 100 != 0 || tm.tm_year % 400 == 0)); +} + +bool isNotLeapYear2(struct tm tm) +{ + return (tm.tm_year % 4 != 0 || (tm.tm_year % 100 == 0 && tm.tm_year % 400 != 0)); +} + + +void inverted_leap_year_check(WORD year, WORD offset, WORD day){ + struct tm tmp; + + tmp.tm_year = year + offset; + + if (isNotLeapYear(tmp)){ + day = 28; + } + + tmp.tm_year = year + offset; + + if(isNotLeapYear2(tmp)){ + day = 28; + } + + + tmp.tm_year = year + offset; + bool isNotLeapYear = (tmp.tm_year % 4 != 0 || (tmp.tm_year % 100 == 0 && tmp.tm_year % 400 != 0)); + + if(isNotLeapYear){ + day = 28; + } + + tmp.tm_year = year + offset; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + + +void simplified_leap_year_check(WORD year, WORD offset){ + struct tm tmp; + + tmp.tm_year = year + offset; + + bool isLeap = !(tmp.tm_year % 4) && (tmp.tm_year % 100 || !(tmp.tm_year % 400)); + if(isLeap){ + // do something + } + + tmp.tm_year = year + offset; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + +void compound_leap_year_check(WORD year, WORD offset, WORD month, WORD day){ + struct tm tmp; + + tmp.tm_year = year + offset; + + bool isLeap = tmp.tm_year % 4 == 0 && (tmp.tm_year % 100 != 0 || tmp.tm_year % 400 == 0) && (month == 2 && day == 29); + + if(isLeap){ + // do something + } + tmp.tm_mday = day; + tmp.tm_mon = month; +} + +void indirect_time_conversion_check(WORD year, WORD offset){ + SYSTEMTIME tmp; + + tmp.wYear = year + offset; + + FILETIME ft; + + // (out-of-scope) GeneralBug: Unchecked call to SystemTimeToFileTime. this may have failed, but we didn't check the return value! + BOOL res = SystemTimeToFileTime(&tmp, &ft); + + // Assume this check of the result is sufficient as an implicit leap year check. + bool x = (res == 0) ? true : false; +} + +void set_time(WORD year, WORD month, WORD day){ + SYSTEMTIME tmp; + + tmp.wYear = year; //$ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + tmp.wMonth = month; + tmp.wDay = day; +} + +void constant_month_on_year_modification1(WORD year, WORD offset, WORD month){ + SYSTEMTIME tmp; + + if(month++ > 12){ + tmp.wMonth = 1; + tmp.wYear = year + 1;// OK since the year is incremented with a known non-leap year month change + } + + if(month++ > 12){ + + set_time(year+1, 1, 31);// OK since the year is incremented with a known non-leap year month change + } +} + +void constant_month_on_year_modification2(WORD year, WORD offset, WORD month){ + SYSTEMTIME tmp; + + if(month++ > 12){ + tmp.wMonth = 1; + tmp.wYear = year + 1;// OK since the year is incremented with a known non-leap year month change + } + + + if(month++ > 12){ + // some hueristics to detect a false positive here rely on variable names + // which is often consistent in the wild. + // This variant uses the variable names yeartmp and monthtmp + WORD yeartmp; + WORD monthtmp; + yeartmp = year + 1; + monthtmp = 1; + set_time(yeartmp, monthtmp, 31);// OK since the year is incremented with a known non-leap year month change + } +} + +typedef struct parent_struct { + SYSTEMTIME t; +} PARENT_STRUCT; + + + +void nested_time_struct(WORD year, WORD offset){ + PARENT_STRUCT ps; + + ps.t.wYear = year + offset; // OK, checked below + + bool isLeap = isLeapYearRaw(ps.t.wYear); + + if(isLeap){ + // do something + } +} + +void intermediate_time_struct(WORD year, WORD offset){ + SYSTEMTIME tm, tm2; + FILETIME ftTime; + + tm.wYear = year + offset; + + tm2.wYear = tm.wYear; + + + while ( !SystemTimeToFileTime( &tm2, &ftTime ) ) + { + /// handle error + } + +} + +void constant_day_on_year_modification1(WORD year, WORD offset, WORD month){ + SYSTEMTIME tmp; + + if(month++ > 12){ + tmp.wDay = 1; + tmp.wYear = year + 1;// OK since the year is incremented with a known non-leap year day + } + + if(month++ > 12){ + + set_time(year+1, month, 1);// OK since the year is incremented with a known non-leap year day + } + + if(month++ > 12){ + + // BAD, year incremented, month unknown in block, and date is set to 31 + // which is dangerous. + set_time(year+1, month, 31);// $ Source + } +} + +void constant_day_on_year_modification2(WORD year, WORD month){ + SYSTEMTIME tmp; + + // FLASE POSITIVE SOURCE: + // flowing into set_time, the set time does pass a constant day + // but the source here and the source of that constant month don't align + // Current heuristics require the source of the constant day align with the + // source and/or the sink of the year modification. + // We could potentially improve this by checking the paths of both the year and day + // flows, but this may be more complex than is warranted for now. + year = year + 1; // $ SPURIOUS: Source + + if(month++ > 12){ + tmp.wDay = 1; + tmp.wYear = year;// OK since the year is incremented with a known non-leap year day + } + + if(month++ > 12){ + + set_time(year, month, 1);// OK since the year is incremented with a known non-leap year day + } + + year = year + 1; // $ Source + + if(month++ > 12){ + + // BAD, year incremented, month unknown in block, and date is set to 31 + // which is dangerous. + set_time(year, month, 31); + } +} + + +void modification_after_conversion1(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; + + year += 1; // $ MISSING: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + +WORD get_civil_year(tm timeinfo){ + return timeinfo.tm_year + 1900; +} + +void modification_after_conversion2(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = get_civil_year(timeinfo); + year += 1; // $ MISSING: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + +void modification_after_conversion_saved_to_other_time_struct1(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; + + year += 1; // $ MISSING: Source + + SYSTEMTIME s; + // FALSE NEGATIVE: missing this because the conversion happens locally before + // the year adjustment, which seems as though it is part of a conversion itself + s.wYear = year; // $ MISSING: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + + + +void modification_after_conversion_saved_to_other_time_struct2(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = get_civil_year(timeinfo); + + year += 1; // $ Source + + SYSTEMTIME s; + s.wYear = year; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + +void modification_after_conversion_saved_to_other_time_struct3(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; + + year = year + 1; // $ MISSING: Source + + SYSTEMTIME s; + // FALSE NEGATIVE: missing this because the conversion happens locally before + // the year adjustment, which seems as though it is part of a conversion itself + s.wYear = year; // $ MISSING: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + + +void year_saved_to_variable_then_modified1(tm timeinfo){ + // A modified year is not directly assigned to the year, rather, the year is + // saved to a variable, modified, used, but never assigned back. + WORD year = timeinfo.tm_year; + + // NOTE: should we even try to detect cases like this? + // Our current rationale is that a year in a struct is more dangerous than a year in isolation + // A year in isolation is harder to interpret + year += 1; // MISSING: $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] +} + +void modification_before_conversion1(tm timeinfo){ + timeinfo.tm_year += 1; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; +} + +void modification_before_conversion2(tm timeinfo){ + timeinfo.tm_year += 1; // $ Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = get_civil_year(timeinfo); +} + + + +void year_saved_to_variable_then_modified_with_leap_check1(tm timeinfo){ + // A modified year is not directly assigned to the year, rather, the year is + // saved to a variable, modified, used, but never assigned back. + WORD year = timeinfo.tm_year; + + year += 1; + + // performing a check is considered good enough, even if not used correctly + bool b = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + + +void modification_after_conversion_with_leap_check1(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; + + year += 1; + + // performing a check is considered good enough, even if not used correctly + bool b = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +void modification_after_conversion_with_leap_check2(tm timeinfo){ + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = get_civil_year(timeinfo); + + year += 1; + + // performing a check is considered good enough, even if not used correctly + bool b = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +void modification_before_conversion_with_leap_check1(tm timeinfo){ + timeinfo.tm_year += 1; + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = timeinfo.tm_year + 1900; + + // performing a check is considered good enough, even if not used correctly + bool b = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +void modification_before_conversion_with_leap_check2(tm timeinfo){ + timeinfo.tm_year += 1; + // convert a tm year into a civil year, then modify after conversion + // This case shows a false negative where the year might be used and it is incorrectly modified, + // and never reassigned to another struct. + WORD year = get_civil_year(timeinfo); + + // performing a check is considered good enough, even if not used correctly + bool b = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +void odd_leap_year_check1(tm timeinfo){ + timeinfo.tm_year += 1; + + + // Using an odd sytle of checking divisible by 4 presumably as an optimization trick + if(((timeinfo.tm_year & 3) == 0) && (timeinfo.tm_year % 100 != 0 || timeinfo.tm_year % 400 == 0)) + { + // do something + } +} + +void odd_leap_year_check2(tm timeinfo){ + timeinfo.tm_year += 1; // $ SPURIOUS: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + + // Using an odd sytle of checking divisible by 4 presumably as an optimization trick + // but also check unrelated conditions on the year as an optimization to rule out irrelevant years + // for gregorian leap years + if(timeinfo.tm_mon == 2 && ((timeinfo.tm_year & 3) == 0) && (timeinfo.tm_year <= 1582 || timeinfo.tm_year % 100 != 0 || timeinfo.tm_year % 400 == 0)) + { + // do something + } +} + +void odd_leap_year_check3(tm timeinfo){ + timeinfo.tm_year += 1; // $ SPURIOUS: Alert[cpp/microsoft/public/leap-year/unchecked-after-arithmetic-year-modification] + + // Using an odd sytle of checking divisible by 4 presumably as an optimization trick + // but also check unrelated conditions on the year as an optimization to rule out irrelevant years + // for gregorian leap years + if(timeinfo.tm_mon == 2 && (timeinfo.tm_year % 4) == 0 && (timeinfo.tm_year <= 1582 || timeinfo.tm_year % 100 != 0 || timeinfo.tm_year % 400 == 0)) + { + // do something + } +} + +