[JSC] Implement `String#split` in C++
Source/JavaScriptCore/builtins/BuiltinNames.h
Source/JavaScriptCore/builtins/StringPrototype.js
Source/JavaScriptCore/runtime/RegExpObjectInlines.h
JSC는 ECMAScript builtin 최적화에 두 가지 계층을 갖추고 있습니다. 하나는 self-hosted JS builtin으로, 바이트코드로 컴파일된 뒤 범용 JIT로 처리됩니다. 다른 하나는 DFG intrinsic으로, 타입에 특화된 node를 통해 C++ 직접 호출 코드를 생성합니다. Watchpoint는 일종의 무효화 훅입니다. 최적화된 코드는 특정 불변 조건(예: RegExp.prototype[@@split]이 변경되지 않았음)을 가정한 뒤, 그 조건이 깨질 때 발동되는 watchpoint를 등록하게 됩니다.
이 commit은 String.prototype.split을 self-hosted JS builtin에서 C++ host function으로 이전하고, 새로운 StringSplit DFG node를 도입했습니다. 이 node는 separator가 문자열인 경우 operationStringSplit으로, primordial RegExp separator인 경우 operationStringSplitRegExp로 분기합니다. Fast path를 보호하는 watchpoint set은 세 가지입니다. m_stringSymbolSplitWatchpointSet, m_regExpSpeciesWatchpointSet, 그리고 regExpPrimordialPropertiesWatchpointSet에 추가된 splitSymbol입니다. 인스턴스별 override는 새로 도입된 isSymbolSplitFastAndNonObservable 내부의 structure check에서 탐지됩니다. 이 구조는 기존의 isSymbolReplaceFastAndNonObservable을 그대로 따른 것입니다.
String#split(sep) call
|
[DFG StringSplit node]
|
watchpoints valid?
yes / no
|
sep type check
|- String --> operationStringSplit
|- RegExp? --> isSymbolSplitFastAndNonObservable
|- yes --> operationStringSplitRegExp (C++ fast)
|- no --> RegExp.prototype[@@split] (JS, observable)
Significance
새로운 fast-path gate에 추가된 세 개의 watchpoint와 structure check는 String#replace C++ 마이그레이션 당시의 bug surface와 동일한 구조입니다. 이 형태에서 watchpoint race와 structure check bypass 유형의 버그가 반복적으로 발생했습니다. Split 집약적인 workload에서 11~23%의 성능 향상이 이 commit의 주목받는 성과이기는 합니다. 그러나 watchpoint로 보호되는 fast path가 추가될 때마다, gate의 불변 조건이 완전히 보존되지 않으면 관찰 가능한 side effect가 누출되는 새로운 window가 열립니다.
Audit directions
- Watchpoint invalidation races.
m_stringSymbolSplitWatchpointSet과m_regExpSpeciesWatchpointSet은 신규 도입된 watchpoint입니다. Fast-path guard 통과 이후 C++ 연산이 완료되기 전에 어느 한쪽 watchpoint가 발동되거나, JIT 코드 패치와의 무효화 순서에 결함이 있다면, attacker는@@split이 교체된 regex를 재컴파일 없이 JIT에서 그대로 처리하게 만들 수 있습니다. 이는 기존@@replacewatchpoint race와 동일한 유형의 버그입니다. - Structure-ID recycling vs.
isSymbolSplitFastAndNonObservable. 인스턴스별@@splitoverride는 structure check로 탐지됩니다. Structure ID는 GC 이후 재사용됩니다. 타이밍을 정밀하게 조절한 allocation sequence로 structure ID 재사용을 유도하면, 변경된 RegExp가 structure check를 통과하는 것처럼 보이도록 만들 수 있습니다.replace상대 함수와 diff를 비교해 차이점을 점검할 필요가 있습니다. - Separator type dispatch boundary. DFG node는 type speculation을 기반으로 문자열 경로와 RegExp 경로 중 하나를 선택합니다. Separator 객체에
toString과Symbol.split이 모두 정의되어 있다면 어떻게 처리될까요. Separator가 primitive가 아닌 래핑된 String 객체인 경우는 어떨까요. C++ fast path는 spec의 명시적typeof분기를 따랐던 기존 JS builtin과 이러한 케이스를 다르게 처리할 가능성이 있습니다.