Skip to content

Commit 2f43e99

Browse files
committed
gh-157500: Serialize time.tzset() with localtime and strftime
tzset() mutates process-global libc timezone state that localtime_r() and strftime("%Z") read. Hold a PyMutex around time-module paths that touch that state and copy tm_zone onto the stack before decoding it.
1 parent 4d3b405 commit 2f43e99

3 files changed

Lines changed: 87 additions & 4 deletions

File tree

Lib/test/test_time.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,44 @@ def test_tzset(self):
503503
del environ['TZ']
504504
time.tzset()
505505

506+
@unittest.skipUnless(hasattr(time, "tzset"),
507+
"time module has no attribute tzset")
508+
def test_tzset_race_with_localtime(self):
509+
from os import environ
510+
org_TZ = environ.get('TZ', None)
511+
errors = []
512+
barrier = threading.Barrier(4)
513+
514+
def setter():
515+
barrier.wait()
516+
for i in range(200):
517+
environ['TZ'] = 'UTC0' if i & 1 else 'EST5EDT'
518+
time.tzset()
519+
520+
def reader():
521+
barrier.wait()
522+
for _ in range(200):
523+
try:
524+
time.localtime()
525+
time.strftime('%Z')
526+
except Exception as e:
527+
errors.append(e)
528+
529+
threads = [threading.Thread(target=setter) for _ in range(2)]
530+
threads += [threading.Thread(target=reader) for _ in range(2)]
531+
try:
532+
for t in threads:
533+
t.start()
534+
for t in threads:
535+
t.join()
536+
self.assertEqual(errors, [])
537+
finally:
538+
if org_TZ is not None:
539+
environ['TZ'] = org_TZ
540+
elif 'TZ' in environ:
541+
del environ['TZ']
542+
time.tzset()
543+
506544
def test_insane_timestamps(self):
507545
# It's possible that some platform maps time_t to double,
508546
# and that this test will fail there. This test should
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a data race between ``time.tzset()`` and concurrent
2+
``time.localtime()`` / ``time.strftime()`` calls.

Modules/timemodule.c

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#include "pycore_time.h" // _PyTimeFraction
99

1010
#include <time.h> // clock()
11+
#include <string.h> // strncpy()
12+
13+
static PyMutex timezone_mutex = {0};
1114
#ifdef HAVE_SYS_TIMES_H
1215
# include <sys/times.h> // times()
1316
#endif
@@ -567,6 +570,21 @@ GMT). When 'seconds' is not passed in, convert the current time instead.\n\
567570
If the platform supports the tm_gmtoff and tm_zone, they are available as\n\
568571
attributes only.");
569572

