CVE-2025-1942
Overview
Background
- `StringChars<CharT>`
- A SpiderMonkey helper that builds up string character data, starting with an inline buffer (
inlineChars_) and, when the content outgrows it, reallocating into a heap-owned buffer. - `CharT`
- The character type parameter:
Latin1Char(1 byte) orchar16_t(2 bytes). A two-byte string hassizeof(CharT) == 2. - `maybeRealloc`
- The method that, on growth, allocates a larger buffer and copies the existing inline characters over before the new characters are written.
- `String.prototype.toUpperCase`
- Case conversion that can make a string longer (some code points upcase to multiple code units), forcing exactly the grow/realloc path here.
Root Cause Analysis
In js/src/vm/StringType.cpp, StringChars<CharT>::maybeRealloc copied the existing inline characters into the freshly allocated buffer with std::memcpy(chars.data(), inlineChars_, InlineLength);. memcpy’s third argument is a byte count, but InlineLength is a count of characters. For CharT == char16_t (two-byte strings), each character is 2 bytes, so this copied only InlineLength bytes — half of the InlineLength * 2 bytes actually occupied by the inline characters. The upper half of the inline region in the new buffer was therefore left with whatever uninitialized heap bytes the allocation returned. When toUpperCase() lengthened the string and subsequent characters were written past that region, the stale uninitialized bytes remained embedded in the final string contents, matching the advisory: “uninitialized memory to be incorporated into the result string.”
The patch changes the copy length to InlineLength * sizeof(CharT), so the full byte extent of the inline characters is copied for both Latin1Char and char16_t, leaving no uninitialized gap. This is the correct byte count for a memcpy of InlineLength elements of size sizeof(CharT).
memcpy was given a character count where it needed a byte count, so for two-byte strings only half the inline characters were copied into the reallocated buffer and the remainder stayed uninitialized heap memory that then leaked into the resulting string; multiplying by sizeof(CharT) copies the full extent.Attack Path
- Attacker script Runs JavaScript on a page (web-reachable, no privilege).
- Trigger growth
Calls
String.prototype.toUpperCase()on a two-byte (char16_t) string whose uppercase form is longer, forcingStringChars<char16_t>::maybeReallocdown the grow path. - Short copy
The reallocated buffer receives only
InlineLengthbytes of theInlineLength*2-byte inline data, leaving uninitialized heap bytes in the gap. - Leak into result Those uninitialized bytes become part of the returned uppercase string, readable by script.
- Primitive Information disclosure: attacker-readable uninitialized heap memory in the content process, usable to defeat ASLR or leak adjacent allocation contents.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/vm/StringType.cpp |
modified |
Files Changed
js/src/vm/StringType.cpp
Audit Directions
- `memcpy`/`memmove` with element countsAudit
StringType.cppand otherCharT-templated string code for copies that pass a character count where a byte count is required (sizeof(CharT)missing). - Other grow/realloc pathsReview every place inline buffers are promoted to heap buffers in the string/StringBuffer code for partial-copy gaps.
- Two-byte vs Latin1 asymmetryLook for length arithmetic that is correct for
Latin1Charbut wrong forchar16_t, the exact asymmetry exploited here.
Patch
diff --git a/js/src/vm/StringType.cpp b/js/src/vm/StringType.cpp
index 2bff1d4aa4b..f368399b875 100644
--- a/js/src/vm/StringType.cpp
+++ b/js/src/vm/StringType.cpp
@@ -2894,7 +2894,7 @@ bool js::StringChars<CharT>::maybeRealloc(JSContext* cx, size_t oldLength,
if (!chars) {
return false;
}
- std::memcpy(chars.data(), inlineChars_, InlineLength);
+ std::memcpy(chars.data(), inlineChars_, InlineLength * sizeof(CharT));
ownedChars_.set(std::move(chars));
return true;