Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentARM64
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker530663440
Fix commit2c3e7f7d4d65 (v8/v8) +56/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-02 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-08-06
Patch gap9 days — fix public in source before the advisory

Background

Write barrier
A code sequence the garbage collector requires whenever a pointer into the heap is stored, recording the mutated slot so the GC can keep its remembered sets and mark state consistent.
Register aliasing
On arm64 the same physical register is exposed as both a 64-bit X view and a 32-bit W view, so w3 and x3 are distinct Register objects that nonetheless refer to the same hardware register.
`MoveObjectAndSlot`
A MacroAssembler helper that, given a destination object register, a destination slot register, and an Operand offset, computes the slot address (object + offset) while carefully arranging which register may be overwritten.
`Operand` extend
An arm64 addressing feature (UXTW, SXTW, etc.) that lets a 32-bit W register be used as an offset with automatic extension, meaning the offset register can legitimately be the W alias of a register already in use.

Root Cause Analysis

The MoveObjectAndSlot helper in the arm64 MacroAssembler decides how to compute a slot address based on whether the offset register overlaps the object registers, and it performed those overlap checks with plain Register inequality (offset.reg() != object). That comparison is exact and treats w3 as different from x3, so when the write-barrier emission supplied the offset as a W register that was the 32-bit alias of the destination object register, the code took the “no overlap” branch and clobbered the register before it had finished using its value.

The fix replaces every exact comparison with Register::Aliases, which returns true when two register operands map to the same underlying hardware register regardless of width, and it rewrites the register-swap sequence to add and subtract the full offset operand (respecting its extend) instead of the bare dst_object register. This restores the invariant that the offset register is never treated as free while it still holds a needed value, so the computed slot address passed into the write barrier is correct. Because a wrong slot address corrupts the GC’s remembered-set bookkeeping, the corrected aliasing checks prevent the collector from later operating on a stale or freed object.

Key insight
The single mistake was comparing arm64 registers for identity with != instead of Aliases, which let the 32-bit W alias of an in-use X register slip through as “non-overlapping”; the fix uses Register::Aliases (and reuses the full offset operand in the swap) so width-aliased registers are correctly recognized as the same register.

Attack Path

  1. Craft a WebAssembly GC array Build a module with an anyref array type and export helpers mkarr and fill, then allocate an array and force it into old space with repeated gc() calls so a real write barrier is required on subsequent stores.
  2. Trigger the aliased offset emission Invoke array.fill (kExprArrayFill) on the promoted array so the arm64 backend emits MoveObjectAndSlot with the element offset passed as a UXTW-extended W register that aliases the destination object register.
  3. Miscompute the slot address The faulty offset.reg() != dst_object check takes the wrong branch and overwrites the object register with the extend applied incorrectly, so the write barrier records or updates the wrong slot address.
  4. Corrupt GC metadata The garbage collector acts on the bad slot, leaving a live reference untracked or a remembered-set entry pointing at the wrong memory, which lets an object be freed while still reachable.
  5. Exploit the dangling reference A subsequent access to the prematurely freed object yields a use-after-free that an attacker grooms into a controlled read/write primitive.

Impact Assessment

An attacker who runs crafted WebAssembly in the renderer gains a use-after-free in V8’s heap, achievable from a single malicious web page with no special privileges on arm64 targets. The corrupted write-barrier bookkeeping can be shaped into arbitrary read/write within the renderer process, forming a strong primitive toward full renderer compromise and, chained with a sandbox escape, remote code execution. The precondition is execution on an arm64 build where MoveObjectAndSlot is emitted with a W-aliased offset register, as produced by the array.fill path in the regression test.

Changed Functions

FunctionChangeNotes
if
test/unittests/assembler/macro-assembler-arm64-unittest.cc
modified

Files Changed

  • test/unittests/assembler/macro-assembler-arm64-unittest.cc
  • src/codegen/arm64/macro-assembler-arm64.cc
  • test/mjsunit/regress/wasm/regress-530663440.js

Audit Directions

  • Register identity comparisons
    Search arm64 (and other multi-width) codegen for ==/!= on Register operands and confirm they use Aliases wherever a W/X alias could reach the same code path.
  • Offset operand reuse
    Review helpers that overwrite an input register based on overlap checks, verifying they operate on the full Operand (including extend) rather than the bare register when a value is still live.
  • Write-barrier address computation
    Audit every backend site that computes a slot address for a write barrier, since a miscomputed slot silently corrupts remembered sets and manifests only later as a use-after-free.
From 2c3e7f7d4d65f274951d58eec078b2aacdfba58e Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <jkummerow@chromium.org>
Date: Tue, 28 Jul 2026 12:51:35 +0200
Subject: [PATCH] [arm64] Fix write barrier register aliasing

Use `r1.Aliases(r2)` to handle W and X registers correctly.

Fixed: 530663440
Change-Id: Ib17e8f186bbea17d2b845202399ce39d21dbf107
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8158501
Auto-Submit: Jakob Kummerow <jkummerow@chromium.org>
Reviewed-by: Manos Koukoutos <manoskouk@chromium.org>
Commit-Queue: Manos Koukoutos <manoskouk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108911}
---
diff --git a/src/codegen/arm64/macro-assembler-arm64.cc b/src/codegen/arm64/macro-assembler-arm64.cc
index 0da0f6d..f6af185 100644
--- a/src/codegen/arm64/macro-assembler-arm64.cc
+++ b/src/codegen/arm64/macro-assembler-arm64.cc
@@ -4223,7 +4223,7 @@
   ASM_CODE_COMMENT(this);
   DCHECK_NE(dst_object, dst_slot);
   // If `offset` is a register, it cannot overlap with `object`.
-  DCHECK_IMPLIES(!offset.IsImmediate(), offset.reg() != object);
+  DCHECK_IMPLIES(!offset.IsImmediate(), !offset.reg().Aliases(object));
 
   // If the slot register does not overlap with the object register, we can
   // overwrite it.
@@ -4237,19 +4237,19 @@
 
   // If the destination object register does not overlap with the offset
   // register, we can overwrite it.
-  if (offset.IsImmediate() || (offset.reg() != dst_object)) {
+  if (offset.IsImmediate() || !offset.reg().Aliases(dst_object)) {
     Mov(dst_object, dst_slot);
     Add(dst_slot, dst_slot, offset);
     return;
   }
 
-  DCHECK_EQ(dst_object, offset.reg());
+  DCHECK(dst_object.Aliases(offset.reg()));
 
   // We only have `dst_slot` and `dst_object` left as distinct registers so we
   // have to swap them. We write this as a add+sub sequence to avoid using a
   // scratch register.
-  Add(dst_slot, dst_slot, dst_object);
-  Sub(dst_object, dst_slot, dst_object);
+  Add(dst_slot, dst_slot, offset);
+  Sub(dst_object, dst_slot, offset);
 }
 
 // If lr_status is kLRHasBeenSaved, lr will be clobbered.
diff --git a/test/mjsunit/regress/wasm/regress-530663440.js b/test/mjsunit/regress/wasm/regress-530663440.js
new file mode 100644
index 0000000..6e67423
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-530663440.js
@@ -0,0 +1,36 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --expose-gc
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const arrT = builder.addArray(kWasmAnyRef);
+
+let fill_sig = makeSig(
+    [kWasmI32, kWasmI32, kWasmI32, kWasmI32, wasmRefType(arrT), kWasmAnyRef],
+    []);
+builder.addFunction('fill', fill_sig).exportFunc().addBody([
+    kExprLocalGet, 4,
+    kExprLocalGet, 0,
+    kExprI32Eqz,
+    kExprLocalGet, 5,
+    kExprI32Const, 1,
+    kGCPrefix, kExprArrayFill, arrT,
+  ]);
+
+builder.addFunction('mkarr', makeSig([kWasmI32], [kWasmAnyRef])).exportFunc()
+  .addBody([
+    kExprLocalGet, 0,
+    kGCPrefix, kExprArrayNewDefault, arrT,
+  ]);
+
+const instance = builder.instantiate();
+
+const arr = instance.exports.mkarr(4);
+gc();  // Promote {arr} to old space.
+gc();
+
+instance.exports.fill(1, 0, 0, 0, arr, {});
diff --git a/test/unittests/assembler/macro-assembler-arm64-unittest.cc b/test/unittests/assembler/macro-assembler-arm64-unittest.cc
index 26a1e3b..2ef1b31 100644
--- a/test/unittests/assembler/macro-assembler-arm64-unittest.cc
+++ b/test/unittests/assembler/macro-assembler-arm64-unittest.cc
@@ -141,25 +141,33 @@
   Register dst_slot;
   Register object;
   Register offset_register = no_reg;
+  Extend offset_extend = NO_EXTEND;
 };
 
 const MoveObjectAndSlotTestCase kMoveObjectAndSlotTestCases[] = {
     {"no overlap", x0, x1, x2},
     {"no overlap", x0, x1, x2, x3},
+    {"no overlap", x0, x1, x2, w3, UXTW},
 
     {"object == dst_object", x2, x1, x2},
     {"object == dst_object", x2, x1, x2, x3},
+    {"object == dst_object", x2, x1, x2, w3, UXTW},
 
     {"object == dst_slot", x1, x2, x2},
     {"object == dst_slot", x1, x2, x2, x3},
+    {"object == dst_slot", x1, x2, x2, w3, UXTW},
 
     {"offset == dst_object", x0, x1, x2, x0},
+    {"offset == dst_object", x0, x1, x2, w0, UXTW},
 
     {"offset == dst_object && object == dst_slot", x0, x1, x1, x0},
+    {"offset == dst_object && object == dst_slot", x0, x1, x1, w0, UXTW},
 
     {"offset == dst_slot", x0, x1, x2, x1},
+    {"offset == dst_slot", x0, x1, x2, w1, UXTW},
 
-    {"offset == dst_slot && object == dst_object", x0, x1, x0, x1}};
+    {"offset == dst_slot && object == dst_object", x0, x1, x0, x1},
+    {"offset == dst_slot && object == dst_object", x0, x1, x0, w1, UXTW}};
 
 // Make sure we include offsets that cannot be encoded in an add instruction.
 const int kOffsets[] = {0, 42, kMaxRegularHeapObjectSize, 0x101001};
@@ -194,7 +202,12 @@
         offset_operand = Operand(offset);
       } else {
         __ Mov(test_case.offset_register, Operand(offset));
-        offset_operand = Operand(test_case.offset_register);
+        if (test_case.offset_extend == NO_EXTEND) {
+          offset_operand = Operand(test_case.offset_register);
+        } else {
+          offset_operand =
+              Operand(test_case.offset_register, test_case.offset_extend);
+        }
       }
 
       std::stringstream comment;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/wasm/regress-530663440.js b/test/mjsunit/regress/wasm/regress-530663440.js
new file mode 100644
index 0000000..6e67423
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-530663440.js
@@ -0,0 +1,36 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --expose-gc
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const arrT = builder.addArray(kWasmAnyRef);
+
+let fill_sig = makeSig(
+    [kWasmI32, kWasmI32, kWasmI32, kWasmI32, wasmRefType(arrT), kWasmAnyRef],
+    []);
+builder.addFunction('fill', fill_sig).exportFunc().addBody([
+    kExprLocalGet, 4,
+    kExprLocalGet, 0,
+    kExprI32Eqz,
+    kExprLocalGet, 5,
+    kExprI32Const, 1,
+    kGCPrefix, kExprArrayFill, arrT,
+  ]);
+
+builder.addFunction('mkarr', makeSig([kWasmI32], [kWasmAnyRef])).exportFunc()
+  .addBody([
+    kExprLocalGet, 0,
+    kGCPrefix, kExprArrayNewDefault, arrT,
+  ]);
+
+const instance = builder.instantiate();
+
+const arr = instance.exports.mkarr(4);
+gc();  // Promote {arr} to old space.
+gc();
+
+instance.exports.fill(1, 0, 0, 0, arr, {});
diff --git a/test/unittests/assembler/macro-assembler-arm64-unittest.cc b/test/unittests/assembler/macro-assembler-arm64-unittest.cc
index 26a1e3b..2ef1b31 100644
--- a/test/unittests/assembler/macro-assembler-arm64-unittest.cc
+++ b/test/unittests/assembler/macro-assembler-arm64-unittest.cc
@@ -141,25 +141,33 @@
   Register dst_slot;
   Register object;
   Register offset_register = no_reg;
