← All reports

[4] Remote snapshot recorder hard-codes DisplayList rendering mode

LowWebKit remote snapshottingLogicError

87adf3a

Low. The defect itself is rendering fidelity in PDF output, not memory safety. What keeps this on the security list is a new sender-chosen enum crossing the WebContent-to-UI-to-WebContent relay, narrowed by a release-mode check to the two supported values.

Paint code often switches strategy depending on what kind of surface the output ultimately lands on, so any stand-in drawing context has to report its destination honestly. In WebKit's remote snapshotting, a WebContent process records paint commands into a RemoteSnapshotRecorderProxy, a context that streams drawing operations to the GPU process, which replays them into a bitmap or a PDF. Paint code asks the context for its RenderingMode (display list vs. PDF document) to choose between drawing paths, so the reported mode is expected to match the real sink.

The angle: under site isolation, a compromised renderer can pick between two supported paint modes for another site's frame recording, which shifts output appearance but not memory safety; the lasting value is the relay-validation pattern.

Enabling remote snapshotting causes two PDF drawing API tests to fail. With RemoteSnapshottingEnabled, content is painted into a RemoteSnapshotRecorderProxy and the GPU process later replays it into a CG PDF context. The recorder always reported RenderingMode::DisplayList, so checks for RenderingMode::PDFDocument in Page::preferredFilterRenderingModes, BackgroundPainter::paintFillLayer and LegacyRenderSVGResourceGradient were skipped, reintroducing the bugs they fixed (filter: invert() not applied, background-clip: text drawn as a solid rectangle). The patch passes a rendering mode when creating the recorder: PDFDocument when the snapshot is sunk into a PDF, DisplayList otherwise, and carries the mode through DrawFrameToSnapshot so cross-site iframes are recorded the same way under site isolation.

Source/WebKit/WebProcess/GPU/graphics/RemoteSnapshotRecorderProxy.cpp

-RemoteSnapshotRecorderProxy::RemoteSnapshotRecorderProxy(const FloatRect& initialClip, RemoteRenderingBackendProxy& renderingBackend)
- : RemoteGraphicsContextProxy(GraphicsContextState::initialIndeterminate(), initialClip, { }, ColorSpace::SRGB(), DrawGlyphsMode::Normal, std::nullopt, RenderingMode::DisplayList, RemoteGraphicsContextIdentifier::generate(), renderingBackend)
+RemoteSnapshotRecorderProxy::RemoteSnapshotRecorderProxy(const FloatRect& initialClip, RenderingMode renderingMode, RemoteRenderingBackendProxy& renderingBackend)
+ : RemoteGraphicsContextProxy(GraphicsContextState::initialIndeterminate(), initialClip, { }, ColorSpace::SRGB(), DrawGlyphsMode::Normal, std::nullopt, renderingMode, RemoteGraphicsContextIdentifier::generate(), renderingBackend)
{
+ ASSERT(renderingMode == RenderingMode::DisplayList || renderingMode == RenderingMode::PDFDocument);
}
...

Source/WebKit/UIProcess/WebPageProxy.cpp

-void WebPageProxy::drawFrameToSnapshot(FrameIdentifier frameID, const IntRect& rect, RemoteSnapshotIdentifier snapshotIdentifier, CompletionHandler<void(bool)>&& completionHandler)
+void WebPageProxy::drawFrameToSnapshot(IPC::Connection& connection, FrameIdentifier frameID, const IntRect& rect, RemoteSnapshotIdentifier snapshotIdentifier, RenderingMode renderingMode, CompletionHandler<void(bool)>&& completionHandler)
{
- sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::DrawFrameToSnapshot(frameID, rect, snapshotIdentifier), WTF::move(completionHandler));
+ MESSAGE_CHECK_COMPLETION(WebProcessProxy::fromConnection(connection), renderingMode == RenderingMode::DisplayList || renderingMode == RenderingMode::PDFDocument, completionHandler(false));
+ sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::DrawFrameToSnapshot(frameID, rect, snapshotIdentifier, renderingMode), WTF::move(completionHandler));
}

The change gives the recorder a constructor-supplied rendering mode, picks that mode at each creation site, and carries it across the site-isolation relay with a UI-process range check.

Recorder and creation sites. RemoteSnapshotRecorderProxy's constructor takes a RenderingMode and forwards it to the RemoteGraphicsContextProxy base in place of the hard-coded DisplayList, with a debug ASSERT that the mode is DisplayList or PDFDocument. RemoteRenderingBackendProxy::createSnapshotRecorder gains the argument. WebPage::drawToSnapshot passes PDFDocument and WebPage::takeRemoteSnapshot passes DisplayList. In WebPageCocoa.mm, the site passing rect uses DisplayList and the site passing mediaBox uses PDFDocument. In WebPageIOS.mm, both sites pass mediaBox: the first uses DisplayList, the second PDFDocument.

Relay. Under site isolation, WebPage::paintRemoteFrameContents sends context.renderingMode() in Messages::WebPageProxy::DrawFrameToSnapshot. The UI process relays it in Messages::WebPage::DrawFrameToSnapshot to the process hosting the cross-site frame, which uses it in WebPage::drawFrameToSnapshot to build its own recorder. WebPageProxy::drawFrameToSnapshot now takes the IPC::Connection& and adds the MESSAGE_CHECK_COMPLETION shown above. Both .messages.in files gain the field. The DrawingToPDF.mm changes generalize enableSiteIsolation into enableFeature and add ...WithRemoteSnapshotting variants of the filter-invert and background-clip tests.

