Chrome · V8 › IC
CVE-2026-113222
UAF in V8 (IC)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/ic/ic.cc |
modified | |
settest/mjsunit/regress/regress-566091009.js |
modified |
Files Changed
src/ic/ic.cctest/mjsunit/regress/regress-566091009.js
Patch
From 243b68bea40f3165551ee9fe0cc51826b81d7efa Mon Sep 17 00:00:00 2001 From: Igor Sheludko <[email protected]> Date: Fri, 09 Oct 2026 11:52:18 +0200 Subject: [PATCH] [ic] Check access check and array prototype chain in element handlers Ensure that element handler computation in KeyedLoadIC::LoadElementHandler and KeyedStoreIC::StoreElementHandler returns slow handlers for receiver maps that require access checks or belong to the Array prototype chain. Fixed: 566091009 TAG=agy CONV=be615bab-5e7a-4aa3-a0cc-c6325a9e2477 Change-Id: Idff0ae62aa44fd67c69dbf1940720b61203de5b9 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8547996 Commit-Queue: Igor Sheludko <[email protected]> Auto-Submit: Igor Sheludko <[email protected]> Reviewed-by: Toon Verwaest <[email protected]> Commit-Queue: Toon Verwaest <[email protected]> Cr-Commit-Position: refs/heads/main@{#110514} --- diff --git a/src/ic/ic.cc b/src/ic/ic.cc index 75f487b..857564c 100644 --- a/src/ic/ic.cc +++ b/src/ic/ic.cc @@ -1761,17 +1761,6 @@ Handle<Object> KeyedLoadIC::LoadElementHandler( DirectHandle<Map> receiver_map, KeyedAccessLoadMode new_load_mode, MaybeDirectHandle<Map> maybe_transition_target) { - // Has a getter interceptor, or is any has and has a query interceptor. - if (receiver_map->has_indexed_interceptor() && - (receiver_map->GetIndexedInterceptor()->has_getter() || - (IsAnyHas() && receiver_map->GetIndexedInterceptor()->has_query())) && - !receiver_map->GetIndexedInterceptor()->non_masking()) { - // TODO(jgruber): Update counter name. - TRACE_HANDLER_STATS(isolate(), KeyedLoadIC_LoadIndexedInterceptorStub); - return IsAnyHas() ? BUILTIN_CODE(isolate(), HasIndexedInterceptorIC) - : BUILTIN_CODE(isolate(), LoadIndexedInterceptorIC); - } - InstanceType instance_type = receiver_map->instance_type(); if (instance_type < FIRST_NONSTRING_TYPE) { TRACE_HANDLER_STATS(isolate(), KeyedLoadIC_LoadIndexedStringDH); @@ -1785,6 +1774,23 @@ if (instance_type == JS_PROXY_TYPE) { return LoadHandler::LoadProxy(isolate()); } + if (receiver_map->is_access_check_needed()) { + TRACE_HANDLER_STATS(isolate(), KeyedLoadIC_SlowStub); + return LoadHandler::LoadSlow(isolate()); + } + + // Has a getter interceptor, or is any has and has a query interceptor. + if (receiver_map->has_indexed_interceptor() && + (receiver_map->GetIndexedInterceptor()->has_getter() || + (IsAnyHas() && receiver_map->GetIndexedInterceptor()->has_query())) && + !receiver_map->GetIndexedInterceptor()->non_masking()) { + DCHECK(!receiver_map->is_access_check_needed()); + // TODO(jgruber): Update counter name. + TRACE_HANDLER_STATS(isolate(), KeyedLoadIC_LoadIndexedInterceptorStub); + return IsAnyHas() ? BUILTIN_CODE(isolate(), HasIndexedInterceptorIC) + : BUILTIN_CODE(isolate(), LoadIndexedInterceptorIC); + } + #if V8_ENABLE_WEBASSEMBLY if (InstanceTypeChecker::IsWasmObject(instance_type)) { // TODO(jgruber): Update counter name. @@ -2871,7 +2877,8 @@ Handle<Object> KeyedStoreIC::StoreElementHandler( DirectHandle<Map> receiver_map, KeyedAccessStoreMode store_mode, MaybeDirectHandle<UnionOf<Smi, Cell>> prev_validity_cell) { - if (!IsJSObjectMap(*receiver_map)) { + if (!IsJSObjectMap(*receiver_map) || receiver_map->is_access_check_needed() || + receiver_map->IsMapInArrayPrototypeChain(isolate())) { // DefineKeyedOwnIC, which is used to define computed fields in instances, // should handled by the slow stub below instead of the proxy stub. if (IsJSProxyMap(*receiver_map) && !IsDefineKeyedOwnIC()) { diff --git a/test/mjsunit/regress/regress-566091009.js b/test/mjsunit/regress/regress-566091009.js new file mode 100644 index 0000000..aaf8c21 --- /dev/null +++ b/test/mjsunit/regress/regress-566091009.js @@ -0,0 +1,69 @@ +// 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: --no-lazy-feedback-allocation --allow-natives-syntax + +(function TestAccessCheck() { + const target = d8.test.createAccessCheckedObject(true); + target[0] = "secret"; + d8.test.setAccessPolicy(target, false); + + const namedObj = { a: 1 }; + + // 1. KeyedLoadIC: transition from named property access to element access. + function read(obj, key) { + return obj[key]; + } + read(namedObj, "a"); + assertThrows(() => read(target, "a"), TypeError); + read([1.1], 0); + assertThrows(() => read(target, 0), TypeError); + + // 2. KeyedHasIC: transition from named property access to element access. + function has(obj, key) { + return key in obj; + } + has(namedObj, "a"); + assertThrows(() => has(target, "a"), TypeError); + has([1.1], 0); + assertThrows(() => has(target, 0), TypeError); + + // 3. KeyedStoreIC: transition from named property access to element access. + function write(obj, key, val) { + obj[key] = val; + } + write(namedObj, "a", 2); + assertThrows(() => write(target, "a", 2), TypeError); + write([1.1], 0, 2.2); + assertThrows(() => write(target, 0, "overwritten"), TypeError); + + d8.test.setAccessPolicy(target, true); + assertEquals("secret", target[0]); +})(); + +(function TestNoElementsProtector() { + function readHole(a) { + return a[0]; + } + const holey = [, 1]; + %PrepareFunctionForOptimization(readHole); + assertEquals(undefined, readHole(holey)); + %OptimizeFunctionOnNextCall(readHole); + assertEquals(undefined, readHole(holey)); + + Object.defineProperty(Object.prototype, "prop", { + set(_) {}, + configurable: true, + }); + function write(o, k, v) { + o[k] = v; + } + write({ prop: 1 }, "prop", 1); + write(Array.prototype, "prop", 1); + write([], 0, 1); + write(Array.prototype, 0, "boom"); + + assertEquals("boom", holey[0]); + assertEquals("boom", readHole(holey)); +})();
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/regress/regress-566091009.js b/test/mjsunit/regress/regress-566091009.js
new file mode 100644
index 0000000..aaf8c21
--- /dev/null
+++ b/test/mjsunit/regress/regress-566091009.js
@@ -0,0 +1,69 @@
+// 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: --no-lazy-feedback-allocation --allow-natives-syntax
+
+(function TestAccessCheck() {
+ const target = d8.test.createAccessCheckedObject(true);
+ target[0] = "secret";
+ d8.test.setAccessPolicy(target, false);
+
+ const namedObj = { a: 1 };
+
+ // 1. KeyedLoadIC: transition from named property access to element access.
+ function read(obj, key) {
+ return obj[key];
+ }
+ read(namedObj, "a");
+ assertThrows(() => read(target, "a"), TypeError);
+ read([1.1], 0);
+ assertThrows(() => read(target, 0), TypeError);
+
+ // 2. KeyedHasIC: transition from named property access to element access.
+ function has(obj, key) {
+ return key in obj;
+ }
+ has(namedObj, "a");
+ assertThrows(() => has(target, "a"), TypeError);
+ has([1.1], 0);
+ assertThrows(() => has(target, 0), TypeError);
+
+ // 3. KeyedStoreIC: transition from named property access to element access.
+ function write(obj, key, val) {
+ obj[key] = val;
+ }
+ write(namedObj, "a", 2);
+ assertThrows(() => write(target, "a", 2), TypeError);
+ write([1.1], 0, 2.2);
+ assertThrows(() => write(target, 0, "overwritten"), TypeError);
+
+ d8.test.setAccessPolicy(target, true);
+ assertEquals("secret", target[0]);
+})();
+
+(function TestNoElementsProtector() {
+ function readHole(a) {
+ return a[0];
+ }
+ const holey = [, 1];
+ %PrepareFunctionForOptimization(readHole);
+ assertEquals(undefined, readHole(holey));
+ %OptimizeFunctionOnNextCall(readHole);
+ assertEquals(undefined, readHole(holey));
+
+ Object.defineProperty(Object.prototype, "prop", {
+ set(_) {},
+ configurable: true,
+ });
+ function write(o, k, v) {
+ o[k] = v;
+ }
+ write({ prop: 1 }, "prop", 1);
+ write(Array.prototype, "prop", 1);
+ write([], 0, 1);
+ write(Array.prototype, 0, "boom");
+
+ assertEquals("boom", holey[0]);
+ assertEquals("boom", readHole(holey));
+})();
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page