Skip to content

ICU-20570 Fix data race in initializeFCD via UInitOnce#4086

Open
sfc-gh-bhannel wants to merge 2 commits into
unicode-org:mainfrom
Snowflake-Labs:bhannel-fix-initializeFCD-data-race
Open

ICU-20570 Fix data race in initializeFCD via UInitOnce#4086
sfc-gh-bhannel wants to merge 2 commits into
unicode-org:mainfrom
Snowflake-Labs:bhannel-fix-initializeFCD-data-race

Conversation

@sfc-gh-bhannel

@sfc-gh-bhannel sfc-gh-bhannel commented Jul 20, 2026

Copy link
Copy Markdown

If multiple threads try to use ICU for the first time in the process concurrently, ICU initialization will have a data race inside of initializeFCD. UInitOnce seems to be the standard pattern used widely in ICU, so I am applying it here as well. Here is a standalone repro of the issue: https://godbolt.org/z/9PPK84KT7

Reproduction and Testing I added a new unit test which reproduces the issue under TSAN:
Before fix:

mkdir -p ~/icu/build-tsan && cd ~/icu/build-tsan
../icu4c/source/configure \
  CC="$CLANG" CXX="$CLANGXX" \
  CPPFLAGS="-fsanitize=thread -g" CXXFLAGS="-std=c++17" LDFLAGS="-fsanitize=thread" \
  --enable-debug --disable-release
gmake -j$(nproc)
TSAN_OPTIONS="halt_on_error=0" LD_LIBRARY_PATH=lib:tools/ctestfw test/intltest/intltest utility/MultithreadTest/TestInitializeFCD
...
=== Handling test: utility/MultithreadTest/TestInitializeFCD: ===
   utility {
      MultithreadTest {
         TestInitializeFCD {
         } OK:   TestInitializeFCD  (257ms) 
      } OK:   MultithreadTest  (258ms) 
   } OK:   utility  (258ms)

After fix:

mkdir -p ~/icu/build-tsan && cd ~/icu/build-tsan
../icu4c/source/configure \
  CC="$CLANG" CXX="$CLANGXX" \
  CPPFLAGS="-fsanitize=thread -g" CXXFLAGS="-std=c++17" LDFLAGS="-fsanitize=thread" \
  --enable-debug --disable-release
