From dcddbd483b8902409f97c778fe9ade2ee533c696 Mon Sep 17 00:00:00 2001
From: Florian Weimer <fweimer@redhat.com>
Date: Wed, 14 Jan 2015 16:33:52 +0100
Subject: [PATCH] Make time zone file parser more robust [BZ #17715]

This commit does not yet scrub the TZ environment variable in
AT_SECURE mode, as suggested in the bug.

2015-01-14  Florian Weimer  <fweimer@redhat.com>

	[BZ #17715]
	* time/tzfile.c (__tzfile_read): Check for large values of
	tzh_ttisstdcnt and tzh_ttisgmtcnt.  Reject overlong POSIX time
	zone specifiers.
	* time/tzset.c (__tzset_parse_tz): Guard against large time zone
	specifiers.
	* timezone/Makefile (tests): Add tst-tzset.
	(tst-tzset.out): Dependencies on time zone files.
	(tst-tzset-ENV): Set TZDIR.
	(testdata/XT%): Copy crafted time zone files.
	* timezone/README: Mention crafted time zone files.
	* timezone/testdata/XT1, timezone/testdata/XT2,
	timezone/testdata/XT3, timezone/testdata/XT4: New time zone test
	files.
	* timezone/tst-tzset.c: New test.

diff --git a/NEWS b/NEWS
index 3bdc96a..4998485 100644
--- a/NEWS
+++ b/NEWS
@@ -15,9 +15,9 @@ Version 2.21
   17501, 17506, 17508, 17522, 17555, 17570, 17571, 17572, 17573, 17574,
   17582, 17583, 17584, 17585, 17589, 17594, 17601, 17608, 17616, 17625,
   17630, 17633, 17634, 17635, 17647, 17653, 17657, 17658, 17664, 17665,
-  17668, 17682, 17717, 17719, 17722, 17723, 17724, 17725, 17732, 17733,
-  17744, 17745, 17746, 17747, 17748, 17775, 17777, 17780, 17781, 17782,
-  17791, 17793, 17796, 17797, 17803, 17806, 17834
+  17668, 17682, 17715, 17717, 17719, 17722, 17723, 17724, 17725, 17732,
+  17733, 17744, 17745, 17746, 17747, 17748, 17775, 17777, 17780, 17781,
+  17782, 17791, 17793, 17796, 17797, 17803, 17806, 17834
 
 * Optimized strcpy, stpcpy, strncpy, stpncpy, strcmp, and strncmp
   implementations for powerpc64/powerpc64le.
@@ -48,6 +48,16 @@ Version 2.21
   infinite loop if the DNS response contained a PTR record of an unexpected
   format.
 
+* The time zone file parser has been made more robust against crafted time
+  zone files, avoiding heap buffer overflows related to the processing of
+  the tzh_ttisstdcnt and tzh_ttisgmtcnt fields, and a stack overflow due to
+  large time zone data files.
+
+* Privileged executables (running SUID/SGID) unset the TZ environment
+  variable if it refers to a file outside of the default TZDIR directory.
+  Overly long time zone specifiers in the TZ variable no longer result in
+  stack overflows and crashes.
+
 * The minimum GCC version that can be used to build this version of the GNU
   C Library is GCC 4.6.  Older GCC versions, and non-GNU compilers, can
   still be used to compile programs using the GNU C Library.
diff --git a/time/tzfile.c b/time/tzfile.c
index bcb408f..52897c7 100644
--- a/time/tzfile.c
+++ b/time/tzfile.c
@@ -200,6 +200,9 @@ __tzfile_read (const char *file, size_t extra, char **extrap)
   num_isstd = (size_t) decode (tzhead.tzh_ttisstdcnt);
   num_isgmt = (size_t) decode (tzhead.tzh_ttisgmtcnt);
 
+  if (num_isstd > num_types || num_isgmt > num_types)
+    goto lose;
+
   /* For platforms with 64-bit time_t we use the new format if available.  */
   if (sizeof (time_t) == 8 && trans_width == 4
       && tzhead.tzh_version[0] != '\0')
@@ -434,6 +437,10 @@ __tzfile_read (const char *file, size_t extra, char **extrap)
 	goto lose;
 
       tzspec_len = st.st_size - off - 1;
+      if (tzspec_len >= 256)
+	/* POSIX time zone specifiers are much shorter than 256
+	   characters.  */
+	goto lose;
       char *tzstr = alloca (tzspec_len);
       if (getc_unlocked (f) != '\n'
 	  || (__fread_unlocked (tzstr, 1, tzspec_len - 1, f)
diff --git a/time/tzset.c b/time/tzset.c
index 8bc7a2e..59fd10d 100644
--- a/time/tzset.c
+++ b/time/tzset.c
@@ -176,11 +176,14 @@ __tzset_parse_tz (tz)
   memset (tz_rules, '\0', sizeof tz_rules);
   tz_rules[0].name = tz_rules[1].name = "";
 
-  /* Get the standard timezone name.  */
-  char *tzbuf = strdupa (tz);
+  /* POSIX time zone specifiers are much shorter than 256 characters.  */
+  char tzbuf[256];
+  if (strlen (tz) > sizeof (tzbuf) - 1)
+    goto out;
 
+  /* Get the standard timezone name.  */
   int consumed;
-  if (sscanf (tz, "%[A-Za-z]%n", tzbuf, &consumed) != 1)
+  if (sscanf (tz, "%255[A-Za-z]%n", tzbuf, &consumed) != 1)
     {
       /* Check for the quoted version.  */
       char *wp = tzbuf;
@@ -227,7 +230,7 @@ __tzset_parse_tz (tz)
   /* Get the DST timezone name (if any).  */
   if (*tz != '\0')
     {
-      if (sscanf (tz, "%[A-Za-z]%n", tzbuf, &consumed) != 1)
+      if (sscanf (tz, "%255[A-Za-z]%n", tzbuf, &consumed) != 1)
 	{
 	  /* Check for the quoted version.  */
 	  char *wp = tzbuf;
diff --git a/timezone/Makefile b/timezone/Makefile
index 17424b8..5f18545 100644
--- a/timezone/Makefile
+++ b/timezone/Makefile
@@ -25,7 +25,7 @@ include ../Makeconfig
 extra-objs := scheck.o ialloc.o
 
 others	:= zdump zic
-tests	:= test-tz tst-timezone
+tests	:= test-tz tst-timezone tst-tzset
 
 # pacificnew doesn't compile; if it is to be used, it should be included in
 # northamerica.
@@ -90,9 +90,11 @@ $(objpfx)tst-timezone.out: $(addprefix $(testdata)/, \
 				       Australia/Melbourne \
 				       America/Sao_Paulo Asia/Tokyo \
 				       Europe/London)
+$(objpfx)tst-tzset.out: $(addprefix $(testdata)/XT, 1 2 3 4)
 
 test-tz-ENV = TZDIR=$(testdata)
 tst-timezone-ENV = TZDIR=$(testdata)
+tst-tzset-ENV = TZDIR=$(testdata)
 
 # Note this must come second in the deps list for $(built-program-cmd) to work.
 zic-deps = $(objpfx)zic $(leapseconds) yearistype
@@ -114,6 +116,8 @@ $(testdata)/America/Sao_Paulo: southamerica $(zic-deps)
 $(testdata)/Asia/Tokyo: asia $(zic-deps)
 	$(build-testdata)
 
+$(testdata)/XT%: testdata/XT%
+	cp $< $@
 
 $(objpfx)tzselect: tzselect.ksh $(common-objpfx)config.make
 	sed -e 's|/bin/bash|$(BASH)|' \
diff --git a/timezone/README b/timezone/README
index 7a5e31c..2268f8e 100644
--- a/timezone/README
+++ b/timezone/README
@@ -15,3 +15,6 @@ version of the tzcode and tzdata packages.
 
 These packages may be found at ftp://ftp.iana.org/tz/releases/.  Commentary
 should be addressed to tz@iana.org.
+
+The subdirectory testdata contains manually edited data files for
+regression testing purposes.
diff --git a/timezone/testdata/XT1 b/timezone/testdata/XT1
new file mode 100644
index 0000000000000000000000000000000000000000..67d7ee0ba59389b4aaea0bdce15ff6bf7056c5ef
GIT binary patch
diff --git a/timezone/testdata/XT2 b/timezone/testdata/XT2
new file mode 100644
index 0000000000000000000000000000000000000000..069189e34998662d526b3645891d515a31fd6495
GIT binary patch
diff --git a/timezone/testdata/XT3 b/timezone/testdata/XT3
new file mode 100644
index 0000000000000000000000000000000000000000..fbf5efff9ed5560de8c9fec46c84dc3dafc8f23f
GIT binary patch
diff --git a/timezone/testdata/XT4 b/timezone/testdata/XT4
new file mode 100644
index 0000000000000000000000000000000000000000..990a9763da2c00d82ad235033005047903585b25
GIT binary patch
diff --git a/timezone/tst-tzset.c b/timezone/tst-tzset.c
new file mode 100644
index 0000000..20b92f6
--- /dev/null
+++ b/timezone/tst-tzset.c
@@ -0,0 +1,152 @@
+/* tzset tests with crafted time zone data.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <http://www.gnu.org/licenses/>.  */
+
+#define _GNU_SOURCE 1
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+/* Returns the name of a large TZ file.  */
+static char *
+create_tz_file (void)
+{
+  char * path = strdup ("/tmp/tst-tzset-XXXXXX");
+  if (path == NULL)
+    {
+      printf ("strdup failed: %m\n");
+      exit (1);
+    }
+  int fd = mkstemp (path);
+  if (fd < 0)
+    {
+      printf ("mkstemp failed: %m\n");
+      exit (1);
+    }
+  static const char data[] = {
+    0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x58, 0x54, 0x47, 0x00, 0x00, 0x00,
+    0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x04, 0xf8, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x58, 0x54, 0x47, 0x00, 0x00,
+    0x00, 0x0a, 0x58, 0x54, 0x47, 0x30, 0x0a
+  };
+  ssize_t ret = write (fd, data, sizeof (data));
+  if (ret < 0)
+    {
+      printf ("write failed: %m\n");
+      exit (1);
+    }
+  if ((size_t) ret != sizeof (data))
+    {
+      printf ("Short write\n");
+      exit (1);
+    }
+  if (lseek (fd, 64 * 1024 * 1024, SEEK_CUR) < 0)
+    {
+      printf ("lseek failed: %m\n");
+      exit (1);
+    }
+  if (write (fd, "", 1) != 1)
+    {
+      printf ("Single-byte write failed\n");
+      exit (1);
+    }
+  if (close (fd) != 0)
+    {
+      printf ("close failed: %m\n");
+      exit (1);
+    }
+  return path;
+}
+
+static int
+do_test (void)
+{
+  int errors = 0;
+  for (int i = 1; i <= 4; ++i)
+    {
+      char tz[16];
+      snprintf (tz, sizeof (tz), "XT%d", i);
+      if (setenv ("TZ", tz, 1) < 0)
+	{
+	  printf ("setenv failed: %m\n");
+	  return 1;
+	}
+      tzset ();
+      if (strcmp (tzname[0], tz) == 0)
+	{
+	  printf ("Unexpected success for %s\n", tz);
+	  ++errors;
+	}
+    }
+
+  /* Large TZ file.  */
+  {
+    char *path = create_tz_file ();
+    if (setenv ("TZ", path, 1) < 0)
+      {
+	printf ("setenv failed: %m\n");
+	return 1;
+      }
+    /* This will succeed on 64-bit architectures, and fail on 32-bit
+       architectures.  It used to crash on 32-bit.  */
+    tzset ();
+    unlink (path);
+    free (path);
+  }
+
+  /* Large TZ variable. */
+  {
+    size_t length = 64 * 1024 * 1024;
+    char *value = malloc (length + 1);
+    if (value == NULL)
+      {
+	puts ("malloc failed: %m");
+	return 1;
+      }
+    value[length] = '\0';
+    memset (value, ' ', length);
+    value[0] = 'U';
+    value[1] = 'T';
+    value[2] = 'C';
+    if (setenv ("TZ", value, 1) < 0)
+      {
+	printf ("setenv failed: %m\n");
+	return 1;
+      }
+    tzset ();
+  }
+  
+  return errors > 0;
+}
+
+#define TEST_FUNCTION do_test ()
+#include "../test-skeleton.c"
-- 
2.1.0

