From 7ff82277cd1453b49b52339f8a666773015e752d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Fri, 18 Sep 2026 06:00:45 -0400 Subject: [PATCH] Stopped the Linux port losing a mutex wake-up to a suspend signal This is the ThreadX half of the defect fixed for ThreadX SMP in an earlier pull request. The Linux port suspends a thread with a signal whose handler calls sigsuspend and does not return until the thread is resumed, and it takes its critical section with a bare pthread_mutex_lock through tx_linux_mutex_lock. A thread can therefore be signalled while it is parked on _tx_linux_mutex. glibc waits for a contended mutex in a loop that re-arms the futex wait after a signal, and this handler never returns to it. The next unlock hands its wake-up to that thread, which will not act on it, and any other thread parked on the mutex is never woken, leaving the mutex free with waiters on it. tx_linux_mutex_lock now calls a helper that waits with pthread_mutex_timedlock and retries, so the wait is re-armed every TX_LINUX_MUTEX_RETRY_NSEC and a lost wake-up costs one retry period instead of the process. The period is one millisecond. pthread_mutex_timedlock needs _GNU_SOURCE under -std=c99, which both of this port's build systems already define. These are the only two ports affected: a sigsuspend-based suspend handler exists in the Linux and SMP Linux ports and nowhere else, and those two are also the only ports taking a critical section with pthread_mutex_lock. Unlike the SMP port, this one has never been observed to deadlock, which is consistent with one emulated core and far less suspend and resume traffic. The fix is by inspection, and verified as breaking nothing: all seven configurations pass with retries disabled, 105/105 in five and 100/100 in two. Assisted-by: Claude Code (Opus 5) --- ports/linux/gnu/inc/tx_port.h | 13 ++++++- ports/linux/gnu/src/tx_thread_schedule.c | 45 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/ports/linux/gnu/inc/tx_port.h b/ports/linux/gnu/inc/tx_port.h index 1f7e25f39..db837e4f9 100644 --- a/ports/linux/gnu/inc/tx_port.h +++ b/ports/linux/gnu/inc/tx_port.h @@ -511,7 +511,7 @@ VOID _tx_thread_interrupt_restore(UINT previous_posture); #define TX_RESTORE _tx_linux_debug_entry_insert("RESTORE", __FILE__, __LINE__); \ _tx_thread_interrupt_restore(tx_saved_posture); #endif /* TX_LINUX_DEBUG_ENABLE */ -#define tx_linux_mutex_lock(p) pthread_mutex_lock(&p) +#define tx_linux_mutex_lock(p) _tx_linux_mutex_lock_retry(&p) #define tx_linux_mutex_unlock(p) pthread_mutex_unlock(&p) #define tx_linux_mutex_recursive_unlock(p) {\ int _recursive_count = (int)tx_linux_mutex_recursive_count;\ @@ -552,6 +552,17 @@ extern CHAR _tx_version_id[]; /* Define externals for the Linux port of ThreadX. */ extern pthread_mutex_t _tx_linux_mutex; + +/* Define how long a thread waits on the Linux mutex before retrying. A thread + parked on the mutex can be suspended by the port's signal handler and so never + act on the wake-up the next unlock sends it, which leaves the wake-up lost and + every other waiter parked on a mutex that is free. */ + +#ifndef TX_LINUX_MUTEX_RETRY_NSEC +#define TX_LINUX_MUTEX_RETRY_NSEC 1000000 +#endif + +void _tx_linux_mutex_lock_retry(pthread_mutex_t *mutex); extern sem_t _tx_linux_semaphore; extern sem_t _tx_linux_semaphore_no_idle; extern ULONG _tx_linux_global_int_disabled_flag; diff --git a/ports/linux/gnu/src/tx_thread_schedule.c b/ports/linux/gnu/src/tx_thread_schedule.c index eeff680fc..405ef4b87 100644 --- a/ports/linux/gnu/src/tx_thread_schedule.c +++ b/ports/linux/gnu/src/tx_thread_schedule.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -201,6 +203,49 @@ struct timespec ts; } } +/* Define the ThreadX Linux mutex lock function. The wait is timed and retried + rather than left to pthread_mutex_lock, because a thread can be signalled into + the port's suspend handler while it is parked on this mutex. That handler does + not return until the thread is resumed, so the wake-up the next unlock sends is + delivered to a thread that never retries and is lost. Any other thread parked + on the mutex then waits on a mutex that is free. Retrying on a timeout costs + nothing when the mutex is handed over normally, and turns that lost wake-up into + a delay of at most the retry period. */ + +void _tx_linux_mutex_lock_retry(pthread_mutex_t *mutex) +{ + +INT linux_status; +struct timespec ts; + + + do + { + + /* Set the deadline for this attempt. */ + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec = ts.tv_nsec + TX_LINUX_MUTEX_RETRY_NSEC; + if (ts.tv_nsec >= 1000000000) + { + + ts.tv_nsec = ts.tv_nsec - 1000000000; + ts.tv_sec++; + } + + linux_status = pthread_mutex_timedlock(mutex, &ts); + + /* Anything but the deadline expiring is a real failure to obtain the + mutex, so stop retrying. */ + if ((linux_status != 0) && (linux_status != ETIMEDOUT)) + { + + break; + } + + } while (linux_status != 0); +} + + void _tx_thread_delete_port_completion(TX_THREAD *thread_ptr, UINT tx_saved_posture) { INT linux_status;