← WebKit Silent-Fix Report — 2026-W22

08911bd034  Concurrent HashMap access leads to MTE crashes

severity medium class Race confidence 0.70 WebCore PlatformScreen exploitable-grade
Mike Wyrzykowski Thu May 28 19:34:45 2026 -0700 full: 08911bd034c30613cc2a3b07c587138a7cf281ac bug report ↗ view on GitHub ↗
Primitive: concurrent HashMap access to screen data
Triage note: Serializes screen-data map access that was hit concurrently (MTE crashes), a real data-race/memory-safety fix.
Contents

The bug at a glance

The definitive memory-safety fix for the same concurrent-HashMap-access class as cf20d123b3: the free functions getScreenProperties()/screenData() exposed a mutable process-global ScreenProperties (and its screenDataMap HashMap) that could be read off the main thread while the main thread mutated it, causing MTE crashes. This commit removes those functions entirely and replaces them with a ThreadSafeRefCounted PlatformScreen singleton with copy-on-write, resettable semantics so a worker holding a ref keeps a stable snapshot while the main thread swaps in a new instance. Broad, cross-platform memory-safety hardening reachable from web content (WebGPU, media codecs, PDF, model), but the corrupted data is display metadata and the race needs specific timing, so medium.

Instead of guarding the global with a lock on every access (which still hands out a reference to a mutable shared map), the singleton is made immutable-per-instance and swapped atomically under a lock: readers take a Ref<const PlatformScreen> snapshot; writers, via updateSingletonProperties, mutate in place only if hasOneRef() (no reader holds it) and otherwise create a brand-new PlatformScreen. So a worker iterating an old instance’s screenDataMap is walking data nobody will mutate – the map is only ever replaced, never mutated underneath a live reader.

Root cause

