tzset robustness [BZ#17715]

Message ID 54B6E99E.4030109@redhat.com
State Superseded
Headers

Commit Message

Florian Weimer Jan. 14, 2015, 10:11 p.m. UTC
  This patch removes two different unbounded alloca calls, and also fixes 
the TZ parser issue identified here:

   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772705

This is not my preferred approach.  I would rather like to sanitize TZ 
in AT_SECURE mode, so that specifying a file from a non-default TZDIR 
does not work.  However, this alternative approach is a bit involved 
because the current ld.so setup code is not fit to handle 
content-dependent environment variable scrubbing.

Tested on x86_64-redhat-linux-gnu with no regressions.  I also ran the 
tests in time/ and timezone/ on i386-redhat-linux-gnu, with no failures. 
  The test case from the Debian bug no longer crashes after installing 
the new glibc version (also on i386).
  

Comments

Rich Felker Jan. 15, 2015, 1:39 p.m. UTC | #1
On Wed, Jan 14, 2015 at 11:11:42PM +0100, Florian Weimer wrote:
> This patch removes two different unbounded alloca calls, and also
> fixes the TZ parser issue identified here:
> 
>   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772705
> 
> This is not my preferred approach.  I would rather like to sanitize
> TZ in AT_SECURE mode, so that specifying a file from a non-default
> TZDIR does not work.  However, this alternative approach is a bit
> involved because the current ld.so setup code is not fit to handle
> content-dependent environment variable scrubbing.

Doing the scrubbing in the dynamic linker does not seem appropriate
anyway. It wouldn't solve the problem for static-linked binaries and
it would wrongly remove environment entries rather than just ignoring
them. Instead, tzset should be doing its own path enforcement based on
the presence of getauxval(AT_SECURE) or similar (e.g. the proposed
issetugid function).

Rich
  
Florian Weimer Jan. 15, 2015, 1:45 p.m. UTC | #2
On 01/15/2015 02:39 PM, Rich Felker wrote:
> On Wed, Jan 14, 2015 at 11:11:42PM +0100, Florian Weimer wrote:
>> This patch removes two different unbounded alloca calls, and also
>> fixes the TZ parser issue identified here:
>>
>>    https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772705
>>
>> This is not my preferred approach.  I would rather like to sanitize
>> TZ in AT_SECURE mode, so that specifying a file from a non-default
>> TZDIR does not work.  However, this alternative approach is a bit
>> involved because the current ld.so setup code is not fit to handle
>> content-dependent environment variable scrubbing.
>
> Doing the scrubbing in the dynamic linker does not seem appropriate
> anyway. It wouldn't solve the problem for static-linked binaries and

I have working scrubbing for statically linked binaries, I think.

> it would wrongly remove environment entries rather than just ignoring
> them. Instead, tzset should be doing its own path enforcement based on
> the presence of getauxval(AT_SECURE) or similar (e.g. the proposed
> issetugid function).

We already do that, but we aren't consistent about it: We scrub TZDIR 
unconditionally (which is cleared in AT_SECURE mode), but we pass TZ 
variables containing absolute paths to subprocesses.  The latter means 
that the TZDIR scrubbing isn't effective.
  
Rich Felker Jan. 15, 2015, 2:02 p.m. UTC | #3
On Thu, Jan 15, 2015 at 02:45:55PM +0100, Florian Weimer wrote:
> On 01/15/2015 02:39 PM, Rich Felker wrote:
> >On Wed, Jan 14, 2015 at 11:11:42PM +0100, Florian Weimer wrote:
> >>This patch removes two different unbounded alloca calls, and also
> >>fixes the TZ parser issue identified here:
> >>
> >>   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772705
> >>
> >>This is not my preferred approach.  I would rather like to sanitize
> >>TZ in AT_SECURE mode, so that specifying a file from a non-default
> >>TZDIR does not work.  However, this alternative approach is a bit
> >>involved because the current ld.so setup code is not fit to handle
> >>content-dependent environment variable scrubbing.
> >
> >Doing the scrubbing in the dynamic linker does not seem appropriate
> >anyway. It wouldn't solve the problem for static-linked binaries and
> 
> I have working scrubbing for statically linked binaries, I think.
> 
> >it would wrongly remove environment entries rather than just ignoring
> >them. Instead, tzset should be doing its own path enforcement based on
> >the presence of getauxval(AT_SECURE) or similar (e.g. the proposed
> >issetugid function).
> 
> We already do that, but we aren't consistent about it: We scrub
> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
> pass TZ variables containing absolute paths to subprocesses.  The
> latter means that the TZDIR scrubbing isn't effective.

I fail to see how removing env vars behind the program's back is
conforming. I understand that the _intent_ is to improve security, but
IMO any contract violation such as this is a potential cause of
vulnerabilities in itself (e.g. as a silly example, suppose the child
process you were executing is a tool that examines the environment and
exits with 0/1 to tell if the environment contained anything
dangerous).

Rich
  
Joseph Myers Jan. 15, 2015, 8:25 p.m. UTC | #4
On Wed, 14 Jan 2015, Florian Weimer wrote:

> @@ -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)

