CVE-2025-12443
Overview
Background
- `XRRigidTransform.matrix` / `XRView.projectionMatrix`
- WebXR attributes that return a cached 16-element
Float32Array(column-major 4x4 matrix) built from internalgfx::Transformor view data. - Detached `ArrayBuffer`
- Script can detach a typed array’s backing buffer (for example by transferring it with
postMessageorstructuredClone). 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.ccthat builds agfx::Transformby reading 16 floats from aDOMFloat32Arraywithgfx::Transform::ColMajorF(m->Data()), without checking the length. - `DCHECK` versus `CHECK`
DCHECKis compiled out of release builds, whileCHECKis 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.
DCHECK. The patch recomputes a valid 16-element array and makes the length check a release CHECK.Attack Path
- Obtain a matrix
Script reads
XRRigidTransform.matrix(orXRView.projectionMatrixduring an XR session), which caches aFloat32Arrayinmatrix_array_orprojection_matrix_. - Detach its buffer
The page transfers the array’s
ArrayBuffer, which detaches the cached object. - Getter returns an empty array
On the next access, the pre-patch getter saw
IsDetached()and returnedDOMFloat32Array::Create(0)instead of rebuilding the matrix. - Length-trusting conversion
A Blink path that turns that result back into a transform with
DOMFloat32ArrayToTransformreads 16 floats from the 0-length array, because only aDCHECKguarded it (the specific caller is inferred).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/xr/xr_rigid_transform.cc |
modified |
Files Changed
third_party/blink/renderer/modules/xr/xr_rigid_transform.ccthird_party/blink/renderer/modules/xr/xr_utils.ccthird_party/blink/renderer/modules/xr/xr_utils.hthird_party/blink/renderer/modules/xr/xr_view.ccthird_party/blink/renderer/modules/xr/xr_view.h
Audit Directions
- Detached-buffer fallbacks that return wrong-shaped dataSearch Blink for getters that return
DOMFloat32Array::Create(0)(or similar empty arrays) whenIsDetached()is true, then follow each caller that assumes a fixed length (matrices, quaternions,DOMMatrixand WebGPU or WebGL helpers). - Fixed-length readers guarded only by `DCHECK`Look for
Data()ordata()pointers passed to fixed-size readers such asColMajorF,memcpyorstd::arraycopies where the length check is aDCHECK, and treat any script-controllable length as a release-build OOB risk. - Caching typed arrays exposed to scriptAny
MemberorNotSharedtyped-array cache that is handed to JavaScript can be detached. Check that internal code always uses the underlying C++ state (for example*matrix_orview_data_) rather than reading back the script-visible array.
Patch
From 6c5963ad6b4541f5fa0812607fd36a979c48c0f1 Mon Sep 17 00:00:00 2001 From: Alexander Cooper <[email protected]> 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 <[email protected]> Reviewed-by: Brandon Jones <[email protected]> Auto-Submit: Alexander Cooper <[email protected]> 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_; };
Original Bug Report
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::ColMajorFassumes 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 handsXRRaya fresh zero-lengthDOMFloat32Array, which is then forwarded toTransform::ColMajorFwithout 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
- Launch Chromium/Chrome debug
- 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 - Observe
FATAL:third_party/blink/renderer/modules/xr/xr_utils.cc:26] DCHECK failed: m->length() == 16u (0 vs. 16)
REPRODUCTION CASE - OOB READ
- Launch Chromium/Chrome release
- Run poc-min-leak.js
- Observe leaked marked data
Similar Findings (related patterns in current code)
XRView::projectionMatrix()returns a zero-lengthDOMFloat32Arraywhen detached, mirroring theXRRigidTransform::matrix()behavior. While not currently passed toTransform::ColMajorF, this could become exploitable if used in transform code.- Location:
third_party/blink/renderer/modules/xr/xr_view.cclines 79–85
- Location:
WTFFloatVectorToTransform(const Vector<float>&)forwards rawm.data()toTransform::ColMajorFwith 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.cclines 30–33
- Location:
CREDIT INFORMATION
Reporter credit: Aisle Research