[4] Nested DrawDisplayList replay ControlFactory confinement
Per-context isolation held everywhere — except one nested replay edge.
Rated High because a compromised WebContent process fully controls the serialized display-list stream and could drive multiple RemoteRenderingBackends to concurrently mutate and reclaim shared NSCell/WebControlView objects in the GPU process, giving a race-conditioned use-after-free across the sandbox boundary; that the concurrency yields a genuine UAF rather than a benign data race is not demonstrable from the diff alone, which caps the confidence but not the severity given the GPU-process reach.
DisplayList::applyItem() only special-cases DrawControlPart, so a nested DrawDisplayList item falls through to item.apply(context) and calls the single-arg GraphicsContext::drawDisplayList, which substitutes ControlFactory::singleton(). A compromised WebContent process can wrap DrawControlPart items in a nested DrawDisplayList and replay it on multiple RemoteRenderingBackends, racing the singleton ControlFactoryMac's shared NSCell state across GPU-process work-queue threads. The fix threads ControlFactory through DrawDisplayList::apply() and special-cases DrawDisplayList in applyItem() so nested replay uses the per-context factory.
Source/WebCore/platform/graphics/displaylists/DisplayListItem.cpp
Source/WebCore/platform/graphics/displaylists/DisplayListItems.cpp
Source/WebCore/platform/graphics/controls/ControlPart.cpp
LayoutTests/ipc/nested-display-list-draw-control-part-crash.html
Patch Details
Three coordinated changes thread a per-context ControlFactory& through nested replay. First, DisplayList::applyItem() gains a switchOn case for DrawDisplayList that calls item.apply(context, controlFactory) instead of letting it fall through to the generic [&](const auto& item){ item.apply(context); } branch. Second, DrawDisplayList::apply gains a ControlFactory& parameter and calls the two-arg context.drawDisplayList(m_displayList, controlFactory) rather than the single-arg overload that substitutes ControlFactory::singleton(). Third, ControlPart::setOverrideControlFactory is moved out-of-line, exported, and hardened to reset m_platformControl = nullptr when the override factory changes, so a cached PlatformControl built from the old factory is not reused. The regression test nests DrawControlPart items inside a DrawDisplayList and replays it across four RemoteRenderingBackends concurrently.
Loss of per-context object confinement when a nested/recursive dispatch path falls through to a default handler that substitutes a shared process-wide singleton.
Background
WebKit records drawing operations into a serializable DisplayList of Item variants; DisplayList::applyItem() dispatches each item via WTF::switchOn, calling the item's apply(). To isolate the WebContent sandbox from native graphics APIs, drawing is proxied over CoreIPC to RemoteRenderingBackend/RemoteGraphicsContext in the GPU process, where each backend replays on its own work-queue thread. Native form controls (buttons, checkboxes, menus) are rendered via ControlPart subclasses that obtain platform cells from a ControlFactory; on macOS ControlFactoryMac lazily creates and caches shared NSCell/WebControlView objects. ControlFactory::singleton() is a single process-wide factory used when no per-context override is set, via ControlPart::controlFactory() returning m_overrideControlFactory ? *m_overrideControlFactory : ControlFactory::singleton(). A DrawDisplayList item replays another whole DisplayList inline, recursing back into the replay machinery. IPCTestingAPIEnabled lets JavaScript craft and send raw CoreIPC messages, modeling a compromised WebContent process.
Analysis
This is a data race that leads to a concurrent use-after-free on shared AppKit NSCell/WebControlView objects. Before the fix, applyItem() only special-cased DrawControlPart and SetCTM for extra context arguments; every other item, including DrawDisplayList, fell through to the generic branch that invoked the single-argument DrawDisplayList::apply(GraphicsContext&), which called the single-arg GraphicsContext::drawDisplayList(m_displayList). That overload does not carry the per-context ControlFactory the GPU process created for this backend; instead the nested replay's DrawControlPart items resolve their factory via ControlPart::controlFactory(), which with no override returns ControlFactory::singleton(). The GPU process runs each RemoteRenderingBackend on its own work-queue thread, but ControlFactoryMac::singleton() is a single shared instance whose lazily-created NSCell members and shared WebControlView are mutable per-draw state. The per-context factory threading was precisely the mechanism keeping each backend thread confined to its own factory; the nested DrawDisplayList path silently escaped that confinement and reintroduced the shared singleton.
Backend A (WQ thread) Backend B (WQ thread)
───────────────────── ─────────────────────
nested DrawDisplayList nested DrawDisplayList
└─► DrawControlPart └─► DrawControlPart
resolves to ──┐ ┌── resolves to
ControlFactory::singleton() (shared)
│ │
updateCellStates/draw on the SAME NSCell / WebControlView
└────┴──► lazy (re)alloc + teardown race → UAF
The exploit direction, modeled by the test: create several RemoteRenderingBackends, each with an inner list of many DrawControlPart items spanning distinct cell members (ButtonPart/ToggleButtonPart/MenuListPart/SearchFieldPart), wrap each inner list in an outer list via DrawDisplayList, then replay all outer lists in a tight concurrent loop. Each nested replay resolves control parts to ControlFactory::singleton(), so the work-queue threads concurrently call updateCellStates/draw and lazily reinstantiate the same shared objects, racing their allocation and destruction. If two threads race the lazy re-creation or teardown of a shared cell, a dangling AppKit object pointer would be dereferenced, giving an attacker-timed use-after-free in the GPU process. The attacker controls part types, sizes, iteration count, and concurrency, which would influence heap layout and timing but not directly the freed object's contents.
This vulnerability weakens the memory-safety and thread-confinement boundary inside the GPU process. The security model assumes each backend replays against a per-context ControlFactory so that NSCell state is not shared across concurrent threads; nested DrawDisplayList replay violated that invariant. Since the vulnerable code runs in the more-privileged GPU process and the attacker is assumed to already drive raw IPC from WebContent, a successful UAF here is a meaningful step toward sandbox escape rather than a same-process bug.
The applyItem() dispatch table is a maintenance hazard: any item type needing the extra ControlFactory (or baseTransform) context must be explicitly enumerated, and the catch-all branch silently degrades correctness for anything omitted. Whenever a subsystem migrates from a global singleton to per-context instances, every recursion/nesting/fallback edge must be audited, because a single missed edge collapses the whole isolation guarantee.
Note: The drawDisplayList overload semantics, the ControlFactoryMac singleton's shared mutable members, the GPU-process work-queue threading, and that the race is a genuine object-lifetime UAF rather than a benign data race are inferred from the commit message and surrounding design; they are not fully visible in the provided source. The dispatch-table omission the fix corrects is directly visible.
Audit directions
- A
WTF::switchOn(or visitor) dispatch with a generic catch-all branch that drops extra context arguments needed for correctness or isolation. AuditDisplayList::applyItem()and similar variant dispatchers for other item types that should receive per-context state (anything that recurses or replays sub-content) but fall through to the single-argapply(context). GrepSource/WebCore/platform/graphics/displaylists/forswitchOnand compare each item'sapplyoverloads against the arguments threaded inapplyItem. - Singleton fallback re-introduced on a nested/recursive edge after a subsystem migrated to per-context instances. Audit every
::singleton()call reachable during GPU-process display-list replay to confirm none is hit when a per-context factory exists. Grep forControlFactory::singleton()andcontrolFactory()acrossSource/WebCore/platform/graphics/controls/and trace which replay paths reach them without an override set. - Shared mutable native (AppKit/Objective-C) objects touched from multiple GPU work-queue threads. Verify that
ControlFactoryMac's lazily-createdNSCell/WebControlViewmembers and similar cached platform objects are either per-context or protected against concurrent replay. InvestigateRemoteRenderingBackendwork-queue threading to confirm no other display-list item resolves shared platform singletons during concurrent replay. - Check other
GraphicsContextdual-overload APIs (single-arg vs context-carrying, likedrawDisplayList) for callers that accidentally select the singleton-substituting single-arg form; grep for such overload pairs where one variant silently supplies a default global.