573+
#ifdef HAVE_STRUCT_TM_TM_ZONE
574+
static void
575+
snapshot_tm_zone(struct tm *p, char *buf, size_t n)
576+
{
577+
if (p->tm_zone != NULL) {
578+
strncpy(buf, p->tm_zone, n - 1);
579+
buf[n - 1] = '\0';
580+
}
581+
else {
582+
buf[0] = '\0';
583+
}
584+
p->tm_zone = buf;
585+
}
586+
#endif
587+
570588
static PyObject *
571589
time_localtime(PyObject *module, PyObject *args)
572590
{
@@ -575,11 +593,18 @@ time_localtime(PyObject *module, PyObject *args)
575593

576594
if (!parse_time_t_args(args, "|O:localtime", &when))
577595
return NULL;
578-
if (_PyTime_localtime(when, &buf) != 0)
596+
597+
PyMutex_Lock(&timezone_mutex);
598+
if (_PyTime_localtime(when, &buf) != 0) {
599+
PyMutex_Unlock(&timezone_mutex);
579600
return NULL;
601+
}
580602

581603
time_module_state *state = get_time_state(module);
582604
#ifdef HAVE_STRUCT_TM_TM_ZONE
605+
char zone_buf[64];
606+
snapshot_tm_zone(&buf, zone_buf, sizeof(zone_buf));
607+
PyMutex_Unlock(&timezone_mutex);
583608
return tmtotuple(state, &buf);
584609
#else
585610
{
@@ -588,6 +613,7 @@ time_localtime(PyObject *module, PyObject *args)
588613
time_t gmtoff;
589614
strftime(zone, sizeof(zone), "%Z", &buf);
590615
gmtoff = timegm(&buf) - when;
616+
PyMutex_Unlock(&timezone_mutex);
591617
return tmtotuple(state, &local, zone, gmtoff);
592618
}
593619
#endif
@@ -884,8 +910,12 @@ time_strftime(PyObject *module, PyObject *args)
884910
time_module_state *state = get_time_state(module);
885911
if (tup == NULL) {
886912
time_t tt = time(NULL);
887-
if (_PyTime_localtime(tt, &buf) != 0)
913+
PyMutex_Lock(&timezone_mutex);
914+
if (_PyTime_localtime(tt, &buf) != 0) {
915+
PyMutex_Unlock(&timezone_mutex);
888916
return NULL;
917+
}
918+
PyMutex_Unlock(&timezone_mutex);
889919
}
890920
else if (!gettmarg(state, tup, &buf,
891921
"iiiiiiiii;strftime(): illegal time tuple argument") ||
@@ -941,8 +971,10 @@ time_strftime(PyObject *module, PyObject *args)
941971
}
942972
if (fmtlen) {
943973
format[fmtlen] = 0;
974+
PyMutex_Lock(&timezone_mutex);
944975
PyObject *unicode = time_strftime1(&outbuf, &bufsize,
945976
format, fmtlen, &buf);
977+
PyMutex_Unlock(&timezone_mutex);
946978
if (unicode == NULL) {
947979
goto error;
948980
}
@@ -1043,8 +1075,12 @@ time_asctime(PyObject *module, PyObject *args)
10431075
time_module_state *state = get_time_state(module);
10441076
if (tup == NULL) {
10451077
time_t tt = time(NULL);
1046-
if (_PyTime_localtime(tt, &buf) != 0)
1078+
PyMutex_Lock(&timezone_mutex);
1079+
if (_PyTime_localtime(tt, &buf) != 0) {
1080+
PyMutex_Unlock(&timezone_mutex);
10471081
return NULL;
1082+
}
1083+
PyMutex_Unlock(&timezone_mutex);
10481084
}
10491085
else if (!gettmarg(state, tup, &buf,
10501086
"iiiiiiiii;asctime(): illegal time tuple argument") ||
@@ -1069,8 +1105,12 @@ time_ctime(PyObject *self, PyObject *args)
10691105
struct tm buf;
10701106
if (!parse_time_t_args(args, "|O:ctime", &tt))
10711107
return NULL;
1072-
if (_PyTime_localtime(tt, &buf) != 0)
1108+
PyMutex_Lock(&timezone_mutex);
1109+
if (_PyTime_localtime(tt, &buf) != 0) {
1110+
PyMutex_Unlock(&timezone_mutex);
10731111
return NULL;
1112+
}
1113+
PyMutex_Unlock(&timezone_mutex);
10741114
return _asctime(&buf);
10751115
}
10761116

@@ -1174,15 +1214,18 @@ time_tzset(PyObject *self, PyObject *unused)
11741214
return NULL;
11751215
}
11761216

1217+
PyMutex_Lock(&timezone_mutex);
11771218
#if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
11781219
tzset();
11791220
#endif
11801221

11811222
/* Reset timezone, altzone, daylight and tzname */
11821223
if (init_timezone(m) < 0) {
1224+
PyMutex_Unlock(&timezone_mutex);
11831225
Py_DECREF(m);
11841226
return NULL;
11851227
}
1228+
PyMutex_Unlock(&timezone_mutex);
11861229
Py_DECREF(m);
11871230
if (PyErr_Occurred())
11881231
return NULL;

0 commit comments

Comments
 (0)