Skip to content

Commit eeadf5f

Browse files
authored
bpo-31339: Rewrite time.asctime() and time.ctime() (#3293)
* bpo-31339: Rewrite time.asctime() and time.ctime() Backport and adapt the _asctime() function from the master branch to not depend on the implementation of asctime() and ctime() from the external C library. This change fixes a bug when Python is run using the musl C library. * bound checks for time.asctime() * bound checks for time.strftime()
1 parent 8905fb8 commit eeadf5f

File tree

3 files changed

+195
-27
lines changed

3 files changed

+195
-27
lines changed

Lib/test/test_time.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
import time
33
import unittest
44
import sys
5+
import sysconfig
6+
7+
8+
# Max year is only limited by the size of C int.
9+
SIZEOF_INT = sysconfig.get_config_var('SIZEOF_INT') or 4
10+
TIME_MAXYEAR = (1 << 8 * SIZEOF_INT - 1) - 1
511

612

713
class TimeTestCase(unittest.TestCase):
@@ -45,6 +51,66 @@ def test_strftime(self):
4551
with self.assertRaises(ValueError):
4652
time.strftime('%f')
4753

54+
def _bounds_checking(self, func):
55+
# Make sure that strftime() checks the bounds of the various parts
56+
# of the time tuple (0 is valid for *all* values).
57+
58+
# The year field is tested by other test cases above
59+
60+
# Check month [1, 12] + zero support
61+
func((1900, 0, 1, 0, 0, 0, 0, 1, -1))
62+
func((1900, 12, 1, 0, 0, 0, 0, 1, -1))
63+
self.assertRaises(ValueError, func,
64+
(1900, -1, 1, 0, 0, 0, 0, 1, -1))
65+
self.assertRaises(ValueError, func,
66+
(1900, 13, 1, 0, 0, 0, 0, 1, -1))
67+
# Check day of month [1, 31] + zero support
68+
func((1900, 1, 0, 0, 0, 0, 0, 1, -1))
69+
func((1900, 1, 31, 0, 0, 0, 0, 1, -1))
70+
self.assertRaises(ValueError, func,
71+
(1900, 1, -1, 0, 0, 0, 0, 1, -1))
72+
self.assertRaises(ValueError, func,
73+
(1900, 1, 32, 0, 0, 0, 0, 1, -1))
74+
# Check hour [0, 23]
75+
func((1900, 1, 1, 23, 0, 0, 0, 1, -1))
76+
self.assertRaises(ValueError, func,
77+
(1900, 1, 1, -1, 0, 0, 0, 1, -1))
78+
self.assertRaises(ValueError, func,
79+
(1900, 1, 1, 24, 0, 0, 0, 1, -1))
80+
# Check minute [0, 59]
81+
func((1900, 1, 1, 0, 59, 0, 0, 1, -1))
82+
self.assertRaises(ValueError, func,
83+
(1900, 1, 1, 0, -1, 0, 0, 1, -1))
84+
self.assertRaises(ValueError, func,
85+
(1900, 1, 1, 0, 60, 0, 0, 1, -1))
86+
# Check second [0, 61]
87+
self.assertRaises(ValueError, func,
88+
(1900, 1, 1, 0, 0, -1, 0, 1, -1))
89+
# C99 only requires allowing for one leap second, but Python's docs say
90+
# allow two leap seconds (0..61)
91+
func((1900, 1, 1, 0, 0, 60, 0, 1, -1))
92+
func((1900, 1, 1, 0, 0, 61, 0, 1, -1))
93+
self.assertRaises(ValueError, func,
94+
(1900, 1, 1, 0, 0, 62, 0, 1, -1))
95+
# No check for upper-bound day of week;
96+
# value forced into range by a ``% 7`` calculation.
97+
# Start check at -2 since gettmarg() increments value before taking
98+
# modulo.
99+
self.assertEqual(func((1900, 1, 1, 0, 0, 0, -1, 1, -1)),
100+
func((1900, 1, 1, 0, 0, 0, +6, 1, -1)))
101+
self.assertRaises(ValueError, func,
102+
(1900, 1, 1, 0, 0, 0, -2, 1, -1))
103+
# Check day of the year [1, 366] + zero support
104+
func((1900, 1, 1, 0, 0, 0, 0, 0, -1))
105+
func((1900, 1, 1, 0, 0, 0, 0, 366, -1))
106+
self.assertRaises(ValueError, func,
107+
(1900, 1, 1, 0, 0, 0, 0, -1, -1))
108+
self.assertRaises(ValueError, func,
109+
(1900, 1, 1, 0, 0, 0, 0, 367, -1))
110+
111+
def test_strftime_bounding_check(self):
112+
self._bounds_checking(lambda tup: time.strftime('', tup))
113+
48114
def test_strftime_bounds_checking(self):
49115
# Make sure that strftime() checks the bounds of the various parts
50116
#of the time tuple (0 is valid for *all* values).
@@ -123,15 +189,15 @@ def test_asctime(self):
123189
time.asctime(time.gmtime(self.t))
124190
self.assertRaises(TypeError, time.asctime, 0)
125191
self.assertRaises(TypeError, time.asctime, ())
126-
# XXX: Posix compiant asctime should refuse to convert
127-
# year > 9999, but Linux implementation does not.
128-
# self.assertRaises(ValueError, time.asctime,
129-
# (12345, 1, 0, 0, 0, 0, 0, 0, 0))
130-
# XXX: For now, just make sure we don't have a crash:
131-
try:
132-
time.asctime((12345, 1, 1, 0, 0, 0, 0, 1, 0))
133-
except ValueError:
134-
pass
192+
193+
# Max year is only limited by the size of C int.
194+
asc = time.asctime((TIME_MAXYEAR, 6, 1) + (0,) * 6)
195+
self.assertEqual(asc[-len(str(TIME_MAXYEAR)):], str(TIME_MAXYEAR))
196+
self.assertRaises(OverflowError, time.asctime,
197+
(TIME_MAXYEAR + 1,) + (0,) * 8)
198+
self.assertRaises(TypeError, time.asctime, 0)
199+
self.assertRaises(TypeError, time.asctime, ())
200+
self.assertRaises(TypeError, time.asctime, (0,) * 10)
135201

136202
@unittest.skipIf(not hasattr(time, "tzset"),
137203
"time module has no attribute tzset")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Rewrite time.asctime() and time.ctime(). Backport and adapt the _asctime()
2+
function from the master branch to not depend on the implementation of
3+
asctime() and ctime() from the external C library. This change fixes a bug
4+
when Python is run using the musl C library.

Modules/timemodule.c

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,76 @@ gettmarg(PyObject *args, struct tm *p)
388388
return 1;
389389
}
390390

