[PROPOSED] Reject ".." in TZ only when privileged
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
Paul Eggert via tz <tz@iana.org> writes:
@@ -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
No we don't, it's a default-off build option. DES -- Dag-Erling Smørgrav - des@des.no
On 2026-07-01 02:48, Dag-Erling Smørgrav wrote:
- (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 No we don't, it's a default-off build option.
Thanks for letting me know. I installed the attached to fix the comment.
participants (2)
-
Dag-Erling Smørgrav -
Paul Eggert