← All issues

[8] GetByStatus walks prototype chain for direct/private property opcodes

Severity: Medium | Component: JSC bytecode IC status + DFG | b07dca9

DFG가 prototype-walk 가정 하에 GetByIdDirect/GetPrivateNameById를 특수화할 수 있었던 JIT IC status helper를 수정한 패치입니다. non-own slot에서 controlled property fetch로 이어지는지 여부는 downstream lowering에 달려 있으며, diff만으로는 확인되지 않습니다. 이러한 점에서 Medium으로 평가합니다.

GetByStatus를 계산하는 과정에서는, prototype walk를 수행하기 전에 먼저 direct property access인지 확인해야 합니다. direct access는 prototype을 참조해서는 안 되기 때문입니다.

Source/JavaScriptCore/bytecode/GetByStatus.cpp

-GetByStatus GetByStatus::computeFor(JSGlobalObject* globalObject, const StructureSet& set, CacheableIdentifier identifier)
+GetByStatus GetByStatus::computeFor(JSGlobalObject* globalObject, const StructureSet& set, CacheableIdentifier identifier, GetByStatus::LookupMode mode)
{
- // 이 코드는 GetByIdDirect에서도 사용됩니다. 이 함수는 direct property만 조회하므로,
- // prototype chain을 지원하려면 GetById와 GetByIdDirect를 별도로 처리해야 합니다.
...
- if (auto result = attempToFold())
- return result.value();
+ // prototype walk는 일반 access인 경우에만 수행해야 합니다.
+ // direct access에서는 prototype을 참조하지 않습니다.
+ if (mode == GetByStatus::LookupMode::Normal) {
+ if (auto result = attempToFold())
+ return result.value();
+ }

Source/JavaScriptCore/dfg/DFGNode.h

+ GetByStatus::LookupMode propertyLookupMode()
+ {
+ switch (op()) {
+ case GetByIdDirect:
+ case GetByIdDirectFlush:
+ case GetPrivateNameById:
+ return GetByStatus::LookupMode::Direct;
+ case GetById:
+ case GetByIdFlush:
+ case GetByIdMegamorphic:
+ return GetByStatus::LookupMode::Normal;
+ default:
+ RELEASE_ASSERT_NOT_REACHED();
+ }
+ }

GetByStatus에 새로운 LookupMode enum(Normal/Direct)이 추가되었고, computeFor의 파라미터로도 포함되었습니다. computeFor 내부의 prototype walk(attempToFold() 블록)는 이제 mode == LookupMode::Normal인 경우에만 실행됩니다. Node::propertyLookupMode()가 추가되어 DFG opcode를 적절한 mode에 매핑합니다. DFGAbstractInterpreterInlines.h::executeEffectsDFGConstantFoldingPhase::foldConstants는 node로부터 mode를 결정하도록 수정되었습니다. DFGByteCodeParserop_get_from_scopeNormal로 고정됩니다. global property lookup은 global object의 prototype chain을 순회해야 하기 때문입니다. 함수가 direct property만 조회한다고 기술한 낡은 주석은 제거되었습니다.

JIT IC status 계산 과정의 speculation-soundness 위반: prototype 순회를 금지하는 opcode에 공유 helper가 prototype-chain 의미론을 적용하는 패턴.

GetByStatusGetById 계열 bytecode가 런타임에서 수행한 동작의 요약입니다. 관찰된 structure, offset, prototype walk 정보를 담아, DFG가 load를 inline할지, 결과를 constant-fold할지, 아니면 generic IC로 전환할지를 결정하는 근거가 됩니다. GetById는 ECMAScript [[Get]]을 완전히 수행하며 prototype chain을 순회합니다. 반면 GetByIdDirect는 own property에만 접근하며, 해당 property가 own property가 아닌 경우 반드시 undefined를 반환해야 합니다. GetPrivateNameById는 private class field를 읽는 opcode로, receiver의 brand를 키로 사용하며 스펙상 own property만 접근하도록 제한됩니다. computeFor 내부의 attempToFold는 prototype chain을 순회해 property를 탐색하고, 발견 시 Simple variant로 fold하는 역할을 맡습니다. DFG의 AbstractInterpreterConstantFoldingPhase는 반환된 GetByStatus를 바탕으로 IR을 특수화합니다.

GetByStatus::computeFor는 항상 attempToFold()를 호출했으며, 이 함수는 prototype chain을 순회합니다. 문제는 DFG pass에서 모든 GetBy* node에 대해 예외 없이 호출되었다는 점입니다. GetByIdDirect, GetByIdDirectFlush, GetPrivateNameById처럼 런타임 의미론상 own property만 접근해야 하는 opcode도 마찬가지였습니다. 낡은 주석은 이 함수가 direct property만 조회한다고 기술하고 있었는데, 작성 당시에는 정확한 설명이었습니다. 하지만 attempToFold가 나중에 추가되면서 사실과 달라졌습니다.

결과적으로, DFG는 direct-property load를 prototype에서 resolve된 것처럼 fold하거나 특수화할 수 있었습니다. 올바른 own-property-or-undefined 의미론 대신, prototype 객체의 slot을 가리키는 offset/structure를 담은 GetByStatus::Simple variant가 생성됩니다. 이후 JIT가 해당 variant의 structure/offset을 기반으로 값을 실체화하는 코드를 생성하면, 두 가지 상황이 발생할 가능성이 있습니다. GetByIdDirect의 경우, 런타임 의미론상 undefined를 반환해야 할 자리에서 prototype slot의 값이 대신 반환됩니다. GetPrivateNameById의 경우, private field의 brand/own-slot 의미론이 우회될 가능성이 있습니다. downstream lowering에서 충분한 structure check를 삽입하는지 여부에 따라 결과가 달라집니다. type-tracking을 혼란시키는 의미론적 일탈부터 non-own slot에서의 controlled property fetch까지, 영향 범위는 다양합니다.

이 취약점은 own property 전용 연산에 대한 JIT speculation의 soundness를 약화시켰습니다. 위반된 불변 조건은 GetByIdDirect/GetByIdDirectFlush/GetPrivateNameById가 prototype-walk 가정 하에 최적화되어서는 안 된다는 것입니다. 패치 이전 코드는 DFG가 prototype 순회를 통해 property를 탐색했다고 약속하는 Simple GetByStatus를 생성할 수 있도록 허용했습니다. 낡은 주석은 JIT speculation 분야에서 잠재적 버그가 반복해서 발생하는 원인 중 하나입니다. 기존 주석은 작성 당시 함수의 동작을 정확하게 기술하고 있었지만, attempToFold가 나중에 추가될 때 주석도, 원래 계약에 의존하던 호출 지점도 함께 업데이트되지 않았습니다. propertyLookupMode()의 default arm에 RELEASE_ASSERT_NOT_REACHED()를 포함한 명시적 LookupMode를 도입한 것은 올바른 방어 방법입니다. 새로운 GetBy* opcode는 반드시 호출 경계에서 lookup 의미론을 명시적으로 선언해야 합니다.