gmake -j$(nproc)
TSAN_OPTIONS="halt_on_error=0" LD_LIBRARY_PATH=lib:tools/ctestfw test/intltest/intltest utility/MultithreadTest/TestInitializeFCD
...
=== Handling test: utility/MultithreadTest/TestInitializeFCD: ===
   utility {
      MultithreadTest {
         TestInitializeFCD {
==================
WARNING: ThreadSanitizer: data race (pid=2548260)
  Read of size 8 at 0xffffab2d2e78 by thread T16:
    #0 initializeFCD(UErrorCode*) /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:92:9 (libicui18n.so.79+0x571608)
    #1 usearch_openFromCollator_79 /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:593:9 (libicui18n.so.79+0x570f08)
    #2 usearch_open_79 /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:544:35 (libicui18n.so.79+0x570cbc)
    #3 icu_79::StringSearch::StringSearch(icu_79::UnicodeString const&, icu_79::UnicodeString const&, icu_79::Locale const&, icu_79::BreakIterator*, UErrorCode&) /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/stsearch.cpp:39:18 (libicui18n.so.79+0x4e1510)
    #4 TestInitializeFCDThread::run() /home/bhannel/icu/build-tsan/test/intltest/../../../icu4c/source/test/intltest/tsmthred.cpp:1398:22 (intltest+0x92ff28)
    #5 void std::__invoke_impl<void, void (SimpleThread::*)(), SimpleThread*>(std::__invoke_memfun_deref, void (SimpleThread::*&&)(), SimpleThread*&&) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:73:14 (intltest+0xc1fdd4)
    #6 std::__invoke_result<void (SimpleThread::*)(), SimpleThread*>::type std::__invoke<void (SimpleThread::*)(), SimpleThread*>(void (SimpleThread::*&&)(), SimpleThread*&&) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:95:14 (intltest+0xc1fc58)
    #7 void std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:264:13 (intltest+0xc1fbf0)
    #8 std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>::operator()() /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:271:11 (intltest+0xc1fb70)
    #9 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>>::_M_run() /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:215:13 (intltest+0xc1f954)
    #10 <null> <null> (libstdc++.so.6+0xd28f8) (BuildId: ce3c93e2f96852be40b95ff93f79d247e5e0b1b0)

  Previous write of size 8 at 0xffffab2d2e78 by thread T14:
    #0 initializeFCD(UErrorCode*) /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:93:19 (libicui18n.so.79+0x57163c)
    #1 usearch_openFromCollator_79 /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:593:9 (libicui18n.so.79+0x570f08)
    #2 usearch_open_79 /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/usearch.cpp:544:35 (libicui18n.so.79+0x570cbc)
    #3 icu_79::StringSearch::StringSearch(icu_79::UnicodeString const&, icu_79::UnicodeString const&, icu_79::Locale const&, icu_79::BreakIterator*, UErrorCode&) /home/bhannel/icu/build-tsan/i18n/../../icu4c/source/i18n/stsearch.cpp:39:18 (libicui18n.so.79+0x4e1510)
    #4 TestInitializeFCDThread::run() /home/bhannel/icu/build-tsan/test/intltest/../../../icu4c/source/test/intltest/tsmthred.cpp:1398:22 (intltest+0x92ff28)
    #5 void std::__invoke_impl<void, void (SimpleThread::*)(), SimpleThread*>(std::__invoke_memfun_deref, void (SimpleThread::*&&)(), SimpleThread*&&) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:73:14 (intltest+0xc1fdd4)
    #6 std::__invoke_result<void (SimpleThread::*)(), SimpleThread*>::type std::__invoke<void (SimpleThread::*)(), SimpleThread*>(void (SimpleThread::*&&)(), SimpleThread*&&) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:95:14 (intltest+0xc1fc58)
    #7 void std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>::_M_invoke<0ul, 1ul>(std::_Index_tuple<0ul, 1ul>) /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:264:13 (intltest+0xc1fbf0)
    #8 std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>::operator()() /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:271:11 (intltest+0xc1fb70)
    #9 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (SimpleThread::*)(), SimpleThread*>>>::_M_run() /opt/rh/devtoolset-10/root/usr/lib/gcc/aarch64-redhat-linux/10/../../../../include/c++/10/thread:215:13 (intltest+0xc1f954)
    #10 <null> <null> (libstdc++.so.6+0xd28f8) (BuildId: ce3c93e2f96852be40b95ff93f79d247e5e0b1b0)

Checklist

  • Required: Issue filed: https://unicode-org.atlassian.net/browse/ICU-20570
    • This is a closely related, but not identical issue. I was unable to create a new issue in the ICU Jira workspace. Clicking the "Create" button does nothing - there is not even an error popup or console log.
  • Required: The PR title must be prefixed with a JIRA Issue number. Example: "ICU-NNNNN Fix xyz"
  • Required: Each commit message must be prefixed with a JIRA Issue number. Example: "ICU-NNNNN Fix xyz"
  • Issue accepted (done by Technical Committee after discussion)
  • Tests included, if applicable
  • API docs and/or User Guide docs changed or added, if applicable
  • Approver: Feel free to merge on my behalf

@CLAassistant

CLAassistant commented Jul 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sfc-gh-bhannel
sfc-gh-bhannel force-pushed the bhannel-fix-initializeFCD-data-race branch from 344df34 to 3b22d4e Compare July 20, 2026 21:02
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/usearch.cpp is different
  • icu4c/source/test/intltest/tsmthred.cpp is now changed in the branch
  • icu4c/source/test/intltest/tsmthred.h is now changed in the branch
  • tsan_test_results.txt is now changed in the branch

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

Add TestInitializeFCD to utility/MultithreadTest — the suite run under
TSAN in ICU's upstream CI. The test spawns 16 threads that concurrently
construct StringSearch objects, preceded by u_cleanup() to reset
g_nfcImpl. A spin barrier (u_atomic_int32_t gate) ensures all threads
converge on initializeFCD simultaneously for reliable race detection.

On unfixed builds, TSAN reports a data race at usearch.cpp:93
(concurrent write/read of g_nfcImpl). With the UInitOnce fix the test
passes cleanly (exit 0, no TSAN warnings).

tsan_test_results.txt records the full before/after commands and output.
Replace the racy null-check in initializeFCD() with umtx_initOnce so
g_nfcImpl is initialized exactly once across concurrent callers:

- Add static UInitOnce gSearchNFCInitOnce (using {} for ICU 79 compat;
  U_INITONCE_INITIALIZER was removed after ICU 61)
- Extract initNFCImpl() as the once-init callback
- Call umtx_initOnce(gSearchNFCInitOnce, &initNFCImpl, status) in place
  of the racy if (g_nfcImpl == nullptr) block
- Reset gSearchNFCInitOnce in usearch_cleanup() so u_cleanup() works
@sfc-gh-bhannel
sfc-gh-bhannel force-pushed the bhannel-fix-initializeFCD-data-race branch from 3b22d4e to 2d4d972 Compare July 20, 2026 21:06
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/test/intltest/tsmthred.cpp is different
  • tsan_test_results.txt is no longer changed in the branch

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@sfc-gh-bhannel
sfc-gh-bhannel marked this pull request as ready for review July 20, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants