"Mo"
Published on

That Time I Found a Y2K22 Bug

Authors

At the beginning of this year, I had a good laugh at Exchange's expense because of a "harebrained Y2K22" bug. Never did I expect to actually have to debug a Y2K22 bug a few days later.

Maybe you can now get a good laugh at my expense.

The Bug

We've got a project at work where we help one of our clients with an older codebase that we have some history with. Usually, said help comes down to getting on VPN, checking some data in the database and answering questions. This time, though, we got a bug report about a specific feature of the system.

The feature in question was a API that would generate a calendar feed through a REST-like call. It supported various content types, meaning the caller could use a query string of format=json or format=ical or similar to change the response type. The bug report indicated that the event feed was returning an HTTP 500 error. Here's where it gets weird, though: I discovered that the JSON response worked great while the iCal response errored out. Stranger still, it was only recent events that resulted in the 500 response.

The Buggy Code

The title already spoiled my findings, but here is the code snippet that resulted in the bug:

var icalSequence = int.Parse(new DateTimeOffset(event.EventDate, new TimeSpan(event.TimeZoneId, 0, 0)).UtcDateTime.ToString("yyMMddHHmm"));

What this is populating is the iCal sequence number. This indicates to the calling system the sequence for the events being returned. So... the first event can get a sequence 0, then 1 and so on.

The above code, though, works off of a system where the sequence isn't predetermined. Events are generated based on approvals, other events in the system and so on. So, the author of the code attempted to base the Sequence on the date by generating a numeric value based on the event's date... like so:

EventDate.UtcDateTime.ToString("yyMMddHHmm")

The event's date is converted to UTC, then returned in a two-digit year, 2-digit month and so on. Makes sense, right? Earlier events will get an earlier sequence, meaning the sequential nature of events works as expected... and had worked for over 10 years by this point.

What happened?

The clock happened. The clock plus int.MaxValue. The iCal RFC dictates that sequence is an integer. The largest integer value is 2,147,483,647, which based on our above logic, means two digit years of 22 are too big. I had found a Y2K22 bug.

The Fix

This is where it gets crazy. I thought I could just use an unsigned int instead, but the spec doesn't spell out int vs uint or similar. I didn't want to trust/assume that client applications would be able to read the iCal feed with the twice as large sequence, even though it would let us push the bug out to the year 2042.

I figured I had a couple of options...

  1. Drop portions of the date and lose precision. Chances are really good that these events probably won't happen even on the same day... but there are hourly events.
  2. Use a different date format that would still convert to a number and sort as expected.

I ended up doing something weird. I used a date method that I had never seen before called ToOADate. See the ToOADate docs.

From the docs:

An OLE Automation date is implemented as a floating-point number whose integral component is the number of days before or after midnight, 30 December 1899, and whose fractional component represents the time on that day divided by 24. For example, midnight, 31 December 1899 is represented by 1.0; 6 A.M., 1 January 1900 is represented by 2.25; midnight, 29 December 1899 is represented by -1.0; and 6 A.M., 29 December 1899 is represented by -1.25.

Because we're dealing with ongoing events, we don't have to worry about precision to older dates. It means we've got more numbers to work with. On top of that, the fractional portion represents the time. In other words, we can round that and still not lose a lot of precision.

Here was my fix, with comments and all:

/// <summary>
/// Converts the date into an iCal sequence (INT). The previous code used the following approach:
/// <code>
/// var icalSequence = int.Parse(new DateTimeOffset(event.EventDate, new TimeSpan(event.TimeZoneId, 0, 0)).UtcDateTime.ToString("yyMMddHHmm"));
/// </code>
/// BUT this caused issues because 2022 dates were too large for integers.
///
/// In addition, we can't change to use a uint or larger because iCal's SEQUENCE is specified as an
/// integer in the spec <em>and</em> in the iCal library.
///
/// This approach uses the OLE Automation date format, which is a decimal based date. Because that
/// also can't safely be turned into an integer, we're rounding and losing some precision, but only at
/// most minutes of precision.
///
/// So... a date like "01/21/2022 09:36:49 am" becomes
/// - 44582.6505671296
/// - then 44582.6506
/// - then 445826506
/// - which is a safe integer
///
/// This approach is safe up to the year at least 2470.
/// </summary>
/// <param name="date">The date to convert to an iCal sequence</param>
/// <returns>The iCal sequence</returns>
private static int GetSequenceFromDate(DateTime date, Event event)
{
    var utcDate = new DateTimeOffset(
        date,
        new TimeSpan(event.TimeZoneId, 0, 0)).UtcDateTime;
    var oaDate = utcDate.ToOADate();

    var roundedOaDateWithoutDecimals = Math.Round(oaDate * 10000);

    return (int)roundedOaDateWithoutDecimals;
}

As the comment states, this is safe up to the year 2470. I wouldn't suggest using this method in any normal situation. If I had to work on a non-legacy system that needed to support sequences, I'd have just included sequential numeriic value 0, 1, 2 number in the data store. This is an odd scenario for sure.

Wrapping up, yes, I did introduce a Y2470 bug. Sorry future maintainer. If this code lasts into the 25th century, then I'm guessing we're not in the Star Trek timeline.