Medium watch CVSS 4.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebXR
DescriptionOut of bounds read in WebXR
ComponentWebXR
Sub-componentWebXR
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker452071845
Fix commit6c5963ad6b45 (chromium/src) +8/-18
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedAisle Research
Reported2025-10-15 (per vendor advisory)
Fix landed2025-10-16 (public git)
Disclosed2025-10-28
Patch gap12 days — fix public in source before the advisory

Background

`XRRigidTransform.matrix` / `XRView.projectionMatrix`
WebXR attributes that return a cached 16-element Float32Array (column-major 4x4 matrix) built from internal gfx::Transform or view data.
Detached `ArrayBuffer`
Script can detach a typed array’s backing buffer (for example by transferring it with postMessage or structuredClone). After that the array’s length is 0 and its data pointer no longer refers to the original storage.
`DOMFloat32ArrayToTransform`
A Blink helper in xr_utils.cc that builds a gfx::Transform by reading 16 floats from a DOMFloat32Array with gfx::Transform::ColMajorF(m->Data()), without checking the length.
`DCHECK` versus `CHECK`
DCHECK is compiled out of release builds, while CHECK is enforced in every build.

Root Cause Analysis

Before the patch, XRRigidTransform::matrix() and the projection-matrix getter in xr_view.cc (the enclosing function is not named in the hunk) handled a page-detached cached array by returning a new zero-length DOMFloat32Array. The removed comment says this was done because a detached buffer “breaks the inspector”. That broke the invariant that these getters always return a 16-element matrix. The only guard in DOMFloat32ArrayToTransform was DCHECK_EQ(m->length(), 16u), which does nothing in release builds. Any internal caller that took the getter’s result and turned it back into a transform would then read 16 floats through ColMajorF(m->Data()) from a 0-length buffer, which is an out-of-bounds read. The diff does not show which internal caller does this, so naming the exact sink is (inferred).

The fix rebuilds the array whenever the cached one is null or detached (transformationMatrixToDOMFloat32Array(*matrix_) and transformationMatrixToDOMFloat32Array(view_data_->ProjectionMatrix()), with projection_matrix_ made mutable), so the getters always return 16 elements. It also turns the length check into CHECK_EQ, so a wrong-length array now crashes safely instead of being read past its end.

Key insight
The detached-buffer fallback returned a 0-length “matrix” where every consumer expected 16 floats, and the only length check was a debug-only DCHECK. The patch recomputes a valid 16-element array and makes the length check a release CHECK.

Attack Path

  1. Obtain a matrix Script reads XRRigidTransform.matrix (or XRView.projectionMatrix during an XR session), which caches a Float32Array in matrix_array_ or projection_matrix_.
  2. Detach its buffer The page transfers the array’s ArrayBuffer, which detaches the cached object.
  3. Getter returns an empty array On the next access, the pre-patch getter saw IsDetached() and returned DOMFloat32Array::Create(0) instead of rebuilding the matrix.
  4. Length-trusting conversion A Blink path that turns that result back into a transform with DOMFloat32ArrayToTransform reads 16 floats from the 0-length array, because only a DCHECK guarded it (the specific caller is inferred).

Impact Assessment

The advisory rates this a medium-severity “Out of bounds read in WebXR”. The read happens in the renderer process and could expose up to 64 bytes of adjacent heap memory as matrix values, which might then be observable to script (inferred). It is reached through the WebXR API from ordinary web content, with no compromised renderer needed. XRRigidTransform can be constructed from script where WebXR is exposed (secure contexts), while the XRView path needs an active XR session. The patch does not show anything beyond an information-disclosure read inside the sandboxed renderer.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/xr/xr_rigid_transform.cc
modified

Files Changed

  • third_party/blink/renderer/modules/xr/xr_rigid_transform.cc
  • third_party/blink/renderer/modules/xr/xr_utils.cc
  • third_party/blink/renderer/modules/xr/xr_utils.h
  • third_party/blink/renderer/modules/xr/xr_view.cc
  • third_party/blink/renderer/modules/xr/xr_view.h

