tzcode: Limit TZ for setugid programs

The zoneinfo parser can be told to read any file the program can access
by setting TZ to either an absolute path, or a path relative to the
zoneinfo directory.  For setugid programs, we previously had a hack from
OpenBSD which rejects values of TZ deemed unsafe, but that was rather
arbitrary (anything containing a dot, for instance).  Leverage openat()
with AT_RESOLVE_BENEATH instead.

For simplicity, move the TZ change detection code to after we've opened
the file, and stat the file descriptor rather than the name.

Reviewed by:	jhb
Differential Revision:	https://reviews.freebsd.org/D52029
This commit is contained in:
Dag-Erling Smørgrav
2025-08-21 18:34:32 +02:00
parent f5efc80429
commit b6ea2513f7
+45 -25
View File
@@ -408,13 +408,13 @@ scrub_abbrs(struct state *sp)
* 1 if the time zone has changed * 1 if the time zone has changed
*/ */
static int static int
tzfile_changed(const char *name) tzfile_changed(const char *name, int fd)
{ {
static char old_name[PATH_MAX]; static char old_name[PATH_MAX];
static struct stat old_sb; static struct stat old_sb;
struct stat sb; struct stat sb;
if (stat(name, &sb) != 0) if (_fstat(fd, &sb) != 0)
return -1; return -1;
if (strcmp(name, old_name) != 0) { if (strcmp(name, old_name) != 0) {
@@ -446,8 +446,10 @@ union input_buffer {
+ 4 * TZ_MAX_TIMES]; + 4 * TZ_MAX_TIMES];
}; };
#ifndef __FreeBSD__
/* TZDIR with a trailing '/' rather than a trailing '\0'. */ /* TZDIR with a trailing '/' rather than a trailing '\0'. */
static char const tzdirslash[sizeof TZDIR] = TZDIR "/"; static char const tzdirslash[sizeof TZDIR] = TZDIR "/";
#endif /* !__FreeBSD__ */
/* Local storage needed for 'tzloadbody'. */ /* Local storage needed for 'tzloadbody'. */
union local_storage { union local_storage {
@@ -460,6 +462,7 @@ union local_storage {
struct state st; struct state st;
} u; } u;
#ifndef __FreeBSD__
/* The name of the file to be opened. Ideally this would have no /* The name of the file to be opened. Ideally this would have no
size limits, to support arbitrarily long Zone names. size limits, to support arbitrarily long Zone names.
Limiting Zone names to 1024 bytes should suffice for practical use. Limiting Zone names to 1024 bytes should suffice for practical use.
@@ -467,6 +470,7 @@ union local_storage {
file_analysis as that struct is allocated anyway, as the other file_analysis as that struct is allocated anyway, as the other
union member. */ union member. */
char fullname[max(sizeof(struct file_analysis), sizeof tzdirslash + 1024)]; char fullname[max(sizeof(struct file_analysis), sizeof tzdirslash + 1024)];
#endif /* !__FreeBSD__ */
}; };
/* Load tz data from the file named NAME into *SP. Read extended /* Load tz data from the file named NAME into *SP. Read extended
@@ -480,7 +484,11 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
register int fid; register int fid;
register int stored; register int stored;
register ssize_t nread; register ssize_t nread;
#ifdef __FreeBSD__
int serrno;
#else /* !__FreeBSD__ */
register bool doaccess; register bool doaccess;
#endif /* !__FreeBSD__ */
register union input_buffer *up = &lsp->u.u; register union input_buffer *up = &lsp->u.u;
register int tzheadsize = sizeof(struct tzhead); register int tzheadsize = sizeof(struct tzhead);
@@ -494,6 +502,7 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
if (name[0] == ':') if (name[0] == ':')
++name; ++name;
#ifndef __FreeBSD__
#ifdef SUPPRESS_TZDIR #ifdef SUPPRESS_TZDIR
/* Do not prepend TZDIR. This is intended for specialized /* Do not prepend TZDIR. This is intended for specialized
applications only, due to its security implications. */ applications only, due to its security implications. */
@@ -502,9 +511,7 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
doaccess = name[0] == '/'; doaccess = name[0] == '/';
#endif #endif
if (!doaccess) { if (!doaccess) {
#ifndef __FreeBSD__
char const *dot; char const *dot;
#endif /* !__FreeBSD__ */
if (sizeof lsp->fullname - sizeof tzdirslash <= strlen(name)) if (sizeof lsp->fullname - sizeof tzdirslash <= strlen(name))
return ENAMETOOLONG; return ENAMETOOLONG;
@@ -514,7 +521,6 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
memcpy(lsp->fullname, tzdirslash, sizeof tzdirslash); memcpy(lsp->fullname, tzdirslash, sizeof tzdirslash);
strcpy(lsp->fullname + sizeof tzdirslash, name); strcpy(lsp->fullname + sizeof tzdirslash, name);
#ifndef __FreeBSD__
/* Set doaccess if NAME contains a ".." file name /* Set doaccess if NAME contains a ".." file name
component, as such a name could read a file outside component, as such a name could read a file outside
the TZDIR virtual subtree. */ the TZDIR virtual subtree. */
@@ -524,35 +530,49 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
doaccess = true; doaccess = true;
break; break;
} }
#endif /* !__FreeBSD__ */
name = lsp->fullname; name = lsp->fullname;
} }
#ifndef __FreeBSD__
if (doaccess && access(name, R_OK) != 0) if (doaccess && access(name, R_OK) != 0)
return errno; return errno;
#endif /* !__FreeBSD__ */ #else /* __FreeBSD__ */
#ifdef DETECT_TZ_CHANGES if (issetugid()) {
if (doextend) { const char *relname = name;
/* if (strncmp(relname, TZDIR "/", strlen(TZDIR) + 1) == 0)
* Detect if the timezone file has changed. Check relname += strlen(TZDIR) + 1;
* 'doextend' to ignore TZDEFRULES; the tzfile_changed() int dd = _open(TZDIR, O_DIRECTORY | O_RDONLY);
* function can only keep state for a single file. if (dd < 0)
*/ return errno;
switch (tzfile_changed(name)) { fid = _openat(dd, relname, O_RDONLY | O_BINARY, AT_RESOLVE_BENEATH);
case -1: serrno = errno;
return errno; _close(dd);
case 0: errno = serrno;
return 0; } else
case 1: #endif
break;
}
}
#endif /* DETECT_TZ_CHANGES */
fid = _open(name, O_RDONLY | O_BINARY); fid = _open(name, O_RDONLY | O_BINARY);
if (fid < 0) if (fid < 0)
return errno; return errno;
#ifdef DETECT_TZ_CHANGES
if (doextend) {
/*
* Detect if the timezone file has changed. Check 'doextend' to
* ignore TZDEFRULES; the tzfile_changed() function can only
* keep state for a single file.
*/
switch (tzfile_changed(name, fid)) {
case -1:
serrno = errno;
_close(fid);
return serrno;
case 0:
_close(fid);
return 0;
case 1:
break;
}
}
#endif /* DETECT_TZ_CHANGES */
nread = _read(fid, up->buf, sizeof up->buf); nread = _read(fid, up->buf, sizeof up->buf);
if (nread < tzheadsize) { if (nread < tzheadsize) {
int err = nread < 0 ? errno : EINVAL; int err = nread < 0 ? errno : EINVAL;