Is it possible to have tzspec_len == 0 here?  The code doesn't look safe 
if tzspec_len is 0 - it would pass (size_t)-1 to __fread_unlocked.

This code is for the case where time_t is 4-byte (and so size_t is 
4-byte).  tzspec_len is of type size_t.  st.st_size is of type off64_t (st 
is struct stat64), so 8-byte.  If st.st_size < off + 2 we didn't get here, 
but if st.st_size is off + 4GB + 1 it seems to me you could then get 
tzspec_len being 0.  (This file is opened with fopen not fopen64 so the 
open should fail if it's a large file at fopen time, but one might suppose 
it only becomes a large file between the fopen call and the fstat64 call.)
  
Carlos O'Donell Jan. 16, 2015, 8:27 p.m. UTC | #5
On 01/15/2015 09:02 AM, Rich Felker wrote:
> On Thu, Jan 15, 2015 at 02:45:55PM +0100, Florian Weimer wrote:
>> On 01/15/2015 02:39 PM, Rich Felker wrote:
>>> On Wed, Jan 14, 2015 at 11:11:42PM +0100, Florian Weimer wrote:
>>>> This patch removes two different unbounded alloca calls, and also
>>>> fixes the TZ parser issue identified here:
>>>>
>>>>   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772705
>>>>
>>>> This is not my preferred approach.  I would rather like to sanitize
>>>> TZ in AT_SECURE mode, so that specifying a file from a non-default
>>>> TZDIR does not work.  However, this alternative approach is a bit
>>>> involved because the current ld.so setup code is not fit to handle
>>>> content-dependent environment variable scrubbing.
>>>
>>> Doing the scrubbing in the dynamic linker does not seem appropriate
>>> anyway. It wouldn't solve the problem for static-linked binaries and
>>
>> I have working scrubbing for statically linked binaries, I think.
>>
>>> it would wrongly remove environment entries rather than just ignoring
>>> them. Instead, tzset should be doing its own path enforcement based on
>>> the presence of getauxval(AT_SECURE) or similar (e.g. the proposed
>>> issetugid function).
>>
>> We already do that, but we aren't consistent about it: We scrub
>> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
>> pass TZ variables containing absolute paths to subprocesses.  The
>> latter means that the TZDIR scrubbing isn't effective.
> 
> I fail to see how removing env vars behind the program's back is
> conforming. I understand that the _intent_ is to improve security, but
> IMO any contract violation such as this is a potential cause of
> vulnerabilities in itself (e.g. as a silly example, suppose the child
> process you were executing is a tool that examines the environment and
> exits with 0/1 to tell if the environment contained anything
> dangerous).

I can't help but agree. Removing env vars is a bad idea, ignoring them
is the only way I'd handle this.

c.
  
Florian Weimer Jan. 20, 2015, 1:18 p.m. UTC | #6
On 01/16/2015 09:27 PM, Carlos O'Donell wrote:
>>> We already do that, but we aren't consistent about it: We scrub
>>> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
>>> pass TZ variables containing absolute paths to subprocesses.  The
>>> latter means that the TZDIR scrubbing isn't effective.
>>
>> I fail to see how removing env vars behind the program's back is
>> conforming. I understand that the _intent_ is to improve security, but
>> IMO any contract violation such as this is a potential cause of
>> vulnerabilities in itself (e.g. as a silly example, suppose the child
>> process you were executing is a tool that examines the environment and
>> exits with 0/1 to tell if the environment contained anything
>> dangerous).
> 
> I can't help but agree. Removing env vars is a bad idea, ignoring them
> is the only way I'd handle this.

On the other hand, looking at TZDIR at all is non-confirming as well.