Audit Directions

  • Detached-buffer fallbacks that return wrong-shaped data
    Search Blink for getters that return DOMFloat32Array::Create(0) (or similar empty arrays) when IsDetached() is true, then follow each caller that assumes a fixed length (matrices, quaternions, DOMMatrix and WebGPU or WebGL helpers).
  • Fixed-length readers guarded only by `DCHECK`
    Look for Data() or data() pointers passed to fixed-size readers such as ColMajorF, memcpy or std::array copies where the length check is a DCHECK, and treat any script-controllable length as a release-build OOB risk.
  • Caching typed arrays exposed to script
    Any Member or NotShared typed-array cache that is handed to JavaScript can be detached. Check that internal code always uses the underlying C++ state (for example *matrix_ or view_data_) rather than reading back the script-visible array.
From 6c5963ad6b4541f5fa0812607fd36a979c48c0f1 Mon Sep 17 00:00:00 2001
From: Alexander Cooper <alcooper@chromium.org>
Date: Thu, 16 Oct 2025 10:54:37 -0700
Subject: [PATCH] [WebXR] Update detached matrix handling

Updates a few places that handle an array representation of a matrix
being detached by returning a 0-length array to simply recompute the
array that should be present, based on the presence of other data. This
more closely matches the spec, which for both of these cases essentially
state that if the value is not null to check if it's detached, and if it
is not detached to return the value. The steps following both of these
checks then recompute the matrix, so technically our current impl is
not spec-compliant to that.

Further, we remove one unused array to transform conversion helper and
update another to a CHECK from a DCHECK to match best practices for
invariants.

Fixed: 452071845
Change-Id: Idf765fe5717d59ae63c71e8253784ff4473dea5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7046576
Commit-Queue: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Brandon Jones <bajones@chromium.org>
Auto-Submit: Alexander Cooper <alcooper@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1530947}
---

diff --git a/third_party/blink/renderer/modules/xr/xr_rigid_transform.cc b/third_party/blink/renderer/modules/xr/xr_rigid_transform.cc
index 68afd463..d0a8cc6 100644
--- a/third_party/blink/renderer/modules/xr/xr_rigid_transform.cc
+++ b/third_party/blink/renderer/modules/xr/xr_rigid_transform.cc
@@ -108,15 +108,8 @@
 
 NotShared<DOMFloat32Array> XRRigidTransform::matrix() {
   EnsureMatrix();
-  if (!matrix_array_) {
-    matrix_array_ = transformationMatrixToDOMFloat32Array(*matrix_);
-  }
-
   if (!matrix_array_ || matrix_array_->IsDetached()) {
-    // A page may take the matrix_array_ value and detach it so matrix_array_ is
-    // a detached array buffer.  This breaks the inspector, so return an empty
-    // array instead.
-    return NotShared<DOMFloat32Array>(DOMFloat32Array::Create(0));
+    matrix_array_ = transformationMatrixToDOMFloat32Array(*matrix_);
   }
 
   return matrix_array_;
diff --git a/third_party/blink/renderer/modules/xr/xr_utils.cc b/third_party/blink/renderer/modules/xr/xr_utils.cc
index 55188dd..307b902 100644
--- a/third_party/blink/renderer/modules/xr/xr_utils.cc
+++ b/third_party/blink/renderer/modules/xr/xr_utils.cc
@@ -23,15 +23,10 @@
 }
 
 gfx::Transform DOMFloat32ArrayToTransform(NotShared<DOMFloat32Array> m) {
-  DCHECK_EQ(m->length(), 16u);
+  CHECK_EQ(m->length(), 16u);
   return gfx::Transform::ColMajorF(m->Data());
 }
 
