d309c9d8e6c852b9ce4eb4a43cd15d57085638a4 SerializedImageBuffer still leaks cross-RemoteRenderingBackend data through GraphicsContext for unaccelerated buffers
Triage note: ImageBufferShareableBitmapBackend::releaseGraphicsContext previously did nothing; the fix makes it null m_context (lazily recreated via context()) and adds hasGraphicsContext(), with a MESSAGE_CHECK(!imageBuffer->hasGraphicsContext()) in RemoteRenderingBackend::moveToSerializedBuffer whose added comment states a backend that cannot release its context still holds state the adopting backend would touch from its own thread. This is a GPU-process cross-backend dangling-state/UAF fix.
The bug at a glance
ImageBufferShareableBitmapBackend::releaseGraphicsContext() was a no-op, so when RemoteRenderingBackend::moveToSerializedBuffer handed an ImageBuffer to a different RemoteRenderingBackend it still carried a live GraphicsContext holding references to resources owned by the originating backend. The adopting backend (running on its own GPU-process work-queue thread) would then touch that stale cross-backend state. The diff proves a dangling cross-backend/cross-thread state defect in the GPU process and ships a MESSAGE_CHECK to reject such a buffer; the concrete freed-object use is the plausible consequence but the actual free site is not in this diff, so UAF is the (inferred) escalation of a proven lifetime/ownership defect. High severity is reasonable for a GPU-process cross-context handoff bug.
A virtual method that silently did nothing: the serialization handoff carefully ‘released’ the graphics context to shed cross-backend state, but for the ShareableBitmap backend that release was a documented no-op, so the detaching step the code relied on never happened.
Root cause
The invariant is that an ImageBuffer transferred between RemoteRenderingBackends must carry no GraphicsContext, because the context holds state referencing resources owned by the source backend and would be touched from the destination backend’s work-queue thread. ImageBufferBackend::releaseGraphicsContext() is the mechanism to detach a buffer from its surroundings.
Pre-patch sequence: 1. moveToSerializedBuffer() asserts hasOneRef() and calls imageBuffer->releaseGraphicsContext() to shed cross-backend state; 2. for the ShareableBitmap backend releaseGraphicsContext() is { /* Do nothing. This is only relevant for IOSurface backends */ }, so m_context (a live GraphicsContext) survives; 3. the serialized buffer is added to the shared resource cache and later adopted by a different RemoteRenderingBackend; 4. the adopting backend, on its own work-queue thread, uses the buffer’s still-live GraphicsContext, whose internal state references resources (and threading context) belonging to the original backend — cross-backend dangling state accessed cross-thread.
The fix makes ImageBufferShareableBitmapBackend::releaseGraphicsContext() actually null m_context, and gives context() a lazy-recreate path that rebuilds the GraphicsContext from m_bitmap on demand (factored into adoptGraphicsContext()). It adds hasGraphicsContext() across the backend hierarchy and, critically, a MESSAGE_CHECK(!imageBuffer->hasGraphicsContext(), “ImageBuffer cannot release its GraphicsContext”) in moveToSerializedBuffer so any backend that cannot shed its context is refused the transfer rather than silently handing over live state. The base ImageBufferBackend default for releaseGraphicsContext() changes from ASSERT_NOT_REACHED() to a real no-op, and hasGraphicsContext() defaults to true (so an un-overridden backend is conservatively treated as still holding a context and blocked from transfer). NullImageBufferBackend is migrated to a lazily-created unique_ptr so it too can release and report correctly.
Key code
No-op release replaced with real teardown + transfer gate
// was: void releaseGraphicsContext() final { /* Do nothing. ... IOSurface backends */ }
void ImageBufferShareableBitmapBackend::releaseGraphicsContext() { m_context = nullptr; }
bool hasGraphicsContext() const final { return !!m_context; }
// RemoteRenderingBackend::moveToSerializedBuffer:
imageBuffer->releaseGraphicsContext();
MESSAGE_CHECK(!imageBuffer->hasGraphicsContext(), "ImageBuffer cannot release its GraphicsContext");
Fix approach
The patch turns the ShareableBitmap backend’s releaseGraphicsContext() into a real teardown (null m_context) and makes context() lazily rebuild the context from the owned ShareableBitmap when next needed, so dropping the context on transfer is safe and reversible. It then adds hasGraphicsContext() throughout the ImageBufferBackend hierarchy and a MESSAGE_CHECK in moveToSerializedBuffer that refuses to serialize any buffer still holding a context — converting a silent cross-backend leak into a hard IPC failure. Defaults are made conservative (base hasGraphicsContext() returns true, releaseGraphicsContext() becomes a harmless no-op instead of ASSERT_NOT_REACHED), so a backend that forgets to override is blocked from transfer rather than leaking. After the fix, the serialized-buffer handoff provably carries no live GraphicsContext and no cross-backend resource references.
Patch walkthrough
Source/WebKit/WebProcess/GPU/graphics/ImageBufferShareableBitmapBackend.cpp— Replaces the no-op releaseGraphicsContext() with one that nulls m_context, adds adoptGraphicsContext() and a lazy context() that recreates the GraphicsContext from m_bitmap, and adds hasGraphicsContext().Source/WebKit/GPUProcess/graphics/RemoteRenderingBackend.cpp— After releaseGraphicsContext(), adds MESSAGE_CHECK(!imageBuffer->hasGraphicsContext(), …) so a buffer that cannot shed its context is rejected before being adopted by another backend.Source/WebCore/platform/graphics/ImageBufferBackend.h— Changes the base releaseGraphicsContext() from ASSERT_NOT_REACHED() to a no-op and adds a virtual hasGraphicsContext() defaulting to true, with comments defining the detach contract.Source/WebCore/platform/graphics/ImageBuffer.{cpp,h}— Adds ImageBuffer::hasGraphicsContext() delegating to the backend (false when no backend).Source/WebCore/platform/graphics/NullImageBufferBackend.{cpp,h}— Migrates m_context to a lazily-created unique_ptr and implements releaseGraphicsContext()/hasGraphicsContext() so the null backend participates correctly.Source/WebCore/platform/graphics/cg/ImageBufferIOSurfaceBackend.{cpp,h} and Shared/RemoteLayerTree/DynamicContentScaling* and cocoa ImageBufferShareableMappedIOSurfaceBitmapBackend.h— Add hasGraphicsContext() overrides across the remaining backends so every concrete backend reports context presence consistently.
Background
RemoteRenderingBackend (GPU process) — The GPU-process object that owns and services ImageBuffers for one client; buffers can be serialized and moved to a different RemoteRenderingBackend, each running work on its own work-queue thread.
ImageBuffer / ImageBufferBackend — ImageBuffer is the thread-safe ref-counted drawing surface; the backend is the storage-specific implementation (ShareableBitmap, IOSurface, Null) providing the GraphicsContext.
GraphicsContext state and cross-backend references — A GraphicsContext caches drawing state that can reference resources owned by other objects/backends; releasing it is how an ImageBuffer is detached from its surroundings before changing owners.
moveToSerializedBuffer / shared resource cache — The handoff path that moves an ImageBuffer (asserted hasOneRef) into a shared cache for adoption by another backend; it must first strip any owner-specific state.
ShareableBitmap backend — A backend whose pixels live in shared memory; its GraphicsContext can be recreated from the bitmap, which is what makes a lazy null-and-rebuild release safe.
Vulnerability window
- Setup — A ShareableBitmap-backed ImageBuffer is created and drawn into in the GPU process, acquiring a live GraphicsContext.
- Trigger — The client requests moveToSerializedBuffer to hand the buffer to another RemoteRenderingBackend.
- Pre-patch — releaseGraphicsContext() is a no-op, so the buffer is serialized with its live GraphicsContext and cross-backend references intact.
- Adoption — The destination backend adopts the buffer and, on its own work-queue thread, touches the stale context state.
- Post-patch — The context is actually dropped (and lazily rebuildable); hasGraphicsContext() is false so the MESSAGE_CHECK passes, or a non-releasing backend is rejected.
Proof of concept
No regression test ships in the diff. Conceptual trigger (inferred): in the GPU process, obtain a ShareableBitmap-backed ImageBuffer, draw into it so a GraphicsContext exists, then drive moveToSerializedBuffer and have a second RemoteRenderingBackend adopt and use it; pre-patch the adopting backend touches the retained cross-backend context state from its own thread. The web path is Canvas/CanvasRenderingContext usage that routes through the GPU process plus a buffer transfer between rendering backends (inferred); the diff itself proves only the ownership/detach defect and the defensive check.
Exploitation
- Reach — GPU-process rendering reachable from web content via remote 2D canvas/layer rendering that uses ShareableBitmap backends.
- Trigger — Cause an ImageBuffer with a live context to be moved to another RemoteRenderingBackend (moveToSerializedBuffer).
- Primitive — The adopting backend accesses a GraphicsContext holding references to resources owned by the original backend, from a different work-queue thread — cross-backend dangling state / data race on shared GPU-process state.
- Escalation (inferred) — If the referenced resources are freed or mutated by the original owner, this becomes a use-after-free/type-confusion on GPU-process heap; reliability depends on controlling the freed resource’s reclamation and the adopting thread’s access timing, which are not shown in this diff.
Detection & hunting
For defenders and detection engineers:
- Serialized buffer still holding a context —
- Cross-thread GraphicsContext access —
- Backends missing overrides —
Remaining risk in the patch
- Free site not in this diff — The diff proves retained cross-backend state and adds a gate, but does not show where the referenced resources are freed/mutated; whether the access is a true UAF vs a benign stale read is unverified here (inferred).
- Lazy context() recreation — context() now lazily rebuilds from m_bitmap; a path that calls context() after release (re-materializing a context) and then transfers again must re-release, or the gate must re-run — the single MESSAGE_CHECK covers only the moveToSerializedBuffer moment.
- DynamicContentScaling composite — DynamicContentScalingBifurcatedImageBuffer::hasGraphicsContext() ORs three sources (m_context, base, backend); if any sub-backend under-reports context presence, a bifurcated buffer could still pass the gate while holding state.
Audit directions
- Other no-op virtual overrides on security paths —
- Cross-RemoteRenderingBackend transfers —
- Work-queue thread ownership —