A wrapper context hard-codes its self-reported output kind, so downstream code branching on that kind takes the wrong fidelity path.

RenderingMode. A WebCore enum describing what kind of surface a GraphicsContext or ImageBuffer ultimately draws into. DisplayList means operations are recorded for later replay, and PDFDocument means they are destined for a PDF context. Paint code queries GraphicsContext::renderingMode() to choose a drawing strategy, for example rendering a CSS filter or a text-clipped background differently for vector PDF output than for a raster surface.

Remote snapshotting. With RemoteSnapshottingEnabled, the WebContent process paints into a RemoteSnapshotRecorderProxy (a RemoteGraphicsContextProxy subclass) instead of painting in-process. The GPU process replays the stream into the final sink, a bitmap or a CG PDF context. m_remoteSnapshotState on WebPage holds the active recorder and a MainRunLoopSuccessCallbackAggregator that completes once every contributing frame has drawn.

Site-isolation relay. Under site isolation, cross-site iframes live in separate WebContent processes. When the root process meets a remote frame during snapshot painting (WebPage::paintRemoteFrameContents), it sends DrawFrameToSnapshot to the UI process, and WebPageProxy::drawFrameToSnapshot forwards it to the process containing that frame, which records into the same snapshot identifier.

MESSAGE_CHECK_COMPLETION. The UI-process validator idiom: if the condition fails, the sending process is treated as misbehaving and terminated, and the completion handler runs with a failure value.

This is a logic error: a context misreporting its output kind. Before the fix, RemoteSnapshotRecorderProxy always built its RemoteGraphicsContextProxy base with RenderingMode::DisplayList, whatever the recording's destination. Content headed for PDF output was painted into this recorder, so renderingMode() reported DisplayList and any WebCore branch on PDFDocument took its non-PDF path. The commit message names Page::preferredFilterRenderingModes, BackgroundPainter::paintFillLayer and LegacyRenderSVGResourceGradient as such branch points; their sources are outside the supplied context, so that attribution is relayed as-is. The new ...WithRemoteSnapshotting tests target filter: invert() and background-clip: text in PDF output. The commit message describes the failures as the filter not being applied and the text clip drawn as a solid rectangle.

The site-isolation leg had the same flaw one level deeper. WebPage::drawFrameToSnapshot also defaulted to DisplayList when building a cross-site frame's recorder, so the fix has to carry the mode across two IPC hops.

  WebContent A (root)          UIProcess                          WebContent B (cross-site frame)
  ───────────────────          ─────────                          ──────────────────────────────
  paintRemoteFrameContents
   DrawFrameToSnapshot(frame, rect, id, mode) ─►
                               MESSAGE_CHECK: mode in {DisplayList, PDFDocument}
                               forward(frame, rect, id, mode) ───► new RemoteSnapshotRecorderProxy(mode)
                                                                     (ASSERT only; relies on relay check)

The attack-surface change is a new field, WebCore::RenderingMode renderingMode, on an existing relay: WebContent to UI (WebPageProxy::DrawFrameToSnapshot) to a different WebContent process (WebPage::DrawFrameToSnapshot). A sending process now chooses the mode another site's process uses to build its recorder, and that mode flows into every WebCore paint decision keyed on renderingMode(). The UI process enforces that the value is DisplayList or PDFDocument; the receiver's ASSERT is debug-only and leans on that relay check. Nothing checks that either allowed mode matches the actual sink, so a compromised sender could pick either one for a cross-site frame. If the relay check were missing or bypassed, other RenderingMode values (such as accelerated or unaccelerated image-buffer modes) could reach a recorder never designed for them, and what that would do in RemoteGraphicsContextProxy and GPU-side replay cannot be determined from the supplied context.

Exploitability as a memory-safety issue is not established. The pre-fix defect was PDF rendering correctness, and the primitive after the fix is a choice between two modes that only affects rendering output. The open audit question is whether anything downstream, in GPU-side replay or the paint paths keyed on renderingMode(), assumes the reported mode matches the real sink in a way that touches memory layout or buffer sizing rather than drawing choices. If it did, a compromised sender picking PDFDocument for a bitmap-bound snapshot, or the reverse, could be more than cosmetic. Nothing in the supplied context points that way. The bug lives in the sandboxed WebContent and GPU processes, and the new field crosses the WebContent-to-UI boundary where it is range-checked, so neither gives a sandbox escape.

The original bug does not weaken a memory-safety or origin boundary. The security-relevant part is the sender-controlled enum on a cross-site relay. The invariant it maintains is that a compromised WebContent process can steer another site's recorder only into a mode the recorder supports.

Insight: a proxy context standing in for a real one has to faithfully report the properties paint code branches on (rendering mode, color space, accelerated-ness), or behavior quietly diverges once a feature flag moves painting out of process. The pre-existing non-remote CSSFilterInvertAppliedOnPDF and BackgroundClipText tests suggest the in-process path already handled these behaviors. Site isolation also turns any per-snapshot parameter into a cross-process relay: values once implied by local state now travel over IPC, and the UI process becomes the place that must bound them. The pattern to watch is a value moving from locally derived to received from another WebContent process as part of a correctness fix, with validation added at the relay but only ASSERTed at the receiver.