[PATCH 2/6] libstdc++: Fix shared_timed_mutex for negative timeouts [PR116586]
Jonathan Wakely
jwakely@redhat.com
Mon Oct 27 18:39:53 GMT 2025
On Sun, 14 Sep 2025 at 21:21 +0100, Mike Crowe wrote:
>Passing a timeout from before the epoch to
>std::shared_timed_mutex::try_lock_until or
>std::shared_timed_mutex::try_lock_shared_until causes the POSIX rwlock
>functions to be passed an invalid negative timeout which results in them
>returning EINVAL.
>
>thread.timedmutex.requirements.general in the C++ standard says:
>> If abs_time has already passed, the function attempts to obtain
>> ownership without blocking (as if by calling try_lock()).
>
>and a time before the epoch has clearly already passed (see description
>of libstdc++/PR116586). Let's treat such negative times as being at the
>epoch so that these methods work correctly. Add test cases to prove that
>this, and potential similar problems are no longer present.
>
> PR libstdc++-v3/116586
>
>libstdc++-v3/ChangeLog:
>
> * include/std/shared_mutex: (shared_timed_mutex::try_lock_until,
> shared_timed_mutex::try_lock_shared_until): Ensure that
> negative timeout is not passed to POSIX rwlock functions.
> * testsuite/30_threads/shared_timed_mutex/try_lock_until/116586.cc:
> New test.
In r16-4427-gfb558b78108ff2 I pushed just the new test, without the
code changes. When fixing https://gcc.gnu.org/PR122401 I found that
the new test hangs on older Glibc. I observed it on 2.28 but
presumably other versions without your clocklock extensions are
affected.
Suggested fix below ...
>
>Signed-off-by: Mike Crowe <mac@mcrowe.com>
>---
> libstdc++-v3/include/std/shared_mutex | 20 ++++
> .../try_lock_until/116586.cc | 94 +++++++++++++++++++
> 2 files changed, 114 insertions(+)
> create mode 100644 libstdc++-v3/testsuite/30_threads/shared_timed_mutex/try_lock_until/116586.cc
>
>diff --git a/libstdc++-v3/include/std/shared_mutex b/libstdc++-v3/include/std/shared_mutex
>index 94c8532399d..ef0f1df5a2b 100644
>--- a/libstdc++-v3/include/std/shared_mutex
>+++ b/libstdc++-v3/include/std/shared_mutex
>@@ -529,6 +529,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> static_cast<long>(__ns.count())
> };
>
>+ if (__ts.tv_sec < 0 || __ts.tv_nsec < 0) {
>+ __ts.tv_sec = 0;
>+ __ts.tv_nsec = 0;
>+ }
>+
> int __ret = __glibcxx_rwlock_timedwrlock(&_M_rwlock, &__ts);
> // On self-deadlock, we just fail to acquire the lock. Technically,
> // the program violated the precondition.
>@@ -555,6 +560,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> static_cast<long>(__ns.count())
> };
>
>+ if (__ts.tv_sec < 0 || __ts.tv_nsec < 0) {
>+ __ts.tv_sec = 0;
>+ __ts.tv_nsec = 0;
>+ }
>+
> int __ret = pthread_rwlock_clockwrlock(&_M_rwlock, CLOCK_MONOTONIC,
> &__ts);
> // On self-deadlock, we just fail to acquire the lock. Technically,
>@@ -605,6 +615,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> static_cast<long>(__ns.count())
> };
>
>+ if (__ts.tv_sec < 0 || __ts.tv_nsec < 0) {
>+ __ts.tv_sec = 0;
>+ __ts.tv_nsec = 0;
>+ }
>+
> int __ret;
> // Unlike for lock(), we are not allowed to throw an exception so if
> // the maximum number of read locks has been exceeded, or we would
>@@ -645,6 +660,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> static_cast<long>(__ns.count())
> };
>
>+ if (__ts.tv_sec < 0 || __ts.tv_nsec < 0) {
>+ __ts.tv_sec = 0;
>+ __ts.tv_nsec = 0;
>+ }
>+
> int __ret = pthread_rwlock_clockrdlock(&_M_rwlock, CLOCK_MONOTONIC,
> &__ts);
> // On self-deadlock, we just fail to acquire the lock. Technically,
>diff --git a/libstdc++-v3/testsuite/30_threads/shared_timed_mutex/try_lock_until/116586.cc b/libstdc++-v3/testsuite/30_threads/shared_timed_mutex/try_lock_until/116586.cc
>new file mode 100644
>index 00000000000..4ed650a78cf
>--- /dev/null
>+++ b/libstdc++-v3/testsuite/30_threads/shared_timed_mutex/try_lock_until/116586.cc
>@@ -0,0 +1,94 @@
>+// { dg-do run { target c++14 } }
>+
>+#include <chrono>
>+#include <shared_mutex>
>+#include <testsuite_hooks.h>
>+
>+namespace chrono = std::chrono;
>+
>+// thread.timedmutex.requirements.general:
>+// If abs_time has already passed, the function attempts to obtain
>+// ownership without blocking (as if by calling try_lock()).
>+
>+template <typename Clock>
>+void
>+test_exclusive_absolute(chrono::nanoseconds offset)
>+{
>+ std::shared_timed_mutex stm;
>+ chrono::time_point<Clock> tp(offset);
>+ VERIFY(stm.try_lock_until(tp));
>+ VERIFY(!stm.try_lock_until(tp));
>+}
>+
>+template <typename Clock>
>+void
>+test_shared_absolute(chrono::nanoseconds offset)
>+{
>+ std::shared_timed_mutex stm;
>+ chrono::time_point<Clock> tp(offset);
>+ VERIFY(stm.try_lock_shared_until(tp));
>+ stm.unlock_shared();
>+
>+ VERIFY(stm.try_lock_for(chrono::seconds{10}));
>+
>+ {
>+ // NPTL will give us EDEADLK if pthread_rwlock_timedrdlock() is called on
>+ // the same thread that already holds the exclusive (write) lock, so let's
>+ // arrange for a different thread to try to acquire the shared lock.
>+ auto t = std::async(std::launch::async, [&stm, tp]() {
>+ VERIFY(!stm.try_lock_shared_until(tp));
>+ });
>+ }
>+}
>+
>+// The type of clock used for the actual wait depends on whether
>+// _GLIBCXX_USE_PTHREAD_RWLOCK_CLOCKLOCK is defined. We might as well just test
>+// both steady_clock and system_clock.
>+template <typename Clock>
>+void
>+test_exclusive_relative(chrono::nanoseconds offset)
>+{
>+ std::shared_timed_mutex stm;
>+ const auto d = -Clock::now().time_since_epoch() + offset;
>+ VERIFY(stm.try_lock_for(d));
>+ VERIFY(!stm.try_lock_for(d));
>+}
>+
>+template <typename Clock>
>+void
>+test_shared_relative(chrono::nanoseconds offset)
>+{
>+ std::shared_timed_mutex stm;
>+ const auto d = -Clock::now().time_since_epoch() + offset;
>+ VERIFY(stm.try_lock_shared_for(d));
>+ stm.unlock_shared();
>+ // Should complete immediately
>+ VERIFY(stm.try_lock_for(chrono::seconds{10}));
>+ VERIFY(!stm.try_lock_shared_for(d));
I think this function needs the same use of std::async as above,
because when _GLIBCXX_USE_PTHREAD_RWLOCK_CLOCKLOCK is not defined we
still use pthread_rwlock_timedrdlock and so we still get EDEADLK (and
then loop forever) here. You won't see a problem with modern Glibc
versions because I think pthread_rwlock_clockrdlock doesn't return
EDEADLK here.
>+}
>+
>+int main()
>+{
>+ // Try once with an offset that ought to result in tv_sec == 0, tv_nsec < 0
>+ // and one with an offset that ought to result in tv_sec < 0, tv_nsec == 0
>+ // for the absolute calls at least. It's not really possible to arrange for
>+ // the relative calls to have tv_nsec == 0 due to time advancing.
>+ for (const chrono::nanoseconds offset : {
>+ // tv_sec == 0, tv_nsec == 0
>+ chrono::nanoseconds{0},
>+ // tv_sec == 0, tv_nsec < 0
>+ chrono::duration_cast<chrono::nanoseconds>(chrono::milliseconds{-10}),
>+ // tv_sec < 0
>+ chrono::duration_cast<chrono::nanoseconds>(chrono::seconds{-10})
>+ }) {
>+ test_exclusive_absolute<chrono::system_clock>(offset);
>+ test_shared_absolute<chrono::system_clock>(offset);
>+ test_exclusive_relative<chrono::system_clock>(offset);
>+ test_shared_relative<chrono::system_clock>(offset);
>+
>+ test_exclusive_absolute<chrono::steady_clock>(offset);
>+ test_shared_absolute<chrono::steady_clock>(offset);
>+ test_exclusive_relative<chrono::steady_clock>(offset);
>+ test_shared_relative<chrono::steady_clock>(offset);
>+ }
>+}
>--
>2.39.5
>
>
More information about the Libstdc++
mailing list