-gfx::Transform WTFFloatVectorToTransform(const Vector<float>& m) {
-  DCHECK_EQ(m.size(), 16u);
-  return gfx::Transform::ColMajorF(m.data());
-}
-
 // Normalize to have length = 1.0
 DOMPointReadOnly* makeNormalizedQuaternion(double x,
                                            double y,
diff --git a/third_party/blink/renderer/modules/xr/xr_utils.h b/third_party/blink/renderer/modules/xr/xr_utils.h
index 87b93d8..f61aa18d6 100644
--- a/third_party/blink/renderer/modules/xr/xr_utils.h
+++ b/third_party/blink/renderer/modules/xr/xr_utils.h
@@ -31,8 +31,6 @@
 
 gfx::Transform DOMFloat32ArrayToTransform(NotShared<DOMFloat32Array>);
 
-gfx::Transform WTFFloatVectorToTransform(const Vector<float>&);
-
 DOMPointReadOnly* makeNormalizedQuaternion(double x,
                                            double y,
                                            double z,
diff --git a/third_party/blink/renderer/modules/xr/xr_view.cc b/third_party/blink/renderer/modules/xr/xr_view.cc
index af482ed..dcd02ac 100644
--- a/third_party/blink/renderer/modules/xr/xr_view.cc
+++ b/third_party/blink/renderer/modules/xr/xr_view.cc
@@ -81,7 +81,8 @@
     // A page may take the projection matrix value and detach it so
     // projection_matrix_ is a detached array buffer.  This breaks the
     // inspector, so return an empty array instead.
-    return NotShared<DOMFloat32Array>(DOMFloat32Array::Create(0));
+    projection_matrix_ =
+        transformationMatrixToDOMFloat32Array(view_data_->ProjectionMatrix());
   }
 
   return projection_matrix_;
diff --git a/third_party/blink/renderer/modules/xr/xr_view.h b/third_party/blink/renderer/modules/xr/xr_view.h
index f90f85a5..549c0895 100644
--- a/third_party/blink/renderer/modules/xr/xr_view.h
+++ b/third_party/blink/renderer/modules/xr/xr_view.h
@@ -74,7 +74,10 @@
   // The transform from the view to the reference space requested by
   // XRFrame::getViewerPose.
   Member<XRRigidTransform> ref_space_from_view_;
-  NotShared<DOMFloat32Array> projection_matrix_;
+  // This is just a cached/converted version of the projection matrix from the
+  // view_data. It's mutable so that we can update it when queried if it was
+  // detached.
+  mutable NotShared<DOMFloat32Array> projection_matrix_;
   Member<XRViewport> viewport_;
 };
 
Loading diff…

Original Bug Report

reported by ig...@aisle.com

Potential out-of-bounds read in Transform::ColMajorF on undersized buffer

Security Bug

We have discovered a potential issue in Chromium that lets WebXR content trigger an out-of-bounds read in the renderer by feeding a zero-length buffer into gfx::Transform::ColMajorF.

Vulnerability Details

  • Transform::ColMajorF assumes the caller has provided 16 floats and blindly reads indices 0–15; the surrounding UNSAFE_TODO macros do not add checks.
  • When a page detaches the cached matrix buffer on an XRRigidTransform, Blink currently hands XRRay a fresh zero-length DOMFloat32Array, which is then forwarded to Transform::ColMajorF without validation.
  • This results in a 64-byte read past the view, leaking heap data back to script via XRRay’s origin/direction, or crashing on debug builds.

Code Snippets:

