diff mbox

Fix BZ 18985 out of bounds access in strftime

Message ID CAPC3xao-5YF_icBWE5yYbaYiUmiAvmb7w9s_G-dqawsx7eoTkQ@mail.gmail.com
State New
Headers show

Commit Message

Paul Pluzhnikov Sept. 20, 2015, 3:14 a.m. UTC
Greetings,

If the general direction of this patch is agreeable, I will add a test
case and send a formal patch.

Thanks,


 #endif

Comments

Paul Eggert Sept. 20, 2015, 6:38 a.m. UTC | #1
Paul Pluzhnikov wrote:
> +  /* Do range checks first.  BZ #18985  */
> +  if (tp->tm_sec < 0 || tp->tm_sec > 60
> +      || tp->tm_min < 0 || tp->tm_min > 59
> +      || tp->tm_hour < 0 || tp->tm_hour > 23
> +      || tp->tm_mday < 0 || tp->tm_mday > 31
> +      || tp->tm_mon < 0 || tp->tm_mon > 11
> +      || tp->tm_wday < 0 || tp->tm_wday > 6
> +      || tp->tm_yday < 0 || tp->tm_yday > 365
> +      || tp->tm_isdst < 0 || tp->tm_isdst > 1)
> +    {
> +      __set_errno (EINVAL);
> +      return 0;
> +    }

That doesn't look right.  If the values are out of range, C11 and POSIX say 
strftime is supposed to store an unspecified string, not return 0.

tzcode strftime stores "?" for out-of-range tm_mon and tm_wday, and looks only 
at the sign of tm_isdst; this conforms to the standard.  FreeBSD strftime is 
similar.  glibc could do the same; that should be both easy and compatible.
Paul Pluzhnikov Sept. 20, 2015, 4:55 p.m. UTC | #2
On Sat, Sep 19, 2015 at 11:38 PM, Paul Eggert <eggert@cs.ucla.edu> wrote:

> tzcode strftime stores "?" for out-of-range tm_mon and tm_wday

... but formats the other values, e.g. it can print something like

  memset (tp, 0, sizeof (*tp));
  tp->tm_hour = 1024;

  strftime(..., "%H %I", tp);  // produces "1024 04"

that doesn't seem very desirable.

Anyway, I'll send a patch to eliminate just the crashes on OOB tm_mon
and tm_wday.
Martin Sebor Sept. 20, 2015, 5:08 p.m. UTC | #3
On 09/20/2015 12:38 AM, Paul Eggert wrote:
> Paul Pluzhnikov wrote:
>> +  /* Do range checks first.  BZ #18985  */
>> +  if (tp->tm_sec < 0 || tp->tm_sec > 60
>> +      || tp->tm_min < 0 || tp->tm_min > 59
>> +      || tp->tm_hour < 0 || tp->tm_hour > 23
>> +      || tp->tm_mday < 0 || tp->tm_mday > 31
>> +      || tp->tm_mon < 0 || tp->tm_mon > 11
>> +      || tp->tm_wday < 0 || tp->tm_wday > 6
>> +      || tp->tm_yday < 0 || tp->tm_yday > 365
>> +      || tp->tm_isdst < 0 || tp->tm_isdst > 1)
>> +    {
>> +      __set_errno (EINVAL);
>> +      return 0;
>> +    }
>
> That doesn't look right.  If the values are out of range, C11 and POSIX
> say strftime is supposed to store an unspecified string, not return 0.
>
> tzcode strftime stores "?" for out-of-range tm_mon and tm_wday, and
> looks only at the sign of tm_isdst; this conforms to the standard.
> FreeBSD strftime is similar.  glibc could do the same; that should be
> both easy and compatible.

I tend to agree, although while I suspect it wasn't the intent,
an argument could be made that the patch is strictly conforming
on the basis that storing the unspecified characters would produce
a string that's longer than maxsize characters, in which case the
function is supposed to return 0.

Martin
Paul Eggert Sept. 20, 2015, 6:39 p.m. UTC | #4
Paul Pluzhnikov wrote:
> ... but formats the other values, e.g. it can print something like
>
>    memset (tp, 0, sizeof (*tp));
>    tp->tm_hour = 1024;
>
>    strftime(..., "%H %I", tp);  // produces "1024 04"
>
> that doesn't seem very desirable.