+  Extend offset_extend = NO_EXTEND;
 };
 
 const MoveObjectAndSlotTestCase kMoveObjectAndSlotTestCases[] = {
     {"no overlap", x0, x1, x2},
     {"no overlap", x0, x1, x2, x3},
+    {"no overlap", x0, x1, x2, w3, UXTW},
 
     {"object == dst_object", x2, x1, x2},
     {"object == dst_object", x2, x1, x2, x3},
+    {"object == dst_object", x2, x1, x2, w3, UXTW},
 
     {"object == dst_slot", x1, x2, x2},
     {"object == dst_slot", x1, x2, x2, x3},
+    {"object == dst_slot", x1, x2, x2, w3, UXTW},
 
     {"offset == dst_object", x0, x1, x2, x0},
+    {"offset == dst_object", x0, x1, x2, w0, UXTW},
 
     {"offset == dst_object && object == dst_slot", x0, x1, x1, x0},
+    {"offset == dst_object && object == dst_slot", x0, x1, x1, w0, UXTW},
 
     {"offset == dst_slot", x0, x1, x2, x1},
+    {"offset == dst_slot", x0, x1, x2, w1, UXTW},
 
-    {"offset == dst_slot && object == dst_object", x0, x1, x0, x1}};
+    {"offset == dst_slot && object == dst_object", x0, x1, x0, x1},
+    {"offset == dst_slot && object == dst_object", x0, x1, x0, w1, UXTW}};
 
 // Make sure we include offsets that cannot be encoded in an add instruction.
 const int kOffsets[] = {0, 42, kMaxRegularHeapObjectSize, 0x101001};
@@ -194,7 +202,12 @@
         offset_operand = Operand(offset);
       } else {
         __ Mov(test_case.offset_register, Operand(offset));
-        offset_operand = Operand(test_case.offset_register);
+        if (test_case.offset_extend == NO_EXTEND) {
+          offset_operand = Operand(test_case.offset_register);
+        } else {
+          offset_operand =
+              Operand(test_case.offset_register, test_case.offset_extend);
+        }
       }
 
       std::stringstream comment;
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Missed write-barrier UAF via register aliasing flaw in ARM64 MoveObjectAndSlot

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports.

Overview: In the ARM64 macro-assembler, MoveObjectAndSlot incorrectly uses CPURegister::operator!= to check for register aliasing. Because this operator includes a register size check, a 32-bit W-register offset mis-evaluates as distinct from its 64-bit X-register counterpart. This causes Liftoff write barriers to clobber the offset register, passing a corrupted slot address to the RecordWrite stub and potentially leading to an in-cage Use-After-Free.

Affected files:

  • src/codegen/arm64/macro-assembler-arm64.cc
  • src/codegen/arm64/register-arm64.h

1. Summary of the Issue (Meant for Human Triage)

A potential layer-1 vulnerability exists in V8’s ARM64 macro-assembler due to a flawed register aliasing check in MacroAssembler::MoveObjectAndSlot. The function uses the standard equality operator (operator!=) on CPURegister objects to determine whether the offset register aliases the destination dst_object register before emitting instructions to set up the write barrier stub arguments.

However, CPURegister::operator== strictly compares the register’s size (reg_size_) alongside its code and type. Consequently, a 32-bit W register and a 64-bit X register with the same physical register code (e.g., w1 and x1) will evaluate as unequal (w1 != x1 returns true).

When V8’s baseline WebAssembly compiler (Liftoff) emits a write barrier for a Wasm GC array operation (like array.fill) using a 32-bit offset (e.g., Operand(w1, UXTW)), it passes this Operand down to the RecordWrite stub. If the register allocator assigns x1 as the destination object and x5 as the slot, MoveObjectAndSlot mis-evaluates the aliasing condition and mistakenly clobbers the w1 offset register with the 64-bit array pointer before the offset is read. This forces the RecordWrite stub to calculate and record a corrupted slot address (cage_base + 2 * compressed(array)). Because the correct slot is never recorded in the garbage collector’s remembered set, any young-generation object written to the array will not be tracked. Upon the next minor GC, this unrecorded old-to-young edge leads to a stale pointer, exposing an in-cage Use-After-Free (UAF) primitive.

2. Proof-of-Concept & Detailed Execution Flow

Note: Our tooling agent does not have the ability to run code. The following execution flow traces the verified mechanism in the codebase, and the steps below represent the potential attack path to trigger the vulnerability.

The Defect: The issue resides in src/codegen/arm64/macro-assembler-arm64.cc inside MoveObjectAndSlot:

// src/codegen/arm64/macro-assembler-arm64.cc:4221-4253
void MacroAssembler::MoveObjectAndSlot(Register dst_object, Register dst_slot,
                                       Register object, Operand offset) {
  ...
  DCHECK_IMPLIES(!offset.IsImmediate(), offset.reg() != object); // Passes

  if (dst_slot != object) { ... return; }                         // Skips if equal
  DCHECK_EQ(dst_slot, object);

  // BUG: operator!= checks reg_size_. w1 != x1 evaluates to true.
  if (offset.IsImmediate() || (offset.reg() != dst_object)) {
    Mov(dst_object, dst_slot);     // x1 := x5 (Clobbers w1!)
    Add(dst_slot, dst_slot, offset); // reads clobbered w1
    return;
  }
  ...
}

The check relies on operator!=, defined in src/codegen/arm64/register-arm64.h:160-165:

constexpr bool operator==(const CPURegister& other) const {
  return RegisterBase::operator==(other) && reg_size_ == other.reg_size_ &&
         reg_type_ == other.reg_type_;
}

Because w1.reg_size_ == 32 and x1.reg_size_ == 64, w1 != x1 evaluates to true.

Potential Trigger Steps & Execution Trace:

  1. An attacker compiles a Wasm module defining an array of mutable references: (type $arr (array (mut anyref))).
  2. The attacker grooms the V8 sandbox memory to allocate an ArrayBuffer backing store mapped into the [4GB, 8GB) range, filling it with forged 64-bit values that decode as old-generation, non-shared heap pages.
  3. The attacker instantiates a Wasm array of type $arr, ensuring it survives to the old generation.
  4. The attacker crafts a function invoking array.fill on the old-generation array, filling it with a young-generation reference.
  5. The attacker shapes the preceding Wasm instructions to force Liftoff to allocate the array object to the 5th general-purpose parameter (kGpParamRegisters[5]=x5) and the 32-bit index operand to the 1st return value of a preceding call (kGpReturnRegisters[1]=x1).
  6. The attacker ensures incremental marking is active (v8_flags.incremental_marking).
  7. During Liftoff compilation, ArrayFillImpl invokes StoreTaggedPointer(dst_addr=x5, offset_reg=x1, offset_imm=0, src, ...) (src/wasm/baseline/arm64/liftoff-assembler-arm64-inl.h:694).
  8. Liftoff constructs a 32-bit offset Operand(offset_reg.W(), UXTW) which resolves to Operand(w1, UXTW).
  9. The store is emitted: StoreTaggedField(src, MemOperand(x5, Operand(w1, UXTW))). The young-generation reference is written to the correct array slot.
  10. A write barrier is emitted: CallRecordWriteStubSaveRegisters(x5, Operand(w1, UXTW), ...).
  11. The generated code enters MoveObjectAndSlot to arrange arguments. dst_object must be x1 and dst_slot must be x5 (WriteBarrierDescriptor::registers()). MoveObjectAndSlot receives dst_object=x1, dst_slot=x5, object=x5, offset=Operand(w1, UXTW).
  12. The check offset.reg() != dst_object evaluates w1 != x1, which erroneously evaluates to true.
  13. Mov(x1, x5) executes. The 64-bit array pointer in x5 clobbers the lower 32 bits (w1) in x1.
  14. Add(x5, x5, w1, uxtw) executes. x5 becomes x5 + UXTW(w1). Due to pointer compression, x5 stores cage_base + 2 * compressed(array) + 2 * kHeapObjectTag.
  15. The RecordWrite stub (src/builtins/builtins-internal-gen.cc:242) receives this corrupted slot address.
  16. Because the address lands within the groomed ArrayBuffer in the [4GB, 8GB) range, the loaded value decodes as an old-generation, non-shared page. InYoungGeneration (:367) and InSharedHeap (:376) both return false.
  17. The stub falls through to IncrementalWriteBarrierMinor (:422), which takes no action on old-gen values. The stub returns cleanly without crashing and without recording the actual array slot.
  18. On the next minor GC, the young-generation reference is scavenged/reclaimed. Because the slot was not recorded, it is not updated by the GC, leaving a dangling stale pointer.
  19. A subsequent Wasm array.get instruction dereferences the stale pointer, achieving an in-cage UAF primitive.

