[v3] tzset robustness [BZ#17715]

Message ID 54E24689.4010108@redhat.com
State Superseded
Headers

Commit Message

Florian Weimer Feb. 16, 2015, 7:35 p.m. UTC
  This patch increases tzset robustness in various ways.   Large timezone
specifiers in the environment variable and in files are rejected, and
various file corruptions are now detected.

This patch does not include the controversial TZ scrubbing feature I
proposed.

Tested on x86_65-redhat-linux-gnu, no regressions.  (Earlier testing was
also done on i386-redhat-linux-gnu.)
  

Comments

Paul Eggert Feb. 16, 2015, 8:11 p.m. UTC | #1
Florian Weimer wrote:
> +  /* POSIX time zone specifiers are much shorter than 256 characters.  */
> +  char tzbuf[256];

This part (and the other parts that assume at most 256 for TZ length) do not 
look correct to me.  The only limit that POSIX places on TZ length is ARG_MAX 
bytes (which must be at least 4096 bytes), so the following shell command 
conforms to POSIX as far as I can see:

$ TZ=PST00000000000000000000000000000000000000000000000000000000000000000000000\
0000000000000000000000000000000000000000000000000000000000000000000000000000000\
0000000000000000000000000000000000000000000000000000000000000000000000000000000\
000000000000000000000000008 date
Mon Feb 16 11:47:31 PST 2015

This shell command works in current glibc, as well as on Solaris.

POSIX does allow the implementation to impose a limit of TZNAME_MAX bytes on a 
time zone abbreviation like "PST".  If the intent is to start imposing a limit 
such as 255 in glibc to avoid denial-of-service issues, any such limit should be 
done consistently and correctly, e.g., sysconf (TZNAME_MAX) should return 255.

For what it's worth, the public-domain tz code limits itself to at most 255 
bytes in a time zone abbreviation taken from the TZ environment variable, and to 
at most 50 bytes in a time zone abbreviation stored in a tz binary file.
  
Florian Weimer Feb. 16, 2015, 8:22 p.m. UTC | #2
On 02/16/2015 09:11 PM, Paul Eggert wrote:
> This part (and the other parts that assume at most 256 for TZ length) do
> not look correct to me.  The only limit that POSIX places on TZ length
> is ARG_MAX bytes (which must be at least 4096 bytes), so the following
> shell command conforms to POSIX as far as I can see:
> 
> $
> TZ=PST00000000000000000000000000000000000000000000000000000000000000000000000\
> 
> 0000000000000000000000000000000000000000000000000000000000000000000000000000000\
> 
> 0000000000000000000000000000000000000000000000000000000000000000000000000000000\
> 
> 000000000000000000000000008 date
> Mon Feb 16 11:47:31 PST 2015
> 
> This shell command works in current glibc, as well as on Solaris.

POSIX talks about “one or more leading zeros”, which I had missed.  (I
assumed the length limit came from the syntax.)

So I'm not sure what to do here.  Get rid of the alloca?  That's going
to be more difficult to review.

> POSIX does allow the implementation to impose a limit of TZNAME_MAX
> bytes on a time zone abbreviation like "PST".  If the intent is to start
> imposing a limit such as 255 in glibc to avoid denial-of-service issues,
> any such limit should be done consistently and correctly, e.g., sysconf
> (TZNAME_MAX) should return 255.

No, I think TZNAME_MAX only applies to the “PST” part.
  
Paul Eggert Feb. 16, 2015, 10:51 p.m. UTC | #3
Florian Weimer wrote:
> So I'm not sure what to do here.  Get rid of the alloca?  That's going
> to be more difficult to review.

I haven't read the code carefully, but if the only reason for the alloca is to 
have a temporary string that one can munge by storing '\0' bytes at strategic 
locations, then I presume that one could rewrite the code to avoid the need to 
make a temporary copy, and that this would make the change harder to review. 
This may be the best we can do, though.

> I think TZNAME_MAX only applies to the “PST” part.

True, but I don't see how this contradicts what I wrote.

There are two limits here: the length limit for the entire TZ string (which 
POSIX does not allow a tight limit for), and the length limit for the time zone 
abbreviation (which is less than the TZ string limit, and which POSIX allows a 
tight limit for).  Currently glibc does not impose much of a limit on either 
quantity.  It sounds like you're thinking of limiting the latter to 255 bytes. 
If so, this new restriction should be done consistently and correctly, e.g., by 
altering how sysconf (_SC_TZNAME_MAX) behaves.

Currently neither glibc nor Solaris impose a tight limit on time zone 
abbreviation length, so this would be an incompatible change.  It probably 
wouldn't affect much real-world code, though.
  

Patch

From 08db9916cb73d77bc5f81572675244e9066ddb30 Mon Sep 17 00:00:00 2001
From: Florian Weimer <fweimer@redhat.com>
Date: Mon, 16 Feb 2015 18:22:44 +0100
Subject: [PATCH] Make time zone file parser more robust [BZ #17715]

2015-02-16  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 781f7a7..9883eb6 100644
--- a/NEWS
+++ b/NEWS
@@ -9,8 +9,14 @@  Version 2.22
 
 * The following bugs are resolved with this release:
 
-  4719, 15467, 15790, 16560, 17569, 17792, 17912, 17932, 17944, 17949,
-  17964, 17965, 17967, 17969.
+  4719, 15467, 15790, 16560, 17569, 17715, 17792, 17912, 17932, 17944,
+  17949, 17964, 17965, 17967, 17969.
+
+* 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.  Overly long time zone specifiers in the TZ
+  variable no longer result in stack overflows and crashes.
 
 Version 2.21
 
diff --git a/time/tzfile.c b/time/tzfile.c
index bcb408f..c2ddd01 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 (__glibc_unlikely (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')
@@ -270,7 +273,10 @@  __tzfile_read (const char *file, size_t extra, char **extrap)
       if (__glibc_unlikely (SIZE_MAX - total_size < tzspec_len))
 	goto lose;
     }
-  if (__glibc_unlikely (SIZE_MAX - total_size - tzspec_len < extra))
+  if (__glibc_unlikely (SIZE_MAX - total_size - tzspec_len < extra
+			|| tzspec_len >= 256))
+    /* POSIX time zone specifiers are much shorter than 256
+       characters.  */
     goto lose;
 
   /* Allocate enough memory including the extra block requested by the
@@ -434,6 +440,10 @@  __tzfile_read (const char *file, size_t extra, char **extrap)
 	goto lose;
 
       tzspec_len = st.st_size - off - 1;
+      if (__glibc_unlikely (tzspec_len == 0 || 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
literal 127
zcmWHE%1kq2Kn4GS04TzYB+3Y6vq1O}A%;Lk2w{C7Jz#x5AR1vL!~iZJWxxdhA3F~_

literal 0
HcmV?d00001

diff --git a/timezone/testdata/XT2 b/timezone/testdata/XT2
new file mode 100644
index 0000000000000000000000000000000000000000..069189e34998662d526b3645891d515a31fd6495
GIT binary patch
literal 127
wcmWHE%1kq2zyQqufdEOA5y)nN@FPM%>O%<Y1L*<l`vK7iBOwNG0VxA60ROxXJ^%m!

literal 0
HcmV?d00001

diff --git a/timezone/testdata/XT3 b/timezone/testdata/XT3
new file mode 100644
index 0000000000000000000000000000000000000000..fbf5efff9ed5560de8c9fec46c84dc3dafc8f23f
GIT binary patch
literal 127
wcmWHE%1kq2zyJb35k@3Y5Ss<Uj|edaGC~OJ1L*<l`vK7iBOwNG0VxA60KSR`WdHyG

literal 0
HcmV?d00001

diff --git a/timezone/testdata/XT4 b/timezone/testdata/XT4
new file mode 100644
index 0000000000000000000000000000000000000000..990a9763da2c00d82ad235033005047903585b25
GIT binary patch
literal 127
wcmWHE%1kq2zyORu5dkDo5T6CYj|edVGC~OJ1L*<l`vK7iBOwNG0VxA60KRGmXaE2J

literal 0
HcmV?d00001

diff --git a/timezone/tst-tzset.c b/timezone/tst-tzset.c
new file mode 100644
index 0000000..91b4ce1
--- /dev/null
+++ b/timezone/tst-tzset.c
@@ -0,0 +1,167 @@ 
+/* 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>
+
+static int do_test (void);
+#define TEST_FUNCTION do_test ()
+#include "../test-skeleton.c"
+
+/* Returns the name of a large TZ file.  */
+static char *
+create_tz_file (off64_t size)
+{
+  char *path;
+  int fd = create_temp_file ("tst-tzset-", &path);
+  if (fd < 0)
+    exit (1);
+
+  // Reopen for large-file support.
+  close (fd);
+  fd = open64 (path, O_WRONLY);
+  if (fd < 0)
+    {
+      printf ("open64 (%s) failed: %m\n", path);
+      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 (lseek64 (fd, size, SEEK_CUR) < 0)
+    {
+      printf ("lseek failed: %m\n");
+      close (fd);
+      return NULL;
+    }
+  if (write (fd, "", 1) != 1)
+    {
+      printf ("Single-byte write failed\n");
+      close (fd);
+      return NULL;
+    }
+  if (close (fd) != 0)
+    {
+      printf ("close failed: %m\n");
+      exit (1);
+    }
+  return path;
+}
+
+static void
+test_tz_file (off64_t size)
+{
+  char *path = create_tz_file (size);
+  if (setenv ("TZ", path, 1) < 0)
+    {
+      printf ("setenv failed: %m\n");
+      exit (1);
+    }
+  tzset ();
+  free (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 files.  */
+
+  /* This will succeed on 64-bit architectures, and fail on 32-bit
+     architectures.  It used to crash on 32-bit.  */
+  test_tz_file (64 * 1024 * 1024);
+
+  /* This will fail on 64-bit and 32-bit architectures.  It used to
+     cause a test timeout on 64-bit and crash on 32-bit if the TZ file
+     open succeeded for some reason (it does not use O_LARGEFILE in
+     regular builds).  */
+  test_tz_file (4LL * 1024 * 1024 * 1024 - 6);
+
+  /* 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;
+}
-- 
2.1.0