[JSC] TypedArray [[Set]] should check receiver before writing to typed array
CVE: CVE-2026-84635 · Safari 27 · Released September 14, 2026 Impact: Processing maliciously crafted web content may lead to an unexpected process termination Apple's description: A logic issue was addressed with improved state management. Credit: Souta Sugiyama
Low — 사양 준수를 맞추는 성격의 수정이며, 어긋난 동작도 JavaScript에서 관찰되는 semantics 범위 안에 머뭅니다. 다만 흥미로운 지점은 남아 있습니다. 쓰기가 엉뚱한 객체에 적용되었고, 명세가 도달 불가능하다고 규정한 지점에서 사용자 callback이 실행되었습니다. receiver 치환을 기반으로 페이지 내부에 격리 계층을 세운 구현이라면 정확히 이 지점에서 깨지게 됩니다.
JavaScript의 property store는 객체 하나가 아니라 둘을 다룹니다. 하나는 쓰기 hook이 실행되는 객체이고, 다른 하나는 최종적으로 property를 보유해야 하는 객체입니다. 명세는 후자를 Receiver라고 부르며, Reflect.set(target, key, value, receiver)의 네 번째 인자가 바로 그것입니다. Typed array는 [[Set]]을 override해서, 숫자 키가 일반 property slot이 아니라 ArrayBuffer의 바이트를 가리키도록 처리합니다. 이때 override 구현이 Receiver를 잊으면 자기 자신의 storage에 값을 기록하게 됩니다. 호출한 쪽은 쓰기가 다른 곳으로 redirect되었다고 믿고 있는 상태입니다.
관전 포인트: 대체 객체에 적용되어야 할 indexed write를 script가 typed array의 backing buffer 쪽으로 유도할 수 있습니다. 나아가 언어 차원에서 실행되지 않는다고 보장한 지점에서 valueOf와 prototype setter를 실행시킬 수 있습니다.
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h
Source/JavaScriptCore/runtime/JSObject.cpp
JSTests/stress/reflect-set.js
Patch Details
TypedArray의 [[Set]] internal method에 ES2021 receiver 규칙을 반영하는 production 변경이 세 군데 이루어졌습니다. 근거가 되는 명세 변경은 tc39/ecma262 PR #1556입니다.
먼저 JSGenericTypedArrayViewInlines.h에서는 JSGenericTypedArrayView<Adaptor>::put이 두 numeric 경로 모두에서 isThisValueAltered(slot, thisObject)를 확인하도록 바뀌었습니다. parseIndex가 받아들이는 key의 경우, receiver가 바뀌어 있으면 처리가 두 갈래로 나뉩니다. thisObject->isDetached() || !thisObject->inBounds(index.value())가 성립하면 함수는 곧바로 true를 반환합니다. 쓰기는 성공한 것으로 보고되지만 실제로는 어디에서도 아무 일이 발생하지 않습니다. 그렇지 않은 경우에는 putByIndex(thisObject, ...)를 호출하는 대신 ordinarySetSlow(globalObject, thisObject, propertyName, value, slot.thisValue(), slot.isStrictMode())로 처리를 넘깁니다. 그 결과 값은 buffer가 아니라 receiver에 기록됩니다. canonical-numeric이지만 index로 파싱되지 않는 key('-0', '1.1', 'NaN')에서는, receiver가 바뀌어 있을 때 toNativeFromValue<Adaptor>(globalObject, value) coercion 이후가 아니라 그 이전에 true를 반환합니다. 여기서는 결과 못지않게 순서가 중요합니다. toNativeFromValue는 operand가 객체이면 사용자가 정의한 valueOf를 호출하기 때문입니다.
JSObject.cpp에서는 generic한 순회 코드 두 곳이, prototype chain 위에 놓인 typed array가 자체 [[Set]]을 가진 exotic object라는 사실을 인식하게 되었습니다. ordinarySetWithOwnDescriptor에는 기존 ProxyObject 특수 처리 바로 뒤에 branch가 추가되었습니다. current != object && isTypedArrayType(current->type())가 성립하면 PutPropertySlot slot(receiver, shouldThrow)를 새로 구성하여 current->methodTable()->put(...)에 처리를 위임합니다. 이때 원래 receiver는 위임 과정에서도 그대로 유지됩니다. JSObject::attemptToInterceptPutByIndexOnHoleForPrototype에도 같은 성격의 branch가 들어갔습니다. 순회 중 typed array에 도달하면 JSArrayBufferView*로 jsCast합니다. 여기서 typedArray->isOutOfBounds() || i >= typedArray->length()이면 putResult = true로 설정하고 true를 반환하여 저장 자체를 삼킵니다. 그렇지 않으면 false를 반환하고, 호출자가 receiver 자신의 indexed storage에 값을 기록합니다. 어느 쪽이든 순회는 다음 prototype으로 넘어가지 않고 typed array에서 종료됩니다.
테스트 쪽 변경은 동작 차이를 정확하게 따라갑니다. JSTests/stress/reflect-set.js는 assertion이 뒤집혔습니다. Reflect.get(object, 0)은 이제 0을, receiver.hasOwnProperty(0)은 true를 반환합니다. 한편 test/built-ins/TypedArrayConstructors/internals/Set/* 항목 13개와 test/staging/sm/TypedArray/set-with-receiver.js가 JSTests/test262/expectations.yaml에서 제거되었습니다.
Background
[[Set]] 프로토콜과 receiver. ECMAScript object model에서 쓰기 연산은 O.[[Set]](P, V, Receiver) 형태입니다. O는 write hook이 실제로 실행되는 객체이고, Receiver는 그와 별개로 최종적으로 property를 갖게 될 객체입니다. Reflect.set(target, key, value, receiver)는 이 네 번째 인자를 스크립트에 그대로 노출합니다. 일반적인 대입문 obj[k] = v는 Receiver === obj 상태로 시작하는데, 알고리즘이 obj의 prototype chain을 거슬러 올라가는 동안에도 이 receiver는 바뀌지 않고 그대로 전달됩니다.
OrdinarySet과 위임 규칙. OrdinarySetWithOwnDescriptor는 기본 [[Set]]의 바탕이 되는 명세 알고리즘입니다. 해당 property가 O의 own property가 아니면 prototype으로 재귀합니다. 그러다 writable한 data property를 찾으면, Receiver 위에 own property를 생성하거나 갱신하는 방식으로 쓰기를 마무리합니다. property를 발견한 prototype 쪽에 기록하는 경우는 없습니다. chain을 따라 탐색하되 쓰기는 receiver에 한다는 이 비대칭이 곧 계약 전체입니다.
Integer-indexed exotic object. Typed array는 [[Set]], [[Get]], [[DefineOwnProperty]]와 그 형제 연산들을 override합니다. 그래서 numeric key는 일반 property slot이 아니라 backing ArrayBuffer의 element를 가리키게 됩니다. 어떤 numeric key가 유효한지는 명세의 두 predicate가 결정합니다. CanonicalNumericIndexString은 ToNumber/ToString을 왕복해도 그대로 남는 property key를 뜻합니다. '0', '-0', '1.1', 'NaN'이 모두 여기에 해당하지만, '-0'과 '1.1'은 실제 element를 가리킬 수 없습니다. IsValidIntegerIndex는 더 좁은 조건입니다. detach되지 않았고, resizable buffer라면 out of bounds도 아닌 buffer 위의 범위 내 정수 index를 말합니다. 이 코드에서 대응되는 JSC 쪽 함수는 isDetached(), inBounds(index), isOutOfBounds(), length()입니다.
JSC가 이 프로토콜을 실어 나르는 방식. PutPropertySlot은 property 저장 과정 전반에 걸쳐 전달되는 객체입니다. receiver는 slot.thisValue()로, strict 여부는 slot.isStrictMode()로 담고 있으며, inline cache 관련 정보도 함께 관리합니다. isThisValueAltered(slot, baseObject)는 slot의 receiver가 현재 put을 실행 중인 객체와 다를 때 true를 반환합니다. "이 [[Set]]은 다른 곳에서 전달된 것"인지를 판별하는 엔진의 표준 검사에 해당합니다. ordinarySetSlow는 명세의 OrdinarySet을 fast path가 아닌 형태로 구현한 것이며, 내부적으로 ordinarySetWithOwnDescriptor를 구동합니다. toNativeFromValue<Adaptor>는 JSValue를 typed array의 element 타입으로 변환하는데, operand가 객체라면 사용자가 정의한 valueOf/toString을 호출한다는 의미가 됩니다.
Hole 가로채기 순회. attemptToInterceptPutByIndexOnHoleForPrototype은 indexed store가 receiver 자신의 indexed storage에서 hole에 떨어졌을 때 JSC가 수행하는 prototype chain 순회입니다. 역할은 그 저장을 가로채려는 prototype을 찾는 것이며, setter나 ProxyObject, custom index handler 등이 대상입니다. false를 반환하면 가로챈 쪽이 없다는 의미이고, 이때 호출자는 값을 receiver 자신의 indexed storage에 기록합니다.
같은 파일의 선행 작업. Bug 217916은 typed array의 index가 아닌 property 이름에 같은 receiver 규칙을 이미 적용했습니다. commit message는 이번 패치를 그 작업의 indexed key 후속편으로 명시하고 있습니다.
Analysis
Root cause는 특수화된 write hook이 property key만을 유일하게 의미 있는 입력으로 취급했다는 점입니다. 그 쓰기가 실제로 누구를 위한 것인지는 전혀 확인하지 않았습니다.
Reflect.set(ta, 0, 42, receiver) ta = new Uint8Array(64)
Before: After:
ta.[[Set]](0, 42, receiver) ta.[[Set]](0, 42, receiver)
parseIndex("0") -> 0 parseIndex("0") -> 0
└─► putByIndex(thisObject, 0, 42) isThisValueAltered? yes
│ ├─ !inBounds/detached ─► return true
▼ └─► ordinarySetSlow(..., receiver)
ta.buffer[0] = 42 │
receiver: untouched ▼
receiver[0] = 42
ta.buffer[0]: untouched
패치 이전에는 JSGenericTypedArrayView<Adaptor>::put이 numeric property 이름을 보면 무조건 자기 자신을 대상으로 동작하라는 지시로 해석했습니다. index로 파싱되면 곧바로 putByIndex(thisObject, ...)로 향했고, canonical-numeric이지만 유효하지 않은 이름이면 toNativeFromValue<Adaptor>를 조건 없이 실행했습니다. 정작 slot의 receiver는 한 번도 참조되지 않았습니다. 여기서 receiver란 Reflect.set의 네 번째 인자이거나, typed array가 prototype chain에 놓여 있을 때 원래 대입을 받은 객체를 말합니다. 이렇게 해서 빠진 invariant는 일반 [[Set]] 경로가 다른 모든 곳에서 지키는 것이자 Background에서 계약 전체라고 부른 바로 그 규칙입니다. prototype에서 발견된 data property 쓰기는 보유자가 아니라 receiver 위에서 완료되어야 한다는 규칙입니다.
여기에 더해, JSObject.cpp의 generic 순회 두 곳은 prototype chain 중간의 typed array가 자체 [[Set]]을 가진 exotic object라는 개념 자체를 갖고 있지 않았습니다. ordinarySetWithOwnDescriptor는 ProxyObject만 특수 처리했을 뿐입니다. attemptToInterceptPutByIndexOnHoleForPrototype 역시 typed array를 그냥 지나쳐 다음 prototype으로 계속 나아갔습니다. 여기서 구체적인 명세 이탈이 세 가지 파생되는데, 각각은 이번 패치가 삭제한 test expectation으로 고정되어 있습니다.
첫 번째는 직접 전달되는 경우이고, 기존 reflect-set.js가 이를 그대로 기록하고 있습니다. Reflect.set(ta, 0, 42, {})는 42를 ta의 backing store에 기록했고, receiver에는 아무것도 생성하지 않았습니다. 제거된 assertion인 Reflect.get(object, 0) === 42와 receiver.hasOwnProperty(0) === false가 바로 그 동작을 정상으로 단정하고 있던 부분이며, 이번 패치는 둘 다 뒤집었습니다. 위 도식의 왼쪽 열이 그 옛 code path입니다. receiver를 그대로 버린 채 putByIndex(thisObject, ...)를 호출하는 흐름입니다.
두 번째는 prototype chain을 타는 경우인데, 스크립트가 실제로 실행되는 쪽이 바로 여기입니다. 어떤 객체의 prototype chain에 typed array가 놓여 있는 상황을 가정합니다. receiver에 대한 indexed 대입은 attemptToInterceptPutByIndexOnHoleForPrototype에 도달했고, 이 순회는 typed array를 지나쳐 chain 위쪽에 정의된 setter까지 내려앉을 수 있었습니다. 제거된 expectation 문자열이 이를 직설적으로 드러냅니다:
key-is-valid-index-prototype-chain-set.js:
'Test262Error: 0 setter should be unreachable! (Testing with Float64Array ...)'
key-is-canonical-invalid-index-prototype-chain-set.js:
'Test262Error: 1 setter should be unreachable! (Testing with Float64Array ...)'
setter가 도달 불가능하다고 단정하는 test262 케이스가, 정작 그 setter가 실행되었기 때문에 실패했습니다. 패치 이전 JSC가 명세상 금지된 지점에서 사용자 JavaScript를 실행했다는 사실을 직접 드러내는 정황입니다. 새로 추가된 branch는 순회를 typed array에서 멈춥니다. out-of-bounds이거나 detach된 상태면 putResult = true로 저장을 삼키고, 범위 안이면 false를 반환하여 호출자가 receiver 위에서 쓰기를 마무리합니다. 어느 경우에도 순회가 위쪽으로 이어지지 않으므로, 상위의 setter에는 도달하지 않습니다.
세 번째 이탈은 목적지의 문제가 아니라 순서의 문제입니다. 외부 receiver와 유효하지 않은 index가 결합된 경우, receiver 검사보다 toNativeFromValue coercion이 먼저 실행되었습니다. 명세가 즉시 true를 반환하라고 요구하는 지점에서 valueOf가 호출된 셈입니다. 삭제된 expectation 세 개(key-is-in-bounds-receiver-is-not-typed-array.js, key-is-out-of-bounds-receiver-is-not-object.js, key-is-out-of-bounds-receiver-is-not-typed-array.js)는 모두 같은 메시지를 담고 있습니다. 'valueOf is not called Expected SameValue(«1», «0») to be true'입니다. 패치는 isThisValueAltered 검사를 coercion보다 위로 올렸습니다. diff에서 해당 branch가 toNativeFromValue 이후가 아니라 이전에 true를 반환하는 이유입니다.
이 버그가 attacker에게 무엇을 주고 무엇을 주지 않는지는 정확하게 짚어둘 필요가 있습니다. 여기서 약해진 경계는 엔진 내부의 JavaScript 언어 semantics 경계입니다. process 경계나 same-origin 경계가 아닙니다. memory safety 위반도 관찰되지 않습니다. putByIndex는 coercion 이후에 isDetached()와 범위를 다시 검사하는 typed array의 setIndex 관용 경로로 수렴합니다. 그래서 패치 이전 경로에서도 out-of-bounds store는 발생하지 않았습니다. 스크립트가 얻은 것은 세 가지입니다. 먼저, receiver가 흡수해야 할 indexed 쓰기를 typed array의 element storage로 돌릴 수 있었습니다. 대상에는 다른 agent에서 관찰되는 SharedArrayBuffer 위의 view도 포함됩니다. 다음으로, receiver 위에 생성되어야 할 own property의 생성을 억제할 수 있었습니다. 마지막으로, 명세가 도달 불가능하다고 선언한 구간에서 valueOf와 prototype setter가 실행되도록 강제할 수 있었습니다. receiver 치환을 기반으로 하는 페이지 내 격리 계층도 함께 고려할 수 있습니다. membrane, realm shim, shadow target을 두고 Reflect.set으로 쓰기를 전달하는 virtualization proxy 등이 여기에 해당합니다. 전달되는 indexed 쓰기를 조종할 수 있는 attacker라면 shadow가 아니라 실제 typed array에 도달할 수 있었습니다. 모델 수준에서 보면 언어 계층에서의 격리된 쓰기 탈출에 해당합니다.
패치 자체는 추적해 둘 만한 새로운 재진입 지점을 하나 도입했습니다. ordinarySetWithOwnDescriptor의 새 branch는 prototype chain 루프 한가운데에서 current->methodTable()->put(...)을 통해 바깥으로 호출을 내보냅니다. 이때 새로 구성된 PutPropertySlot slot(receiver, shouldThrow)이 호출자의 slot을 가립니다. 현재로서는 isTypedArrayType(current->type()) guard가 피호출자 집합을 한정합니다. 그래서 도달 가능한 대상은 JSGenericTypedArrayView::put 하나뿐이고, receiver가 바뀐 경우 이 함수는 typed array에 대해 ordinarySetSlow로 다시 진입합니다. 이 지점에서는 두 가지 invariant가 강제되지 않고 가정으로만 남아 있습니다. 하나는 prototype chain에 cycle이 없다는 조건으로, 이 부분은 다른 곳의 setPrototypeOf가 보장합니다. 다른 하나는 chain이 충분히 짧다는 가정입니다. typed array 하나당 ordinarySetWithOwnDescriptor → put → ordinarySetSlow frame 묶음이 한 번씩 중첩되어도 native stack 예산 안에 머물러야 합니다. chain 길이는 Object.setPrototypeOf를 반복 호출하는 스크립트가 제어합니다. 두 번째 가정이 성립하지 않는 경우 드러나는 문제는 memory corruption이 아니라 native stack 고갈 쪽입니다. 그마저도 해당 순환 경로에 vm.isSafeToRecurseSoft() guard가 없을 때에 한정되며, 아래 audit direction의 마지막 항목이 이 부분을 다룹니다. 가려진 slot은 바깥 slot의 inline cache context를 의도적으로 버리는데, 객체를 넘나드는 위임에서는 이쪽이 보수적인 선택입니다.
typed array의 write hook이 모든 numeric key에 대해 자기 자신을 대상으로 동작하면서, 전달된 indexed 쓰기가 receiver 대신 buffer를 변경하고 명세가 이미 배제한 사용자 callback까지 실행한 패턴.
Insight
JSC의 generic prototype chain 순회는 순회를 중단시켜야 하는 exotic object 목록을 명시적으로 관리합니다. 이번 패치 이전까지 그 목록에 들어 있던 항목은 ProxyObjectType 하나뿐이었습니다. ordinarySetWithOwnDescriptor와 attemptToInterceptPutByIndexOnHoleForPrototype 모두 proxy만 처리하고, 나머지는 전부 일반 경로로 흘려보냈습니다. 일반적이지 않은 [[Set]]을 가진 객체는 두 루프 모두에 사람이 직접 추가해야 합니다. 그래서 이 문제는 일회성 버그라기보다 목록 관리상의 위험에 가깝습니다. 새로운 exotic type이 등장할 때마다 switch 유사 분기 두 곳을 잊지 않고 확장하는 사람의 기억에 순회의 정확성이 걸려 있는 셈입니다. 두 번째로 눈여겨볼 지점은 명세 정합 작업 자체의 형태입니다. tc39/ecma262 PR #1556이 바꾼 알고리즘은 하나였지만, 엔진들은 이를 key 분류별로 나누어 반영해 왔습니다. WebKit의 경우 Bug 217916이 index가 아닌 이름을 먼저 처리했고, index는 몇 년 뒤인 이번 commit에서야 다루어졌습니다. conformance 수정이 key 타입별로 쪼개져 있다면, 아직 반영되지 않은 나머지 절반은 다른 엔진에서도 명세 이탈을 찾기 좋은 지점입니다.
Audit directions
-
프로토콜이 전달한 receiver 대신
this에 기록하는 write hook. 여기서 지켜져야 할 계약은, 특수화된put도 generic 구현과 동일한 위임 규칙을 따라야 한다는 점입니다. Narrow —Source/JavaScriptCore/runtime에서::put(JSCell* cell, JSGlobalObject*와::putByIndex(override를 검색합니다. 각각이slot.thisValue()나isThisValueAltered(slot, thisObject)를 확인하는지 하나씩 점검해야 합니다. 출발점으로는JSArrayBufferView서브클래스,JSModuleNamespaceObject,StringObject,GenericArguments/ClonedArguments, 그리고JSGlobalObject의 static table 경로가 적절합니다. 판별 단서는 다음과 같습니다. override가cell을thisObject로jsCast한 뒤 모든 경로에서thisObject를 통해 값을 기록하고, 함수 본문에thisValue라는 단어가 전혀 등장하지 않는 경우입니다. Wider — 같은 유형은defineOwnProperty와 custom setter hook에서도 반복됩니다.CustomGetterSetter, 그리고Source/WebCore/bindings/js에서 생성되는 WebCore의putByIndex/named property setter가 여기 해당하는데, 이 경로에서는 receiver가 아니라 interface object가 기록 대상으로 쓰입니다. 단서는, 서로 다른 receiver로 binding에 진입했는데도 setter 본문이castedThis를 참조하는 경우입니다. Widest — 이 invariant는 receiver를 전달하는 write protocol을 가진 런타임 전반으로 옮겨집니다. Python의 data descriptor와__setattr__, Ruby의method_missing, JSProxy의settrap, 파생 타입에서 override된 .NET indexer가 모두 그렇습니다. 어떤 코드베이스를 보든 다음 질문을 가져가면 됩니다. write hook이 override되었을 때, 그 override는 여전히 protocol이 지목한 객체에 기록하는가, 아니면 자기 자신에 기록하는가? -
exotic object 처리를 임시 allow-list로 해결하는 prototype chain 순회. 새로운 exotic type이 등장할 때마다 누군가 목록을 직접 늘려야 correctness가 유지됩니다. 항목이 하나라도 빠지면 별다른 신호 없이 일반 semantics로 떨어지게 되는데, 바로 이 점이 위험합니다. Narrow —
Source/JavaScriptCore/runtime/JSObject.cpp에서for (JSObject* current = ...)형태로 순회하면서ProxyObjectType만 따로 처리하는 루프를 전부 점검합니다. 나머지 exotic type에서는 어떤 동작이 이루어지는지 확인해야 합니다.attemptToInterceptPutByIndexOnHoleForPrototype와ordinarySetWithOwnDescriptor는 이번에 typed array까지 포함되었으므로,JSObject::putInlineSlow,putDirectIndex/putByIndexBeyondVectorLength,JSArray::putByIndex, 그리고 getter 쪽getPropertySlot순회부터 살펴보는 편이 좋습니다. 판별 단서는, chain을 순회하는 루프의 type 검사가current->type() == ProxyObjectType하나뿐이고isTypedArrayType이나JSModuleNamespaceObjectType,DataViewType에 대한 분기는 없는 경우입니다. Wider — 같은 형태는 lookup이 delegation chain을 따라가다가 custom semantics를 가진 구성원에서 먼저 멈춰야 하는 곳이면 어디서든 반복됩니다. JSC의Structure기반 lookup cache, WebCore의HTMLCollection/named getter chain,JSScope의 scope chain 해석 루프가 여기 해당합니다. 이 층위의 단서는, 한 가지 구성원 타입만 특별 처리하고 나머지는 전부 동일하다고 가정하는 해석 루프입니다. Widest — 재사용 가능한 invariant는 이렇게 정리됩니다. delegation을 따라가는 순회는 각 link에게 해당 연산을 자신이 담당하는지 직접 물어야 하며, 전부 동일하다고 가정한 뒤 알려진 예외 하나만 특수 처리해서는 안 됩니다. 이 원칙은 Python MRO 순회, 다른 JS 엔진의 prototype 순회, DI container의 resolution chain에도 그대로 적용됩니다. 가져갈 단서는, 타입 스스로 답하는 capability predicate가 아니라 구체 타입을 하나씩 나열해 만든 early exit 목록입니다. -
동작 여부를 결정하기도 전에 실행되는, 사용자에게 관찰되는 coercion. 지켜야 할 invariant는, 명세가 guard 뒤에 배치한 변환을 그 앞으로 끌어올려서는 안 된다는 것입니다. 끌어올려진 변환은 conformance bug인 동시에 user JS로 향하는 re-entrancy window가 되기 때문입니다. Narrow —
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h와JSArrayBufferViewInlines.h, 그리고 typed array prototype builtin에서toNativeFromValue,toBigInt64,toIndex,toNumber호출을 검색합니다. 이때 명세가 그보다 앞에 두는 guard가 모두 실행되었는지 확인해야 합니다. receiver identity, detachment, bounds, writability가 여기에 해당합니다. 판별 단서는 함수 진입부터 변환 지점 사이에isDetached()/inBounds()/isThisValueAltered가 하나도 끼어들지 않은 coercion 호출입니다. Wider — 같은 유형에는 early return 분기보다 먼저 사용자에게 보이는 변환을 실행하는[[Set]]/[[DefineOwnProperty]]/[[Delete]]구현이 모두 포함됩니다.ProxyObject의 trap 인자 준비 과정, same-origin이나 readonly 검사 전에 수행되는 WebCore binding 변환도 마찬가지입니다. 단서는, 명세가 변환을 early return 아래에 배치했는데도 실제 코드에서는return true/return false가 변환보다 아래에 놓여 있는 경우입니다. Widest — 변환 과정에서 attacker 코드가 실행될 수 있는 시스템이라면, side effect의 순서 자체가 명세의 security surface에 해당합니다. C extension fast path에서 호출되는 Python의__index__/__len__, collection 내부에서 호출되는 Java의equals/compareTo, 검증보다 먼저 사용자toJSON을 호출하는 serializer가 모두 그렇습니다. 가져갈 단서는, 연산을 중단시킬 수 있는 guard보다 위에 놓인, callback을 실행할 수 있는 변환입니다. -
새로 추가된 typed array 위임 경로의 stack guard 적용 범위.
ordinarySetWithOwnDescriptor분기가 prototype chain에 놓인 typed array 하나당 한 번씩 재귀하는지, 그리고 그 경로에 stack guard가 걸려 있는지 확인이 필요합니다. Narrow —ordinarySetWithOwnDescriptor→current->methodTable()->put→JSGenericTypedArrayView::put→ordinarySetSlow→ordinarySetWithOwnDescriptor경로를 따라가면서, 이 cycle 안의 frame 중vm.isSafeToRecurseSoft()를 호출하는 곳이 있는지 확인합니다. chain의 깊이는 typed array에Object.setPrototypeOf를 반복 적용하는 방식으로 script가 제어합니다. 판별 단서는, 재귀 깊이가 JS에서 보이는 자료구조의 길이에 따라 결정되는 native 재귀이면서 cycle 어디에도isSafeToRecurse/StackOverflowError검사가 없는 경우입니다. Wider — 사용자가 길이를 제어하는 chain을 통해 도달하는 다른 재귀 object model helper에도 같은 질문이 적용됩니다.getPrototypeChain으로 구동되는 루프,ProxyObjecttrap의 중첩,JSObject::defineOwnProperty의 위임이 여기 해당합니다. generic walk로 다시 진입할 수 있는 method table hook을 전부 나열한 뒤, 각각에 stack guard가 있는지 점검하면 됩니다. Ceiling clause — 이 항목은 JSC에서 멈춥니다. JSC의 method table 재진입 규율과isSafeToRecurseSoft관례에 한정된 질문이기 때문입니다. 앞의 세 패턴과 달리 엔진을 넘나드는 invariant로는 일반화되지 않습니다.