Bug 1077902 - glibc: [RFE] A new API to query time-zone data from the tzdata files.
Summary: glibc: [RFE] A new API to query time-zone data from the tzdata files.
Keywords:
Status: CLOSED UPSTREAM
Alias: None
Product: Fedora
Classification: Fedora
Component: glibc
Version: rawhide
Hardware: All
OS: All
unspecified
medium
Target Milestone: ---
Assignee: Carlos O'Donell
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2014-03-18 19:40 UTC by pjp
Modified: 2020-02-27 15:17 UTC (History)
8 users (show)

Fixed In Version:
Doc Type: Bug Fix
Doc Text:
Clone Of:
Environment:
Last Closed: 2020-02-27 15:17:25 UTC
Type: Bug
Embargoed:


Attachments (Terms of Use)


Links
System ID Private Priority Status Summary Last Updated
Sourceware 25606 0 P2 NEW Add a new API to query time-zone data from the tzdata files. 2020-02-27 15:17:25 UTC

Description pjp 2014-03-18 19:40:34 UTC
Description of problem:

Time-zone data is installed on each machine, in a binary format, under '/usr/share/zoneinfo/' directory by tzdata-*.*.noarch RPM. The current system time zone is identified by a symbolic link '/etc/localtime', which points to one of the binary file under '/usr/share/zoneinfo'. <time.h> routines from glibc read from '/etc/localtime' to perform their functions.

Currently there is no standard way, an API, to programatically query this time-zone database. It is _crucial_ to have such facility because many programs which run inside a chroot(2) jail, need to know the current system time-zone, but cannot access files pointed to by '/etc/localtime'. The only source of time-zone definition for them is the TZ environment variable. (see [1])

To set this TZ environment variable, one needs to know the current value of this variable. Towards this, it'll be extremely helpful to have an API which would return a character string indicating the current system time zone definition. Ex:

         std offset dst [offset],start[/time],end[/time]

    TZ = "NZST -12:00:00 NZDT -13:00:00,M10.1.0,M3.3.0"


API could be like

    char *tz = NULL;

    tz = get_timezone (NULL);
          => returns current system time-zone from /etc/localtime
    
    tz = get_timezone ('Pacific/Auckland')
          => returns TZ string from the said file under /usr/share/zoneinfo.

Also helpful would be to return the said string in a broken-down struct format, similar to localtime(3)

    struct offset_t {
        uint8_t hour;
        uint8_t minute;
        uint8_t second;
    };

    struct tzone {
       char *std_name;
       offset_t std_offset;

       char *dst_name;
       offset_t dst_offset;

       time_t(or struct tm) start_time;
       time_t(or struct tm) end_time;
    } tz;

    tz = localzone (get_timezone (NULL));

Additional info:

zdump(8) command facilitates querying time-zone database in similar fashion.

===
$ zdump /etc/localtime 
/etc/localtime  Wed Mar 19 01:01:54 2014 IST
$
$ zdump 'Asia/Singapore'
Asia/Singapore  Wed Mar 19 03:32:30 2014 SGT
$
===

[1] https://bugzilla.redhat.com/show_bug.cgi?id=1076801

Comment 1 pjp 2014-03-28 17:06:54 UTC
Here is a reference implementation of the proposed new API - gettimezone(). Does it look good to be submitted as patch for 'glibc'?

===
$ cat gettimezone.c

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>

#define TZONEVERSN '2'
#define TZONEMAGIC "TZif"
#define LOCALTZONE "/etc/localtime"


/* gettimezone: reads local timezone definition from '/etc/localtime'
   and returns a POSIX TZ environment variable string or NULL in case
   of an error; See: tzfile(5), tzset(3).

       std offset dst [offset],start[/time],end[/time]

   Ex: TZ="NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0"  */
