From 799ff04090b3bd503af8f63ceddc404ecf3f2afb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:20:35 +0000 Subject: [PATCH 1/3] [JSC] String case conversion: throw when the result does not fit in a String instead of returning the input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String.prototype.toUpperCase, toLowerCase, toLocaleUpperCase and toLocaleLowerCase returned the input string unchanged, with no exception, when the converted string would be longer than StringImpl::MaxLength. Full case mapping grows a string ("ß" to "SS", "ffi" to "FFI", "İ" to "i" U+0307), so '\u00DF'.repeat(2 ** 30) came back from toUpperCase() as 2^30 lowercase sharp s. StringImpl::convertToUppercaseWithoutLocale() and friends returned *this both for "nothing to convert" and for "the result does not fit" (the sharp-s length check, and U_FAILURE after u_strToUpper and u_strToLower). StringPrototype.cpp and the DFG operations read *this as "unchanged". WTF gets try variants of the conversions that can grow. They return nullptr when the converted string cannot be allocated. The ICU two-pass call is one helper that treats U_BUFFER_OVERFLOW_ERROR as "allocate the reported length and convert again" and any other failure as no result. The 8-bit source is widened for ICU into a MallocSpan instead of StringView::upconvertedCharacters(), whose Vector cannot hold 2^30 char16_t and crashed. The non-try functions RELEASE_ASSERT a result. JSC calls the try variants and throws the out-of-memory RangeError on null. toLocaleCase hands the language-sensitive locales to tryConvertTo{Upper,Lower}caseWithLocale instead of its own Vector-backed ICU call, which aborted once a result reached 2^30 characters. --- ...-case-conversion-longer-than-max-length.js | 109 ++++++++ Source/JavaScriptCore/dfg/DFGOperations.cpp | 12 +- .../runtime/StringPrototype.cpp | 36 ++- Source/WTF/wtf/text/StringImpl.cpp | 242 ++++++++++-------- Source/WTF/wtf/text/StringImpl.h | 20 +- Source/WTF/wtf/text/WTFString.cpp | 32 ++- Source/WTF/wtf/text/WTFString.h | 16 +- 7 files changed, 327 insertions(+), 140 deletions(-) create mode 100644 JSTests/stress/string-case-conversion-longer-than-max-length.js diff --git a/JSTests/stress/string-case-conversion-longer-than-max-length.js b/JSTests/stress/string-case-conversion-longer-than-max-length.js new file mode 100644 index 0000000000000..b6ef9b25d2345 --- /dev/null +++ b/JSTests/stress/string-case-conversion-longer-than-max-length.js @@ -0,0 +1,109 @@ +//@ memoryHog! +//@ skip if $addressBits <= 32 +//@ runDefault + +// Full case mapping can make a string longer: "ß" uppercases to "SS", "ffi" to "FFI", and "İ" +// lowercases to "i̇" (two code units). When the converted string would be longer than the +// maximum string length, the conversion has to throw. It used to hand back the input unchanged. +// Results between 2^30 and the maximum length are fine and must neither throw nor crash. + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`bad value: expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); +} + +function shouldThrowOutOfMemory(func) { + let error = null; + try { + func(); + } catch (e) { + error = e; + } + if (String(error) !== "RangeError: Out of memory") + throw new Error(`bad error: ${String(error)}`); +} + +const maxLength = 2 ** 31 - 1; + +function toUpperCase(string) { return string.toUpperCase(); } +noInline(toUpperCase); +function toLowerCase(string) { return string.toLowerCase(); } +noInline(toLowerCase); + +// Warm these up so that the conversions below also run through the DFG's ToUpperCase and +// ToLowerCase nodes, whose slow path is a different function from the baseline one. +for (let i = 0; i < testLoopCount; ++i) { + shouldBe(toUpperCase("Stra\u00DFe" + i), "STRASSE" + i); + shouldBe(toLowerCase("\u0130I" + i), "i\u0307i" + i); +} + +{ + // 8-bit input. Each "ß" adds one character, so the result would be 2^31 long. + const input = "\u00DF".repeat(2 ** 30); + shouldThrowOutOfMemory(() => input.toUpperCase()); + shouldThrowOutOfMemory(() => input.toLocaleUpperCase()); + shouldThrowOutOfMemory(() => input.toLocaleUpperCase("en")); + shouldThrowOutOfMemory(() => toUpperCase(input)); + + // One character less and the result is exactly the maximum length. + const fits = "\u00DF".repeat(2 ** 30 - 1) + "a"; + const upper = fits.toUpperCase(); + shouldBe(upper.length, maxLength); + shouldBe(upper[0], "S"); + shouldBe(upper[maxLength - 1], "A"); +} +gc(); + +{ + // 16-bit input through ICU. Each "ffi" becomes three characters, 3 * 715827883 = 2^31 + 1, which + // does not even fit in ICU's int32_t length. + const input = "\uFB03".repeat(715827883); + shouldThrowOutOfMemory(() => input.toUpperCase()); + shouldThrowOutOfMemory(() => toUpperCase(input)); +} +gc(); + +{ + // 16-bit input through ICU where the result, 3 * 715827878 + 2 * 4 = 2^31 - 6 characters, fits + // in ICU's int32_t length but is too long for a 16-bit String. + const input = "\uFB03".repeat(715827878) + "\u00DF".repeat(4); + shouldThrowOutOfMemory(() => input.toUpperCase()); +} +gc(); + +{ + // Lowercasing grows too: "İ" (U+0130) becomes "i" followed by U+0307 COMBINING DOT ABOVE. + const input = "\u0130".repeat(2 ** 30); + shouldThrowOutOfMemory(() => input.toLowerCase()); + shouldThrowOutOfMemory(() => input.toLocaleLowerCase("en")); + shouldThrowOutOfMemory(() => toLowerCase(input)); +} +gc(); + +{ + // The language-sensitive locales convert through ICU into a separate buffer. The same limit + // applies there, and a result of 2^30 or more characters, which is fine, must not crash. + const input = "\u00DF".repeat(2 ** 30); + shouldThrowOutOfMemory(() => input.toLocaleUpperCase("tr")); + shouldThrowOutOfMemory(() => input.toLocaleUpperCase("lt")); + + const half = "\u00DF".repeat(2 ** 29); + const upper = half.toLocaleUpperCase("az"); + shouldBe(upper.length, 2 ** 30); + shouldBe(upper[0], "S"); + shouldBe(upper[2 ** 30 - 1], "S"); + + const dotless = "i".repeat(2 ** 30).toLocaleUpperCase("tr"); + shouldBe(dotless.length, 2 ** 30); + shouldBe(dotless[2 ** 30 - 1], "\u0130"); +} +gc(); + +{ + // An 8-bit string of 2^30 or more characters whose uppercase form needs 16 bits: "ÿ" becomes "Ÿ" (U+0178). + const input = "a".repeat(2 ** 30) + "\u00FF"; + const upper = input.toUpperCase(); + shouldBe(upper.length, 2 ** 30 + 1); + shouldBe(upper[0], "A"); + shouldBe(upper[2 ** 30], "\u0178"); +} diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 601ce5829698c..8d4c40594524a 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -3883,7 +3883,11 @@ JSC_DEFINE_JIT_OPERATION(operationToUpperCase, JSString*, (JSGlobalObject* globa if (!inputString->length()) OPERATION_RETURN(scope, vm.smallStrings.emptyString()); - String uppercasedString = inputString->is8Bit() ? inputString->convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); + String uppercasedString = inputString->is8Bit() ? inputString->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); + if (uppercasedString.isNull()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + OPERATION_RETURN(scope, nullptr); + } if (uppercasedString.impl() == inputString->impl()) OPERATION_RETURN(scope, string); OPERATION_RETURN(scope, jsString(vm, WTF::move(uppercasedString))); @@ -3902,7 +3906,11 @@ JSC_DEFINE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject* globa if (!inputString->length()) OPERATION_RETURN(scope, vm.smallStrings.emptyString()); - String lowercasedString = inputString->is8Bit() ? inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); + String lowercasedString = inputString->is8Bit() ? inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); + if (lowercasedString.isNull()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + OPERATION_RETURN(scope, nullptr); + } if (lowercasedString.impl() == inputString->impl()) OPERATION_RETURN(scope, string); OPERATION_RETURN(scope, jsString(vm, WTF::move(lowercasedString))); diff --git a/Source/JavaScriptCore/runtime/StringPrototype.cpp b/Source/JavaScriptCore/runtime/StringPrototype.cpp index e75e47785cc7c..adf419505a80d 100644 --- a/Source/JavaScriptCore/runtime/StringPrototype.cpp +++ b/Source/JavaScriptCore/runtime/StringPrototype.cpp @@ -1709,7 +1709,9 @@ JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToLowerCase, (JSGlobalObject* globalObje auto s = sVal->value(globalObject); RETURN_IF_EXCEPTION(scope, { }); - String lowercasedString = s->convertToLowercaseWithoutLocale(); + String lowercasedString = s->tryConvertToLowercaseWithoutLocale(); + if (lowercasedString.isNull()) [[unlikely]] + return JSValue::encode(throwOutOfMemoryError(globalObject, scope)); if (lowercasedString.impl() == s->impl()) return JSValue::encode(sVal); @@ -1745,7 +1747,9 @@ JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToUpperCase, (JSGlobalObject* globalObje auto s = sVal->value(globalObject); RETURN_IF_EXCEPTION(scope, { }); - String uppercasedString = s->convertToUppercaseWithoutLocale(); + String uppercasedString = s->tryConvertToUppercaseWithoutLocale(); + if (uppercasedString.isNull()) [[unlikely]] + return JSValue::encode(throwOutOfMemoryError(globalObject, scope)); if (uppercasedString.impl() == s->impl()) return JSValue::encode(sVal); @@ -1862,32 +1866,24 @@ static EncodedJSValue toLocaleCase(JSGlobalObject* globalObject, CallFrame* call }); // 12. If locale is undefined, let locale be "und". - if (locale.isNull()) { - // Case mapping is locale-independent here, so skip ICU and use the root conversion. - String converted = mode == CaseConversionMode::Lower ? s->convertToLowercaseWithoutLocale() : s->convertToUppercaseWithoutLocale(); - if (converted.impl() == s->impl()) - return JSValue::encode(sVal); - RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, WTF::move(converted)))); - } - - // Delegate the following steps to icu u_strToLower or u_strToUpper. // 13. Let cpList be a List containing in order the code points of S as defined in ES2015, 6.1.4, starting at the first element of S. // 14. For each code point c in cpList, if the Unicode Character Database provides a lower(/upper) case equivalent of c that is either language insensitive or for the language locale, then replace c in cpList with that/those equivalent code point(s). // 15. Let cuList be a new List. // 16. For each code point c in cpList, in order, append to cuList the elements of the UTF-16 Encoding (defined in ES2015, 6.1.4) of c. // 17. Let L be a String whose elements are, in order, the elements of cuList. - - // Most strings lower/upper case will be the same size as original, so try that first. - Vector buffer; - if (!StringImpl::isValidLength(s->length()) || !buffer.tryReserveInitialCapacity(s->length())) [[unlikely]] + // WTF applies the language-sensitive mappings of "az", "el", "lt" and "tr" through ICU, and the + // root mappings for every other locale identifier, including the null one. + if (s->isEmpty()) + return JSValue::encode(sVal); + AtomString localeIdentifier { locale }; + String converted = mode == CaseConversionMode::Lower ? s->tryConvertToLowercaseWithLocale(localeIdentifier) : s->tryConvertToUppercaseWithLocale(localeIdentifier); + if (converted.isNull()) [[unlikely]] return JSValue::encode(throwOutOfMemoryError(globalObject, scope)); - auto convertCase = mode == CaseConversionMode::Lower ? u_strToLower : u_strToUpper; - auto status = callBufferProducingFunction(convertCase, buffer, StringView { s }.upconvertedCharacters().get(), s->length(), locale.utf8().legacyCStringPointer()); - if (U_FAILURE(status)) - return throwVMTypeError(globalObject, scope, String::fromLatin1(u_errorName(status))); // 18. Return L. - RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, String { WTF::move(buffer) }))); + if (converted.impl() == s->impl()) + return JSValue::encode(sVal); + RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, WTF::move(converted)))); } JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToLocaleLowerCase, (JSGlobalObject* globalObject, CallFrame* callFrame)) diff --git a/Source/WTF/wtf/text/StringImpl.cpp b/Source/WTF/wtf/text/StringImpl.cpp index 85dcb2bd35a04..bb229755f5f24 100644 --- a/Source/WTF/wtf/text/StringImpl.cpp +++ b/Source/WTF/wtf/text/StringImpl.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -368,7 +369,56 @@ char32_t NODELETE StringImpl::codePointAt(unsigned i) return span[i]; } -Ref StringImpl::convertToLowercaseWithoutLocale() +// u_strToUpper and u_strToLower have the same signature. +using ICUCaseConvertFunction = decltype(&u_strToUpper); + +// ICU takes UTF-16, so an 8-bit string is widened into buffer first. Returns nullopt when that +// buffer cannot be allocated. StringView::upconvertedCharacters() crashes in that case, and its +// Vector cannot hold the copy of a string with 2^30 or more characters. +static std::optional> charactersForICU(const StringImpl& string, MallocSpan& buffer) +{ + if (!string.is8Bit()) + return string.span16(); + if (!string.length()) + return std::span { }; + buffer = MallocSpan::tryMalloc(string.length() * sizeof(char16_t)); + if (!buffer) + return std::nullopt; + StringImpl::copyCharacters(buffer.mutableSpan(), string.span8()); + return buffer.span(); +} + +// Runs u_strToUpper or u_strToLower over the whole string. The first attempt writes into +// destination, which holds source.size() characters. Returns nullptr when the converted +// string does not fit in a String. +static RefPtr tryConvertCaseWithICU(ICUCaseConvertFunction convert, const char* locale, std::span source, Ref&& destination, std::span destinationCharacters) +{ + ASSERT(destinationCharacters.size() == source.size()); + if (source.empty()) + return WTF::move(destination); + + UErrorCode status = U_ZERO_ERROR; + int32_t convertedLength = convert(destinationCharacters.data(), destinationCharacters.size(), source.data(), source.size(), locale, &status); + if (U_SUCCESS(status) && static_cast(convertedLength) == source.size()) + return WTF::move(destination); + + // U_BUFFER_OVERFLOW_ERROR means the converted string is longer than the source, and + // convertedLength is its length. ICU reports a converted string longer than INT32_MAX + // as U_INDEX_OUTOFBOUNDS_ERROR instead. + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + return nullptr; + + RefPtr converted = StringImpl::tryCreateUninitialized(convertedLength, destinationCharacters); + if (!converted) + return nullptr; + status = U_ZERO_ERROR; + convert(destinationCharacters.data(), destinationCharacters.size(), source.data(), source.size(), locale, &status); + if (U_FAILURE(status)) + return nullptr; + return converted; +} + +RefPtr StringImpl::tryConvertToLowercaseWithoutLocale() { // Note: At one time this was a hot function in the Dromaeo benchmark, specifically the // no-op code path that may return ourself if we find no upper case letters and no invalid @@ -383,13 +433,20 @@ Ref StringImpl::convertToLowercaseWithoutLocale() return convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(i); } - return *this; + return this; } - return convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(0); + return tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(0); +} + +Ref StringImpl::convertToLowercaseWithoutLocale() +{ + RefPtr lowercased = tryConvertToLowercaseWithoutLocale(); + RELEASE_ASSERT(lowercased); + return lowercased.releaseNonNull(); } -Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) +RefPtr StringImpl::tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) { ASSERT(!is8Bit()); auto span = span16(); @@ -413,34 +470,22 @@ Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingInde } // Nothing to do if the string is all ASCII with no uppercase. if (noUpper && !(ored & ~0x7F)) - return *this; + return this; + + std::span data16; + RefPtr newImpl = tryCreateUninitialized(m_length, data16); + if (!newImpl) + return nullptr; if (!(ored & ~0x7F)) { - std::span data16; - auto newImpl = createUninitializedInternalNonEmpty(m_length, data16); copyCharacters(data16, span.first(failingIndex)); for (unsigned i = failingIndex; i < span.size(); ++i) data16[i] = toASCIILower(span[i]); return newImpl; } - int32_t length = m_length; - // Do a slower implementation for cases that include non-ASCII characters. - std::span data16; - auto newImpl = createUninitializedInternalNonEmpty(m_length, data16); - - UErrorCode status = U_ZERO_ERROR; - int32_t realLength = u_strToLower(data16.data(), length, m_data16, m_length, "", &status); - if (U_SUCCESS(status) && realLength == length) - return newImpl; - - newImpl = createUninitialized(realLength, data16); - status = U_ZERO_ERROR; - u_strToLower(data16.data(), realLength, m_data16, m_length, "", &status); - if (U_FAILURE(status)) - return *this; - return newImpl; + return tryConvertCaseWithICU(u_strToLower, "", span, newImpl.releaseNonNull(), data16); } Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) @@ -471,7 +516,7 @@ Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingInde return newImpl; } -Ref StringImpl::convertToUppercaseWithoutLocale() +RefPtr StringImpl::tryConvertToUppercaseWithoutLocale() { // This function could be optimized for no-op cases the way // convertToLowercaseWithoutLocale() is, but in empirical testing, @@ -487,18 +532,27 @@ Ref StringImpl::convertToUppercaseWithoutLocale() for (unsigned i = 0; i < span.size(); ++i) { Latin1Character character = span[i]; if (!isASCII(character) || isASCIILower(character)) [[unlikely]] - return convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(i); + return tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(i); } - return *this; + return this; } - return convertToUppercaseWithoutLocaleUpconvert(); + return tryConvertToUppercaseWithoutLocaleUpconvert(); +} + +Ref StringImpl::convertToUppercaseWithoutLocale() +{ + RefPtr uppercased = tryConvertToUppercaseWithoutLocale(); + RELEASE_ASSERT(uppercased); + return uppercased.releaseNonNull(); } -Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) +RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) { ASSERT(is8Bit()); std::span destination; - auto newImpl = createUninitialized(m_length, destination); + RefPtr newImpl = tryCreateUninitialized(m_length, destination); + if (!newImpl) + return nullptr; auto span = span8(); copyCharacters(destination, span.first(failingIndex)); @@ -533,7 +587,7 @@ Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingInde char16_t upper = u_toupper(character); if (!isLatin1(upper)) [[unlikely]] { // Since this upper-cased character does not fit in an 8-bit string, we need to take the 16-bit path. - return convertToUppercaseWithoutLocaleUpconvert(); + return tryConvertToUppercaseWithoutLocaleUpconvert(); } destination[i] = static_cast(upper); } @@ -542,9 +596,10 @@ Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingInde return newImpl; // We have numberSSCharacters sharp-s characters, but none of the other special characters. - if ((m_length + numberSharpSCharacters) > MaxLength) - return *this; - newImpl = createUninitialized(m_length + numberSharpSCharacters, destination); + // Enough of them take the new length past MaxLength, and then tryCreateUninitialized() gives nullptr. + newImpl = tryCreateUninitialized(static_cast(m_length) + numberSharpSCharacters, destination); + if (!newImpl) + return nullptr; size_t destinationIndex = 0; for (unsigned i = 0; i < span.size(); ++i) { @@ -561,13 +616,16 @@ Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingInde return newImpl; } -Ref StringImpl::convertToUppercaseWithoutLocaleUpconvert() +RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleUpconvert() { - auto upconvertedCharacters = StringView(*this).upconvertedCharacters(); - return convertToUppercaseWithoutLocale16Bit(upconvertedCharacters.span(), 0); + MallocSpan buffer; + auto source16 = charactersForICU(*this, buffer); + if (!source16) + return nullptr; + return tryConvertToUppercaseWithoutLocale16Bit(*source16, 0); } -Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) +RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) { ASSERT(!is8Bit()); auto span = span16(); @@ -577,15 +635,17 @@ Ref StringImpl::convertToUppercaseWithoutLocaleStartingAtFailingInde ASSERT(!isASCIILower(span[i])); } #endif - return convertToUppercaseWithoutLocale16Bit(span, failingIndex); + return tryConvertToUppercaseWithoutLocale16Bit(span, failingIndex); } -Ref StringImpl::convertToUppercaseWithoutLocale16Bit(std::span source16, unsigned failingIndex) +RefPtr StringImpl::tryConvertToUppercaseWithoutLocale16Bit(std::span source16, unsigned failingIndex) { ASSERT(source16.size() == m_length); std::span data16; - auto newImpl = createUninitialized(source16.size(), data16); + RefPtr newImpl = tryCreateUninitialized(source16.size(), data16); + if (!newImpl) + return nullptr; // Characters before the failing index are already known to be ASCII that upper-casing leaves // alone, so they can be copied across without being tested or converted. @@ -602,16 +662,7 @@ Ref StringImpl::convertToUppercaseWithoutLocale16Bit(std::span(m_length)) - return newImpl; - newImpl = createUninitialized(realLength, data16); - status = U_ZERO_ERROR; - u_strToUpper(data16.data(), data16.size(), source16.data(), source16.size(), "", &status); - if (U_FAILURE(status)) - return *this; - return newImpl; + return tryConvertCaseWithICU(u_strToUpper, "", source16, newImpl.releaseNonNull(), data16); } static inline bool NODELETE needsTurkishCasingRules(const AtomString& locale) @@ -638,75 +689,62 @@ static inline bool NODELETE needsLithuanianCasingRules(const AtomString& locale) && (locale.length() == 2 || locale[2] == '-'); } -Ref StringImpl::convertToLowercaseWithLocale(const AtomString& localeIdentifier) +static RefPtr tryConvertCaseWithLocale(const StringImpl& string, ICUCaseConvertFunction convert, const char* locale) +{ + MallocSpan buffer; + auto source16 = charactersForICU(string, buffer); + if (!source16) + return nullptr; + std::span data16; + RefPtr newString = StringImpl::tryCreateUninitialized(source16->size(), data16); + if (!newString) + return nullptr; + return tryConvertCaseWithICU(convert, locale, *source16, newString.releaseNonNull(), data16); +} + +RefPtr StringImpl::tryConvertToLowercaseWithLocale(const AtomString& localeIdentifier) { // Use the more-optimized code path most of the time. - const char* locale; if (needsTurkishCasingRules(localeIdentifier)) { // Passing in the hardcoded locale "tr" is more efficient than // allocating memory just to turn localeIdentifier into a C string, and we assume // there is no difference between the lowercasing for "tr" and "az" locales. // FIXME: Could optimize further by looking for the three sequences that have locale-specific lowercasing. - locale = "tr"; - } else if (needsLithuanianCasingRules(localeIdentifier)) - locale = "lt"; - else - return convertToLowercaseWithoutLocale(); - - // FIXME: Could share more code with convertToLowercaseWithoutLocale. - - if (m_length > MaxLength) - CRASH(); + return tryConvertCaseWithLocale(*this, u_strToLower, "tr"); + } + if (needsLithuanianCasingRules(localeIdentifier)) + return tryConvertCaseWithLocale(*this, u_strToLower, "lt"); + return tryConvertToLowercaseWithoutLocale(); +} - auto upconvertedCharacters = StringView(*this).upconvertedCharacters(); - auto source16 = upconvertedCharacters.span(); - std::span data16; - auto newString = createUninitialized(source16.size(), data16); - UErrorCode status = U_ZERO_ERROR; - size_t realLength = u_strToLower(data16.data(), data16.size(), source16.data(), source16.size(), locale, &status); - if (U_SUCCESS(status) && realLength == source16.size()) - return newString; - newString = createUninitialized(realLength, data16); - status = U_ZERO_ERROR; - u_strToLower(data16.data(), data16.size(), source16.data(), source16.size(), locale, &status); - if (U_FAILURE(status)) - return *this; - return newString; +Ref StringImpl::convertToLowercaseWithLocale(const AtomString& localeIdentifier) +{ + RefPtr lowercased = tryConvertToLowercaseWithLocale(localeIdentifier); + RELEASE_ASSERT(lowercased); + return lowercased.releaseNonNull(); } -Ref StringImpl::convertToUppercaseWithLocale(const AtomString& localeIdentifier) +RefPtr StringImpl::tryConvertToUppercaseWithLocale(const AtomString& localeIdentifier) { // Use the more-optimized code path most of the time. - const char* locale; if (needsTurkishCasingRules(localeIdentifier) && find('i') != notFound) { // Passing in the hardcoded locale "tr" is more efficient than // allocating memory just to turn localeIdentifier into a C string, and we assume // there is no difference between the uppercasing for "tr" and "az" locales. - locale = "tr"; - } else if (needsGreekUppercasingRules(localeIdentifier)) - locale = "el"; - else if (needsLithuanianCasingRules(localeIdentifier)) - locale = "lt"; - else - return convertToUppercaseWithoutLocale(); - - if (m_length > MaxLength) - CRASH(); + return tryConvertCaseWithLocale(*this, u_strToUpper, "tr"); + } + if (needsGreekUppercasingRules(localeIdentifier)) + return tryConvertCaseWithLocale(*this, u_strToUpper, "el"); + if (needsLithuanianCasingRules(localeIdentifier)) + return tryConvertCaseWithLocale(*this, u_strToUpper, "lt"); + return tryConvertToUppercaseWithoutLocale(); +} - auto upconvertedCharacters = StringView(*this).upconvertedCharacters(); - auto source16 = upconvertedCharacters.span(); - std::span data16; - auto newString = createUninitialized(source16.size(), data16); - UErrorCode status = U_ZERO_ERROR; - size_t realLength = u_strToUpper(data16.data(), data16.size(), source16.data(), source16.size(), locale, &status); - if (U_SUCCESS(status) && realLength == source16.size()) - return newString; - newString = createUninitialized(realLength, data16); - status = U_ZERO_ERROR; - u_strToUpper(data16.data(), data16.size(), source16.data(), source16.size(), locale, &status); - if (U_FAILURE(status)) - return *this; - return newString; +Ref StringImpl::convertToUppercaseWithLocale(const AtomString& localeIdentifier) +{ + RefPtr uppercased = tryConvertToUppercaseWithLocale(localeIdentifier); + RELEASE_ASSERT(uppercased); + return uppercased.releaseNonNull(); } Ref StringImpl::foldCase() diff --git a/Source/WTF/wtf/text/StringImpl.h b/Source/WTF/wtf/text/StringImpl.h index 69a412e29b32d..eac13f445e43e 100644 --- a/Source/WTF/wtf/text/StringImpl.h +++ b/Source/WTF/wtf/text/StringImpl.h @@ -477,14 +477,22 @@ class StringImpl : private StringImplShape, public NoVirtualDestructorBase { WTF_EXPORT_PRIVATE Ref convertToASCIILowercase(); WTF_EXPORT_PRIVATE Ref convertToASCIIUppercase(); WTF_EXPORT_PRIVATE Ref convertToLowercaseWithoutLocale(); - WTF_EXPORT_PRIVATE Ref convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); - WTF_EXPORT_PRIVATE Ref convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned); WTF_EXPORT_PRIVATE Ref convertToUppercaseWithoutLocale(); - WTF_EXPORT_PRIVATE Ref convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); - WTF_EXPORT_PRIVATE Ref convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned); WTF_EXPORT_PRIVATE Ref convertToLowercaseWithLocale(const AtomString& localeIdentifier); WTF_EXPORT_PRIVATE Ref convertToUppercaseWithLocale(const AtomString& localeIdentifier); + // Full case mapping can make a string longer. The functions that start with "try" return nullptr + // when the converted string cannot be allocated, for example because it would be longer than + // MaxLength. Their namesakes above without "try" crash in that case. + WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithoutLocale(); + WTF_EXPORT_PRIVATE Ref convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); + WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned); + WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithoutLocale(); + WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); + WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned); + WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithLocale(const AtomString& localeIdentifier); + WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithLocale(const AtomString& localeIdentifier); + Ref foldCase(); WTF_EXPORT_PRIVATE Ref simplifyWhiteSpace(CodeUnitMatchFunction); @@ -591,8 +599,8 @@ class StringImpl : private StringImplShape, public NoVirtualDestructorBase { template static std::expected, UTF8ConversionError> reallocateInternal(Ref&&, unsigned, CharacterType*&); template static Ref createInternal(std::span); WTF_EXPORT_PRIVATE NEVER_INLINE unsigned hashSlowCase() const; - Ref convertToUppercaseWithoutLocaleUpconvert(); - Ref convertToUppercaseWithoutLocale16Bit(std::span source, unsigned failingIndex); + RefPtr tryConvertToUppercaseWithoutLocaleUpconvert(); + RefPtr tryConvertToUppercaseWithoutLocale16Bit(std::span source, unsigned failingIndex); // The bottom bit in the ref count indicates a static (immortal) string. static constexpr uint32_t s_refCountFlagIsStaticString = 0x1; diff --git a/Source/WTF/wtf/text/WTFString.cpp b/Source/WTF/wtf/text/WTFString.cpp index 2a15c0f40b961..13e9578db6b15 100644 --- a/Source/WTF/wtf/text/WTFString.cpp +++ b/Source/WTF/wtf/text/WTFString.cpp @@ -124,15 +124,20 @@ String String::convertToLowercaseWithoutLocale() const SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToLowercaseWithoutLocale() : String { }; } +String String::tryConvertToLowercaseWithoutLocale() const +{ + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToLowercaseWithoutLocale() : String { }; +} + String String::convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const { // FIXME: Should this function, and the many others like it, be inlined? SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : String { }; } -String String::convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const +String String::tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const { - SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex) : String { }; + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex) : String { }; } String String::convertToUppercaseWithoutLocale() const @@ -141,6 +146,11 @@ String String::convertToUppercaseWithoutLocale() const SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToUppercaseWithoutLocale() : String { }; } +String String::tryConvertToUppercaseWithoutLocale() const +{ + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToUppercaseWithoutLocale() : String { }; +} + String String::convertToLowercaseWithLocale(const AtomString& localeIdentifier) const { // FIXME: Should this function, and the many others like it, be inlined? @@ -153,14 +163,24 @@ String String::convertToUppercaseWithLocale(const AtomString& localeIdentifier) SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToUppercaseWithLocale(localeIdentifier) : String { }; } -String String::convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const +String String::tryConvertToLowercaseWithLocale(const AtomString& localeIdentifier) const +{ + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToLowercaseWithLocale(localeIdentifier) : String { }; +} + +String String::tryConvertToUppercaseWithLocale(const AtomString& localeIdentifier) const +{ + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToUppercaseWithLocale(localeIdentifier) : String { }; +} + +String String::tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const { - SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : String { }; + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : String { }; } -String String::convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const +String String::tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const { - SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex) : String { }; + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex) : String { }; } String String::trim(CodeUnitMatchFunction predicate) const diff --git a/Source/WTF/wtf/text/WTFString.h b/Source/WTF/wtf/text/WTFString.h index c2aa88010d7e9..3ce5243e10eed 100644 --- a/Source/WTF/wtf/text/WTFString.h +++ b/Source/WTF/wtf/text/WTFString.h @@ -215,14 +215,22 @@ class String final { [[nodiscard]] WTF_EXPORT_PRIVATE String convertToASCIILowercase() const; [[nodiscard]] WTF_EXPORT_PRIVATE String convertToASCIIUppercase() const; [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithoutLocale() const; - [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned) const; - [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned) const; [[nodiscard]] WTF_EXPORT_PRIVATE String convertToUppercaseWithoutLocale() const; - [[nodiscard]] WTF_EXPORT_PRIVATE String convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const; - [[nodiscard]] WTF_EXPORT_PRIVATE String convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const; [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithLocale(const AtomString& localeIdentifier) const; [[nodiscard]] WTF_EXPORT_PRIVATE String convertToUppercaseWithLocale(const AtomString& localeIdentifier) const; + // Full case mapping can make a string longer. The functions that start with "try" return a null + // string when the converted string cannot be allocated, for example because it would be longer + // than MaxLength. Their namesakes above without "try" crash in that case. + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithoutLocale() const; + [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithoutLocale() const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithLocale(const AtomString& localeIdentifier) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithLocale(const AtomString& localeIdentifier) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String simplifyWhiteSpace(CodeUnitMatchFunction) const; [[nodiscard]] WTF_EXPORT_PRIVATE String trim(CodeUnitMatchFunction) const; From b626c816c09bab09942090a2a5df97400055faeb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:07:57 +0000 Subject: [PATCH 2/3] Keep the inline widening buffer for short strings, shrink in place, free the first attempt early CharactersForICU widens a short 8-bit string into an inline buffer, as StringView::upconvertedCharacters() does, so the Turkish, Lithuanian and Greek paths and the Latin-1 characters that uppercase outside Latin-1 do not pay for a heap allocation they did not need before. When ICU's first pass produces a shorter string (Turkish "I" U+0307 to "i", Greek uppercasing of decomposed text), keep what it wrote and shrink the allocation with tryReallocate() instead of converting a second time. When the first pass overflows, free that buffer before allocating the longer one, so the peak is one result buffer, not two. The stress test drops the duplicate multi-gigabyte passes, is marked slow, and sends the Azerbaijani case through ICU by giving it an "i". --- ...-case-conversion-longer-than-max-length.js | 41 +++---- Source/WTF/wtf/text/StringImpl.cpp | 108 ++++++++++++------ 2 files changed, 93 insertions(+), 56 deletions(-) diff --git a/JSTests/stress/string-case-conversion-longer-than-max-length.js b/JSTests/stress/string-case-conversion-longer-than-max-length.js index b6ef9b25d2345..2e57ba2a45de3 100644 --- a/JSTests/stress/string-case-conversion-longer-than-max-length.js +++ b/JSTests/stress/string-case-conversion-longer-than-max-length.js @@ -1,4 +1,5 @@ //@ memoryHog! +//@ slow! //@ skip if $addressBits <= 32 //@ runDefault @@ -6,6 +7,7 @@ // lowercases to "i̇" (two code units). When the converted string would be longer than the // maximum string length, the conversion has to throw. It used to hand back the input unchanged. // Results between 2^30 and the maximum length are fine and must neither throw nor crash. +// Every block below works on strings of a gigabyte or more, so each takes seconds. function shouldBe(actual, expected) { if (actual !== expected) @@ -41,9 +43,10 @@ for (let i = 0; i < testLoopCount; ++i) { // 8-bit input. Each "ß" adds one character, so the result would be 2^31 long. const input = "\u00DF".repeat(2 ** 30); shouldThrowOutOfMemory(() => input.toUpperCase()); - shouldThrowOutOfMemory(() => input.toLocaleUpperCase()); shouldThrowOutOfMemory(() => input.toLocaleUpperCase("en")); shouldThrowOutOfMemory(() => toUpperCase(input)); + // The language-sensitive locales convert through ICU into a separate buffer, with the same limit. + shouldThrowOutOfMemory(() => input.toLocaleUpperCase("lt")); // One character less and the result is exactly the maximum length. const fits = "\u00DF".repeat(2 ** 30 - 1) + "a"; @@ -59,7 +62,6 @@ gc(); // does not even fit in ICU's int32_t length. const input = "\uFB03".repeat(715827883); shouldThrowOutOfMemory(() => input.toUpperCase()); - shouldThrowOutOfMemory(() => toUpperCase(input)); } gc(); @@ -75,35 +77,30 @@ gc(); // Lowercasing grows too: "İ" (U+0130) becomes "i" followed by U+0307 COMBINING DOT ABOVE. const input = "\u0130".repeat(2 ** 30); shouldThrowOutOfMemory(() => input.toLowerCase()); - shouldThrowOutOfMemory(() => input.toLocaleLowerCase("en")); shouldThrowOutOfMemory(() => toLowerCase(input)); } gc(); { - // The language-sensitive locales convert through ICU into a separate buffer. The same limit - // applies there, and a result of 2^30 or more characters, which is fine, must not crash. - const input = "\u00DF".repeat(2 ** 30); - shouldThrowOutOfMemory(() => input.toLocaleUpperCase("tr")); - shouldThrowOutOfMemory(() => input.toLocaleUpperCase("lt")); - - const half = "\u00DF".repeat(2 ** 29); - const upper = half.toLocaleUpperCase("az"); - shouldBe(upper.length, 2 ** 30); + // A result of 2^30 characters from a language-sensitive locale, which is fine. "ß" is "SS" under + // Azerbaijani rules too, and the "i", which becomes "İ", is what sends the string to them. + const upper = ("\u00DF".repeat(2 ** 29) + "i").toLocaleUpperCase("az"); + shouldBe(upper.length, 2 ** 30 + 1); shouldBe(upper[0], "S"); shouldBe(upper[2 ** 30 - 1], "S"); - - const dotless = "i".repeat(2 ** 30).toLocaleUpperCase("tr"); - shouldBe(dotless.length, 2 ** 30); - shouldBe(dotless[2 ** 30 - 1], "\u0130"); + shouldBe(upper[2 ** 30], "\u0130"); } gc(); { - // An 8-bit string of 2^30 or more characters whose uppercase form needs 16 bits: "ÿ" becomes "Ÿ" (U+0178). - const input = "a".repeat(2 ** 30) + "\u00FF"; - const upper = input.toUpperCase(); - shouldBe(upper.length, 2 ** 30 + 1); - shouldBe(upper[0], "A"); - shouldBe(upper[2 ** 30], "\u0178"); + // 8-bit strings of 2^30 or more characters that have to be widened for ICU: "i" is "İ" in + // Turkish, and "ÿ" uppercases to "Ÿ" (U+0178) everywhere. + const dotted = "i".repeat(2 ** 30).toLocaleUpperCase("tr"); + shouldBe(dotted.length, 2 ** 30); + shouldBe(dotted[2 ** 30 - 1], "\u0130"); + + const diaeresis = toUpperCase("a".repeat(2 ** 30) + "\u00FF"); + shouldBe(diaeresis.length, 2 ** 30 + 1); + shouldBe(diaeresis[0], "A"); + shouldBe(diaeresis[2 ** 30], "\u0178"); } diff --git a/Source/WTF/wtf/text/StringImpl.cpp b/Source/WTF/wtf/text/StringImpl.cpp index bb229755f5f24..ebb72c1a53625 100644 --- a/Source/WTF/wtf/text/StringImpl.cpp +++ b/Source/WTF/wtf/text/StringImpl.cpp @@ -28,9 +28,11 @@ #include "wtf/DebugHeap.h" #include +#include #include #include #include +#include #include #include #include @@ -372,47 +374,87 @@ char32_t NODELETE StringImpl::codePointAt(unsigned i) // u_strToUpper and u_strToLower have the same signature. using ICUCaseConvertFunction = decltype(&u_strToUpper); -// ICU takes UTF-16, so an 8-bit string is widened into buffer first. Returns nullopt when that -// buffer cannot be allocated. StringView::upconvertedCharacters() crashes in that case, and its -// Vector cannot hold the copy of a string with 2^30 or more characters. -static std::optional> charactersForICU(const StringImpl& string, MallocSpan& buffer) -{ - if (!string.is8Bit()) - return string.span16(); - if (!string.length()) - return std::span { }; - buffer = MallocSpan::tryMalloc(string.length() * sizeof(char16_t)); - if (!buffer) - return std::nullopt; - StringImpl::copyCharacters(buffer.mutableSpan(), string.span8()); - return buffer.span(); +// The characters of a string as UTF-16, which is what ICU takes. An 8-bit string is widened into a +// buffer: the inline one when it is short, as with StringView::upconvertedCharacters(), and a heap +// allocation that is allowed to fail otherwise. upconvertedCharacters() itself would crash when +// that allocation fails, and its Vector cannot hold 2^30 characters at all. +class CharactersForICU { + WTF_MAKE_NONCOPYABLE(CharactersForICU); + WTF_MAKE_NONMOVABLE(CharactersForICU); +public: + explicit CharactersForICU(const StringImpl&); + + bool failed() const { return m_failed; } + std::span span() const LIFETIME_BOUND { ASSERT(!m_failed); return m_characters; } + +private: + std::span m_characters; + MallocSpan m_heapBuffer; + std::array m_inlineBuffer; + bool m_failed { false }; +}; + +CharactersForICU::CharactersForICU(const StringImpl& string) +{ + if (!string.is8Bit()) { + m_characters = string.span16(); + return; + } + auto characters8 = string.span8(); + std::span buffer; + if (characters8.size() <= m_inlineBuffer.size()) + buffer = std::span { m_inlineBuffer }.first(characters8.size()); + else { + m_heapBuffer = MallocSpan::tryMalloc(characters8.size() * sizeof(char16_t)); + if (!m_heapBuffer) { + m_failed = true; + return; + } + buffer = m_heapBuffer.mutableSpan(); + } + StringImpl::copyCharacters(buffer, characters8); + m_characters = buffer; } // Runs u_strToUpper or u_strToLower over the whole string. The first attempt writes into -// destination, which holds source.size() characters. Returns nullptr when the converted -// string does not fit in a String. -static RefPtr tryConvertCaseWithICU(ICUCaseConvertFunction convert, const char* locale, std::span source, Ref&& destination, std::span destinationCharacters) +// destination, a new string with one owner that holds source.size() characters. Returns nullptr +// when the converted string does not fit in a String. +static RefPtr tryConvertCaseWithICU(ICUCaseConvertFunction convert, const char* locale, std::span source, RefPtr&& destination, std::span destinationCharacters) { - ASSERT(destinationCharacters.size() == source.size()); + ASSERT(destination && destinationCharacters.size() == source.size()); if (source.empty()) return WTF::move(destination); UErrorCode status = U_ZERO_ERROR; int32_t convertedLength = convert(destinationCharacters.data(), destinationCharacters.size(), source.data(), source.size(), locale, &status); - if (U_SUCCESS(status) && static_cast(convertedLength) == source.size()) - return WTF::move(destination); + if (U_SUCCESS(status)) { + if (static_cast(convertedLength) == source.size()) + return WTF::move(destination); + // Shorter, which takes a mapping like the Turkish one from "I" U+0307 to "i". Keep the + // converted characters and give back the rest of the buffer. + char16_t* characters; + auto shrunk = StringImpl::tryReallocate(destination.releaseNonNull(), convertedLength, characters); + if (!shrunk) + return nullptr; + return WTF::move(*shrunk); + } // U_BUFFER_OVERFLOW_ERROR means the converted string is longer than the source, and // convertedLength is its length. ICU reports a converted string longer than INT32_MAX // as U_INDEX_OUTOFBOUNDS_ERROR instead. - if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + if (status != U_BUFFER_OVERFLOW_ERROR) return nullptr; - RefPtr converted = StringImpl::tryCreateUninitialized(convertedLength, destinationCharacters); + // What the first attempt wrote is of no use. Free it before allocating the longer string. + destinationCharacters = { }; + destination = nullptr; + + std::span convertedCharacters; + RefPtr converted = StringImpl::tryCreateUninitialized(convertedLength, convertedCharacters); if (!converted) return nullptr; status = U_ZERO_ERROR; - convert(destinationCharacters.data(), destinationCharacters.size(), source.data(), source.size(), locale, &status); + convert(convertedCharacters.data(), convertedCharacters.size(), source.data(), source.size(), locale, &status); if (U_FAILURE(status)) return nullptr; return converted; @@ -485,7 +527,7 @@ RefPtr StringImpl::tryConvertToLowercaseWithoutLocaleStartingAtFaili } // Do a slower implementation for cases that include non-ASCII characters. - return tryConvertCaseWithICU(u_strToLower, "", span, newImpl.releaseNonNull(), data16); + return tryConvertCaseWithICU(u_strToLower, "", span, WTF::move(newImpl), data16); } Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) @@ -618,11 +660,10 @@ RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleStartingAtFaili RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleUpconvert() { - MallocSpan buffer; - auto source16 = charactersForICU(*this, buffer); - if (!source16) + CharactersForICU source16 { *this }; + if (source16.failed()) return nullptr; - return tryConvertToUppercaseWithoutLocale16Bit(*source16, 0); + return tryConvertToUppercaseWithoutLocale16Bit(source16.span(), 0); } RefPtr StringImpl::tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) @@ -662,7 +703,7 @@ RefPtr StringImpl::tryConvertToUppercaseWithoutLocale16Bit(std::span return newImpl; // Do a slower implementation for cases that include non-ASCII characters. - return tryConvertCaseWithICU(u_strToUpper, "", source16, newImpl.releaseNonNull(), data16); + return tryConvertCaseWithICU(u_strToUpper, "", source16, WTF::move(newImpl), data16); } static inline bool NODELETE needsTurkishCasingRules(const AtomString& locale) @@ -691,15 +732,14 @@ static inline bool NODELETE needsLithuanianCasingRules(const AtomString& locale) static RefPtr tryConvertCaseWithLocale(const StringImpl& string, ICUCaseConvertFunction convert, const char* locale) { - MallocSpan buffer; - auto source16 = charactersForICU(string, buffer); - if (!source16) + CharactersForICU source16 { string }; + if (source16.failed()) return nullptr; std::span data16; - RefPtr newString = StringImpl::tryCreateUninitialized(source16->size(), data16); + RefPtr newString = StringImpl::tryCreateUninitialized(source16.span().size(), data16); if (!newString) return nullptr; - return tryConvertCaseWithICU(convert, locale, *source16, newString.releaseNonNull(), data16); + return tryConvertCaseWithICU(convert, locale, source16.span(), WTF::move(newString), data16); } RefPtr StringImpl::tryConvertToLowercaseWithLocale(const AtomString& localeIdentifier) From 58cb6898b42858679b9d684661d756475f2e0bff Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:23:24 +0000 Subject: [PATCH 3/3] Make the 8-bit lowercase path fallible too tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit() allocates with tryCreateUninitialized() like the other try functions, so an 8-bit toLowerCase() whose result cannot be allocated throws in JSC instead of crashing in WTF. The result is never longer than the input here, so this only matters when the allocation itself fails. --- Source/JavaScriptCore/dfg/DFGOperations.cpp | 2 +- Source/WTF/wtf/text/StringImpl.cpp | 8 +++++--- Source/WTF/wtf/text/StringImpl.h | 2 +- Source/WTF/wtf/text/WTFString.cpp | 5 ++--- Source/WTF/wtf/text/WTFString.h | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 8d4c40594524a..3694649f95a95 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -3906,7 +3906,7 @@ JSC_DEFINE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject* globa if (!inputString->length()) OPERATION_RETURN(scope, vm.smallStrings.emptyString()); - String lowercasedString = inputString->is8Bit() ? inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); + String lowercasedString = inputString->is8Bit() ? inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex); if (lowercasedString.isNull()) [[unlikely]] { throwOutOfMemoryError(globalObject, scope); OPERATION_RETURN(scope, nullptr); diff --git a/Source/WTF/wtf/text/StringImpl.cpp b/Source/WTF/wtf/text/StringImpl.cpp index ebb72c1a53625..b339a35d6995c 100644 --- a/Source/WTF/wtf/text/StringImpl.cpp +++ b/Source/WTF/wtf/text/StringImpl.cpp @@ -472,7 +472,7 @@ RefPtr StringImpl::tryConvertToLowercaseWithoutLocale() for (unsigned i = 0; i < span.size(); ++i) { Latin1Character character = span[i]; if (!isASCII(character) || isASCIIUpper(character)) [[unlikely]] - return convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(i); + return tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(i); } return this; @@ -530,11 +530,13 @@ RefPtr StringImpl::tryConvertToLowercaseWithoutLocaleStartingAtFaili return tryConvertCaseWithICU(u_strToLower, "", span, WTF::move(newImpl), data16); } -Ref StringImpl::convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) +RefPtr StringImpl::tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) { ASSERT(is8Bit()); std::span data8; - auto newImpl = createUninitializedInternalNonEmpty(m_length, data8); + RefPtr newImpl = tryCreateUninitialized(m_length, data8); + if (!newImpl) + return nullptr; auto span = span8(); copyCharacters(data8, span.first(failingIndex)); diff --git a/Source/WTF/wtf/text/StringImpl.h b/Source/WTF/wtf/text/StringImpl.h index eac13f445e43e..eed1ec35a02c5 100644 --- a/Source/WTF/wtf/text/StringImpl.h +++ b/Source/WTF/wtf/text/StringImpl.h @@ -485,7 +485,7 @@ class StringImpl : private StringImplShape, public NoVirtualDestructorBase { // when the converted string cannot be allocated, for example because it would be longer than // MaxLength. Their namesakes above without "try" crash in that case. WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithoutLocale(); - WTF_EXPORT_PRIVATE Ref convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); + WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); WTF_EXPORT_PRIVATE RefPtr tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned); WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithoutLocale(); WTF_EXPORT_PRIVATE RefPtr tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned); diff --git a/Source/WTF/wtf/text/WTFString.cpp b/Source/WTF/wtf/text/WTFString.cpp index 13e9578db6b15..92a49fcdebf8a 100644 --- a/Source/WTF/wtf/text/WTFString.cpp +++ b/Source/WTF/wtf/text/WTFString.cpp @@ -129,10 +129,9 @@ String String::tryConvertToLowercaseWithoutLocale() const SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToLowercaseWithoutLocale() : String { }; } -String String::convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const +String String::tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const { - // FIXME: Should this function, and the many others like it, be inlined? - SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : String { }; + SUPPRESS_UNCOUNTED_ARG return m_impl ? m_impl->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : String { }; } String String::tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned failingIndex) const diff --git a/Source/WTF/wtf/text/WTFString.h b/Source/WTF/wtf/text/WTFString.h index 3ce5243e10eed..5a20d8d83e877 100644 --- a/Source/WTF/wtf/text/WTFString.h +++ b/Source/WTF/wtf/text/WTFString.h @@ -223,7 +223,7 @@ class String final { // string when the converted string cannot be allocated, for example because it would be longer // than MaxLength. Their namesakes above without "try" crash in that case. [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithoutLocale() const; - [[nodiscard]] WTF_EXPORT_PRIVATE String convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned) const; + [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned) const; [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(unsigned) const; [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithoutLocale() const; [[nodiscard]] WTF_EXPORT_PRIVATE String tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(unsigned failingIndex) const;