gdatetime: Fix overflow checks when constructing from timestamps

GDateTime does overflow checks to see if the timestamp being passed in
is too big to be represented. However, it only does those after
converting from a timestamp to an interval, which involves some
multiplications and additions — and hence can overflow, and cause the
later bounds check to erroneously succeed. This results in a non-NULL
GDateTime being returned which represents completely the wrong date.

Fix the overflow checks (do them earlier) and add some unit tests.

Signed-off-by: Philip Withnall <withnall@endlessm.com>

https://bugzilla.gnome.org/show_bug.cgi?id=782089
This commit is contained in:
Philip Withnall 2017-05-02 23:33:23 +01:00
parent 17395d79eb
commit 9374ecc3cb
2 changed files with 93 additions and 0 deletions

View File

@ -129,6 +129,8 @@ struct _GDateTime
((instant)/USEC_PER_SECOND - UNIX_EPOCH_START * SEC_PER_DAY)
#define UNIX_TO_INSTANT(unix) \
(((unix) + UNIX_EPOCH_START * SEC_PER_DAY) * USEC_PER_SECOND)
#define UNIX_TO_INSTANT_IS_VALID(unix) \
((gint64) (unix) <= INSTANT_TO_UNIX (G_MAXINT64))
#define DAYS_IN_4YEARS 1461 /* days in 4 years */
#define DAYS_IN_100YEARS 36524 /* days in 100 years */
@ -650,6 +652,10 @@ static GDateTime *
g_date_time_new_from_timeval (GTimeZone *tz,
const GTimeVal *tv)
{
if ((gint64) tv->tv_sec > G_MAXINT64 - 1 ||
!UNIX_TO_INSTANT_IS_VALID ((gint64) tv->tv_sec + 1))
return NULL;
return g_date_time_from_instant (tz, tv->tv_usec +
UNIX_TO_INSTANT (tv->tv_sec));
}
@ -679,6 +685,9 @@ static GDateTime *
g_date_time_new_from_unix (GTimeZone *tz,
gint64 secs)
{
if (!UNIX_TO_INSTANT_IS_VALID (secs))
return NULL;
return g_date_time_from_instant (tz, UNIX_TO_INSTANT (secs));
}

View File

@ -139,6 +139,23 @@ test_GDateTime_new_from_unix (void)
g_date_time_unref (dt);
}
/* Check that trying to create a #GDateTime too far in the future reliably
* fails. Previously, the checks for this overflowed and it silently returned
* an incorrect #GDateTime. */
static void
test_GDateTime_new_from_unix_overflow (void)
{
GDateTime *dt;
g_test_bug ("782089");
dt = g_date_time_new_from_unix_utc (G_MAXINT64);
g_assert_null (dt);
dt = g_date_time_new_from_unix_local (G_MAXINT64);
g_assert_null (dt);
}
static void
test_GDateTime_invalid (void)
{
@ -353,6 +370,71 @@ test_GDateTime_new_from_timeval (void)
g_date_time_unref (dt);
}
static gint64
find_maximum_supported_tv_sec (void)
{
glong highest_success = 0, lowest_failure = G_MAXLONG;
GTimeVal tv;
tv.tv_usec = 0;
while (highest_success < lowest_failure - 1)
{
GDateTime *dt;
tv.tv_sec = (highest_success + lowest_failure) / 2;
dt = g_date_time_new_from_timeval_utc (&tv);
if (dt != NULL)
{
highest_success = tv.tv_sec;
g_date_time_unref (dt);
}
else
{
lowest_failure = tv.tv_sec;
}
}
return highest_success;
}
/* Check that trying to create a #GDateTime too far in the future reliably
* fails. With a #GTimeVal, this is subtle, as the tv_usec are added into the
* calculation part-way through. */
static void
test_GDateTime_new_from_timeval_overflow (void)
{
GDateTime *dt;
GTimeVal tv;
g_test_bug ("782089");
tv.tv_sec = G_MAXLONG;
tv.tv_usec = 0;
dt = g_date_time_new_from_timeval_utc (&tv);
g_assert_null (dt);
dt = g_date_time_new_from_timeval_local (&tv);
g_assert_null (dt);
tv.tv_sec = find_maximum_supported_tv_sec ();
tv.tv_usec = G_USEC_PER_SEC - 1;
g_test_message ("Maximum supported GTimeVal.tv_sec = %lu", tv.tv_sec);
dt = g_date_time_new_from_timeval_utc (&tv);
g_assert_nonnull (dt);
g_date_time_unref (dt);
tv.tv_sec++;
tv.tv_usec = 0;
dt = g_date_time_new_from_timeval_utc (&tv);
g_assert_null (dt);
}
static void
test_GDateTime_new_from_timeval_utc (void)
{
@ -1652,8 +1734,10 @@ main (gint argc,
g_test_add_func ("/GDateTime/hash", test_GDateTime_hash);
g_test_add_func ("/GDateTime/new_from_unix", test_GDateTime_new_from_unix);
g_test_add_func ("/GDateTime/new_from_unix_utc", test_GDateTime_new_from_unix_utc);
g_test_add_func ("/GDateTime/new_from_unix/overflow", test_GDateTime_new_from_unix_overflow);
g_test_add_func ("/GDateTime/new_from_timeval", test_GDateTime_new_from_timeval);
g_test_add_func ("/GDateTime/new_from_timeval_utc", test_GDateTime_new_from_timeval_utc);
g_test_add_func ("/GDateTime/new_from_timeval/overflow", test_GDateTime_new_from_timeval_overflow);
g_test_add_func ("/GDateTime/new_full", test_GDateTime_new_full);
g_test_add_func ("/GDateTime/now", test_GDateTime_now);
g_test_add_func ("/GDateTime/printf", test_GDateTime_printf);