It is certainly easier not to scrub the TZ variable because no
additional patch is required.  But I'm worried that the TZ parser
crosses a trust boundary, and we'll have to patch it again (and again).
 Ulrich already tried to robustify the parser in commit
97ac2654b2d831acaa18a2b018b0736245903fd2, BZ #13506, but missed a few
corner cases.  I don't feel very confident that my latest patch
addresses all remaining issues.
  
Rich Felker Jan. 20, 2015, 3:14 p.m. UTC | #7
On Tue, Jan 20, 2015 at 02:18:01PM +0100, Florian Weimer wrote:
> On 01/16/2015 09:27 PM, Carlos O'Donell wrote:
> >>> We already do that, but we aren't consistent about it: We scrub
> >>> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
> >>> pass TZ variables containing absolute paths to subprocesses.  The
> >>> latter means that the TZDIR scrubbing isn't effective.
> >>
> >> I fail to see how removing env vars behind the program's back is
> >> conforming. I understand that the _intent_ is to improve security, but
> >> IMO any contract violation such as this is a potential cause of
> >> vulnerabilities in itself (e.g. as a silly example, suppose the child
> >> process you were executing is a tool that examines the environment and
> >> exits with 0/1 to tell if the environment contained anything
> >> dangerous).
> > 
> > I can't help but agree. Removing env vars is a bad idea, ignoring them
> > is the only way I'd handle this.
> 
> On the other hand, looking at TZDIR at all is non-confirming as well.

Are you sure? The way in which the TZ is processed when it's not a
POSIX TZ string is implementation-defined. Defining that to include
processing of the TZDIR environment variable is perfectly acceptable.
These interfaces are already permitted to access the environment so
there's no issue with concurrent environment access.

Rich
  
Florian Weimer Jan. 20, 2015, 3:37 p.m. UTC | #8
On 01/20/2015 04:14 PM, Rich Felker wrote:
> On Tue, Jan 20, 2015 at 02:18:01PM +0100, Florian Weimer wrote:
>> On 01/16/2015 09:27 PM, Carlos O'Donell wrote:
>>>>> We already do that, but we aren't consistent about it: We scrub
>>>>> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
>>>>> pass TZ variables containing absolute paths to subprocesses.  The
>>>>> latter means that the TZDIR scrubbing isn't effective.
>>>>
>>>> I fail to see how removing env vars behind the program's back is
>>>> conforming. I understand that the _intent_ is to improve security, but
>>>> IMO any contract violation such as this is a potential cause of
>>>> vulnerabilities in itself (e.g. as a silly example, suppose the child
>>>> process you were executing is a tool that examines the environment and
>>>> exits with 0/1 to tell if the environment contained anything
>>>> dangerous).
>>>
>>> I can't help but agree. Removing env vars is a bad idea, ignoring them
>>> is the only way I'd handle this.
>>
>> On the other hand, looking at TZDIR at all is non-confirming as well.
> 
> Are you sure?

No, mainly because I'm not entirely sure what a POSIX TZ string should
look like.

I tried this, based on some test case:

$ TZ=AEST-10AEDST-11,M10.5.0,M3.5.0 strace -eopen date
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/usr/share/zoneinfo/AEST-10AEDST-11,M10.5.0,M3.5.0",
O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
Wed Jan 21 02:30:07 AEDST 2015
+++ exited with 0 +++
$ TZDIR=/tmp TZ=AEST-10AEDST-11,M10.5.0,M3.5.0 strace -eopen date
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/tmp/AEST-10AEDST-11,M10.5.0,M3.5.0", O_RDONLY|O_CLOEXEC) = 3
Tue Jan 20 15:30:14 UTC 2015
+++ exited with 0 +++

The file is copied from /usr/share/zoneinfo/UTC.

This seems to suggest that the glibc behavior is non-compliant.
  
Paul Eggert Jan. 20, 2015, 4:01 p.m. UTC | #9
Florian Weimer wrote:
> This seems to suggest that the glibc behavior is non-compliant.

No, because POSIX reserves the environment variable name TZDIR for the 
implementation (just as it reserves all upper-case-only names).
  
