Subject: mktime(3) fails to convert a time in the "spring forward gap"
To: NetBSD Userlevel Technical Discussion List <tech-userlevel@NetBSD.ORG>
From: Greg A. Woods <woods@weird.com>
List: tech-userlevel
Date: 10/05/2002 16:36:18
It seems NetBSD's (and FreeBSD, at least 4.7-RC's) mktime(3) fails to
convert a time in the "spring forward gap".

I noticed this because the libc mktime() is rejected as broken when
building Emacs 21.2 on NetBSD.

Is this worth fixing?  Apparently some people think it is a problem, and
I suppose any utility that actually tries to use mktime() at a current
time during that gap will get a somewhat surprising failure.  I haven't
even tried looking for a fix yet, or I would have sent a PR.  Should I
just report this to tz@elsie.nci.nih.gov and let any fix filter down?

Here's a slightly modified version of the program from the emacs
configure test unit which demonstrates the bug and which I suppose could
be used in a regression test:

/*
 * tmktime.c - test mktime(3) -- stolen from emacs-21.2 aclocal.m4
 * 
 * Test program from Paul Eggert (eggert@twinsun.com)
 * and Tony Leneis (tony@plaza.ds.adp.com).
 */
#ifdef BSD
# define HAVE_SYS_TIME_H	/* defined */
# define HAVE_UNISTD_H		/* defined */
# define HAVE_ALARM		/* defined */
#endif

#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# if HAVE_SYS_TIME_H
#  include <sys/time.h>
# else
#  include <time.h>
# endif
#endif

#include <stdio.h>
#include <errno.h>

#if HAVE_UNISTD_H
# include <unistd.h>
#endif

#if !HAVE_ALARM
# define alarm(X)	/* empty */
#endif

static time_t   time_t_max;

/* Values we'll use to set the TZ environment variable.  */
static const char *const tz_strings[] = {
	(const char *) 0,			/* XXX does this have the desired effect? */
	"TZ=",					/* not in original test. */
	"TZ=GMT0",
	"TZ=JST-9",
	"TZ=EST+3EDT+2,M10.1.0/00:00:00,M2.3.0/00:00:00"
};

#define N_STRINGS	(sizeof (tz_strings) / sizeof (tz_strings[0]))

/*
 * Fail if mktime fails to convert a date in the "spring-forward" gap.
 * Based on a problem report from Andreas Jaeger.
 *
 * glibc (up to about 1998-10-07) failed this test), NetBSD up to 1.6 fails
 * this test, as does FreeBSD up to 4.7-RC.
 */
static void
spring_forward_gap()
{
	struct tm       tm;

	/*
	 * Use the portable POSIX.1 specification "TZ=PST8PDT,M4.1.0,M10.5.0"
	 * instead of "TZ=America/Vancouver" in order to detect the bug even on
	 * systems that don't support the Olson extension, or don't have the
	 * full zoneinfo tables installed.
	 */
	putenv("TZ=PST8PDT,M4.1.0,M10.5.0");

	tm.tm_year = 98;
	tm.tm_mon = 3;
	tm.tm_mday = 5;
	tm.tm_hour = 2;
	tm.tm_min = 0;
	tm.tm_sec = 0;
	tm.tm_isdst = -1;
	errno = 0;
	if (mktime(&tm) == (time_t) -1) {
		fprintf(stderr, "mktime(): fails for time in spring-forward gap: %s\n", strerror(errno));
		exit(1);
	}
}

static void
mktime_test(now)
	time_t          now;
{
	struct tm      *lt;

	if ((lt = localtime(&now)) && mktime(lt) != now) {
		fprintf(stderr, "localtime() and mktime() disagree on 'now = %ld'!\n", (long) now);
		exit(1);
	}
	now = time_t_max - now;
	if ((lt = localtime(&now)) && mktime(lt) != now) {
		fprintf(stderr, "localtime() and mktime() disagree on 'time_t_max - now = %ld'!\n", now);
		exit(1);
	}
}

/*
 * Based on code from Ariel Faigon.
 */
static void
irix_6_4_bug()
{
	struct tm       tm;

	tm.tm_year = 96;
	tm.tm_mon = 3;
	tm.tm_mday = 0;
	tm.tm_hour = 0;
	tm.tm_min = 0;
	tm.tm_sec = 0;
	tm.tm_isdst = -1;
	mktime(&tm);
	if (tm.tm_mon != 2 || tm.tm_mday != 31) {
		/* XXX also fails on BSDI-1.1 */
		fprintf(stderr, "mktime(): failed irix 6.4 bug\n");
		exit(1);
	}
}

static void
bigtime_test(j)
	int             j;
{
	struct tm       tm;
	time_t          now;

	tm.tm_year = tm.tm_mon = tm.tm_mday = tm.tm_hour = tm.tm_min = tm.tm_sec = j;
	now = mktime(&tm);
	if (now != (time_t) -1) {
		struct tm      *lt = localtime(&now);

		if (!(lt
		      && lt->tm_year == tm.tm_year
		      && lt->tm_mon == tm.tm_mon
		      && lt->tm_mday == tm.tm_mday
		      && lt->tm_hour == tm.tm_hour
		      && lt->tm_min == tm.tm_min
		      && lt->tm_sec == tm.tm_sec
		      && lt->tm_yday == tm.tm_yday
		      && lt->tm_wday == tm.tm_wday
		      && ((lt->tm_isdst < 0 ? -1 : 0 < lt->tm_isdst)
			  == (tm.tm_isdst < 0 ? -1 : 0 < tm.tm_isdst)))) {
			fprintf(stderr, "mktime(): fails bigtime test: j = %d\n", j);
			exit(1);
		}
	} else {
		/*
		 * Some values of j fail -- we don't really care about those --
		 * we only care when it works to know that localtime() gets the
		 * same values back that mktime() used.
		 */
#ifdef DEBUG
		printf("mktime(): failed to convert bigtime: j = %d\n", j);
#endif
	}
}

int
main(argc, argv)
	int             argc;
	char           *argv[];
{
	time_t          t,
	                delta;
	int             i,
	                j;

	/*
	 * This test makes some buggy mktime() implementations loop.  Give up
	 * on all tests after 240 seconds.  A mktime() slower than that isn't
	 * worth using anyway.  (XXX BSDI-1.1 on i386 sx25 takes several
	 * minutes -- how long does a VAX 11/750 take to run this?)
	 */
	alarm(240);

	for (time_t_max = 1; 0 < time_t_max; time_t_max *= 2)
		continue;

	time_t_max--;
	delta = time_t_max / 997;		/* a suitable prime number */
	for (i = 0; i < N_STRINGS; i++) {
		if (tz_strings[i])
			putenv(tz_strings[i]);

		for (t = 0; t <= time_t_max - delta; t += delta)
			mktime_test(t);

		mktime_test((time_t) 60 * 60);
		mktime_test((time_t) 60 * 60 * 24);

		for (j = 1; 0 < j; j *= 2)
			bigtime_test(j);

		bigtime_test(j - 1);
	}
	irix_6_4_bug();
	spring_forward_gap();

	exit(0);
	/* NOTREACHED */
}

-- 
								Greg A. Woods

+1 416 218-0098;            <g.a.woods@ieee.org>;           <woods@robohack.ca>
Planix, Inc. <woods@planix.com>; VE3TCP; Secrets of the Weird <woods@weird.com>