391+
/* Check values of the struct tm fields before it is passed to strftime() and
392+
* asctime(). Return 1 if all values are valid, otherwise set an exception
393+
* and returns 0.
394+
*/
395+
static int
396+
checktm(struct tm* buf)
397+
{
398+
/* Checks added to make sure strftime() and asctime() does not crash Python by
399+
indexing blindly into some array for a textual representation
400+
by some bad index (fixes bug #897625 and #6608).
401+
402+
Also support values of zero from Python code for arguments in which
403+
that is out of range by forcing that value to the lowest value that
404+
is valid (fixed bug #1520914).
405+
406+
Valid ranges based on what is allowed in struct tm:
407+
408+
- tm_year: [0, max(int)] (1)
409+
- tm_mon: [0, 11] (2)
410+
- tm_mday: [1, 31]
411+
- tm_hour: [0, 23]
412+
- tm_min: [0, 59]
413+
- tm_sec: [0, 60]
414+
- tm_wday: [0, 6] (1)
415+
- tm_yday: [0, 365] (2)
416+
- tm_isdst: [-max(int), max(int)]
417+
418+
(1) gettmarg() handles bounds-checking.
419+
(2) Python's acceptable range is one greater than the range in C,
420+
thus need to check against automatic decrement by gettmarg().
421+
*/
422+
if (buf->tm_mon == -1)
423+
buf->tm_mon = 0;
424+
else if (buf->tm_mon < 0 || buf->tm_mon > 11) {
425+
PyErr_SetString(PyExc_ValueError, "month out of range");
426+
return 0;
427+
}
428+
if (buf->tm_mday == 0)
429+
buf->tm_mday = 1;
430+
else if (buf->tm_mday < 0 || buf->tm_mday > 31) {
431+
PyErr_SetString(PyExc_ValueError, "day of month out of range");
432+
return 0;
433+
}
434+
if (buf->tm_hour < 0 || buf->tm_hour > 23) {
435+
PyErr_SetString(PyExc_ValueError, "hour out of range");
436+
return 0;
437+
}
438+
if (buf->tm_min < 0 || buf->tm_min > 59) {
439+
PyErr_SetString(PyExc_ValueError, "minute out of range");
440+
return 0;
441+
}
442+
if (buf->tm_sec < 0 || buf->tm_sec > 61) {
443+
PyErr_SetString(PyExc_ValueError, "seconds out of range");
444+
return 0;
445+
}
446+
/* tm_wday does not need checking of its upper-bound since taking
447+
``% 7`` in gettmarg() automatically restricts the range. */
448+
if (buf->tm_wday < 0) {
449+
PyErr_SetString(PyExc_ValueError, "day of week out of range");
450+
return 0;
451+
}
452+
if (buf->tm_yday == -1)
453+
buf->tm_yday = 0;
454+
else if (buf->tm_yday < 0 || buf->tm_yday > 365) {
455+
PyErr_SetString(PyExc_ValueError, "day of year out of range");
456+
return 0;
457+
}
458+
return 1;
459+
}
460+
391461
#ifdef HAVE_STRFTIME
392462
static PyObject *
393463
time_strftime(PyObject *self, PyObject *args)
@@ -407,8 +477,10 @@ time_strftime(PyObject *self, PyObject *args)
407477
if (tup == NULL) {
408478
time_t tt = time(NULL);
409479
buf = *localtime(&tt);
410-
} else if (!gettmarg(tup, &buf))
480+
} else if (!gettmarg(tup, &buf)
481+
|| !checktm(&buf)) {
411482
return NULL;
483+
}
412484