Suggested Fix: In src/codegen/arm64/macro-assembler-arm64.cc, line 4240 (and 4226) should be updated to use CPURegister::Aliases instead of !=:

  if (offset.IsImmediate() || (!offset.reg().Aliases(dst_object))) {

Additionally, the swap-case at :4251-4252 must be adjusted. It currently naively assumes dst_object and offset are identical without their extension/shift operand modifiers. A complete fix requires converting both != checks to !Aliases and utilizing a scratch register to safely handle extended operands during swaps.

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

Prior Critic Verdict: “The report accurately details a layer-1 bug in MacroAssembler::MoveObjectAndSlot on arm64 (classes H and I). The bug stems from using CPURegister::operator!=, which checks reg_size_, to determine if the offset register aliases the destination object register. A W-register offset (e.g., w1) will not match its corresponding X-register (x1). When Liftoff emits a write barrier and passes a W-register offset with UXTW (e.g. in array.fill), MoveObjectAndSlot mis-evaluates the condition and clobbers the offset register with a Mov before reading it with an Add.

Consequently, the RecordWrite stub receives the wrong slot address (array + compressed(array) instead of array + offset). The correct slot is never recorded in the remembered set. This unrecorded old-to-young edge leads to a stale pointer and an in-cage Use-After-Free (UAF) upon the next minor GC.

This is a layer-1 write primitive on a tier-1 architecture with default flags. The baseline severity for an in-cage UAF is high (1). However, the report correctly applies the ‘contrived-but-plausible’ modifier, downgrading the severity to medium (2) due to two significant constraints:

  1. Reachability depends on a highly specific Liftoff register allocation (dst_addr=x5 and offset_reg=x1). While the report’s trace of PopToRegister makes this theoretically plausible, achieving it requires a precisely crafted Wasm module.
  2. The miscalculated slot address often triggers a release crash (e.g., failing the CSA_CHECK at builtins-internal-gen.cc:211 or hitting an unmapped page), narrowing the exploitation window. Exploitation requires incremental marking to be active and the garbage slot to reside within the mapped cage and decode as an old-gen non-shared page.

All codebase citations, including the operator!= semantics and the MoveObjectAndSlot logic, were verified. The severity rating is sound and adheres to the classification criteria.”

Audit Logs & Code-Reachability Proofs:

  • CPURegister::operator!=: Confirmed at src/codegen/arm64/register-arm64.h:160-165. Compares reg_size_. Aliases() is present at :156 and correctly ignores reg_size_.
  • Operand Registration: Operand::reg() verified to return the exact 32-bit W register instance at src/codegen/arm64/assembler-arm64-inl.h:375-378.
  • Liftoff Offset Emission: Verified at src/wasm/baseline/arm64/liftoff-assembler-arm64-inl.h:699. When offset_imm==0, Liftoff explicitly creates Operand(offset_reg.W(), UXTW).
  • ArrayFillImpl Logic: Confirmed at src/wasm/baseline/liftoff-compiler.cc:11020 calling StoreObjectField(..., obj.gp(), offset.gp(), 0, ...), mapping perfectly to the offset_imm==0 trigger condition.
  • Register Assignment Trace: Verified kGpParamRegisters[5]=x5 and kGpReturnRegisters[1]=x1 at src/wasm/wasm-linkage.h:65-66. Liftoff’s PopToRegister and PopToModifiableRegister deterministically assign based on virtual stack cache locations (src/wasm/baseline/liftoff-assembler.h:433-452).
  • WriteBarrier Stub Fast-Paths: Traced via builtins-internal-gen.cc:362 through :422. Incremental marking triggers InYoungGeneration(value, ...) and InSharedHeap(value, ...). If a controlled value (via ArrayBuffer overlap) is read that tests false for both, the stub returns cleanly without invoking CSA_CHECK(bucket_index < num_buckets) at :211.
  • Environmental Assumptions: The severity evaluation relies on standard 64-bit sandbox pointer compression behavior where low32(x5) == compressed(array).
  • Limitation Caveat: The validator acknowledges [repro-not-constructed] regarding the exact Wasm bytecode sequence required to force x5 and x1 allocation, but accepts it as mechanistically feasible through Liftoff’s single-pass cache state resolution.

Evaluated with target repo at version: HEAD (Google3)


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time.

Note: This bug has been automatically redirected to the top-level Chromium component. Please move it to the actual component: https://b.corp.google.com/components/1456566 once PoCs have been generated.

View on issue tracker