From 856bf9cd6a08ef75fc1bc37aca143c7f88a61ee8 Mon Sep 17 00:00:00 2001 From: hirorogo Date: Thu, 19 Mar 2026 16:35:50 +0000 Subject: [PATCH 1/2] Fix use-after-free race in TransliteratorAlias compoundFilter See #3913 --- icu4c/source/i18n/transreg.cpp | 7 ++++--- icu4c/source/i18n/transreg.h | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/icu4c/source/i18n/transreg.cpp b/icu4c/source/i18n/transreg.cpp index 3ee223030c2f..25ad9ea6009a 100644 --- a/icu4c/source/i18n/transreg.cpp +++ b/icu4c/source/i18n/transreg.cpp @@ -86,7 +86,7 @@ TransliteratorAlias::TransliteratorAlias(const UnicodeString& theAliasID, ID(), aliasesOrRules(theAliasID), transes(nullptr), - compoundFilter(cpdFilter), + compoundFilter(cpdFilter ? cpdFilter->clone() : nullptr), direction(UTRANS_FORWARD), type(TransliteratorAlias::SIMPLE) { } @@ -98,7 +98,7 @@ TransliteratorAlias::TransliteratorAlias(const UnicodeString& theID, ID(theID), aliasesOrRules(idBlocks), transes(adoptedTransliterators), - compoundFilter(cpdFilter), + compoundFilter(cpdFilter ? cpdFilter->clone() : nullptr), direction(UTRANS_FORWARD), type(TransliteratorAlias::COMPOUND) { } @@ -116,6 +116,7 @@ TransliteratorAlias::TransliteratorAlias(const UnicodeString& theID, TransliteratorAlias::~TransliteratorAlias() { delete transes; + delete compoundFilter; } @@ -132,7 +133,7 @@ Transliterator* TransliteratorAlias::create(UParseError& pe, return nullptr; } if (compoundFilter != nullptr) - t->adoptFilter(compoundFilter->clone()); + t->adoptFilter(static_cast(compoundFilter->clone())); break; case COMPOUND: { diff --git a/icu4c/source/i18n/transreg.h b/icu4c/source/i18n/transreg.h index b4346811c445..54fc33e1aa89 100644 --- a/icu4c/source/i18n/transreg.h +++ b/icu4c/source/i18n/transreg.h @@ -105,14 +105,14 @@ class TransliteratorAlias : public UMemory { // Here ID is the ID, aliasID is the idBlock, trans is the // contained RBT, and idSplitPoint is the offset in aliasID // where the contained RBT goes. compoundFilter is the - // compound filter, and it is _not_ owned. + // compound filter, and it is owned (cloned from entry). // 3. Rules // Here ID is the ID, aliasID is the rules string. // idSplitPoint is the UTransDirection. UnicodeString ID; UnicodeString aliasesOrRules; UVector* transes; // owned - const UnicodeSet* compoundFilter; // alias + UnicodeSet* compoundFilter; // owned UTransDirection direction; enum { SIMPLE, COMPOUND, RULES } type; From 76d69f28c5691edaad0db5bca4d64ecd53744b2f Mon Sep 17 00:00:00 2001 From: hirorogo Date: Fri, 3 Apr 2026 14:52:51 +0900 Subject: [PATCH 2/2] Add concurrency test for TransliteratorAlias compoundFilter race Test exercises concurrent createInstance() + unregister() of a transliterator with a compound filter to verify that the owned-clone fix in TransliteratorAlias prevents use-after-free. --- icu4c/source/test/intltest/tsmthred.cpp | 78 +++++++++++++++++++++++++ icu4c/source/test/intltest/tsmthred.h | 1 + 2 files changed, 79 insertions(+) diff --git a/icu4c/source/test/intltest/tsmthred.cpp b/icu4c/source/test/intltest/tsmthred.cpp index 06c13fc5cec7..a1bff3ef2516 100644 --- a/icu4c/source/test/intltest/tsmthred.cpp +++ b/icu4c/source/test/intltest/tsmthred.cpp @@ -78,6 +78,7 @@ void MultithreadTest::runIndexedTest( int32_t index, UBool exec, #if !UCONFIG_NO_FORMATTING TESTCASE_AUTO(Test20104); #endif /* #if !UCONFIG_NO_FORMATTING */ + TESTCASE_AUTO(TestTransliteratorAliasCompoundFilterRace); #endif /* #if !UCONFIG_NO_TRANSLITERATION */ TESTCASE_AUTO_END; } @@ -1367,4 +1368,81 @@ void MultithreadTest::Test20104() { } #endif /* !UCONFIG_NO_FORMATTING */ + +// +// TestTransliteratorAliasCompoundFilterRace +// +// Verify that concurrent createInstance() and unregister() of a transliterator +// whose ID includes a compound filter (e.g. "[:Latin:]Lower") does not trigger +// a use-after-free. Before the fix, TransliteratorAlias stored a raw pointer +// to the entry's compoundFilter; a concurrent unregister() could delete the +// entry while the alias still referenced the filter. +// +// With the fix, TransliteratorAlias clones the compoundFilter, so this test +// should pass cleanly. Without the fix, Thread Sanitizer or ASan would report +// a use-after-free. +// + +static const UnicodeString kRaceAliasID(u"Any-RaceTestCF"); +static const UnicodeString kRaceTargetID(u"[:Latin:]Lower"); + +class TranslitCreateThread : public SimpleThread { +public: + TranslitCreateThread() : fSuccess(true) {} + virtual void run() override; + UBool fSuccess; +}; + +void TranslitCreateThread::run() { + for (int32_t i = 0; i < 50; ++i) { + UErrorCode status = U_ZERO_ERROR; + UParseError pe; + Transliterator *t = Transliterator::createInstance(kRaceAliasID, UTRANS_FORWARD, pe, status); + // The instance may or may not be created depending on timing relative + // to unregister. Either outcome is fine; the goal is no crash / UAF. + delete t; + } +} + +class TranslitUnregisterThread : public SimpleThread { +public: + TranslitUnregisterThread() {} + virtual void run() override; +}; + +void TranslitUnregisterThread::run() { + for (int32_t i = 0; i < 50; ++i) { + Transliterator::unregister(kRaceAliasID); + } +} + +void MultithreadTest::TestTransliteratorAliasCompoundFilterRace() { + // Run several rounds to increase the chance of exposing the race. + static constexpr int NUM_ROUNDS = 5; + static constexpr int NUM_CREATE_THREADS = 4; + + for (int32_t round = 0; round < NUM_ROUNDS; ++round) { + // Register the alias fresh each round. + Transliterator::registerAlias(kRaceAliasID, kRaceTargetID); + + TranslitCreateThread createThreads[NUM_CREATE_THREADS]; + TranslitUnregisterThread unregThread; + + for (auto &t : createThreads) { + t.start(); + } + unregThread.start(); + + for (auto &t : createThreads) { + t.join(); + } + unregThread.join(); + + // Clean up in case unregister didn't run yet. + Transliterator::unregister(kRaceAliasID); + } + // Success = no crash / sanitizer report. + logln("TestTransliteratorAliasCompoundFilterRace: passed (no crash/UAF detected)"); +} + #endif /* !UCONFIG_NO_TRANSLITERATION */ diff --git a/icu4c/source/test/intltest/tsmthred.h b/icu4c/source/test/intltest/tsmthred.h index ef9888382f0d..e31fb8e0ee37 100644 --- a/icu4c/source/test/intltest/tsmthred.h +++ b/icu4c/source/test/intltest/tsmthred.h @@ -49,6 +49,7 @@ class MultithreadTest : public IntlTest void TestBreakTranslit(); void TestIncDec(); void Test20104(); + void TestTransliteratorAliasCompoundFilterRace(); }; #endif