Subject: Re: bug#38958: Timestamp out of range; substituting 2514-05-30 01:53:03.999999999

From: Mark H Weaver <[email-masked]>
Date: Thu, 03 Sep 2020 15:42:02 -0400

Hi,

Ludovic Courtès <[email-masked]> writes:

> The GNU make warnings come from this impenetrable function:
>
> --8<---------------cut here---------------start------------->8---
> FILE_TIMESTAMP
> file_timestamp_cons (const char *fname, time_t stamp, long int ns)
> {
>   int offset = ORDINARY_MTIME_MIN + (FILE_TIMESTAMP_HI_RES ? ns : 0);
>   FILE_TIMESTAMP s = stamp;
>   FILE_TIMESTAMP product = (FILE_TIMESTAMP) s << FILE_TIMESTAMP_LO_BITS;
>   FILE_TIMESTAMP ts = product + offset;
>
>   if (! (s <= FILE_TIMESTAMP_S (ORDINARY_MTIME_MAX)
>          && product <= ts && ts <= ORDINARY_MTIME_MAX))
>     {
>       char buf[FILE_TIMESTAMP_PRINT_LEN_BOUND + 1];
>       const char *f = fname ? fname : _("Current time");
>       ts = s <= OLD_MTIME ? ORDINARY_MTIME_MIN : ORDINARY_MTIME_MAX;
>       file_timestamp_sprintf (buf, ts);
>       OSS (error, NILF,
>            _("%s: Timestamp out of range; substituting %s"), f, buf);
>     }
>
>   return ts;
> }
> --8<---------------cut here---------------end--------------->8---
>
> What’s OLD_MTIME?
>
> --8<---------------cut here---------------start------------->8---
> /* The file does not exist, and we assume that it is older than any
>    actual file.  */
> #define OLD_MTIME 2
>
> /* The smallest and largest ordinary timestamps.  */
> #define ORDINARY_MTIME_MIN (OLD_MTIME + 1)
> --8<---------------cut here---------------end--------------->8---
>
> That would mean that any file with mtime < 3 is considered bogus, but
> then, why wouldn’t things fail on other machines as well?

I spent a bit of time looking at the relevant code in GNU Make.  The
special MTIME values of 0, 1, and 2 seem to apply only to GNU Make's
*internal* representation of the timestamp.  'file_timestamp_cons',
which converts a standard POSIX time to the internal representation,
seems to properly handle times near the POSIX epoch by adding
ORDINARY_MTIME_MIN (via 'offset') to the POSIX time, after multiplying
it by 2^30 (if FILE_TIMESTAMP_HI_RES is enabled).

> I’m looking for ideas!  :-)

Note that the date printed in the warning (ORDINARY_MTIME_MAX),
represented as a POSIX time (seconds past the epoch), is precisely 2^34
seconds minus one nanosecond.

The problem doesn't seem to be that 'stamp' is too small, because if it
were, then the following line in 'file_timestamp_cons',

    ts = s <= OLD_MTIME ? ORDINARY_MTIME_MIN : ORDINARY_MTIME_MAX;

would substitute ORDINARY_MTIME_MIN, which is close to the POSIX epoch,
and the warning message would print a time near 1970, instead of one
near 2514 (ORDINARY_MTIME_MAX).

Rather, it appears that the 'stamp' passed into 'file_timestamp_cons'
was close to or larger than 2^34, which is approximately the largest
timestamp that GNU make supports when FILE_TIMESTAMP is 64 bits and
FILE_TIMESTAMP_HI_RES is enabled.

My guess is that maybe our near-zero timestamps are somewhere being
adjusted downwards by a timezone conversion, using an unsigned integer
type, causing them to wrap around to near the maximum value of that
type.

Note that although 'stamp' usually comes from a file 'mtime' as returned
by stat(2), it can also come from an 'ar' archive member.  In
make-4.3/src/remake.c, 'f_mtime' includes the following code:

--8<---------------cut here---------------start------------->8---
      member_date = ar_member_date (file->hname);
      mtime = (member_date == (time_t) -1
               ? NONEXISTENT_MTIME
               : file_timestamp_cons (file->hname, member_date, 0));
--8<---------------cut here---------------end--------------->8---

      Mark