diff options
Diffstat (limited to 'localtime.c')
| -rw-r--r-- | localtime.c | 869 |
1 files changed, 706 insertions, 163 deletions
diff --git a/localtime.c b/localtime.c index 96737ca6640c..b80a34138370 100644 --- a/localtime.c +++ b/localtime.c @@ -21,22 +21,202 @@ #if HAVE_SYS_STAT_H # include <sys/stat.h> +# ifndef S_ISREG +# define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) /* Ancient UNIX. */ +# endif +#else +struct stat { char st_ctime, st_dev, st_ino; }; +# define dev_t char +# define ino_t char +# define fstat(fd, st) (memset(st, 0, sizeof *(st)), 0) +# define stat(name, st) fstat(0, st) +# define S_ISREG(mode) 1 +#endif + +#ifndef HAVE_STRUCT_STAT_ST_CTIM +# define HAVE_STRUCT_STAT_ST_CTIM 1 +#endif +#if !defined st_ctim && defined __APPLE__ && defined __MACH__ +# define st_ctim st_ctimespec +#endif + +#ifndef THREAD_SAFE +# define THREAD_SAFE 0 #endif -#if !defined S_ISREG && defined S_IFREG -/* Ancient UNIX or recent MS-Windows. */ -# define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) + +#ifndef THREAD_RWLOCK +# define THREAD_RWLOCK 0 +#endif + +#ifndef THREAD_TM_MULTI +# define THREAD_TM_MULTI 0 #endif -#if defined THREAD_SAFE && THREAD_SAFE +#if THREAD_SAFE # include <pthread.h> + +# ifndef THREAD_PREFER_SINGLE +# define THREAD_PREFER_SINGLE 0 +# endif +# if THREAD_PREFER_SINGLE +# ifndef HAVE___ISTHREADED +# if defined __FreeBSD__ || defined __OpenBSD__ +# define HAVE___ISTHREADED 1 +# else +# define HAVE___ISTHREADED 0 +# endif +# endif +# if HAVE___ISTHREADED +extern int __isthreaded; +# else +# if !defined HAVE_SYS_SINGLE_THREADED_H && defined __has_include +# if __has_include(<sys/single_threaded.h>) +# define HAVE_SYS_SINGLE_THREADED_H 1 +# else +# define HAVE_SYS_SINGLE_THREADED_H 0 +# endif +# endif +# ifndef HAVE_SYS_SINGLE_THREADED_H +# if defined __GLIBC__ && 2 < __GLIBC__ + (32 <= __GLIBC_MINOR__) +# define HAVE_SYS_SINGLE_THREADED_H 1 +# else +# define HAVE_SYS_SINGLE_THREADED_H 0 +# endif +# endif +# if HAVE_SYS_SINGLE_THREADED_H +# include <sys/single_threaded.h> +# endif +# endif +# endif + +/* True if the current process might be multi-threaded, + false if it is definitely single-threaded. + If false, it will be false the next time it is called + unless the caller creates a thread in the meantime. + If true, it might become false the next time it is called + if all other threads exit in the meantime. */ +static bool +is_threaded(void) +{ +# if THREAD_PREFER_SINGLE && HAVE___ISTHREADED + return !!__isthreaded; +# elif THREAD_PREFER_SINGLE && HAVE_SYS_SINGLE_THREADED_H + return !__libc_single_threaded; +# else + return true; +# endif +} + +# if THREAD_RWLOCK +static pthread_rwlock_t locallock = PTHREAD_RWLOCK_INITIALIZER; +static int dolock(void) { return pthread_rwlock_rdlock(&locallock); } +static void dounlock(void) { pthread_rwlock_unlock(&locallock); } +# else static pthread_mutex_t locallock = PTHREAD_MUTEX_INITIALIZER; -static int lock(void) { return pthread_mutex_lock(&locallock); } -static void unlock(void) { pthread_mutex_unlock(&locallock); } +static int dolock(void) { return pthread_mutex_lock(&locallock); } +static void dounlock(void) { pthread_mutex_unlock(&locallock); } +# endif +/* Get a lock. Return 0 on success, a positive errno value on failure, + negative if known to be single-threaded so no lock is needed. */ +static int +lock(void) +{ + if (!is_threaded()) + return -1; + return dolock(); +} +static void +unlock(bool threaded) +{ + if (threaded) + dounlock(); +} +#else +static int lock(void) { return -1; } +static void unlock(ATTRIBUTE_MAYBE_UNUSED bool threaded) { } +#endif + +/* If THREADED, upgrade a read lock to a write lock. + Return 0 on success, a positive errno value otherwise. */ +static int +rd2wrlock(ATTRIBUTE_MAYBE_UNUSED bool threaded) +{ +#if THREAD_RWLOCK + if (threaded) { + dounlock(); + return pthread_rwlock_wrlock(&locallock); + } +#endif + return 0; +} + +#if THREAD_SAFE +typedef pthread_once_t once_t; +# define ONCE_INIT PTHREAD_ONCE_INIT #else -static int lock(void) { return 0; } -static void unlock(void) { } +typedef bool once_t; +# define ONCE_INIT false #endif +static void +once(once_t *once_control, void init_routine(void)) +{ +#if THREAD_SAFE + pthread_once(once_control, init_routine); +#else + if (!*once_control) { + *once_control = true; + init_routine(); + } +#endif +} + +enum tm_multi { LOCALTIME_TM_MULTI, GMTIME_TM_MULTI, OFFTIME_TM_MULTI }; + +#if THREAD_SAFE && THREAD_TM_MULTI + +enum { N_TM_MULTI = OFFTIME_TM_MULTI + 1 }; +static pthread_key_t tm_multi_key; +static int tm_multi_key_err; + +static void +tm_multi_key_init(void) +{ + tm_multi_key_err = pthread_key_create(&tm_multi_key, free); +} + +#endif + +/* Return TMP, or a thread-specific struct tm * selected by WHICH. */ +static struct tm * +tm_multi(struct tm *tmp, ATTRIBUTE_MAYBE_UNUSED enum tm_multi which) +{ +#if THREAD_SAFE && THREAD_TM_MULTI + /* It is OK to check is_threaded() separately here; even if it + returns a different value in other places in the caller, + this function's behavior is still valid. */ + if (is_threaded()) { + /* Try to get a thread-specific struct tm *. + Fall back on TMP if this fails. */ + static pthread_once_t tm_multi_once = PTHREAD_ONCE_INIT; + pthread_once(&tm_multi_once, tm_multi_key_init); + if (!tm_multi_key_err) { + struct tm *p = pthread_getspecific(tm_multi_key); + if (!p) { + p = malloc(N_TM_MULTI * sizeof *p); + if (p && pthread_setspecific(tm_multi_key, p) != 0) { + free(p); + p = NULL; + } + } + if (p) + return &p[which]; + } + } +#endif + return tmp; +} + /* Unless intptr_t is missing, pacify gcc -Wcast-qual on char const * exprs. Use this carefully, as the casts disable type checking. This is a macro so that it can be used in static initializers. */ @@ -64,6 +244,47 @@ typedef intmax_t iinntt; #endif static_assert(IINNTT_MIN < INT_MIN && INT_MAX < IINNTT_MAX); +#ifndef HAVE_STRUCT_TIMESPEC +# define HAVE_STRUCT_TIMESPEC 1 +#endif +#if !HAVE_STRUCT_TIMESPEC +struct timespec { time_t tv_sec; long tv_nsec; }; +#endif + +#if !defined CLOCK_MONOTONIC_COARSE && defined CLOCK_MONOTONIC +# define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC +#endif +#ifndef CLOCK_MONOTONIC_COARSE +# undef clock_gettime +# define clock_gettime(id, t) ((t)->tv_sec = time(NULL), (t)->tv_nsec = 0, 0) +#endif + +/* How many seconds to wait before checking the default TZif file again. + Negative means no checking. Default to 61 if DETECT_TZ_CHANGES + (as circa 2025 FreeBSD builds its localtime.c with -DDETECT_TZ_CHANGES), + and to -1 otherwise. */ +#ifndef TZ_CHANGE_INTERVAL +# ifdef DETECT_TZ_CHANGES +# define TZ_CHANGE_INTERVAL 61 +# else +# define TZ_CHANGE_INTERVAL (-1) +# endif +#endif +static_assert(TZ_CHANGE_INTERVAL < 0 || HAVE_SYS_STAT_H); + +/* The change detection interval. */ +#if TZ_CHANGE_INTERVAL < 0 || !defined __FreeBSD__ +enum { tz_change_interval = TZ_CHANGE_INTERVAL }; +#else +/* FreeBSD uses this private-but-extern var in its internal test suite. */ +int __tz_change_interval = TZ_CHANGE_INTERVAL; +# define tz_change_interval __tz_change_interval +#endif + +/* The type of monotonic times. + This is the system time_t, even if USE_TIMEX_T #defines time_t below. */ +typedef time_t monotime_t; + /* On platforms where offtime or mktime might overflow, strftime.c defines USE_TIMEX_T to be true and includes us. This tells us to #define time_t to an internal type timex_t that is @@ -82,16 +303,16 @@ static_assert(IINNTT_MIN < INT_MIN && INT_MAX < IINNTT_MAX); # define time_t timex_t # if MKTIME_FITS_IN(LONG_MIN, LONG_MAX) typedef long timex_t; -# define TIME_T_MIN LONG_MIN -# define TIME_T_MAX LONG_MAX +# define TIME_T_MIN LONG_MIN +# define TIME_T_MAX LONG_MAX # elif MKTIME_FITS_IN(LLONG_MIN, LLONG_MAX) typedef long long timex_t; -# define TIME_T_MIN LLONG_MIN -# define TIME_T_MAX LLONG_MAX +# define TIME_T_MIN LLONG_MIN +# define TIME_T_MAX LLONG_MAX # else typedef intmax_t timex_t; -# define TIME_T_MIN INTMAX_MIN -# define TIME_T_MAX INTMAX_MAX +# define TIME_T_MIN INTMAX_MIN +# define TIME_T_MAX INTMAX_MAX # endif # ifdef TM_GMTOFF @@ -104,14 +325,11 @@ typedef intmax_t timex_t; # endif #endif -#ifndef TZ_ABBR_CHAR_SET -# define TZ_ABBR_CHAR_SET \ - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :+-._" -#endif /* !defined TZ_ABBR_CHAR_SET */ - -#ifndef TZ_ABBR_ERR_CHAR -# define TZ_ABBR_ERR_CHAR '_' -#endif /* !defined TZ_ABBR_ERR_CHAR */ +/* Placeholders for platforms lacking openat. */ +#ifndef AT_FDCWD +# define AT_FDCWD (-1) /* any negative value will do */ +static int openat(int dd, char const *path, int oflag) { unreachable (); } +#endif /* Port to platforms that lack some O_* flags. Unless otherwise specified, the flags are standardized by POSIX. */ @@ -125,12 +343,76 @@ typedef intmax_t timex_t; #ifndef O_CLOFORK # define O_CLOFORK 0 #endif +#ifndef O_DIRECTORY +# define O_DIRECTORY 0 +#endif #ifndef O_IGNORE_CTTY # define O_IGNORE_CTTY 0 /* GNU/Hurd */ #endif #ifndef O_NOCTTY # define O_NOCTTY 0 #endif +#ifndef O_PATH +# define O_PATH 0 +#endif +#ifndef O_REGULAR +# define O_REGULAR 0 +#endif +#ifndef O_RESOLVE_BENEATH +# define O_RESOLVE_BENEATH 0 +#endif +#ifndef O_SEARCH +# define O_SEARCH 0 +#endif + +#if !HAVE_ISSETUGID + +# if !defined HAVE_SYS_AUXV_H && defined __has_include +# if __has_include(<sys/auxv.h>) +# define HAVE_SYS_AUXV_H 1 +# endif +# endif +# ifndef HAVE_SYS_AUXV_H +# if defined __GLIBC__ && 2 < __GLIBC__ + (19 <= __GLIBC_MINOR__) +# define HAVE_SYS_AUXV_H 1 +# else +# define HAVE_SYS_AUXV_H 0 +# endif +# endif +# if HAVE_SYS_AUXV_H +# include <sys/auxv.h> +# endif + +/* Return 1 if the process is privileged, 0 otherwise. */ +static int +issetugid(void) +{ +# if HAVE_SYS_AUXV_H && defined AT_SECURE + unsigned long val; + errno = 0; + val = getauxval(AT_SECURE); + if (val || errno != ENOENT) + return !!val; +# endif +# if HAVE_GETRESUID + { + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + if (0 <= getresuid (&ruid, &euid, &suid)) { + if ((ruid ^ euid) | (ruid ^ suid)) + return 1; + if (0 <= getresgid (&rgid, &egid, &sgid)) + return !!((rgid ^ egid) | (rgid ^ sgid)); + } + } +# endif +# if HAVE_GETEUID + return geteuid() != getuid() || getegid() != getgid(); +# else + return 0; +# endif +} +#endif #ifndef WILDABBR /* @@ -173,6 +455,26 @@ static char const *utc = etc_utc + sizeof "Etc/" - 1; # define TZDEFRULESTRING ",M3.2.0,M11.1.0" #endif +/* If compiled with -DOPENAT_TZDIR, then when accessing a relative + name like "America/Los_Angeles", first open TZDIR (default + "/usr/share/zoneinfo") as a directory and then use the result in + openat with "America/Los_Angeles", rather than the traditional + approach of opening "/usr/share/zoneinfo/America/Los_Angeles". + Although the OPENAT_TZDIR approach is less efficient, suffers from + spurious EMFILE and ENFILE failures, and is no more secure in practice, + it is how bleeding edge FreeBSD did things from August 2025 + through at least September 2025. */ +#ifndef OPENAT_TZDIR +# define OPENAT_TZDIR 0 +#endif + +/* If compiled with -DSUPPRESS_TZDIR, do not prepend TZDIR to relative TZ. + This is intended for specialized applications only, due to its + security implications. */ +#ifndef SUPPRESS_TZDIR +# define SUPPRESS_TZDIR 0 +#endif + /* Limit to time zone abbreviation length in proleptic TZ strings. This is distinct from TZ_MAX_CHARS, which limits TZif file contents. It defaults to 254, not 255, so that desigidx_type can be an unsigned char. @@ -259,18 +561,22 @@ static struct tm *timesub(time_t const *, int_fast32_t, struct state const *, struct tm *); static bool tzparse(char const *, struct state *, struct state const *); -#ifdef ALL_STATE +#ifndef ALL_STATE +# define ALL_STATE 0 +#endif + +#if ALL_STATE static struct state * lclptr; static struct state * gmtptr; -#endif /* defined ALL_STATE */ - -#ifndef ALL_STATE +#else static struct state lclmem; static struct state gmtmem; static struct state *const lclptr = &lclmem; static struct state *const gmtptr = &gmtmem; #endif /* State Farm */ +/* Maximum number of bytes in an efficiently-handled TZ string. + Longer strings work, albeit less efficiently. */ #ifndef TZ_STRLEN_MAX # define TZ_STRLEN_MAX 255 #endif /* !defined TZ_STRLEN_MAX */ @@ -296,7 +602,7 @@ static int lcl_is_set; # if SUPPORT_C89 static struct tm tm; -#endif +# endif # if 2 <= HAVE_TZNAME + TZ_TIME_T char *tzname[2] = { UNCONST(wildabbr), UNCONST(wildabbr) }; @@ -460,7 +766,7 @@ scrub_abbrs(struct state *sp) /* Reject overlong abbreviations. */ for (i = 0; i < sp->charcnt - (TZNAME_MAXIMUM + 1); ) { - int len = strlen(&sp->chars[i]); + int len = strnlen(&sp->chars[i], TZNAME_MAXIMUM + 1); if (TZNAME_MAXIMUM < len) return EOVERFLOW; i += len + 1; @@ -468,14 +774,75 @@ scrub_abbrs(struct state *sp) /* Replace bogus characters. */ for (i = 0; i < sp->charcnt; ++i) - if (strchr(TZ_ABBR_CHAR_SET, sp->chars[i]) == NULL) - sp->chars[i] = TZ_ABBR_ERR_CHAR; + switch (sp->chars[i]) { + case '\0': + case '+': case '-': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case ':': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': + case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': + case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': + case 'V': case 'W': case 'X': case 'Y': case 'Z': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': + case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': + case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': + case 'v': case 'w': case 'x': case 'y': case 'z': + break; + + default: + sp->chars[i] = '_'; + break; + } return 0; } #endif +/* Return true if the TZif file with descriptor FD changed, + or may have changed, since the last time we were called. + Return false if it did not change. + If *ST is valid it is the file's current status; + otherwise, update *ST to the status if possible. */ +static bool +tzfile_changed(int fd, struct stat *st) +{ + /* If old_ctim.tv_sec, these variables hold the corresponding part + of the file's metadata the last time this function was called. */ + static struct timespec old_ctim; + static dev_t old_dev; + static ino_t old_ino; + + if (!st->st_ctime && fstat(fd, st) < 0) { + /* We do not know the file's state, so reset. */ + old_ctim.tv_sec = 0; + return true; + } else { + /* Use the change time, as it changes more reliably; mod time can + be set back with futimens etc. Use subsecond timestamp + resolution if available, as this can help distinguish files on + non-POSIX platforms where st_dev and st_ino are unreliable. */ + struct timespec ctim; +#if HAVE_STRUCT_STAT_ST_CTIM + ctim = st->st_ctim; +#else + ctim.tv_sec = st->st_ctime; + ctim.tv_nsec = 0; +#endif + + if ((ctim.tv_sec ^ old_ctim.tv_sec) | (ctim.tv_nsec ^ old_ctim.tv_nsec) + | (st->st_dev ^ old_dev) | (st->st_ino ^ old_ino)) { + old_ctim = ctim; + old_dev = st->st_dev; + old_ino = st->st_ino; + return true; + } + + return false; + } +} + /* Input buffer for data read from a compiled tz file. */ union input_buffer { /* The first part of the buffer, interpreted as a header. */ @@ -487,8 +854,15 @@ union input_buffer { + 4 * TZ_MAX_TIMES]; }; -/* TZDIR with a trailing '/' rather than a trailing '\0'. */ -static char const tzdirslash[sizeof TZDIR] = TZDIR "/"; +/* TZDIR with a trailing '/'. It is null-terminated if OPENAT_TZDIR. */ +#if !OPENAT_TZDIR +ATTRIBUTE_NONSTRING +#endif +static char const tzdirslash[sizeof TZDIR + OPENAT_TZDIR] = TZDIR "/"; +enum { tzdirslashlen = sizeof TZDIR }; +#ifdef PATH_MAX +static_assert(tzdirslashlen <= PATH_MAX); /* Sanity check; assumed below. */ +#endif /* Local storage needed for 'tzloadbody'. */ union local_storage { @@ -501,33 +875,38 @@ union local_storage { struct state st; } u; - /* The name of the file to be opened. Ideally this would have no - size limits, to support arbitrarily long Zone names. - Limiting Zone names to 1024 bytes should suffice for practical use. - However, there is no need for this to be smaller than struct - file_analysis as that struct is allocated anyway, as the other - union member. */ - char fullname[max(sizeof(struct file_analysis), sizeof tzdirslash + 1024)]; +#if defined PATH_MAX && !OPENAT_TZDIR && !SUPPRESS_TZDIR + /* The name of the file to be opened. */ + char fullname[PATH_MAX]; +#endif }; /* These tzload flags can be ORed together, and fit into 'char'. */ enum { TZLOAD_FROMENV = 1 }; /* The TZ string came from the environment. */ enum { TZLOAD_TZSTRING = 2 }; /* Read any newline-surrounded TZ string. */ +enum { TZLOAD_TZDIR_SUB = 4 }; /* TZ should be a file under TZDIR. */ /* Load tz data from the file named NAME into *SP. Respect TZLOADFLAGS. - Use *LSP for temporary storage. Return 0 on + Use **LSPP for temporary storage. Return 0 on success, an errno value on failure. */ static int tzloadbody(char const *name, struct state *sp, char tzloadflags, - union local_storage *lsp) + union local_storage **lspp) { register int i; register int fid; register int stored; register ssize_t nread; - register bool doaccess; - register union input_buffer *up = &lsp->u.u; + char const *relname; + union local_storage *lsp = *lspp; + union input_buffer *up; register int tzheadsize = sizeof(struct tzhead); + int dd = AT_FDCWD; + int oflags = (O_RDONLY | O_BINARY | O_CLOEXEC | O_CLOFORK + | O_IGNORE_CTTY | O_NOCTTY | O_REGULAR); + int err; + struct stat st; + st.st_ctime = 0; sp->goback = sp->goahead = false; @@ -539,66 +918,117 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags, if (name[0] == ':') ++name; -#ifdef SUPPRESS_TZDIR - /* Do not prepend TZDIR. This is intended for specialized - applications only, due to its security implications. */ - doaccess = true; + + relname = name; + + /* If the program is privileged, NAME is TZDEFAULT or + subsidiary to TZDIR. Also, NAME is not a device. */ + if (name[0] == '/' && strcmp(name, TZDEFAULT) != 0) { + if (!SUPPRESS_TZDIR + && strncmp(relname, tzdirslash, tzdirslashlen) == 0) + for (relname += tzdirslashlen; *relname == '/'; relname++) + continue; + else if (issetugid()) + return ENOTCAPABLE; + else if (!O_REGULAR) { + /* Check for devices, as their mere opening could have + unwanted side effects. Though racy, there is no + portable way to fix the races. This check is needed + only for files not otherwise known to be non-devices. */ + if (stat(name, &st) < 0) + return errno; + if (!S_ISREG(st.st_mode)) + return EINVAL; + } + } + + if (relname[0] != '/') { + if (!OPENAT_TZDIR || !O_RESOLVE_BENEATH) { + /* Fail if a relative name contains a non-terminal ".." component, + as such a name could read a non-directory outside TZDIR + when AT_FDCWD and O_RESOLVE_BENEATH are not available. */ + char const *component; + for (component = relname; component[0]; component++) + if (component[0] == '.' && component[1] == '.' + && component[2] == '/' + && (component == relname || component[-1] == '/')) + return ENOTCAPABLE; + } + + if (OPENAT_TZDIR && !SUPPRESS_TZDIR) { + /* Prefer O_SEARCH or O_PATH if available; + O_RDONLY should be OK too, as TZDIR is invariably readable. + O_DIRECTORY should be redundant but might help + on old platforms that mishandle trailing '/'. */ + dd = open(tzdirslash, + ((O_SEARCH ? O_SEARCH : O_PATH ? O_PATH : O_RDONLY) + | O_BINARY | O_CLOEXEC | O_CLOFORK | O_DIRECTORY)); + if (dd < 0) + return errno; + oflags |= O_RESOLVE_BENEATH; + } + } + + if (!OPENAT_TZDIR && !SUPPRESS_TZDIR && name[0] != '/') { + char *cp; + size_t fullnamesize; +#ifdef PATH_MAX + size_t namesizemax = PATH_MAX - tzdirslashlen; + size_t namelen = strnlen (name, namesizemax); + if (namesizemax <= namelen) + return ENAMETOOLONG; #else - doaccess = name[0] == '/'; + size_t namelen = strlen (name); #endif - if (!doaccess) { - char const *dot; - if (sizeof lsp->fullname - sizeof tzdirslash <= strlen(name)) - return ENAMETOOLONG; + fullnamesize = tzdirslashlen + namelen + 1; /* Create a string "TZDIR/NAME". Using sprintf here would pull in stdio (and would fail if the resulting string length exceeded INT_MAX!). */ - memcpy(lsp->fullname, tzdirslash, sizeof tzdirslash); - strcpy(lsp->fullname + sizeof tzdirslash, name); - - /* Set doaccess if NAME contains a ".." file name - component, as such a name could read a file outside - the TZDIR virtual subtree. */ - for (dot = name; (dot = strchr(dot, '.')); dot++) - if ((dot == name || dot[-1] == '/') && dot[1] == '.' - && (dot[2] == '/' || !dot[2])) { - doaccess = true; - break; - } - + if (ALL_STATE || sizeof *lsp < fullnamesize) { + lsp = malloc(max(sizeof *lsp, fullnamesize)); + if (!lsp) + return HAVE_MALLOC_ERRNO ? errno : ENOMEM; + *lspp = lsp; + } + cp = mempcpy(lsp, tzdirslash, tzdirslashlen); + cp = mempcpy(cp, name, namelen); + *cp = '\0'; +#if defined PATH_MAX && !OPENAT_TZDIR && !SUPPRESS_TZDIR name = lsp->fullname; - } - if (doaccess && (tzloadflags & TZLOAD_FROMENV)) { - /* Check for security violations and for devices whose mere - opening could have unwanted side effects. Although these - checks are racy, they're better than nothing and there is - no portable way to fix the races. */ - if (access(name, R_OK) < 0) - return errno; -#ifdef S_ISREG - { - struct stat st; - if (stat(name, &st) < 0) - return errno; - if (!S_ISREG(st.st_mode)) - return EINVAL; - } +#else + name = (char *) lsp; #endif } - fid = open(name, (O_RDONLY | O_BINARY | O_CLOEXEC | O_CLOFORK - | O_IGNORE_CTTY | O_NOCTTY)); - if (fid < 0) - return errno; - nread = read(fid, up->buf, sizeof up->buf); - if (nread < tzheadsize) { - int err = nread < 0 ? errno : EINVAL; - close(fid); + fid = OPENAT_TZDIR ? openat(dd, relname, oflags) : open(name, oflags); + err = errno; + if (0 <= dd) + close(dd); + if (fid < 0) return err; + + /* If detecting changes to the the primary TZif file's state and + the file's status is unchanged, save time by returning now. + Otherwise read the file's contents. Close the file either way. */ + if (0 <= tz_change_interval && (tzloadflags & TZLOAD_FROMENV) + && !tzfile_changed(fid, &st)) + err = -1; + else { + if (ALL_STATE && !lsp) { + lsp = malloc(sizeof *lsp); + if (!lsp) + return HAVE_MALLOC_ERRNO ? errno : ENOMEM; + *lspp = lsp; + } + up = &lsp->u.u; + nread = read(fid, up->buf, sizeof up->buf); + err = tzheadsize <= nread ? 0 : nread < 0 ? errno : EINVAL; } - if (close(fid) < 0) - return errno; + close(fid); + if (err) + return err < 0 ? 0 : err; + for (stored = 4; stored <= 8; stored *= 2) { char version = up->tzhead.tzh_version[0]; bool skip_datablock = stored == 4 && version; @@ -792,9 +1222,11 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags, break; } if (! (j < charcnt)) { - int tsabbrlen = strlen(tsabbr); + int tsabbrlen = strnlen(tsabbr, TZ_MAX_CHARS - j); if (j + tsabbrlen < TZ_MAX_CHARS) { - strcpy(sp->chars + j, tsabbr); + char *cp = sp->chars + j; + cp = mempcpy(cp, tsabbr, tsabbrlen); + *cp = '\0'; charcnt = j + tsabbrlen + 1; ts->ttis[i].tt_desigidx = j; gotabbr++; @@ -845,19 +1277,20 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags, static int tzload(char const *name, struct state *sp, char tzloadflags) { -#ifdef ALL_STATE - union local_storage *lsp = malloc(sizeof *lsp); - if (!lsp) { - return HAVE_MALLOC_ERRNO ? errno : ENOMEM; - } else { - int err = tzloadbody(name, sp, tzloadflags, lsp); - free(lsp); - return err; - } + int r; + union local_storage *lsp0; + union local_storage *lsp; +#if ALL_STATE + lsp = NULL; #else union local_storage ls; - return tzloadbody(name, sp, tzloadflags, &ls); + lsp = &ls; #endif + lsp0 = lsp; + r = tzloadbody(name, sp, tzloadflags, &lsp); + if (lsp != lsp0) + free(lsp); + return r; } static const int mon_lengths[2][MONSPERYEAR] = { @@ -1421,12 +1854,11 @@ tzparse(const char *name, struct state *sp, struct state const *basep) } sp->charcnt = charcnt; cp = sp->chars; - memcpy(cp, stdname, stdlen); - cp += stdlen; + cp = mempcpy(cp, stdname, stdlen); *cp++ = '\0'; if (dstlen != 0) { - memcpy(cp, dstname, dstlen); - *(cp + dstlen) = '\0'; + cp = mempcpy(cp, dstname, dstlen); + *cp = '\0'; } return true; } @@ -1440,6 +1872,22 @@ gmtload(struct state *const sp) #if !USE_TIMEX_T || !defined TM_GMTOFF +/* Return true if primary cached time zone data are fresh, + i.e., if this function is known to have recently returned false. + A call is recent if it occurred less than tz_change_interval seconds ago. + NOW should be the current time. */ +static bool +fresh_tzdata(monotime_t now) +{ + /* If nonzero, the time of the last false return. */ + static monotime_t last_checked; + + if (last_checked && now - last_checked < tz_change_interval) + return true; + last_checked = now; + return false; +} + /* Initialize *SP to a value appropriate for the TZ setting NAME. Respect TZLOADFLAGS. Return 0 on success, an errno value on failure. */ @@ -1460,7 +1908,8 @@ zoneinit(struct state *sp, char const *name, char tzloadflags) return 0; } else { int err = tzload(name, sp, tzloadflags); - if (err != 0 && name && name[0] != ':' && tzparse(name, sp, NULL)) + if (err != 0 && name && name[0] != ':' && !(tzloadflags & TZLOAD_TZDIR_SUB) + && tzparse(name, sp, NULL)) err = 0; if (err == 0) err = scrub_abbrs(sp); @@ -1468,60 +1917,130 @@ zoneinit(struct state *sp, char const *name, char tzloadflags) } } +/* Like tzset(), but in a critical section. + If THREADED && THREAD_RWLOCK the caller has a read lock, + and this function might upgrade it to a write lock. + If WALL, act as if TZ is unset; although always false in this file, + a wrapper .c file's obsolete and ineffective tzsetwall function can use it. + If tz_change_interval is positive the time is NOW; otherwise ignore NOW. */ static void -tzset_unlocked(void) -{ - char const *name = getenv("TZ"); - struct state *sp = lclptr; - int lcl = name ? strlen(name) < sizeof lcl_TZname : -1; - if (lcl < 0 - ? lcl_is_set < 0 - : 0 < lcl_is_set && strcmp(lcl_TZname, name) == 0) - return; -# ifdef ALL_STATE +tzset_unlocked(bool threaded, bool wall, monotime_t now) +{ + char const *name; + struct state *sp; + char tzloadflags; + size_t namelen; + bool writing = false; + + for (;;) { + name = wall ? NULL : getenv("TZ"); + sp = lclptr; + tzloadflags = TZLOAD_FROMENV | TZLOAD_TZSTRING; + namelen = sizeof lcl_TZname + 1; /* placeholder for no name */ + + if (name) { + namelen = strnlen(name, sizeof lcl_TZname); + + /* Abbreviate a string like "/usr/share/zoneinfo/America/Los_Angeles" + to its shorter equivalent "America/Los_Angeles". */ + if (!SUPPRESS_TZDIR && tzdirslashlen < namelen + && memcmp(name, tzdirslash, tzdirslashlen) == 0) { + char const *p = name + tzdirslashlen; + while (*p == '/') + p++; + if (*p && *p != ':') { + name = p; + namelen = strnlen(name, sizeof lcl_TZname); + tzloadflags |= TZLOAD_TZDIR_SUB; + } + } + } + + if ((tz_change_interval <= 0 ? tz_change_interval < 0 : fresh_tzdata(now)) + && (name + ? 0 < lcl_is_set && strcmp(lcl_TZname, name) == 0 + : lcl_is_set < 0)) + return; + + if (!THREAD_RWLOCK || writing) + break; + if (rd2wrlock(threaded) != 0) + return; + writing = true; + } + +# if ALL_STATE if (! sp) lclptr = sp = malloc(sizeof *lclptr); # endif if (sp) { - if (zoneinit(sp, name, TZLOAD_FROMENV | TZLOAD_TZSTRING) != 0) { + int err = zoneinit(sp, name, tzloadflags); + if (err != 0) { zoneinit(sp, "", 0); - strcpy(sp->chars, UNSPEC); + /* Abbreviate with "-00" if there was an error. + Do not treat a missing TZDEFAULT file as an error. */ + if (name || err != ENOENT) + strcpy(sp->chars, UNSPEC); + } + if (namelen < sizeof lcl_TZname) { + char *cp = lcl_TZname; + cp = mempcpy(cp, name, namelen); + *cp = '\0'; } - if (0 < lcl) - strcpy(lcl_TZname, name); } settzname(); - lcl_is_set = lcl; + lcl_is_set = (sizeof lcl_TZname > namelen) - (sizeof lcl_TZname < namelen); } #endif +#if !defined TM_GMTOFF || !USE_TIMEX_T + +/* If tz_change_interval is positive, + return the current time as a monotonically nondecreasing value. + Otherwise the return value does not matter. */ +static monotime_t +get_monotonic_time(void) +{ + struct timespec now; + now.tv_sec = 0; + if (0 < tz_change_interval) + clock_gettime(CLOCK_MONOTONIC_COARSE, &now); + return now.tv_sec; +} +#endif + #if !USE_TIMEX_T + void tzset(void) { - if (lock() != 0) + monotime_t now = get_monotonic_time(); + int err = lock(); + if (0 < err) { + errno = err; return; - tzset_unlocked(); - unlock(); + } + tzset_unlocked(!err, false, now); + unlock(!err); } #endif static void -gmtcheck(void) +gmtcheck1(void) { - static bool gmt_is_set; - if (lock() != 0) - return; - if (! gmt_is_set) { -#ifdef ALL_STATE - gmtptr = malloc(sizeof *gmtptr); +#if ALL_STATE + gmtptr = malloc(sizeof *gmtptr); #endif - if (gmtptr) - gmtload(gmtptr); - gmt_is_set = true; - } - unlock(); + if (gmtptr) + gmtload(gmtptr); +} + +static void +gmtcheck(void) +{ + static once_t gmt_once = ONCE_INIT; + once(&gmt_once, gmtcheck1); } #if NETBSD_INSPIRED && !USE_TIMEX_T @@ -1542,10 +2061,25 @@ tzalloc(char const *name) return sp; } +#ifndef FREE_PRESERVES_ERRNO +# if ((defined _POSIX_VERSION && 202405 <= _POSIX_VERSION) \ + || (defined __GLIBC__ && 2 < __GLIBC__ + (33 <= __GLIBC_MINOR__)) \ + || defined __OpenBSD__ || defined __sun) +# define FREE_PRESERVES_ERRNO 1 +# else +# define FREE_PRESERVES_ERRNO 0 +# endif +#endif + void tzfree(timezone_t sp) { + int err; + if (!FREE_PRESERVES_ERRNO) + err = errno; free(sp); + if (!FREE_PRESERVES_ERRNO) + errno = err; } /* @@ -1686,15 +2220,16 @@ localtime_rz(struct state *restrict sp, time_t const *restrict timep, static struct tm * localtime_tzset(time_t const *timep, struct tm *tmp, bool setname) { + monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return NULL; } - if (setname || !lcl_is_set) - tzset_unlocked(); + if (0 <= tz_change_interval || setname || !lcl_is_set) + tzset_unlocked(!err, false, now); tmp = localsub(lclptr, timep, setname, tmp); - unlock(); + unlock(!err); return tmp; } @@ -1704,7 +2239,7 @@ localtime(const time_t *timep) # if !SUPPORT_C89 static struct tm tm; # endif - return localtime_tzset(timep, &tm, true); + return localtime_tzset(timep, tm_multi(&tm, LOCALTIME_TM_MULTI), true); } struct tm * @@ -1756,7 +2291,7 @@ gmtime(const time_t *timep) # if !SUPPORT_C89 static struct tm tm; # endif - return gmtime_r(timep, &tm); + return gmtime_r(timep, tm_multi(&tm, GMTIME_TM_MULTI)); } # if STD_INSPIRED @@ -1765,14 +2300,19 @@ gmtime(const time_t *timep) Callers can instead use localtime_rz with a fixed-offset zone. */ struct tm * -offtime(const time_t *timep, long offset) +offtime_r(time_t const *restrict timep, long offset, struct tm *restrict tmp) { gmtcheck(); + return gmtsub(gmtptr, timep, offset, tmp); +} +struct tm * +offtime(time_t const *timep, long offset) +{ # if !SUPPORT_C89 static struct tm tm; # endif - return gmtsub(gmtptr, timep, offset, &tm); + return offtime_r(timep, offset, tm_multi(&tm, OFFTIME_TM_MULTI)); } # endif @@ -2020,8 +2560,8 @@ mktmcpy(struct tm *dest, struct tm const *src) static time_t time2sub(struct tm *const tmp, - struct tm *(*funcp)(struct state const *, time_t const *, - int_fast32_t, struct tm *), + struct tm *funcp(struct state const *, time_t const *, + int_fast32_t, struct tm *), struct state const *sp, const int_fast32_t offset, bool *okayp, @@ -2239,8 +2779,8 @@ label: static time_t time2(struct tm * const tmp, - struct tm *(*funcp)(struct state const *, time_t const *, - int_fast32_t, struct tm *), + struct tm *funcp(struct state const *, time_t const *, + int_fast32_t, struct tm *), struct state const *sp, const int_fast32_t offset, bool *okayp) @@ -2258,8 +2798,8 @@ time2(struct tm * const tmp, static time_t time1(struct tm *const tmp, - struct tm *(*funcp)(struct state const *, time_t const *, - int_fast32_t, struct tm *), + struct tm *funcp(struct state const *, time_t const *, + int_fast32_t, struct tm *), struct state const *sp, const int_fast32_t offset) { @@ -2347,15 +2887,16 @@ static time_t mktime(struct tm *tmp) { + monotime_t now = get_monotonic_time(); time_t t; int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } - tzset_unlocked(); + tzset_unlocked(!err, false, now); t = mktime_tzname(lclptr, tmp, true); - unlock(); + unlock(!err); return t; } @@ -2464,16 +3005,17 @@ time2posix_z(struct state *sp, time_t t) time_t time2posix(time_t t) { + monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } - if (!lcl_is_set) - tzset_unlocked(); + if (0 <= tz_change_interval || !lcl_is_set) + tzset_unlocked(!err, false, now); if (lclptr) t = time2posix_z(lclptr, t); - unlock(); + unlock(!err); return t; } @@ -2509,16 +3051,17 @@ posix2time_z(struct state *sp, time_t t) time_t posix2time(time_t t) { + monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } - if (!lcl_is_set) - tzset_unlocked(); + if (0 <= tz_change_interval || !lcl_is_set) + tzset_unlocked(!err, false, now); if (lclptr) t = posix2time_z(lclptr, t); - unlock(); + unlock(!err); return t; } |
