Fix race condition in Synchronizer.addLast() causing missed wakeThread() calls#3161
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a TOCTOU race in SWT’s Synchronizer.addLast() that could leave the UI thread sleeping indefinitely despite pending async work, and adds a regression test to catch missed wake-ups.
Changes:
- Make
Synchronizer.addLast()wake the UI thread based on whether the just-added lock is at the queue head (peek() == lock), avoiding the non-atomicisEmpty()/add()sequence. - Add a concurrent-producer regression test validating that
asyncExec()tasks always complete under drain/sleep cycling. - Register the new test in the widget test suite.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/widgets/Synchronizer.java | Removes isEmpty()-then-add() race by switching to add() then head-check to decide waking. |
| tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Synchronizer.java | Adds stress/regression test for missed wakeThread() behavior under concurrent asyncExec() producers. |
| tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllWidgetTests.java | Includes the new Synchronizer regression test in the suite. |
...tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Synchronizer.java
Outdated
Show resolved
Hide resolved
...tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Synchronizer.java
Outdated
Show resolved
Hide resolved
c77b376 to
26c3960
Compare
26c3960 to
32f0486
Compare
calls The old implementation checked messages.isEmpty() before calling messages.add(), creating a window where the UI thread could drain the queue and enter sleep() between the isEmpty() check (which saw items) and the add() call. Because wake was set to false from the stale isEmpty() result, wakeThread() was never called and the Display would block indefinitely despite having a pending message. Fix by adding the lock first, then using peek() == lock (reference equality on ConcurrentLinkedQueue) to detect that our item landed at the head, which means the queue was empty — wake the thread only then. Adds a regression test with concurrent producers to validate the fix. Fixes eclipse-platform#3151 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
32f0486 to
f669e73
Compare
|
@laeubi with your interest and expertise in concurrency topics, do you have any concern regarding this solution? To me it looks sound but since the Synchronizer is a very central class, having another pair of eyes on it may not hurt. The change was generated by Claude. I also asked Copilot for a solution, which produced the same (which is not very surprising as they both used Sonnet 4.6). Would be nice to get this into M1 so that we have sufficient time for implicit testing before the next release. |
laeubi
left a comment
There was a problem hiding this comment.
It looks sound to me as well, I'd rather think we should merge it sooner than later (so right now) as it might be manifesting already with the test failures for the BusyIndicator tests.
@akurtakov WDYT?
|
Looks good to me too. Merging. |
Problem
Synchronizer.addLast()had a TOCTOU race introduced in #77 (replacingsynchronizedblocks withConcurrentLinkedQueue). The two steps — checkingisEmpty()and callingadd()— were not atomic, creating this window:isEmpty()→false(queue has items) →wake = falsesleep()→ blocks inWaitMessage()/g_main_context_query()messages.add(lock)if (wake)→false→wakeThread()never calledFix
Add the lock first, then use
peek() == lock(reference equality onConcurrentLinkedQueue) to detect that our item landed at the head of the queue — meaning the queue was effectively empty at insertion time and the UI thread needs waking:If the UI thread consumed our lock between
add()andpeek(),peek()returnsnullor another lock — the task was already processed, no wake needed.RunnableLockobjects are never pooled or reused, so reference equality is safe.Testing
Added
Test_org_eclipse_swt_widgets_Synchronizerwith a regression test that runs 4 concurrent producer threads each posting 1000asyncExectasks while the UI thread alternately drains and sleeps. AtimerExecsentinel bounds the test duration so a hung Display produces a clear assertion failure rather than an infinite hang in CI.Fixes #3151