← WebKit — 2026-W36

e77a4dbb49b6fc37c73c4dc1ef34222d14d56fa7  OOB Access in BindGroupLayout::errorValidatingBindGroupCompatibility — auto-generated layouts with decayed WeakPtr compare equal

severity high class TypeConfusion confidence 0.7 signal score 10.57 Source/WebGPU/ patch-now AI review CONFIRMED
Primitive: A decayed WeakPtr to an auto-generated PipelineLayout made unrelated bind group layouts compare equal, so a BindGroup could feed wrong Metal argument-buffer slots to a pipeline compiled against a different layout.
Triage note: The non-RC_REMOVE_SEC_ACCEL_3 fix replaces WeakPtr<PipelineLayout> with a non-decaying uint64 identifier (uniqueId) in BindGroupLayout compatibility checks, adds argumentBufferIndices/bufferSizeArgumentBufferIndices comparison in equalEntries, and rejects auto-generated bind group layouts passed to createPipelineLayout per the WebGPU [[exclusivePipeline]] spec. The comments state a BindGroup encoded against one layout would feed wrong slots to a pipeline compiled against another, i.e. GPU memory type confusion.
Structural signals: smartptr_member_added (+4.87), null_guard (+1.2), msg_strong (+3.0), area:Source/WebGPU/ (+1.5)

The bug at a glance

In WebGPU (GPU process), BindGroupLayout compatibility used a WeakPtr to identify the auto-generated pipeline layout that produced a layout. When that PipelineLayout was destroyed while a BindGroup still referenced the layout, the WeakPtr decayed to null, so two unrelated auto-generated layouts both compared as having a null owner and were judged equal. A BindGroup encoded against one layout could then be bound to a pipeline compiled against a different layout, feeding wrong Metal argument-buffer slots — a GPU-memory type/offset confusion. The diff proves the equality-collapse mechanism and adds argument-buffer-index comparison plus a spec-mandated rejection, so high severity as a GPU-process memory-safety bug is justified; full RCE is the realistic ceiling but (inferred).

A WeakPtr used as an identity token: once it decays to null, ’no owner’ equals ’no owner’, so destroying a pipeline layout silently makes distinct auto-generated bind group layouts interchangeable in the compatibility check that is supposed to keep resources pinned to the right Metal slots.

Root cause

The invariant (WebGPU [[exclusivePipeline]] / bind-group compatibility) is that two bind group layouts are group-equivalent only if their owning auto-generated pipeline is the same, so a BindGroup encoded for one layout’s Metal argument-buffer indices is only ever used with a pipeline compiled against the same indices. Pre-patch, BindGroupLayout stored WeakPtr m_autogeneratedPipelineLayout and compared autogeneratedPipelineLayout() pointers.

Pre-patch sequence: 1. two auto-layout pipelines each generate their own BindGroupLayout, each with a WeakPtr back to its auto-generated PipelineLayout; 2. the PipelineLayouts are destroyed (they are only weakly held here) while BindGroups still reference the layouts; 3. both WeakPtrs decay to null, so autogeneratedPipelineLayout() returns nullptr for both; 4. the compatibility check autogeneratedPipelineLayout() != otherLayout.autogeneratedPipelineLayout() is nullptr != nullptr == false, so the two unrelated layouts are declared compatible; 5. additionally, equalEntries() did not compare argumentBufferIndices / bufferSizeArgumentBufferIndices, so two layouts agreeing on API-visible fields but differing on where each resource and each buffer length lives in the Metal argument buffer also passed; 6. a BindGroup encoded against layout A is bound to a pipeline compiled against layout B, so resources and buffer-length values land in the wrong argument-buffer slots.

The fix replaces the WeakPtr with a non-decaying uint64 identifier: m_autogeneratedPipelineLayoutIdentifier set from PipelineLayout::uniqueId(). The compatibility check becomes isAutoGenerated() && this != &otherLayout && (!id() || id() != otherLayout.id()) -> mismatch, so two auto-generated layouts from different pipelines never compare equal even after their PipelineLayouts are destroyed, and identifier 0 (no constructed PipelineLayout) is only compatible with itself. equalEntries() now also compares argumentBufferIndices and bufferSizeArgumentBufferIndices so slot layout is part of equivalence. Finally createPipelineLayout() rejects any externally-supplied bind group layout that isAutoGenerated() (the [[exclusivePipeline]]!=null rule), except on the internal auto-layout path (isAutogenerated) which legitimately passes the layouts it just generated.

Key code

Non-decaying identifier replaces decaying WeakPtr for layout identity

// field: was WeakPtr<const PipelineLayout>; now:
uint64_t m_autogeneratedPipelineLayoutIdentifier { 0 };
void setAutogeneratedPipelineLayout(const PipelineLayout& pl) { m_autogeneratedPipelineLayoutIdentifier = pl.uniqueId(); }
// compatibility check:
if (isAutoGenerated() != otherLayout.isAutoGenerated())
    return @"Auto-generated layouts mismatch";
if (isAutoGenerated() && (this != &otherLayout)
    && (!autogeneratedPipelineLayoutIdentifier()
        || autogeneratedPipelineLayoutIdentifier() != otherLayout.autogeneratedPipelineLayoutIdentifier()))
    return @"Auto-generated layouts mismatch";

Fix approach