// ui/gfx/geometry/transform.cc (lines 115-130, current HEAD)
Transform Transform::ColMajorF(const float a[16]) {
  if (AllTrue(Float4{UNSAFE_TODO(a[1]), UNSAFE_TODO(a[2]), UNSAFE_TODO(a[3]),
                     UNSAFE_TODO(a[4])} == Float4{0, 0, 0, 0} &
              Float4{UNSAFE_TODO(a[6]), UNSAFE_TODO(a[7]), UNSAFE_TODO(a[8]),
                     UNSAFE_TODO(a[9])} == Float4{0, 0, 0, 0} &
              Float4{UNSAFE_TODO(a[10]), UNSAFE_TODO(a[11]), UNSAFE_TODO(a[14]),
                     UNSAFE_TODO(a[15])} == Float4{1, 0, 0, 1})) {
    return Transform(a[0], UNSAFE_TODO(a[5]), UNSAFE_TODO(a[12]),
                     UNSAFE_TODO(a[13]));
  }
  return Transform(a[0], UNSAFE_TODO(a[1]), UNSAFE_TODO(a[2]),
                   UNSAFE_TODO(a[3]), UNSAFE_TODO(a[4]), UNSAFE_TODO(a[5]),
                   UNSAFE_TODO(a[6]), UNSAFE_TODO(a[7]), UNSAFE_TODO(a[8]),
                   UNSAFE_TODO(a[9]), UNSAFE_TODO(a[10]), UNSAFE_TODO(a[11]),
                   UNSAFE_TODO(a[12]), UNSAFE_TODO(a[13]), UNSAFE_TODO(a[14]),
                   UNSAFE_TODO(a[15]));
}
// third_party/blink/renderer/modules/xr/xr_rigid_transform.cc (lines 115-120, current HEAD)
if (!matrix_array_ || matrix_array_->IsDetached()) {
  // A page may take the matrix_array_ value and detach it so matrix_array_ is
  // a detached array buffer.  This breaks the inspector, so return an empty
  // array instead.
  return NotShared<DOMFloat32Array>(DOMFloat32Array::Create(0));
}
// third_party/blink/renderer/modules/xr/xr_ray.cc (lines 38-40, current HEAD)
XRRay::XRRay(XRRigidTransform* transform, ExceptionState& exception_state) {
  NotShared<DOMFloat32Array> m = transform->matrix();
  Set(DOMFloat32ArrayToTransform(m), exception_state);
}
// third_party/blink/renderer/modules/xr/xr_utils.cc (lines 25-28, current HEAD)
gfx::Transform DOMFloat32ArrayToTransform(NotShared<DOMFloat32Array> m) {
  DCHECK_EQ(m->length(), 16u);
  return gfx::Transform::ColMajorF(m->Data());
}

VERSION

  • Chrome Version: Chromium 143.0.7470.0, Chromium 141.0.7390.65 built on Debian GNU/Linux 13 (trixie)
  • Operating System: Debian GNU/Linux 13 (trixie)

REPRODUCTION CASE - DEBUG BUILD

  1. Launch Chromium/Chrome debug
  2. In a renderer console:
    const t = new XRRigidTransform();
    const arr = t.matrix;
    const ch = new MessageChannel();
    ch.port1.postMessage(arr.buffer, [arr.buffer]); // detaches arr
    const ray = new XRRay(t);                       // triggers ColMajorF read
    
  3. Observe FATAL:third_party/blink/renderer/modules/xr/xr_utils.cc:26] DCHECK failed: m->length() == 16u (0 vs. 16)

REPRODUCTION CASE - OOB READ

  1. Launch Chromium/Chrome release
  2. Run poc-min-leak.js
  3. Observe leaked marked data

Similar Findings (related patterns in current code)

  • XRView::projectionMatrix() returns a zero-length DOMFloat32Array when detached, mirroring the XRRigidTransform::matrix() behavior. While not currently passed to Transform::ColMajorF, this could become exploitable if used in transform code.
    • Location: third_party/blink/renderer/modules/xr/xr_view.cc lines 79–85
  • WTFFloatVectorToTransform(const Vector<float>&) forwards raw m.data() to Transform::ColMajorF with only a DCHECK on size. This is currently unused but should be hardened to prevent future misuse.
    • Location: third_party/blink/renderer/modules/xr/xr_utils.cc lines 30–33

CREDIT INFORMATION

Reporter credit: Aisle Research

View on issue tracker
Uploaded PoC files