char *
gettimezone (const char *tzname)
{
    int n = 0;
    FILE *fp = NULL;
    char *tz = NULL, s[4];

    errno = 0;
    if (!tzname)
        tzname = LOCALTZONE;

    fp = fopen (tzname, "rce");
    if (!fp)
        return tz;

    if (fread (s, sizeof (char), 4, fp) != 4)
        goto err;
    if (strncmp (TZONEMAGIC, s, 4))
        goto err;

    if (fread (s, sizeof (char), 1, fp) != 1)
        goto err;
    if (TZONEVERSN != *s)
        goto err;

    if (fseek (fp, -1, SEEK_END) < 0)
        goto err;
    if (fread (s, sizeof (char), 1, fp) != 1)
        goto err;
    if ('\n' != *s)
        goto err;

    n = -2;
    while (!fseek (fp, n, SEEK_END))
    {
        if (fread (s, sizeof (char), 1, fp) != 1)
            goto err;
        if ('\n' == *s)
            break;

        n--;
    }
    fseek (fp, ++n, SEEK_END);

    n = -(n + 1);
    tz = calloc (n, sizeof (char));
    if (!tz)
        goto err;

    if (fread (tz, sizeof (char), n, fp) != n)
    {
        free (tz);
        tz = NULL;
    }

err:
    fclose (fp);
    return tz;
}


int
main (int argc, char *argv[])
{
    char *tz = NULL;

    tz = gettimezone (NULL);
    printf ("tz: %s\n", tz);
    if (tz)
        free (tz);

    return 0;
}
===

