← All issues

Sampled mprotect write-guard for DOMWrapperWorld::m_wrappers corruption

d2af128beb0b21

DOMWrapperWorld::m_wrappers is the central hash map in the JS/DOM binding layer mapping DOM objects to their live Weak<JSObject> wrapper handles. Every time JS touches a DOM node, cacheWrapper inserts or updates an entry; the GC sweeps stale entries by calling JSC::WeakImpl::clear() during rehash, which dereferences a garbage address if the table is corrupt. This commit adds sampled instrumentation to catch the stray write.

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)));
+}

A new WrapperMapTableMalloc allocator places the hash table's backing on page-aligned mmap memory kept PROT_READ at rest; a WrapperMutationScope RAII guard briefly unlocks it to PROT_READ|WRITE during cacheWrapper/uncacheWrapper/clearWrappers. Enabled in roughly 1/64 processes at startup via weakRandomNumber, so a stray write outside a mutation scope faults immediately at the write site rather than corrupting memory that crashes much later.

This reveals an active, unresolved production heap corruption in WebKit's JS/DOM binding layer — the same garbage-pointer fingerprint appears in topologically unrelated hash tables (m_wrappers, CodeBlockSet), consistent with a freed object reused as a hash-table backing and then written by a dangling pointer.

During hash-table growth mid-mutation, the old backing is freed and a new one allocated; if m_wrappersTableBase is updated to the new backing before the old is released, the scope destructor's mprotect hits the new address while old pages are still live — verify the interleaving of noteTableBacking, forgetTableBacking, and mprotect during rehash. m_wrappersTableWritableDepth is a nested-scope refcount: an exception or a re-entrant cacheWrapper during a GC triggered inside a mutation could leave it positive while the page is read-only, or zero while writable, breaking the guarantee. Most importantly, the underlying corruption is unresolved — a stray write placing an attacker-controlled value into m_wrappers as a WeakImpl*, later dereferenced during GC sweep, is a potential controlled-read or type-confusion primitive worth investigating as an exploitable UAF.