← All reports

[Site Isolation] OffscreenCanvas transferred to a cross-site iframe never paints

Component: WebKit Site Isolation / OffscreenCanvas | 28001e7

LayoutTests/http/tests/site-isolation/offscreen-canvas-cross-site-frames-stay-in-order.html

+// Frames committed from a cross-site iframe reach the layer both directly from the UI process and in
+// the placeholder process's rendering updates. The last frame committed must be the one displayed.
...
+ if (event.data.type == "child-ack") {
+ const colors = [];
+ for (let i = 0; i < 30; ++i)
+ colors.push(i % 2 ? "blue" : "red");
+ colors.push("green");
+ frame.contentWindow.postMessage({ type: "draw-sequence", colors }, "*");
+ return;
+ }
...
+frame.addEventListener("load", () => {
+ const offscreen = canvas.transferControlToOffscreen();
+ frame.contentWindow.postMessage({ type: "offscreen-canvas", canvas: offscreen }, "*", [offscreen]);
+});

OffscreenCanvas lets a worker or another context render into a <canvas> via transferControlToOffscreen(). The <canvas> element becomes a placeholder, and frames reach its CALayer through an async contents delegate fed by a PlaceholderRenderingContextSource, which previously assumed the producer lived in the same process. Under Site Isolation the producer, the placeholder and the GPU-process ImageBuffers can sit in different processes, with the UI process mediating. ImageBuffer transfer works by the sender writing a handle, after which the recipient builds its own description of the buffer from that handle. The recipient therefore trusts sender-supplied metadata (size, scale, purpose, rendering mode), and IOSurfaces carry Mach send rights the sender may retain.

This commit makes DetachedOffscreenCanvas serializable across processes, so an OffscreenCanvas postMessage'd to a cross-site iframe now paints the original <canvas> placeholder; previously it was dropped silently because of [NotSerialized]. PlaceholderRenderingContextSource is split into an abstract interface, a same-process LocalPlaceholderRenderingContextSource, and a new WebKit-side RemotePlaceholderRenderingContextSource. The remote source transfers GPU-process buffer ownership and notifies the UI process, which applies the frame to the placeholder layer. The commit also adds validation of transferred ImageBuffer handles and stops a claimed IOSurface from being recycled into the recipient's surface pool.

Cross-site iframe (producer)            UI Process                    Placeholder (<canvas>) process
        │                                  │                                  │
        │                                  │◄── register(PlaceholderID) ──────┤ (on create; unregister on destroy)
        │                                  │◄── layer-changed(layerID) ───────┤
  GPU: takeTransferredBuffer               │                                  │
        ├── commitFrame(ID, frameID, ──────►│                                  │
        │    handle)                       ├─ known? ─yes─► asyncSetLayerContents(layer)  [fast path]
        │                                  ├─ forward buffer-transfer ───────►│ accepts ownership (readback)
        │                                  │   (throttled; superseded frames released)
        │                                  ├─ unknown? hold newest frame, ping claimed owner
        │                                  │◄── pong (follows any registration)
        │◄── placeholder gone ─────────────┤  deliver or release held frame
        │    (stop sending)                │

Cross-site OffscreenCanvas transfer now works through new UI-process routing, GPU-process buffer handoff and a capability-style identifier, which adds several new cross-process trust boundaries. The ImageBuffer hardening shows the transfer path was already exposed to a lying sender.

This is a high-value target: it adds cross-process attack surface and contains a security fix. Narrow starting points: