← All issues

Sampled mprotect write-guard for DOMWrapperWorld::m_wrappers corruption

d2af128beb0b21

DOMWrapperWorld::m_wrappers는 JS/DOM binding layer의 핵심 hash map입니다. DOM 객체와 살아있는 Weak<JSObject> wrapper handle을 연결합니다. JS가 DOM 노드에 접근할 때마다 cacheWrapper가 항목을 삽입하거나 갱신합니다. GC는 rehash 과정에서 JSC::WeakImpl::clear()를 호출해 stale 항목을 정리하는데, 테이블이 손상된 상태라면 이 시점에 garbage address를 역참조하게 됩니다. 이번 commit은 이런 stray write를 포착하기 위한 sampled instrumentation을 추가했습니다.

Source/WebCore/bindings/js/DOMWrapperWorld.cpp

+void* WrapperMapTableMalloc::allocate(size_t size)
+{
+ size_t pageSize = WTF::pageSize();
+ size_t rounded = (requested + pageSize - 1) & ~(pageSize - 1);
+ void* base = mmap(nullptr, rounded, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ RELEASE_ASSERT(base != MAP_FAILED);
+ if (auto* world = WrapperMutationScope::currentlyMutatedWorld())
+ world->noteTableBacking(base, rounded);
+ return base;
+}
+
+void DOMWrapperWorld::setWrappersTableWritable(bool writable)
+{
+ if (writable) {
+ if (m_wrappersTableWritableDepth++)
+ return;
+ } else {
+ ASSERT(m_wrappersTableWritableDepth);
+ if (--m_wrappersTableWritableDepth)
+ return;
+ }
+ if (m_wrappersTableBase)
+ RELEASE_ASSERT(!mprotect(m_wrappersTableBase, m_wrappersTableSize,
+ PROT_READ | (writable ? PROT_WRITE : 0)));
+}

새로 추가된 WrapperMapTableMalloc allocator는 hash table의 backing을 page-aligned mmap 메모리에 배치하고, 평소에는 PROT_READ 상태로 유지합니다. cacheWrapper / uncacheWrapper / clearWrappers 호출 중에는 WrapperMutationScope RAII guard가 해당 페이지를 일시적으로 PROT_READ|WRITE로 전환합니다. 이 기능은 시작 시 weakRandomNumber를 통해 약 1/64 프로세스에서 활성화됩니다. 덕분에 mutation scope 밖에서 stray write가 발생하면, 나중에 메모리 손상으로 crash가 이어지는 대신 write 발생 지점에서 즉시 fault가 발생합니다.

WebKit JS/DOM binding layer에서 현재 진행 중인 미해결 production heap corruption이 존재함을 드러냅니다. 동일한 garbage-pointer 패턴이 위상적으로 무관한 hash table(m_wrappers, CodeBlockSet) 양쪽에서 관찰됩니다. 이는 해제된 객체가 hash table backing으로 재사용된 후, dangling pointer에 의해 기록되는 패턴과 일치합니다.

Mutation 도중 hash table이 확장되면 기존 backing이 해제되고 새 backing이 할당됩니다. 만약 기존 backing이 해제되기 전에 m_wrappersTableBase가 새 backing을 가리키도록 갱신된다면, scope destructor에서 호출하는 mprotect는 새 주소를 대상으로 합니다. 이 시점에 기존 페이지는 여전히 살아있는 상태입니다. Rehash 과정에서 noteTableBacking, forgetTableBacking, mprotect의 실행 순서를 점검해야 합니다.

m_wrappersTableWritableDepth는 중첩 scope를 위한 reference count입니다. Mutation 내부에서 GC가 트리거될 때 exception이 발생하거나 cacheWrapper가 재진입되는 경우를 가정할 수 있습니다. 이때 페이지가 read-only 상태인데 depth 값이 양수로 남거나, 반대로 writable 상태인데 0이 되는 불일치가 발생할 수 있습니다. 어느 쪽이든 write-guard 보장이 깨집니다.

가장 중요한 점은 근본적인 corruption이 아직 해결되지 않았다는 사실입니다. 공격자가 제어하는 값이 WeakImpl*로서 m_wrappers에 stray write로 삽입되는 시나리오를 가정할 수 있습니다. 이후 GC sweep 중에 해당 값이 역참조되면, controlled-read 또는 type-confusion primitive로 이어질 가능성이 있습니다. exploit 가능한 UAF로서 추가 조사가 필요한 지점입니다.