27eb122967d3188a3ea63a018bcff533df3d88e0 [JSC] Reject when search-string is longer than the target-string early
Triage note: StringView::find adds `if (matchLength > subjectLength - start) return notFound;` and the DFG/StringPrototype indexOf/startsWith/endsWith/includes operations move length checks (length-start < searchLength) before resolving the argument view; previously a search string longer than the remaining subject could drive the searcher to read past the buffer. Reachable from String.prototype.* fast paths.
The bug at a glance
StringView::find (Source/WTF/wtf/text/StringView.cpp) could drive the AdaptiveStringSearcher to read past the end of the subject when the match string is longer than the remaining subject; the fix adds if (matchLength > subjectLength - start) return notFound;. Correspondingly the JSC DFG operations (Source/JavaScriptCore/dfg/DFGOperations.cpp) and StringPrototype host functions (Source/JavaScriptCore/runtime/StringPrototype.cpp) for indexOf/lastIndexOf/startsWith/endsWith/includes are reordered to compute the length check from the unresolved argument length and bail before resolving the search view. This is an out-of-bounds read reachable from String.prototype.* fast paths in the WebContent process JS engine, an information-disclosure-grade primitive. Severity high is justified as an OOB read; it is a read primitive, not memory corruption, so RCE would require chaining (inferred).
A missing matchLength <= subjectLength - start precondition in the core string searcher, papered over at each JSC call site by length checks that previously ran only after the argument’s rope view was materialized — so reordering the checks and hardening the searcher close the same hole from both ends.
Root cause
The invariant is that a substring searcher may only inspect subject bytes in [start, subjectLength); a match longer than the remaining subject can never fit and must short-circuit before the searcher indexes the buffer. In StringView::find(AdaptiveStringSearcherTables&, …) the code returned early only for !matchLength (empty needle). With a non-empty needle longer than subjectLength-start, the adaptive (Boyer-Moore-Horspool-style) searcher would index subject positions near subjectLength assuming at least matchLength bytes remain, reading past the subject buffer.
Pre-patch call-site sequence (e.g. operationStringIndexOf): 1. auto otherView = argument->view(globalObject); resolves/materializes the argument rope first. 2. unsigned argumentLength = otherView->length(); 3. proceed into the fast path / searcher. Several sibling operations computed the length relation only implicitly or after view resolution, so a search string longer than the subject slid into the searcher. The patch flips the order: read unsigned argumentLength = argument->length(); (and baseLength = base->length()) up front, apply if (baseLength < argumentLength) return -1; / if (length - start < searchLength) return false; BEFORE calling argument->view(), then resolve the view. operationStringIndexOfWithIndex/LastIndexOf(WithIndex)/StartsWith(WithIndex)/EndsWith(WithEndPosition) and stringProtoFuncLastIndexOf/StartsWith/EndsWith and stringIncludesImpl all get this reorder; endsWith also precomputes end = min(endPosition, length) and checks end < suffixLength before taking the view.
Types/why it matters: JSString::view() materializes a rope into contiguous storage and can throw/allocate, so doing length math on the logical length() first is both safe and lets the bounds check run before any buffer is indexed. The backstop is StringView::find’s own if (matchLength > subjectLength - start) return notFound; — note subjectLength-start is safe because start<=subjectLength on these paths. Bugclass OOB-read matches the diff: the StringView.cpp hunk is the direct proof of the over-read.
Key code
Core searcher backstop plus a representative call-site reorder
// WTF StringView::find backstop:
if (!matchLength)
return start;
if (matchLength > subjectLength - start)
return notFound;
// JSC operationStringStartsWith: check before resolving the view
unsigned length = base->length();
unsigned prefixLength = prefix->length();
if (length < prefixLength)
OPERATION_RETURN(scope, false);
auto prefixView = prefix->view(globalObject);
Fix approach
Two complementary fixes. (1) StringView::find gains an unconditional precondition if (matchLength > subjectLength - start) return notFound; so the searcher never indexes beyond the subject regardless of caller. (2) Every JSC indexOf/lastIndexOf/startsWith/endsWith/includes operation (both the DFG JIT operations and the StringPrototype host functions) is reordered to derive the length relation from the unresolved JSString length() and return the no-match result before calling view(), so the materialized view is only taken once the lengths are known compatible. endsWith variants additionally compute the clamped end position first. The net effect is that the fast paths bail on too-long needles, and even a mis-ordered caller is caught by the searcher’s own guard.
Patch walkthrough
Source/WTF/wtf/text/StringView.cpp— StringView::find adds the load-bearing bounds checkif (matchLength > subjectLength - start) return notFound;immediately after the empty-needle check, preventing the AdaptiveStringSearcher from reading past the subject when the needle exceeds the remaining subject.Source/JavaScriptCore/dfg/DFGOperations.cpp— operationStringIndexOf/IndexOfWithIndex/LastIndexOf/LastIndexOfWithIndex/StartsWith/StartsWithWithIndex/EndsWith/EndsWithWithEndPosition now read base/argument length() up front, perform the length comparison and early-return before argument->view(), and use the logical lengths for the rope fast-path thresholds.Source/JavaScriptCore/runtime/StringPrototype.cpp— stringProtoFuncLastIndexOf/StartsWith/EndsWith and stringIncludesImpl move the length checks ahead of the search->view() call (using search->length()/otherJSString->length()), so a too-long search bails before any view is taken or the searcher runs.
Background
StringView::find / AdaptiveStringSearcher — WTF’s substring search; for non-trivial needles it uses an adaptive (Horspool-style) searcher with skip tables that index subject bytes assuming the needle can still fit in the remaining subject.
JSString rope and view() — A JSString may be a rope (unresolved concatenation); view()/resolveRope materializes it into contiguous characters, which allocates and can throw — so it is relatively expensive and side-effecting.
DFG JIT operations — operationString* are out-of-line C++ helpers the DFG/FTL JIT calls for String.prototype fast paths; they run with the full attacker-controlled argument strings.
minLengthForRopeWalk / tryFindOneChar — Single-character fast paths that walk a rope without full materialization; the patch keeps them but gates them on the pre-resolved lengths so start/end indices stay in range.
notFound sentinel — StringView::find returns WTF::notFound when no match exists; returning it early for an impossible (too-long) needle is the safe no-match result.
Vulnerability window
- Setup — Attacker script prepares a short subject string and a longer search string (and optionally a position argument) and warms the String.prototype method so the DFG fast path is taken.
- Trigger — Call e.g. shortSubject.includes(longerNeedle) or .indexOf/.startsWith/.endsWith so the operation enters the searcher with needle longer than the remaining subject.
- Over-read — Pre-patch, StringView::find’s searcher indexes subject positions assuming matchLength bytes remain, reading past the subject’s backing buffer.
- Observe — The match/no-match (or returned index) reflects out-of-bounds bytes, turning adjacent heap contents into an observable oracle.
Proof of concept
No regression test is in the diff; the code above is derived from the fixed call sites. Calling String.prototype.includes/indexOf/startsWith/endsWith with a search string longer than the (remaining) subject drives the pre-patch StringView::find searcher to read past the subject buffer. Looping warms the DFG operationString* path. The observable signal is the boolean/index result reflecting out-of-bounds bytes; precise leakage depends on heap layout (inferred).
Exploitation
- Reach — Pure JS: String.prototype.indexOf/lastIndexOf/startsWith/endsWith/includes on attacker-controlled strings in the WebContent process.
- Trigger — Supply a needle longer than the remaining subject so the length precondition is violated and the searcher over-reads.
- Primitive — Out-of-bounds read of heap bytes adjacent to the subject string buffer; the method’s boolean/index result acts as a comparison oracle over those bytes.
- Escalation (open) — Converting the oracle into a byte-granular heap disclosure requires grooming the subject buffer against sensitive neighbors and repeating the compare; this is a read-only primitive, so corruption/RCE would need a separate bug (inferred).
Detection & hunting
For defenders and detection engineers:
- ASan heap-buffer-overflow (read) in StringView::find / AdaptiveStringSearcher — Fuzz String.prototype.{indexOf,lastIndexOf,startsWith,endsWith,includes} with needle length > subject length under ASan.
- Searcher invoked with matchLength > subjectLength - start — Add an assertion/telemetry at StringView::find for the too-long-needle case and watch for pre-patch hits.
Remaining risk in the patch
- Unsigned length arithmetic — Several call sites now compute
length - start < searchLengthin unsigned; correctness depends on start<=length always holding on those paths (the code clamps start via std::min to length, so it holds in the shown hunks, but any unclamped caller would underflow). - Other StringView::find callers — The WTF-level guard now covers all callers, but non-JSC users of StringView::find that previously relied on their own (possibly missing) checks are only protected by the new backstop; the diff does not enumerate them (inferred).
Audit directions
- Other StringView/StringImpl search entry points — Audit find/reverseFind/contains/startsWith/endsWith at the WTF StringView and StringImpl level for callers that index before validating needle vs remaining length.
- DFG/FTL string intrinsics — Review other operationString* / string intrinsics that resolve argument views before length validation for the same ordering hazard.
- Rope fast paths — Check tryGetCharAt/tryFindOneChar and other rope-walk fast paths for index computations (start, end-1) that assume in-range positions.