Rich Felker Jan. 20, 2015, 4:54 p.m. UTC | #10
On Tue, Jan 20, 2015 at 04:37:01PM +0100, Florian Weimer wrote:
> On 01/20/2015 04:14 PM, Rich Felker wrote:
> > On Tue, Jan 20, 2015 at 02:18:01PM +0100, Florian Weimer wrote:
> >> On 01/16/2015 09:27 PM, Carlos O'Donell wrote:
> >>>>> We already do that, but we aren't consistent about it: We scrub
> >>>>> TZDIR unconditionally (which is cleared in AT_SECURE mode), but we
> >>>>> pass TZ variables containing absolute paths to subprocesses.  The
> >>>>> latter means that the TZDIR scrubbing isn't effective.
> >>>>
> >>>> I fail to see how removing env vars behind the program's back is
> >>>> conforming. I understand that the _intent_ is to improve security, but
> >>>> IMO any contract violation such as this is a potential cause of
> >>>> vulnerabilities in itself (e.g. as a silly example, suppose the child
> >>>> process you were executing is a tool that examines the environment and
> >>>> exits with 0/1 to tell if the environment contained anything
> >>>> dangerous).
> >>>
> >>> I can't help but agree. Removing env vars is a bad idea, ignoring them
> >>> is the only way I'd handle this.
> >>
> >> On the other hand, looking at TZDIR at all is non-confirming as well.
> > 
> > Are you sure?
> 
> No, mainly because I'm not entirely sure what a POSIX TZ string should
> look like.
> 
> I tried this, based on some test case:
> 
> $ TZ=AEST-10AEDST-11,M10.5.0,M3.5.0 strace -eopen date
> open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
> open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
> open("/usr/share/zoneinfo/AEST-10AEDST-11,M10.5.0,M3.5.0",
> O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
> Wed Jan 21 02:30:07 AEDST 2015
> +++ exited with 0 +++
> $ TZDIR=/tmp TZ=AEST-10AEDST-11,M10.5.0,M3.5.0 strace -eopen date
> open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
> open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
> open("/tmp/AEST-10AEDST-11,M10.5.0,M3.5.0", O_RDONLY|O_CLOEXEC) = 3
> Tue Jan 20 15:30:14 UTC 2015
> +++ exited with 0 +++
> 
> The file is copied from /usr/share/zoneinfo/UTC.
> 
> This seems to suggest that the glibc behavior is non-compliant.

Yes, glibc is wrongly attempting to process strings as filenames
first, then as POSIX-format TZ strings. I'd like to see this fixed,
but I suspect too many users would complain.

Rich
  
Paul Eggert Jan. 20, 2015, 5:57 p.m. UTC | #11
On 01/20/2015 08:54 AM, Rich Felker wrote:
> Yes, glibc is wrongly attempting to process strings as filenames
> first, then as POSIX-format TZ strings.

This is OK, so long as all the file names actually installed in the 
system cannot be confused with valid POSIX-format TZ strings.  That is 
the case with the standard time zone database normally installed in 
GNU/Linux systems (at least it is now; long ago there were ambiguities 
but they were removed many years ago).
  
Florian Weimer Jan. 22, 2015, 9:54 a.m. UTC | #12
On 01/20/2015 05:01 PM, Paul Eggert wrote:
> Florian Weimer wrote:
>> This seems to suggest that the glibc behavior is non-compliant.
> 
> No, because POSIX reserves the environment variable name TZDIR for the
> implementation (just as it reserves all upper-case-only names).

Hmm. Does that mean that scrubbing TZ and TZDIR in AT_SECURE mode would
also be compliant?

Anyway, this part of the discussion is only about potential future patch
I might submit.  What about the last iteration of the existing parser fixes?

<https://sourceware.org/ml/libc-alpha/2015-01/msg00360.html>

Okay to commit?
  
Paul Eggert Jan. 22, 2015, 5:48 p.m. UTC | #13
On 01/22/2015 01:54 AM, Florian Weimer wrote:
> Does that mean that scrubbing TZ and TZDIR in AT_SECURE mode would
> also be compliant?
I doubt it, though I can't easily cite chapter and verse on that.
  
Rich Felker Jan. 25, 2015, 6:21 a.m. UTC | #14
On Thu, Jan 22, 2015 at 10:54:57AM +0100, Florian Weimer wrote:
> On 01/20/2015 05:01 PM, Paul Eggert wrote:
> > Florian Weimer wrote:
> >> This seems to suggest that the glibc behavior is non-compliant.
> > 
> > No, because POSIX reserves the environment variable name TZDIR for the
> > implementation (just as it reserves all upper-case-only names).
> 
> Hmm. Does that mean that scrubbing TZ and TZDIR in AT_SECURE mode would
> also be compliant?

No. Silently removing or modifying env vars is never compliant.

Rich
  

Patch

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
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..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