This better matches the behavior of tzcode 2025b and earlier,
and is more compatible with what FreeBSD does.
* localtime.c (fstatat) [!AT_FDCWD]: New placeholder function.
(tzloadbody): Use O_RESOLVE_BENEATH only when privileged.
Use similar logic when on platforms lacking O_RESOLVE_BENEATH;
this entails moving the stat check to just before the open/openat,
in using fstatat when using openat.
---
localtime.c | 47 ++++++++++++++++++++++++++++++-----------------
1 file changed, 30 insertions(+), 17 deletions(-)
diff --git a/localtime.c b/localtime.c
index 0091b81e..e3b8bb94 100644
--- a/localtime.c
+++ b/localtime.c
@@ -227,7 +227,7 @@ struct timespec { time_t tv_sec; long tv_nsec; };
/* 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),
+ (as circa 2026 FreeBSD builds its localtime.c with -DDETECT_TZ_CHANGES),
and to -1 otherwise. */
#ifndef TZ_CHANGE_INTERVAL
# ifdef DETECT_TZ_CHANGES
@@ -288,10 +288,12 @@ typedef intmax_t timex_t;
# endif
#endif
-/* Placeholders for platforms lacking openat. */
+/* Placeholders for platforms lacking AT_FCWD, openat, and fstatat. */
#ifndef AT_FDCWD
# define AT_FDCWD (-1) /* any negative value will do */
static int openat(int dd, char const *path, int oflag) { unreachable (); }
+static int fstatat(int dd, char const *path, struct stat *st, int flags)
+{ unreachable(); }
#endif
/* Port to platforms that lack some O_* flags. Unless otherwise
@@ -425,8 +427,7 @@ static char const *utc = etc_utc + sizeof "Etc/" - 1;
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. */
+ bleeding edge FreeBSD started doing it this way in August 2025. */
#ifndef OPENAT_TZDIR
# define OPENAT_TZDIR 0
#endif
@@ -917,6 +918,7 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
int dd = AT_FDCWD;
int oflags = (O_RDONLY | O_BINARY | O_CLOEXEC | O_CLOFORK
| O_IGNORE_CTTY | O_NOCTTY | O_REGULAR);
+ bool might_escape = false;
int err;
struct stat st;
st.st_ctime = 0;
@@ -943,16 +945,8 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
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;
- }
+ else
+ might_escape = true;
}
if (relname[0] != '/') {
@@ -964,8 +958,12 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
for (component = relname; component[0]; component++)
if (component[0] == '.' && component[1] == '.'
&& component[2] == '/'
- && (component == relname || component[-1] == '/'))
- return ENOTCAPABLE;
+ && (component == relname || component[-1] == '/')) {
+ if (issetugid())
+ return ENOTCAPABLE;
+ might_escape = true;
+ break;
+ }
}
if (OPENAT_TZDIR && !SUPPRESS_TZDIR) {
@@ -978,7 +976,10 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
| O_BINARY | O_CLOEXEC | O_CLOFORK | O_DIRECTORY));
if (dd < 0)
return errno;
- oflags |= O_RESOLVE_BENEATH;
+ if (O_RESOLVE_BENEATH && issetugid()) {
+ oflags |= O_RESOLVE_BENEATH;
+ might_escape = false;
+ }
}
}
@@ -1014,6 +1015,18 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
#endif
}
+ /* For a platform that lacks O_REGULAR and a file that might
+ be outside TZDIR, check that it is a regular file,
+ as merely opening a device could have unwanted side effects.
+ Though racy, there is no portable way to fix the race. */
+ if (!O_REGULAR && might_escape) {
+ /* (oflags & O_RESOLVE_BENEATH) must be zero here. */
+ if ((OPENAT_TZDIR ? fstatat(dd, relname, &st, 0) : stat(name, &st))
+ < 0)
+ return errno;
+ if (!S_ISREG(st.st_mode))
+ return EINVAL;
+ }
fid = OPENAT_TZDIR ? openat(dd, relname, oflags) : open(name, oflags);
err = errno;
if (0 <= dd)
--
2.54.0