CodeQL queries 1.24

Skip to end of metadata
Go to start of metadata

Name: Year field changed using an arithmetic operation without checking for leap year

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.

ID: cpp/leap-year/unchecked-after-arithmetic-year-modification

Kind: problem

Severity: warning

Precision: medium

Query: UncheckedLeapYearAfterYearModification.ql
/**
 * @name Year field changed using an arithmetic operation without checking for leap year
 * @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
 * @problem.severity warning
 * @id cpp/leap-year/unchecked-after-arithmetic-year-modification
 * @precision medium
 * @tags leap-year
 */

import cpp
import LeapYear

from Variable var, LeapYearFieldAccess yfa
where
  exists(VariableAccess va |
    yfa.getQualifier() = va and
    var.getAnAccess() = va and
    // The year is modified with an arithmetic operation. Avoid values that are likely false positives
    yfa.isModifiedByArithmeticOperationNotForNormalization() 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*()
      )
      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, LeapYearCheckConfiguration config
      |
        source = var.getAnAccess() and
        config.hasFlow(DataFlow::exprNode(source), DataFlow::exprNode(fc.getAnArgument()))
      )
      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,
        LeapYearCheckConfiguration config
      |
        vacheck = var.getAnAccess() and
        yfacheck.getQualifier() = vacheck and
        config.hasFlow(DataFlow::exprNode(yfacheck), DataFlow::exprNode(fc.getAnArgument()))
      )
      or
      // If there is a successor or predecessor that sets the month = 1
      exists(MonthFieldAccess mfa, AssignExpr ae |
        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() = 1
      )
    )
  )
select yfa,
  "Field $@ on variable $@ has been modified, but no appropriate check for LeapYear was found.",
  yfa.getTarget(), yfa.getTarget().toString(), var, var.toString()

The leap year rule for the Gregorian calendar, which has become the internationally accepted civil calendar, is: every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.

A leap year bug occurs when software (in any language) is written without consideration of leap year logic, or with flawed logic to calculate leap years; which typically results in incorrect results.

The impact of these bugs may range from almost unnoticeable bugs such as an incorrect date, to severe bugs that affect reliability, availability or even the security of the affected system.

When performing arithmetic operations on a variable that represents a year, it is important to consider that the resulting value may not be a valid date.

The typical example is doing simple year arithmetic (i.e. date.year++) without considering if the resulting value will be a valid date or not.

Recommendation

When modifying a year field on a date structure, verify if the resulting year is a leap year.

Example

In this example, we are adding 1 year to the current date. This may work most of the time, but on any given February 29th, the resulting value will be invalid.

SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);

// Flawed logic may result in invalid date
st.wYear++;

// The following code may fail
SystemTimeToFileTime(&st, &ft);

To fix this bug, check the result for leap year.

SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);

// Flawed logic may result in invalid date
st.wYear++;

// Check for leap year, and adjust the date accordingly
bool isLeapYear = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
st.wDay = st.wMonth == 2 && st.wDay == 29 && !isLeapYear ? 28 : st.wDay;

if (!SystemTimeToFileTime(&st, &ft))
{
	// handle error
}

References