The patch stops using an object pointer that can be nulled by destruction as an identity token and instead stores the owning auto-generated PipelineLayout’s stable uint64 uniqueId. Because the identifier never decays, two auto-generated layouts from different pipelines remain distinguishable even after their PipelineLayouts are freed, and the ’no owner’ sentinel (0) is made compatible only with itself. It adds the missing argumentBufferIndices / bufferSizeArgumentBufferIndices equality in equalEntries so that layouts differing only in Metal slot assignment are treated as incompatible. It also enforces the WebGPU spec rule that createPipelineLayout must reject bind group layouts obtained from an auto-layout pipeline’s getBindGroupLayout(), closing the avenue where an auto-generated layout’s emitter-assigned indices get compiled into an externally-created pipeline. Net effect: layout equivalence now reflects real Metal argument-buffer slot identity, not a possibly-null pointer.

Patch walkthrough

  • Source/WebGPU/WebGPU/BindGroupLayout.h — Replaces WeakPtr m_autogeneratedPipelineLayout with uint64_t m_autogeneratedPipelineLayoutIdentifier and swaps the accessor/mutator signatures to the identifier form.
  • Source/WebGPU/WebGPU/BindGroupLayout.mm — equalEntries() now compares argumentBufferIndices and bufferSizeArgumentBufferIndices; the compatibility check uses the non-decaying identifier (with 0 only self-compatible); setAutogeneratedPipelineLayout stores pl.uniqueId().
  • Source/WebGPU/WebGPU/PipelineLayout.mm — createPipelineLayout rejects externally-supplied auto-generated bind group layouts (createInvalid) per [[exclusivePipeline]], exempting the internal auto-layout path; and sets the identifier via setAutogeneratedPipelineLayout(*this) for its generated layouts.

Background

WebGPU auto-generated layout / [[exclusivePipeline]] — A pipeline created with layout:‘auto’ generates bind group layouts bound exclusively to it; the spec forbids reusing such a layout with a different pipeline, modeled by each layout’s [[exclusivePipeline]] slot.

BindGroupLayout compatibility — Two layouts are group-equivalent only if every API-visible field and their exclusive-pipeline identity match; equivalence governs whether a BindGroup made for one may be used with a pipeline expecting the other.

Metal argument buffer indices — On the Metal backend each binding (and each buffer-length value) is assigned a specific argument-buffer slot; two layouts can agree on API fields yet place resources at different slots, so slot indices must be part of equality.

WeakPtr decay — A WeakPtr becomes null when its referent is destroyed; using it as an identity means two distinct-but-both-destroyed referents collapse to the same null value, breaking inequality.

uniqueId() stable identity — PipelineLayout::uniqueId() is a monotonically unique integer that survives the object’s destruction as a value, giving a non-decaying identity for comparison.

Vulnerability window

  1. Setup — Create two auto-layout pipelines; each exposes an auto-generated BindGroupLayout via getBindGroupLayout, and BindGroups are made against them.
  2. Trigger — Release the owning auto-generated PipelineLayouts while the BindGroups/layouts persist, decaying the WeakPtrs to null.
  3. Confusion — The compatibility check sees null==null and (lacking slot comparison) declares the two layouts equivalent.
  4. Misbind — A BindGroup encoded for layout A is accepted for a pipeline compiled against layout B, routing resources/lengths to wrong Metal argument-buffer slots.
  5. Post-patch — Stable identifiers and slot-index comparison keep the layouts incompatible; createPipelineLayout rejects foreign auto-generated layouts.

Proof of concept

No regression test ships in the diff. Conceptual trigger (inferred) via the WebGPU JS API: create two distinct render/compute pipelines with layout:‘auto’, retrieve each one’s getBindGroupLayout(0), create a BindGroup against pipeline A’s layout, then drop references to the pipelines/their PipelineLayouts so the WeakPtrs decay; arrange for the compatibility check to then treat A’s and B’s auto layouts as equal and issue a draw/dispatch binding A’s BindGroup to pipeline B. The misassigned Metal argument-buffer slots cause resources/buffer-length values to be read from the wrong slots (inferred exact slot effect).

Exploitation

  1. Reach — WebGPU is reachable from web content; the bug lives in the GPU process WebGPU/Metal backend.
  2. Trigger — Produce two auto-generated layouts, destroy their PipelineLayouts to decay the WeakPtrs, and bind a BindGroup across them.
  3. Primitive — GPU-side type/offset confusion: a resource (buffer/texture/sampler) or a buffer-length value is consumed from the wrong Metal argument-buffer slot by a shader compiled for a different layout.
  4. Escalation (inferred) — Wrong slot assignment can turn into out-of-bounds or type-confused GPU memory access (e.g. a buffer length mismatched to a resource), potentially an info leak or corruption in the GPU process; reliability depends on controlling which resources occupy the confused slots and shader behavior, not shown in the diff.

Detection & hunting

For defenders and detection engineers:

  • Auto-generated layout reused across pipelines —
  • Compatibility mismatch after layout destruction —
  • Slot-index divergence —

Remaining risk in the patch

  • Identifier-0 class — Identifier 0 means ‘auto-generated layout whose PipelineLayout was never constructed’ and is only self-compatible; if two different layouts can both legitimately end up with id 0 and also need to be compatible, or if uniqueId() can ever be 0 for a real layout, the sentinel semantics could misclassify (inferred).
  • uniqueId uniqueness/reuse — Correctness now depends on PipelineLayout::uniqueId() never being reused within the relevant lifetime; the diff does not show its generation, so wraparound/reuse is unverified (inferred).
  • Non-auto layouts — The slot-index comparison in equalEntries now applies generally, but the identifier guard is gated on isAutoGenerated(); any slot-assignment divergence in explicitly-created layouts relies solely on the new equalEntries checks.

Audit directions

  • Other WeakPtr-as-identity uses —
  • Metal argument-buffer slot assumptions —
  • createPipelineLayout / getBindGroupLayout spec rules —

Before / after

Loading diff…