That helps the programmer more than returning 0 would. I occasionally need to 
format hours outside the 00-23 range, e.g., the "26" in the POSIX-style TZ 
string "IST-2IDT,M3.4.4/26,M10.5.0" for Israel's current daylight-saving rules. 
And even if the 1024 is a bug, one can debug it more easily by seeing the 1024 
in the output than by puzzling over a 0 return code.

Although as Martin Sebor mentions we could pretend that strftime is generating 
an infinitely-long string and use this pretence to return 0, that would be less 
useful not to mention less compatible with existing practice.

> Anyway, I'll send a patch to eliminate just the crashes on OOB tm_mon
> and tm_wday.

Don't forget out-of-range tm_isdst: there's one place the code has undefined 
behavior if tm_isdst exceeds 1.
Paul Pluzhnikov Sept. 20, 2015, 6:45 p.m. UTC | #5
On Sun, Sep 20, 2015 at 11:39 AM, Paul Eggert <eggert@cs.ucla.edu> wrote:

>>    tp->tm_hour = 1024;
>>    strftime(..., "%H %I", tp);  // produces "1024 04"
>>
>> that doesn't seem very desirable.
>
> That helps the programmer more than returning 0 would.

Yes, you convinced me that returning 0 is the wrong thing to do.

The remaining question was whether tm_hour==1024 should map to "1024"
or to "?". Your other example suggests that "1024" is more useful.

> Don't forget out-of-range tm_isdst: there's one place the code has undefined
> behavior if tm_isdst exceeds 1.

Thanks!
Mike Frysinger Sept. 20, 2015, 11:10 p.m. UTC | #6
On 20 Sep 2015 11:45, Paul Pluzhnikov wrote:
> On Sun, Sep 20, 2015 at 11:39 AM, Paul Eggert wrote:
> >>    tp->tm_hour = 1024;
> >>    strftime(..., "%H %I", tp);  // produces "1024 04"
> >>
> >> that doesn't seem very desirable.
> >
> > That helps the programmer more than returning 0 would.
> 
> Yes, you convinced me that returning 0 is the wrong thing to do.
> 
> The remaining question was whether tm_hour==1024 should map to "1024"
> or to "?". Your other example suggests that "1024" is more useful.

as i mentioned in the previous thread, i think dumping more details would
be nicer, and still permissible by the standard.  using tm_hour=1024, we
might have something like:

$ cat test.c
#include <stdio.h>
#include <string.h>
#include <time.h>
int main()
{   
	char buf[1024];
	struct tm tm;
	memset(&tm, 0, sizeof(tm));
	tm.tm_hour = 1024;
	size_t ret = strftime(buf, sizeof(buf), "%H %I", &tm);
	printf("buf = %s\n", buf);
}

$ ./a.out
buf = INVALID<H/tm_hour=1024> 1012

i'm not set on any particular structured format ... just including more
details to make it overly obvious where the problem lies.
-mike
Paul Pluzhnikov Sept. 20, 2015, 11:16 p.m. UTC | #7
On Sun, Sep 20, 2015 at 4:10 PM, Mike Frysinger <vapier@gentoo.org> wrote:

> buf = INVALID<H/tm_hour=1024> 1012

It's fairly trivial to modify my patch to show "INVALID tm_wday" instead of "?".

Doing that for tm_hour is just a bit more involved, though not that
much. But it seems that that would be something Paul E. *doesn't* want
in his "IST-2IDT,M3.4.4/26,M10.5.0" example.
Mike Frysinger Sept. 21, 2015, 1:23 a.m. UTC | #8
On 20 Sep 2015 16:16, Paul Pluzhnikov wrote:
> On Sun, Sep 20, 2015 at 4:10 PM, Mike Frysinger wrote:
> > buf = INVALID<H/tm_hour=1024> 1012
> 
> It's fairly trivial to modify my patch to show "INVALID tm_wday" instead of "?".

using a const string is fine.  better than just "?" imo.

> Doing that for tm_hour is just a bit more involved, though not that
> much. But it seems that that would be something Paul E. *doesn't* want
> in his "IST-2IDT,M3.4.4/26,M10.5.0" example.