413485
/* Checks added to make sure strftime() does not crash Python by
414486
indexing blindly into some array for a textual representation
@@ -558,27 +630,51 @@ Parse a string to a time tuple according to a format specification.\n\
558630
See the library reference manual for formatting codes (same as strftime()).");
559631

560632

633+
static PyObject *
634+
_asctime(struct tm *timeptr)
635+
{
636+
/* Inspired by Open Group reference implementation available at
637+
* http://pubs.opengroup.org/onlinepubs/009695399/functions/asctime.html */
638+
static const char wday_name[7][4] = {
639+
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
640+
};
641+
static const char mon_name[12][4] = {
642+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
643+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
644+
};
645+
PyObject *unicode, *str;
646+
/* PyString_FromString() cannot be used because it doesn't support %3d */
647+
unicode = PyUnicode_FromFormat(
648+
"%s %s%3d %.2d:%.2d:%.2d %d",
649+
wday_name[timeptr->tm_wday],
650+
mon_name[timeptr->tm_mon],
651+
timeptr->tm_mday, timeptr->tm_hour,
652+
timeptr->tm_min, timeptr->tm_sec,
653+
1900 + timeptr->tm_year);
654+
if (unicode == NULL) {
655+
return NULL;
656+
}
657+
658+
str = PyUnicode_AsASCIIString(unicode);
659+
Py_DECREF(unicode);
660+
return str;
661+
}
662+
561663
static PyObject *
562664
time_asctime(PyObject *self, PyObject *args)
563665
{
564666
PyObject *tup = NULL;
565667
struct tm buf;
566-
char *p;
567668
if (!PyArg_UnpackTuple(args, "asctime", 0, 1, &tup))
568669
return NULL;
569670
if (tup == NULL) {
570671
time_t tt = time(NULL);
571672
buf = *localtime(&tt);
572-
} else if (!gettmarg(tup, &buf))
573-
return NULL;
574-
p = asctime(&buf);
575-
if (p == NULL) {
576-
PyErr_SetString(PyExc_ValueError, "invalid time");
673+
} else if (!gettmarg(tup, &buf)
674+
|| !checktm(&buf)) {
577675
return NULL;
578676
}
579-
if (p[24] == '\n')
580-
p[24] = '\0';
581-
return PyString_FromString(p);
677+
return _asctime(&buf);
582678
}
583679

584680
PyDoc_STRVAR(asctime_doc,
@@ -593,7 +689,7 @@ time_ctime(PyObject *self, PyObject *args)
593689
{
594690
PyObject *ot = NULL;
595691
time_t tt;
596-
char *p;
692+
struct tm *buf;
597693

598694
if (!PyArg_UnpackTuple(args, "ctime", 0, 1, &ot))
599695
return NULL;
@@ -607,14 +703,16 @@ time_ctime(PyObject *self, PyObject *args)
607703
if (tt == (time_t)-1 && PyErr_Occurred())
608704
return NULL;
609705
}
610-
p = ctime(&tt);
611-
if (p == NULL) {
612-
PyErr_SetString(PyExc_ValueError, "unconvertible time");
613-
return NULL;
706+
buf = localtime(&tt);
707+
if (buf == NULL) {
708+
#ifdef EINVAL
709+
if (errno == 0) {
710+
errno = EINVAL;
711+
}
712+
#endif
713+
return PyErr_SetFromErrno(PyExc_ValueError);
614714
}
615-
if (p[24] == '\n')
616-
p[24] = '\0';
617-
return PyString_FromString(p);
715+
return _asctime(buf);
618716
}
619717

620718
PyDoc_STRVAR(ctime_doc,

0 commit comments

Comments
 (0)