Comment 2 Carlos O'Donell 2014-03-28 18:33:57 UTC
(In reply to pjp from comment #1)
> Here is a reference implementation of the proposed new API - gettimezone().
> Does it look good to be submitted as patch for 'glibc'?

It's a start, but we need to do several revisions. A bugzilla is not a good place to this kind of design. You should post upstream to libc-help and we can flesh out the design on the mailing list.

We can leave this bug open here as an RFE and reference the upstream discussion.

Comment 3 bugdal 2014-04-03 19:38:03 UTC
The approach being taken here is the wrong fix. A POSIX-style TZ string cannot duplicate what's contained in a zoneinfo file; that's the whole reason zoneinfo exists and is used. At best it can give a poor approximation that treats all historical times according to the same rules used at present day (and nowadays, daylight time rules change every few years, so that's really unacceptable for users in locations with daylight time).

The proper approach is just to COPY /etc/localtime into the chroot. The original identity (which zoneinfo file it was copied from) does not matter; all that matters are the contents of the file. Note that if /etc/localtime is a symlink (this is the situation on some systems) the actual file is resolves to should be copied since the symlink can't escape the chroot (unless you're also copying the whole zoneinfo tree into the chroot; then the symlink is fine).

Comment 4 Carlos O'Donell 2014-04-03 21:53:08 UTC
(In reply to bugdal from comment #3)
> The approach being taken here is the wrong fix. A POSIX-style TZ string
> cannot duplicate what's contained in a zoneinfo file; that's the whole
> reason zoneinfo exists and is used. At best it can give a poor approximation
> that treats all historical times according to the same rules used at present
> day (and nowadays, daylight time rules change every few years, so that's
> really unacceptable for users in locations with daylight time).
> 
> The proper approach is just to COPY /etc/localtime into the chroot. The
> original identity (which zoneinfo file it was copied from) does not matter;
> all that matters are the contents of the file. Note that if /etc/localtime
> is a symlink (this is the situation on some systems) the actual file is
> resolves to should be copied since the symlink can't escape the chroot
> (unless you're also copying the whole zoneinfo tree into the chroot; then
> the symlink is fine).

A POSIX-style TZ can absolutely duplicate what's contained in a zoneinfo file using :character implementation-defined meaning as is supported in glibc.

Copying /etc/localtime may not be possible as the chroot might be read-only or the part that matters might be read-only and part of a union filesystem or core image used by a container.

There must be a way for a host application to query the locale timezone, and pass that information to a process in the guest chroot. At present there is no API for this and you have to rely on distribution specific information.

With this API it would be as easy as:

char *ctz
ctz = gettimezone (NULL);
/* Launch a process with env TZ=*ctz */
free (ctz);

Otherwise how do you do it? If you actually cobble together the information from the various interfaces you will absolutely fail because the standard information isn't enough to be correct 100% of the time.

Comment 5 bugdal 2014-04-03 22:18:47 UTC
There's no reason to think that a zoneinfo file would even fit in the environment; even if it does, it would likely consume enough of the environment/argument space to make a problem for somebody.

Comment 6 pjp 2014-04-04 05:12:05 UTC
(In reply to bugdal from comment #3)
> The proper approach is just to COPY /etc/localtime into the chroot.

  This was the first thing I tried, somehow it does not work. Besides, it is not the best solution IMO. Because one: it may not be always possible and second: it requires user/administrator to ensure that the 'localtime' inside chroot is same as that of the system.

  On the contrary, inheriting the TZ variable is much better and less intrusive solution. Many applications do the same. For ex. see

vsftpd: vsf_sysutil_tzset(void) -> ../vsftpd-3.0.2/sysutil.c
postfix: -> http://www.postfix.org/postconf.5.html   (search for "TZ")

Comment 7 Carlos O'Donell 2014-04-04 05:39:41 UTC
(In reply to bugdal from comment #5)
> There's no reason to think that a zoneinfo file would even fit in the
> environment; even if it does, it would likely consume enough of the
> environment/argument space to make a problem for somebody.

You are correct in that you have no assurances from any standard on the size of the zoneinfo file. However we are now in implemetnation-defined territory. The largest compiled zoneinfo file on Linux is 10kb or ~12kb if you encode it for safe serialization via an env var. I don't see that being a problem for anyone on Linux, and it is one way to get around the problem of needing to have the zoneinfo in the chroot'd processes filesystem.

I would have suggested the gettimezone function return :/path/to/zone/file and let the implementation assume it has the matching tzdata installed in the chroot. It doesn't require root, it doesn't require a read-write chroot, doesn't make assumptions about the location of /etc/localtime or that it's a symlink or not, doesn't need version-2-format with an inaccurate POSIX-TZ-style string at the end etc. etc.

In the end the implementation of such an API is something that can change as we so choose because :character can become an opaque identifier e.g. if you can't open it you can try it as an identifier.

This discussion should continue upstream.

Comment 8 Paul Eggert 2014-04-22 16:38:27 UTC
> This discussion should continue upstream.

Where is "upstream"?  I'm seeing pings about this, e.g., <https://sourceware.org/ml/libc-alpha/2014-04/msg00426.html> but no working implementation and no discussion. There's also a wiki <https://sourceware.org/glibc/wiki/NewTimeZoneAPI> but this is not showing movement either.

Comment 9 pjp 2014-04-23 06:14:22 UTC
(In reply to Paul Eggert from comment #8)
> Where is "upstream"?  I'm seeing pings about this, e.g.,
> <https://sourceware.org/ml/libc-alpha/2014-04/msg00426.html> but no working
> implementation and no discussion. There's also a wiki
> <https://sourceware.org/glibc/wiki/NewTimeZoneAPI> but this is not showing
> movement either.

  I'm unsure about next course of action towards designing the new API. Are we to go with a new API or extend the tzalloc() one? And how do we go about either of those cases?

Comment 10 Paul Eggert 2014-10-16 01:15:56 UTC
I'm not sure what exactly is meant by "the tzalloc() one", but I would favor adding a tzcode-style tzalloc/tzfree/localtime_rz/mktime_z interface to the GNU C library, and as I understand it that should address the problem.  These interfaces were added in tzcode 2014g and came from NetBSD.  tzalloc(NULL) returns the same time zone info that unsetting TZ would give you, and this seems to be what was originally requested.

Comment 11 pjp 2014-10-16 14:41:52 UTC
(In reply to Paul Eggert from comment #10)
> I'm not sure what exactly is meant by "the tzalloc() one", but I would favor

Please see -> https://sourceware.org/glibc/wiki/NewTimeZoneAPI

It lists all the options that were discussed/debated on the list.

Comment 12 Paul Eggert 2014-10-16 16:53:22 UTC
(In reply to pjp from comment #11)

> Please see -> https://sourceware.org/glibc/wiki/NewTimeZoneAPI

Thanks, in that case tzalloc(NULL) a la NetBSD/tzcode2014g solves the problem and is the way to go for glibc.

Comment 13 pjp 2014-10-28 19:15:59 UTC
(In reply to Paul Eggert from comment #12)
> Thanks, in that case tzalloc(NULL) a la NetBSD/tzcode2014g solves the
> problem and is the way to go for glibc.

You mean...?
  -> http://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libc/time/localtime.c?rev=1.91&content-type=text/x-cvsweb-markup&only_with_tag=MAIN

tzalloc(NULL) leads to using TZDEFAULT variable value, which seems analogous to LOCALTZONE in c#1 above.

tzalloc(name)   /* name = NULL */
 -> zoneinit(sp, name=NULL);
  -> tzload(name=NULL, sp, true);
   -> tzloadbody(name=NULL, ...),  
      ...
      if (! name) {
          name = TZDEFAULT;
          if (! name)
              return EINVAL;
      }

Comment 14 Paul Eggert 2014-10-29 07:52:44 UTC
Yes, the idea is to use tzalloc(NULL) to get a time zone object that behaves like /etc/localtime does on RHEL.

I should mention that the object tzalloc returns cannot be passed to a subsidiary command, though it can be shared among threads and forked children.  For example, the current process can call tzalloc(NULL), then call chroot, and then still have access to the time zone data; but once it (or a child) execs or calls tzfree, it will lose access to that bit of time zone data.

Comment 15 Paul Eggert 2019-10-23 17:40:11 UTC
I see that a needinfo tag has been added. To flesh out what's needed for compatibility with tzcode and NetBSD, here's the API, in <time.h>. I'm taking the liberty of clearing the needinfo tag; if I'm mistaken and some info besides this needed, please let me know.


typedef struct __whatever *timezone_t; // Some pointer type.
timezone_t tzalloc(char const *__tzstring);
void tzfree(timezone_t __tz);
struct tm *localtime_rz(timezone_t restrict __tz, time_t const *restrict __timer, struct tm *restrict __tp);
time_t mktime_z(timezone_t restrict __tz, struct tm *restrict __tp);

localtime_rz and mktime_z act like localtime_r and mktime, except with a new TZ argument specifying the timezone. A null TZ gives you UTC (possibly with leap seconds, depending on the platform).

tzalloc(TZSTRING) allocates and returns a timezone object described by a TZSTRING value, which is interpreted the same way that the TZ environment variable is interpreted. It returns NULL (setting errno) if TZSTRING is not a valid timezone description or if memory is exhausted. A null-pointer TZSTRING is interpreted the same way as an unset TZ environment variable.

tzfree(TZ) frees all resources associated with TZ. This may invalidate data in struct tm objects set by localtime_rz and mktime_z (i.e., the tm_zone pointers may no longer be valid).

Unlike localtime_r and mktime, localtime_rz and mktime_z are reentrant and do not need to obtain locks when operating in multithreaded code.

Comment 16 Carlos O'Donell 2020-02-27 15:17:25 UTC
I've filed this bug upstream:
"Bug 25606 - Add a new API to query time-zone data from the tzdata files."
https://sourceware.org/bugzilla/show_bug.cgi?id=25606

I want to track this upstream instead of here in Fedora. This needs to be resolved upstream and then any changes would immediately make it into Fedora Rawhide.

I'm marking this bug CLOSED/UPSTREAM, and will continue to track this upstream.


Note You need to log in before you can comment on or make changes to this bug.