Pre-patch, PlatformScreen exposed free functions over a single NeverDestroyed<ScreenProperties>: getScreenProperties() returned a const& to it, screenData(displayID) indexed its screenDataMap, primaryScreenDisplayID() read its primaryDisplayID, and setScreenProperties() overwrote it. These were discovered (per the message, in issue #4632) to be callable off the main thread, so a worker iterating screenDataMap.values() concurrently with a main-thread setScreenProperties()/mutation raced on a non-thread-safe WTF::HashMap, producing MTE tag-mismatch crashes.

This commit removes screenProperties()/getScreenProperties()/primaryScreenDisplayID()/setScreenProperties()/screenData()/screenContentsFormatsForTesting()/setScreenContentsFormatsForTesting() as free functions and introduces class PlatformScreen : public ThreadSafeRefCounted<PlatformScreen>. It holds an immutable-in-practice ScreenProperties m_properties and offers instance methods screenData(), primaryScreenDisplayID(), screenProperties(), screenDatas(), and (HDR) screenContentsFormatsForTesting(), each LIFETIME_BOUND to the instance.

Access is a snapshot: static singleton() takes a Locker on platformScreenLock() and returns instance().get() as Ref<const PlatformScreen>. Because the returned object is const and ref-counted, a caller (e.g. a worker) safely iterates its screenDataMap for as long as it holds the Ref; nothing mutates that instance.

Updates are copy-on-write and resettable: updateSingletonProperties(ScreenProperties&&) locks platformScreenLock(), and if the current instance hasOneRef() (only the singleton slot references it, i.e. no reader holds a snapshot) it updates m_properties in place; otherwise it replaces the singleton slot with a freshly PlatformScreen::create()’d instance. Thus, if a worker holds a ref, the update forks: two (or K) PlatformScreen instances live until the readers drop their refs, and no reader ever sees its map mutated underneath it. instance() returns a Ref<PlatformScreen>& guarded WTF_REQUIRES_LOCK(platformScreenLock()).

All producers (WebProcess::setScreenProperties, ScreenManagerGtk/WPE::collectScreenProperties, WebProcess parameters) now call PlatformScreen::updateSingletonProperties(WTF::move(properties)) with move semantics, and setScreenProperties takes ScreenProperties&&. All consumers across PlatformScreenMac/IOS/Gtk/WPE, VP9UtilitiesCocoa, GStreamerRegistryScanner, GPUCanvasContextCocoa, UnifiedPDFPlugin, WebModelPlayer, HTMLMediaElement, HTMLModelElement, Internals, etc. are converted to Ref screen = PlatformScreen::singleton(); then screen->screenData(...)/screen->primaryScreenDisplayID(). Type declarations (PlatformDisplayID, PlatformGPUID, DynamicRangeMode) move from PlatformScreen.h into ScreenProperties.h to fix include layering (PlatformScreen.h now includes ScreenProperties.h and ThreadSafeRefCounted.h). In GPUCanvasContextCocoa.mm the observer callback and gather loop now take a singleton snapshot instead of touching the raw global, superseding cf20d123b3’s main-thread-hop mitigation.

Key code

Copy-on-write / resettable singleton update under lock (PlatformScreen.cpp)

Ref<const PlatformScreen> PlatformScreen::singleton()
{
    Locker locker { platformScreenLock() };
    return instance().get();
}

void PlatformScreen::updateSingletonProperties(ScreenProperties&& properties)
{
    Locker locker { platformScreenLock() };
    Ref<PlatformScreen>& platformScreenRef = PlatformScreen::instance();

    // If we have the only reference, we can update in place
    if (platformScreenRef->hasOneRef())
        platformScreenRef->m_properties = WTF::move(properties);
    else
        platformScreenRef = PlatformScreen::create(WTF::move(properties));
}

Patch walkthrough

  • Source/WebCore/platform/PlatformScreen.h — Removes the free-function API (getScreenProperties/setScreenProperties/screenData/primaryScreenDisplayID/screenContentsFormatsForTesting) and adds class PlatformScreen : public ThreadSafeRefCounted<PlatformScreen> with static singleton() returning Ref<const PlatformScreen>, static updateSingletonProperties(ScreenProperties&&), and const instance methods (screenData, primaryScreenDisplayID, screenProperties, screenDatas, screenContentsFormatsForTesting) marked LIFETIME_BOUND. Private create()/instance()/ctor and ScreenProperties m_properties. Now includes ScreenProperties.h and ThreadSafeRefCounted.h; moves PlatformDisplayID/PlatformGPUID/DynamicRangeMode out.
  • Source/WebCore/platform/PlatformScreen.cpp — Implements platformScreenLock() (a static Lock), instance() (WTF_REQUIRES_LOCK) holding the singleton Ref, the ctor/create(), singleton() (Locker + return instance().get()), the const accessors reading m_properties, and updateSingletonProperties(): under the lock, update in place if hasOneRef() else create() a new instance – the copy-on-write/reset core. Removes the old ASSERT(isMainThread()) global accessor and all deleted free functions.
  • Source/WebCore/platform/ScreenProperties.h — Drops its include of PlatformScreen.h and instead defines PlatformDisplayID, PlatformGPUID, DynamicRangeMode and forward-declares ContentsFormat here, resolving the header cycle created by PlatformScreen.h now including ScreenProperties.h.
  • Source/WebCore/html/canvas/GPUCanvasContextCocoa.mm — The ScreenPropertiesChangedObserver callback and updateScreenHeadroomFromScreenPropertiesIfNeeded now do Ref screen = PlatformScreen::singleton(); and read screen->screenData(displayID)/screen->screenProperties().screenDataMap from that snapshot, removing the raw getScreenProperties()/screenData() global access and the earlier main-thread-hop workaround.
  • Source/WebCore/platform/mac/PlatformScreenMac.mm — Every consumer (primaryOpenGLDisplayMask, displayMaskForDisplay, gpuIDForDisplay, screenIsMonochrome, screenHasInvertedColors, screenDepth[PerComponent], screenRectForDisplay/PrimaryScreen, currentEDRHeadroomForDisplay, etc.) is rewritten to take Ref screen/platformScreen = PlatformScreen::singleton(); and call the instance methods; the local screenProperties(Widget*) helper is removed.
  • Source/WebKit/WebProcess/cocoa/WebProcessCocoa.mm + glib/WebProcessGLib.cpp + WebProcess.h — WebProcess::setScreenProperties now takes ScreenProperties&& and calls WebCore::PlatformScreen::updateSingletonProperties(WTF::move(properties)); the various setScreenProperties/parameters paths move properties in instead of copying.
  • Source/WebKit/UIProcess/gtk/ScreenManagerGtk.cpp + wpe/ScreenManagerWPE.cpp — collectScreenProperties() publishes via WebCore::PlatformScreen::updateSingletonProperties(ScreenProperties { properties }) instead of the removed setScreenProperties().
  • Source/WebCore/platform/graphics/cocoa/VP9UtilitiesCocoa.mm, gstreamer/GStreamerRegistryScanner.cpp, ios/PlatformScreenIOS.mm, wpe/PlatformScreenWPE.cpp, gtk/PlatformScreenGtk.cpp, testing/Internals.cpp, Modules/model-element/HTMLModelElement.cpp, html/HTMLMediaElement.cpp, WebProcess/Model/WebModelPlayer.mm, WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm, UIProcess/API/glib/WebKitProtocolHandler.cpp, UIProcess/Cocoa/WebProcessPoolCocoa.mm — All remaining callers of the old free functions are converted to take a PlatformScreen::singleton() snapshot and call instance methods, completing removal of unsynchronized global screen access across every platform and subsystem.

Background

ThreadSafeRefCounted<PlatformScreen> — Base giving atomic ref-counting so a Ref<const PlatformScreen> can be safely held and dropped across threads; combined with immutability-per-instance it makes snapshots race-free.

Copy-on-write singleton — updateSingletonProperties mutates the current instance only when hasOneRef() (no reader holds it), otherwise swaps in a new instance. Live readers keep their old, never-mutated snapshot until they release it – possibly K instances coexist briefly.

platformScreenLock() — A static WTF::Lock serializing access to the singleton slot; instance() is annotated WTF_REQUIRES_LOCK(platformScreenLock()) so the compiler enforces that the raw Ref slot is only touched under the lock.

getScreenProperties()/screenData() (removed) — The old free functions that returned a reference into a mutable process-global ScreenProperties/HashMap. Their existence allowed unsynchronized cross-thread reads; the commit deletes them outright rather than patching individual callers.

ScreenProperties / screenDataMap — The per-process aggregate of display metadata (EDR headroom, color, depth, rects, GPU IDs) keyed by PlatformDisplayID in a HashMap. It is the shared state that was being raced on.

Include-layering fix — PlatformDisplayID, PlatformGPUID and DynamicRangeMode were moved from PlatformScreen.h to ScreenProperties.h because PlatformScreen.h now includes ScreenProperties.h; this breaks the header cycle the class introduces.

Vulnerability window

  1. Discovery — getScreenProperties()/screenData() were found (issue #4632) to be called off the main thread, racing the main thread’s mutation of the global screenDataMap and causing MTE crashes.
  2. Interim fix — cf20d123b3 forced GPUCanvasContextCocoa’s specific read onto the main thread and added an ASSERT(isMainThread()) tripwire.
  3. Root-cause rework — This commit removes the mutable-global free-function API entirely and introduces a ThreadSafeRefCounted PlatformScreen singleton with copy-on-write, resettable semantics.
  4. Reader snapshot — Consumers take Ref<const PlatformScreen> = singleton() and read immutable instance data; a held snapshot is never mutated underneath the reader.
  5. Writer swap — updateSingletonProperties updates in place only if hasOneRef(), else forks a new instance; old instances live until readers drop refs.
  6. Fanout — Every platform/subsystem caller (Mac/iOS/Gtk/WPE screen code, WebGPU, media codecs, PDF, model, Internals) is migrated to the singleton API.

Triggering

No test added (a data race across threads/platforms). Conceptual trigger: from web content, drive a subsystem that reads screen properties off the main thread (e.g. WebGPU EDR headroom computation, or media-codec HDR capability checks iterating screenDataMap) while the main thread repeatedly publishes new ScreenProperties (display reconfiguration / WebProcess::setScreenProperties); pre-patch the concurrent HashMap read/mutation faults under MTE, post-patch the reader holds an immutable snapshot and the writer forks a new instance.

Exploitation

  1. Establish concurrent access — Get a worker/GPU/media thread reading the process-global ScreenProperties while the main thread mutates it via setScreenProperties on display changes.
  2. Race the map — Concurrent iteration during a HashMap rehash/insert reads relocated or freed bucket memory – a UAF/type-confusion read of ScreenData.
  3. Constrained data — The raced data is display metadata (headroom, depth, color, GPU IDs); no direct control-flow primitive is evident, so the realistic outcome is corruption/inconsistent state or an MTE-caught crash (DoS).
  4. Post-fix — After this commit the shared instance is immutable to readers and swapped atomically, removing the race and thus the exploitation surface for this global.

Detection & hunting

For defenders and SOC / detection engineers:

  • MTE crash in getScreenProperties()/screenData() off-main (pre-patch) — Backtraces reading the global ScreenProperties/screenDataMap from a non-main thread are the signature; post-patch such call sites no longer exist.
  • ThreadSanitizer race on the screen global — TSan reports pairing a main-thread setScreenProperties writer with an off-main reader of screenDataMap confirm the class.
  • Callers still using removed free functions — Any out-of-tree or missed in-tree caller of getScreenProperties/screenData/primaryScreenDisplayID/setScreenProperties will fail to compile; a build break flags an unmigrated access path.

Audit directions

  • Completeness of migration — Confirm no remaining code path reads screen properties via a stale reference or reintroduces a by-reference mutable global; verify every platform (Mac/iOS/Gtk/WPE) and subsystem (WebGPU, media, PDF, model, Internals) uses PlatformScreen::singleton() snapshots.
  • CoW correctness — Review updateSingletonProperties’ hasOneRef() decision for TOCTOU under the lock and confirm that in-place update is only taken when truly no reader holds a snapshot; ensure LIFETIME_BOUND accessors cannot outlive their Ref.
  • Other mutable process-global singletons — Search WebCore for other NeverDestroyed/global containers exposed by-reference and mutated on the main thread while read elsewhere (font caches, color-space caches, settings mirrors); apply the same snapshot/CoW or locking discipline.
  • Lock contention / performance — Assess whether the per-access Locker on platformScreenLock() and potential K-instance fanout under frequent display updates introduce contention or memory growth on hot paths (per-frame headroom queries, codec capability checks).

Before / after

Loading diff…