Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebXR
DescriptionOut of bounds read in WebXR
ComponentWebXR
Bug ClassOOB
Tracker452071845
Fix commit6c5963ad6b45 (chromium/src) +8/-18
CISA KEVNot listed
CreditedAisle Research
Disclosed2025-10-28

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
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