strftime("%Z") invalidated by TZ change: is this a fair restriction?
This is an issue that occured to me in the process of converting tzcode to support my proposed thread-safe API. I think the following restriction is necessary to the POSIX 200x spec. If a 'struct tm' broken-down time structure is created by localtime() or localtime_r(), or modified by mktime(), and the value of the "TZ" environment variable is subsequently modified, the results of the %Z and %z strftime() and wcsftime() conversion specifiers are undefined, when strftime() or wcsftime() is called with such a broken-down time structure. If the broken-down time structure is subsequently modified by a successful call to mktime(), the results of these conversion specifiers are once again defined. If a 'struct tm' broken-down time structure is modified by gmtime(), it is implementation-defined whether the result of the %Z and %z strftime() and wcsftime() conversion specifiers shall refer to UTC or the current local time zone, when strftime() or wcsftime() is called with such a broken-down time structure. I think it's too late to get this restriction into the Austin Group revised POSIX spec, but some version of it probably ought to go into the tzcode man pages (along with documentation of what tzcode does for the second case, which is dependent on the value of TM_ZONE and TM_GMTOFF at compile-time). The motivating example for the first two paragraphs is the code attached below (test-tzchange.c). It produces the following output on FreeBSD 4.3 (which uses a slightly old version of tzcode, with some local changes that shouldn't affect this; it defines TM_ZONE and TM_GMTOFF): $ ./test-tzchange Fri 04 May 11:11:00 2001 EDT Fri 04 May 11:11:00 2001 PPT Fri 04 May 11:11:00 2001 PDT This occurs because lclptr->chars has a different value for the two time zones; since test_tm->tm_zone is a pointer into lclptr->chars, it ends up pointing somewhere bogus, and thus claiming that "Pacific Peace Time" was used in 2001. I don't think it's possible to fix this in tzcode without either breaking binary compatibility of 'struct tm', leaking memory on a TZ change, or breaking %Z for non-current time zones. The motivating example for the third paragraph is that System V (at least Solaris) and tzcode do different things, and I don't think either one is about to change. (System V always gets its %Z from tzname[i], I think.) The underlying motivation for all this is that I don't see any sensible way to set tm_zone, for struct tm's produced by reentrant timezone functions, that doesn't end up leading to free pointer dereferences following tz_free(). However, if the analogous operation for process-wide timezones is *already* undefined, it's clearly not nearly as much of a problem. What do people think about this? #include <stdio.h> #include <time.h> #include <stdlib.h> /* #define OLD_NAMES */ #ifdef OLD_NAMES /* Define this on Solaris */ #define FIRST_NAME "US/Eastern" #define SECOND_NAME "US/Pacific" #else /* Proper Olson tzdata names. */ #define FIRST_NAME "America/New_York" #define SECOND_NAME "America/Los_Angeles" #endif int main() { struct tm* test_tm; time_t then = 988989060; char buf[256]; /* Compatibility names are used, rather than new Olson names, so as to * test on Solaris. */ if (putenv("TZ=" FIRST_NAME) != 0) { fprintf(stderr, "putenv failed (1)\n"); exit(1); } if ((test_tm = localtime(&then)) == NULL) { fprintf(stderr, "localtime failed\n"); exit(1); } if (strftime(buf, sizeof(buf), "%a %d %h %H:%M:%S %Y %Z", test_tm) == 0) { fprintf(stderr, "strftime failed (1)\n"); exit(1); } printf("%s\n", buf); if (putenv("TZ=" SECOND_NAME) != 0) { fprintf(stderr, "putenv failed (2)\n"); exit(1); } tzset(); if (strftime(buf, sizeof(buf), "%a %d %h %H:%M:%S %Y %Z", test_tm) == 0) { fprintf(stderr, "strftime failed (2)\n"); exit(1); } printf("%s\n", buf); test_tm->tm_isdst = -1; if (mktime(test_tm) == -1) { fprintf(stderr, "mktime failed\n"); exit(1); } if (strftime(buf, sizeof(buf), "%a %d %h %H:%M:%S %Y %Z", test_tm) == 0) { fprintf(stderr, "strftime failed (3)\n"); exit(1); } printf("%s\n", buf); exit(0); } -- Jonathan Lennox lennox@cs.columbia.edu
From: Jonathan Lennox <lennox@cs.columbia.edu> Date: Mon, 11 Jun 2001 16:01:10 -0400 (EDT)
If a 'struct tm' broken-down time structure is created by localtime() or localtime_r(), or modified by mktime(), and the value of the "TZ" environment variable is subsequently modified, the results of the %Z and %z strftime() and wcsftime() conversion specifiers are undefined, when strftime() or wcsftime() is called with such a broken-down time structure.
If the broken-down time structure is subsequently modified by a successful call to mktime(),
or by localtime_r()
the results of these conversion specifiers are once again defined.
If a 'struct tm' broken-down time structure is modified by gmtime(),
or by gmtime_r()
it is implementation-defined whether the result of the %Z and %z strftime() and wcsftime() conversion specifiers shall refer to UTC or the current local time zone, when strftime() or wcsftime() is called with such a broken-down time structure.
I would make it undefined rather than implementation defined. Otherwise, you'll need to the address the issue of what happens if TZ is changed between the invocation of gmtime and the invocation of strftime with %Z.
I think it's too late to get this restriction into the Austin Group revised POSIX spec
It's not too late to file an interpretation request against the current standard, with a note that the same problem occurs in the latest draft (and mentioning the new functionality with %z etc). You can file a request by visiting <http://pasc.opengroup.org/interps/>. I would do it now, while you're thinking about it.
I don't see any sensible way to set tm_zone, for struct tm's produced by reentrant timezone functions, that doesn't end up leading to free pointer dereferences following tz_free().
If you have control of struct tm, you can make tm_zone of type char[8] rather than char *, and insist on a limit for time zone abbreviations. POSIX allows this limit; it's called TZNAME_MAX, which POSIX requires to be at least 6. If you do this, you don't need to worry about pointer dereferences. Admittedly it's not nice to have arbitrary limits. I would make the limit at least 7 myself.
participants (2)
-
Jonathan Lennox -
Paul Eggert