i'm not sure that's readily obvious that there's a failure.  seems like it's
easy to pass on to other layers which also might not notice until it's much
too late.
-mike
Martin Sebor Sept. 21, 2015, 1:58 a.m. UTC | #9
On 09/20/2015 05:10 PM, Mike Frysinger wrote:
> On 20 Sep 2015 11:45, Paul Pluzhnikov wrote:
>> On Sun, Sep 20, 2015 at 11:39 AM, Paul Eggert wrote:
>>>>     tp->tm_hour = 1024;
>>>>     strftime(..., "%H %I", tp);  // produces "1024 04"
>>>>
>>>> that doesn't seem very desirable.
>>>
>>> That helps the programmer more than returning 0 would.
>>
>> Yes, you convinced me that returning 0 is the wrong thing to do.
>>
>> The remaining question was whether tm_hour==1024 should map to "1024"
>> or to "?". Your other example suggests that "1024" is more useful.
>
> as i mentioned in the previous thread, i think dumping more details would
> be nicer, and still permissible by the standard.  using tm_hour=1024, we
> might have something like:
>
> $ cat test.c
> #include <stdio.h>
> #include <string.h>
> #include <time.h>
> int main()
> {
> 	char buf[1024];
> 	struct tm tm;
> 	memset(&tm, 0, sizeof(tm));
> 	tm.tm_hour = 1024;
> 	size_t ret = strftime(buf, sizeof(buf), "%H %I", &tm);
> 	printf("buf = %s\n", buf);
> }
>
> $ ./a.out
> buf = INVALID<H/tm_hour=1024> 1012

This approach will cause the function to exceed the size of
the destination buffer when it's sized just right for valid
input. This will then lead to two failure modes for the same
problem: strftime producing "<INVALID>" for invalid input,
and returning zero for the same invalid input, depending on
the size of the destination buffer.

Martin

>
> i'm not set on any particular structured format ... just including more
> details to make it overly obvious where the problem lies.
> -mike
>
Andreas Schwab Sept. 21, 2015, 7:30 a.m. UTC | #10
Mike Frysinger <vapier@gentoo.org> writes:

> $ ./a.out
> buf = INVALID<H/tm_hour=1024> 1012

I don't think that makes any sense.  strftime should do nothing special
for numeric formats.

Andreas.
Mike Frysinger Sept. 25, 2015, 2:11 p.m. UTC | #11
On 21 Sep 2015 09:30, Andreas Schwab wrote:
> Mike Frysinger <vapier@gentoo.org> writes:
> > $ ./a.out
> > buf = INVALID<H/tm_hour=1024> 1012
> 
> I don't think that makes any sense.  strftime should do nothing special
> for numeric formats.

i'm fine with outputting INVALID<fmt/field> for any invalid field.
the =%i part doesn't need to be there.
-mike
diff mbox

Patch

diff --git a/time/strftime_l.c b/time/strftime_l.c
index b48ef34..6a64b8a 100644
--- a/time/strftime_l.c
+++ b/time/strftime_l.c
@@ -40,6 +40,7 @@ 
 #endif

 #include <ctype.h>
+#include <errno.h>
 #include <sys/types.h>         /* Some systems define `time_t' here.  */

 #ifdef TIME_WITH_SYS_TIME
@@ -497,6 +498,20 @@  __strftime_internal (s, maxsize, format, tp,
tzset_called ut_argument
       ut_argument_spec
       LOCALE_PARAM_DECL
 {
+  /* Do range checks first.  BZ #18985  */
+  if (tp->tm_sec < 0 || tp->tm_sec > 60
+      || tp->tm_min < 0 || tp->tm_min > 59
+      || tp->tm_hour < 0 || tp->tm_hour > 23
+      || tp->tm_mday < 0 || tp->tm_mday > 31
+      || tp->tm_mon < 0 || tp->tm_mon > 11
+      || tp->tm_wday < 0 || tp->tm_wday > 6
+      || tp->tm_yday < 0 || tp->tm_yday > 365
+      || tp->tm_isdst < 0 || tp->tm_isdst > 1)
+    {
+      __set_errno (EINVAL);
+      return 0;
+    }
+
 #if defined _LIBC && defined USE_IN_EXTENDED_LOCALE_MODEL
   struct __locale_data *const current = loc->__locales[LC_TIME];