Skip to content

Commit 6d586fc

Browse files
Fix sem_clockwait detection (#985)
When building against glibc headers older than 2.30 (e.g., Debian Stretch's `glibc 2.24`), configure cannot detect `sem_clockwait` (added in glibc 2.30). This causes CPython to fall back to `sem_timedwait` with CLOCK_REALTIME, making `threading.Event.wait()` hang indefinitely when the system clock jumps backward (e.g., NTP sync). This PR adds patches that declare `sem_clockwait` as a weak symbol on Linux when configure doesn't detect it. At runtime, the weak symbol resolves to the real function on `glibc 2.30+` or to NULL on older glibc. The patched code checks this at runtime and falls back to `sem_timedwait` gracefully. Version-specific patches for CPython 3.10, 3.11, 3.12, 3.13+ (including 3.14) and 3.15 (currently in alpha) account for differences in the threading internals across versions. The issue was first discovered in commaai/openpilot#33993 (comment)
1 parent 2cf27ad commit 6d586fc

6 files changed

+503
-0
lines changed

cpython-unix/build-cpython.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,25 @@ if [ -n "${CROSS_COMPILING}" ]; then
658658
# TODO: There are probably more of these, see #599.
659659
fi
660660

661+
# Apply weak sem_clockwait patch for runtime detection on old glibc.
662+
# When building against glibc headers older than 2.30, configure cannot detect
663+
# sem_clockwait, causing threading.Event.wait() to use CLOCK_REALTIME instead of
664+
# CLOCK_MONOTONIC. This makes waits hang when the system clock jumps backward.
665+
# The patch declares sem_clockwait as a weak symbol and checks at runtime.
666+
if [[ "${PYBUILD_PLATFORM}" != macos* ]]; then
667+
if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_15}" ]; then
668+
patch -p1 -i ${ROOT}/patch-sem-clockwait-weak-3.15.patch
669+
elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" ]; then
670+
patch -p1 -i ${ROOT}/patch-sem-clockwait-weak-3.13.patch
671+
elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_12}" ]; then
672+
patch -p1 -i ${ROOT}/patch-sem-clockwait-weak-3.12.patch
673+
elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]; then
674+
patch -p1 -i ${ROOT}/patch-sem-clockwait-weak-3.11.patch
675+
else
676+
patch -p1 -i ${ROOT}/patch-sem-clockwait-weak-3.10.patch
677+
fi
678+
fi
679+
661680
# Adjust the Python startup logic (getpath.py) to properly locate the installation, even when
662681
# invoked through a symlink or through an incorrect argv[0]. Because this Python is relocatable, we
663682
# don't get to rely on the fallback to the compiled-in installation prefix.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
--- a/Python/thread_pthread.h
2+
+++ b/Python/thread_pthread.h
3+
@@ -87,6 +87,18 @@
4+
#endif
5+
#endif
6+
7+
+/* When building against glibc headers older than 2.30, configure cannot
8+
+ * detect sem_clockwait. Declare it as a weak symbol so it
9+
+ * resolves to NULL on old glibc and to the real function on glibc 2.30+.
10+
+ * This enables monotonic clock waits at runtime when available, preventing
11+
+ * hangs when the system clock jumps backward (e.g., NTP sync). */
12+
+#if defined(__linux__) && !defined(HAVE_SEM_CLOCKWAIT)
13+
+#include <time.h>
14+
+__attribute__((weak)) extern int sem_clockwait(sem_t *, clockid_t,
15+
+ const struct timespec *);
16+
+#define HAVE_SEM_CLOCKWAIT 1
17+
+#define _Py_SEM_CLOCKWAIT_WEAK 1
18+
+#endif
19+
20+
/* Whether or not to use semaphores directly rather than emulating them with
21+
* mutexes and condition variables:
22+
@@ -443,9 +455,7 @@
23+
sem_t *thelock = (sem_t *)lock;
24+
int status, error = 0;
25+
struct timespec ts;
26+
-#ifndef HAVE_SEM_CLOCKWAIT
27+
_PyTime_t deadline = 0;
28+
-#endif
29+
30+
(void) error; /* silence unused-but-set-variable warning */
31+
dprintf(("PyThread_acquire_lock_timed(%p, %lld, %d) called\n",
32+
@@ -455,29 +465,35 @@
33+
Py_FatalError("Timeout larger than PY_TIMEOUT_MAX");
34+
}
35+
36+
- if (microseconds > 0) {
37+
-#ifdef HAVE_SEM_CLOCKWAIT
38+
- monotonic_abs_timeout(microseconds, &ts);
39+
+#ifdef _Py_SEM_CLOCKWAIT_WEAK
40+
+ int use_clockwait = (sem_clockwait != NULL);
41+
#else
42+
- MICROSECONDS_TO_TIMESPEC(microseconds, ts);
43+
+ int use_clockwait = 1;
44+
+#endif
45+
46+
- if (!intr_flag) {
47+
- /* cannot overflow thanks to (microseconds > PY_TIMEOUT_MAX)
48+
- check done above */
49+
- _PyTime_t timeout = _PyTime_FromNanoseconds(microseconds * 1000);
50+
- deadline = _PyTime_GetMonotonicClock() + timeout;
51+
+ if (microseconds > 0) {
52+
+ if (use_clockwait) {
53+
+ monotonic_abs_timeout(microseconds, &ts);
54+
+ } else {
55+
+ MICROSECONDS_TO_TIMESPEC(microseconds, ts);
56+
+
57+
+ if (!intr_flag) {
58+
+ /* cannot overflow thanks to (microseconds > PY_TIMEOUT_MAX)
59+
+ check done above */
60+
+ _PyTime_t timeout = _PyTime_FromNanoseconds(microseconds * 1000);
61+
+ deadline = _PyTime_GetMonotonicClock() + timeout;
62+
+ }
63+
}
64+
-#endif
65+
}
66+
67+
while (1) {
68+
if (microseconds > 0) {
69+
-#ifdef HAVE_SEM_CLOCKWAIT
70+
- status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
71+
- &ts));
72+
-#else
73+
- status = fix_status(sem_timedwait(thelock, &ts));
74+
-#endif
75+
+ if (use_clockwait) {
76+
+ status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
77+
+ &ts));
78+
+ } else {
79+
+ status = fix_status(sem_timedwait(thelock, &ts));
80+
+ }
81+
}
82+
else if (microseconds == 0) {
83+
status = fix_status(sem_trywait(thelock));
84+
@@ -494,8 +510,7 @@
85+
86+
// sem_clockwait() uses an absolute timeout, there is no need
87+
// to recompute the relative timeout.
88+
-#ifndef HAVE_SEM_CLOCKWAIT
89+
- if (microseconds > 0) {
90+
+ if (!use_clockwait && microseconds > 0) {
91+
/* wait interrupted by a signal (EINTR): recompute the timeout */
92+
_PyTime_t dt = deadline - _PyTime_GetMonotonicClock();
93+
if (dt < 0) {
94+
@@ -516,18 +531,13 @@
95+
microseconds = 0;
96+
}
97+
}
98+
-#endif
99+
}
100+
101+
/* Don't check the status if we're stopping because of an interrupt. */
102+
if (!(intr_flag && status == EINTR)) {
103+
if (microseconds > 0) {
104+
if (status != ETIMEDOUT) {
105+
-#ifdef HAVE_SEM_CLOCKWAIT
106+
- CHECK_STATUS("sem_clockwait");
107+
-#else
108+
- CHECK_STATUS("sem_timedwait");
109+
-#endif
110+
+ CHECK_STATUS(use_clockwait ? "sem_clockwait" : "sem_timedwait");
111+
}
112+
}
113+
else if (microseconds == 0) {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
--- a/Python/thread_pthread.h
2+
+++ b/Python/thread_pthread.h
3+
@@ -89,6 +89,18 @@
4+
#endif
5+
#endif
6+
7+
+/* When building against glibc headers older than 2.30, configure cannot
8+
+ * detect sem_clockwait. Declare it as a weak symbol so it
9+
+ * resolves to NULL on old glibc and to the real function on glibc 2.30+.
10+
+ * This enables monotonic clock waits at runtime when available, preventing
11+
+ * hangs when the system clock jumps backward (e.g., NTP sync). */
12+
+#if defined(__linux__) && !defined(HAVE_SEM_CLOCKWAIT)
13+
+#include <time.h>
14+
+__attribute__((weak)) extern int sem_clockwait(sem_t *, clockid_t,
15+
+ const struct timespec *);
16+
+#define HAVE_SEM_CLOCKWAIT 1
17+
+#define _Py_SEM_CLOCKWAIT_WEAK 1
18+
+#endif
19+
20+
/* Whether or not to use semaphores directly rather than emulating them with
21+
* mutexes and condition variables:
22+
@@ -463,32 +475,32 @@
23+
timeout = _PyTime_FromNanoseconds(-1);
24+
}
25+
26+
-#ifdef HAVE_SEM_CLOCKWAIT
27+
- struct timespec abs_timeout;
28+
- // Local scope for deadline
29+
- {
30+
- _PyTime_t deadline = _PyTime_Add(_PyTime_GetMonotonicClock(), timeout);
31+
- _PyTime_AsTimespec_clamp(deadline, &abs_timeout);
32+
- }
33+
+#ifdef _Py_SEM_CLOCKWAIT_WEAK
34+
+ int use_clockwait = (sem_clockwait != NULL);
35+
#else
36+
+ int use_clockwait = 1;
37+
+#endif
38+
+ struct timespec abs_timeout;
39+
_PyTime_t deadline = 0;
40+
- if (timeout > 0 && !intr_flag) {
41+
+ if (use_clockwait) {
42+
+ _PyTime_t dl = _PyTime_Add(_PyTime_GetMonotonicClock(), timeout);
43+
+ _PyTime_AsTimespec_clamp(dl, &abs_timeout);
44+
+ } else if (timeout > 0 && !intr_flag) {
45+
deadline = _PyDeadline_Init(timeout);
46+
}
47+
-#endif
48+
49+
while (1) {
50+
if (timeout > 0) {
51+
-#ifdef HAVE_SEM_CLOCKWAIT
52+
- status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
53+
- &abs_timeout));
54+
-#else
55+
- _PyTime_t abs_time = _PyTime_Add(_PyTime_GetSystemClock(),
56+
- timeout);
57+
- struct timespec ts;
58+
- _PyTime_AsTimespec_clamp(abs_time, &ts);
59+
- status = fix_status(sem_timedwait(thelock, &ts));
60+
-#endif
61+
+ if (use_clockwait) {
62+
+ status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
63+
+ &abs_timeout));
64+
+ } else {
65+
+ _PyTime_t abs_time = _PyTime_Add(_PyTime_GetSystemClock(),
66+
+ timeout);
67+
+ struct timespec ts;
68+
+ _PyTime_AsTimespec_clamp(abs_time, &ts);
69+
+ status = fix_status(sem_timedwait(thelock, &ts));
70+
+ }
71+
}
72+
else if (timeout == 0) {
73+
status = fix_status(sem_trywait(thelock));
74+
@@ -505,8 +517,7 @@
75+
76+
// sem_clockwait() uses an absolute timeout, there is no need
77+
// to recompute the relative timeout.
78+
-#ifndef HAVE_SEM_CLOCKWAIT
79+
- if (timeout > 0) {
80+
+ if (!use_clockwait && timeout > 0) {
81+
/* wait interrupted by a signal (EINTR): recompute the timeout */
82+
timeout = _PyDeadline_Get(deadline);
83+
if (timeout < 0) {
84+
@@ -514,18 +525,13 @@
85+
break;
86+
}
87+
}
88+
-#endif
89+
}
90+
91+
/* Don't check the status if we're stopping because of an interrupt. */
92+
if (!(intr_flag && status == EINTR)) {
93+
if (timeout > 0) {
94+
if (status != ETIMEDOUT) {
95+
-#ifdef HAVE_SEM_CLOCKWAIT
96+
- CHECK_STATUS("sem_clockwait");
97+
-#else
98+
- CHECK_STATUS("sem_timedwait");
99+
-#endif
100+
+ CHECK_STATUS(use_clockwait ? "sem_clockwait" : "sem_timedwait");
101+
}
102+
}
103+
else if (timeout == 0) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
--- a/Python/thread_pthread.h
2+
+++ b/Python/thread_pthread.h
3+
@@ -96,6 +96,19 @@
4+
#undef HAVE_SEM_CLOCKWAIT
5+
#endif
6+
7+
+/* When building against glibc headers older than 2.30, configure cannot
8+
+ * detect sem_clockwait. Declare it as a weak symbol so it
9+
+ * resolves to NULL on old glibc and to the real function on glibc 2.30+.
10+
+ * This enables monotonic clock waits at runtime when available, preventing
11+
+ * hangs when the system clock jumps backward (e.g., NTP sync). */
12+
+#if defined(__linux__) && !defined(HAVE_SEM_CLOCKWAIT)
13+
+#include <time.h>
14+
+__attribute__((weak)) extern int sem_clockwait(sem_t *, clockid_t,
15+
+ const struct timespec *);
16+
+#define HAVE_SEM_CLOCKWAIT 1
17+
+#define _Py_SEM_CLOCKWAIT_WEAK 1
18+
+#endif
19+
+
20+
/* Whether or not to use semaphores directly rather than emulating them with
21+
* mutexes and condition variables:
22+
*/
23+
@@ -456,32 +469,32 @@
24+
timeout = _PyTime_FromNanoseconds(-1);
25+
}
26+
27+
-#ifdef HAVE_SEM_CLOCKWAIT
28+
- struct timespec abs_timeout;
29+
- // Local scope for deadline
30+
- {
31+
- _PyTime_t deadline = _PyTime_Add(_PyTime_GetMonotonicClock(), timeout);
32+
- _PyTime_AsTimespec_clamp(deadline, &abs_timeout);
33+
- }
34+
+#ifdef _Py_SEM_CLOCKWAIT_WEAK
35+
+ int use_clockwait = (sem_clockwait != NULL);
36+
#else
37+
+ int use_clockwait = 1;
38+
+#endif
39+
+ struct timespec abs_timeout;
40+
_PyTime_t deadline = 0;
41+
- if (timeout > 0 && !intr_flag) {
42+
+ if (use_clockwait) {
43+
+ _PyTime_t dl = _PyTime_Add(_PyTime_GetMonotonicClock(), timeout);
44+
+ _PyTime_AsTimespec_clamp(dl, &abs_timeout);
45+
+ } else if (timeout > 0 && !intr_flag) {
46+
deadline = _PyDeadline_Init(timeout);
47+
}
48+
-#endif
49+
50+
while (1) {
51+
if (timeout > 0) {
52+
-#ifdef HAVE_SEM_CLOCKWAIT
53+
- status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
54+
- &abs_timeout));
55+
-#else
56+
- _PyTime_t abs_time = _PyTime_Add(_PyTime_GetSystemClock(),
57+
- timeout);
58+
- struct timespec ts;
59+
- _PyTime_AsTimespec_clamp(abs_time, &ts);
60+
- status = fix_status(sem_timedwait(thelock, &ts));
61+
-#endif
62+
+ if (use_clockwait) {
63+
+ status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
64+
+ &abs_timeout));
65+
+ } else {
66+
+ _PyTime_t abs_time = _PyTime_Add(_PyTime_GetSystemClock(),
67+
+ timeout);
68+
+ struct timespec ts;
69+
+ _PyTime_AsTimespec_clamp(abs_time, &ts);
70+
+ status = fix_status(sem_timedwait(thelock, &ts));
71+
+ }
72+
}
73+
else if (timeout == 0) {
74+
status = fix_status(sem_trywait(thelock));
75+
@@ -498,8 +511,7 @@
76+
77+
// sem_clockwait() uses an absolute timeout, there is no need
78+
// to recompute the relative timeout.
79+
-#ifndef HAVE_SEM_CLOCKWAIT
80+
- if (timeout > 0) {
81+
+ if (!use_clockwait && timeout > 0) {
82+
/* wait interrupted by a signal (EINTR): recompute the timeout */
83+
timeout = _PyDeadline_Get(deadline);
84+
if (timeout < 0) {
85+
@@ -507,18 +519,13 @@
86+
break;
87+
}
88+
}
89+
-#endif
90+
}
91+
92+
/* Don't check the status if we're stopping because of an interrupt. */
93+
if (!(intr_flag && status == EINTR)) {
94+
if (timeout > 0) {
95+
if (status != ETIMEDOUT) {
96+
-#ifdef HAVE_SEM_CLOCKWAIT
97+
- CHECK_STATUS("sem_clockwait");
98+
-#else
99+
- CHECK_STATUS("sem_timedwait");
100+
-#endif
101+
+ CHECK_STATUS(use_clockwait ? "sem_clockwait" : "sem_timedwait");
102+
}
103+
}
104+
else if (timeout == 0) {

0 commit comments

Comments
 (0)