# Research on CVE-2026-19174

# Nghiên cứu CVE-2026-19174
## Lỗ hổng
Một lỗ hổng cực kì đơn giản xuất hiện trong V8 JavaScript engine đó là lỗi Interger Overflow ở trong WebAssembly code space reservation tồn tại trong 4 năm kể từ cuối năm 2022, điều bất ngờ là lỗ hổng này không phải nhận luồng nhập từ hacker hay bị tấn công, bản thân nó đã là bị lỗi ngay khi phát hành.


Đoạn code chứa lỗi:


```cpp
DeserializationUnit NativeModuleDeserializer::ReadCode(int fn_index,
                                                       Reader* reader) {
  uint8_t code_kind = reader->Read<uint8_t>();
  if (code_kind == kLazyFunction) {
    lazy_functions_.push_back(fn_index);
    return {};
  }
  if (code_kind == kEagerFunction) {
    eager_functions_.push_back(fn_index);
    return {};
  }

  int constant_pool_offset = reader->Read<int>();
  int safepoint_table_offset = reader->Read<int>();
  int handler_table_offset = reader->Read<int>();
  int code_comment_offset = reader->Read<int>();
  int unpadded_binary_size = reader->Read<int>();
  int stack_slot_count = reader->Read<int>();
  uint32_t tagged_parameter_slots = reader->Read<uint32_t>();
  int code_size = reader->Read<int>();
  int reloc_size = reader->Read<int>();
  int source_position_size = reader->Read<int>();
  int protected_instructions_size = reader->Read<int>();
  WasmCode::Kind kind = reader->Read<WasmCode::Kind>();
  ExecutionTier tier = reader->Read<ExecutionTier>();

  DCHECK(IsAligned(code_size, kCodeAlignment));
  DCHECK_GE(remaining_code_size_, code_size);
  if (current_code_space_.size() < static_cast<size_t>(code_size)) {
    // Allocate the next code space. Don't allocate more than 90% of
    // {kMaxCodeSpaceSize}, to leave some space for jump tables.
    size_t max_reservation = RoundUp<kCodeAlignment>(
        v8_flags.wasm_max_code_space_size_mb * MB * 9 / 10);
    size_t code_space_size = std::min(max_reservation, remaining_code_size_);
    std::tie(current_code_space_, current_jump_tables_) =
        native_module_->AllocateForDeserializedCode(code_space_size);
    DCHECK_EQ(current_code_space_.size(), code_space_size);
    CHECK(current_jump_tables_.is_valid());
  }
```

Nhìn qua hàm `NativeModuleDeserializer::ReadCode` có chức năng chính là giải mã và nạp mã máy của các hàm WebAssembly từ dữ liệu bộ nhớ đệm vào không gian bộ nhớ thực thi của V8.

Các chức năng cụ thể của nó như:
1. Đọc thuộc tính và kích thước hàm
2. Quản lý không gian bộ nhớ Code Space
3. Cấp phát bộ nhớ mới
4. Tạo đối tượng `DeserializationUnit`

Nhưng hàm này lại bị một lỗi nghiêm trọng


Cụ thể lỗi `interger overflow` ở đoạn code:


```cpp
...
size_t max_reservation = RoundUp<kCodeAlignment>(
        v8_flags.wasm_max_code_space_size_mb * MB * 9 / 10);
...
```


Cụ thể ở đây:
`v8_flags.wasm_max_code_space_size_mb` là một số nguyên không dấu 32-bit với giá trị hằng số là `1024 = 2^10`
`MB` là một `constexpr int` có giá trị hằng số `1024 * 1024 = 2^20`
Khi thực hiện phép tính này ta có:


```
2^10 * 2^20 * 9 = 9663676416
```
Tương đương với `921.6MiB`
Các biến số này đều là biến số 32-bit cho nên giá trị max của nó sẽ bằng:
```
2^32-1 = 4294967295 #tương ứng với việc tất cả đều là bit số 1 (tất cả 32bit)
```


![image](https://hackmd.io/_uploads/rkIMtOcYMe.png)
khi ta cố gắng nhân một số nguyên vượt quá giá trị biểu diễn của một thanh ghi như vậy
Thanh ghi chỉ lấy giá trị đúng 32bit từ bit nhỏ nhất trở đi


Tức giá trị `9663676416` sẽ không được lưu một cách đúng đắn trong thanh ghi (vì vượt quá giới hạn biểu diễn của số nguyên/của thanh ghi)
![image](https://hackmd.io/_uploads/S1aIYO5Kfl.png)
lúc này nó chỉ lấy 32bit kể từ bit nhỏ nhất tức giá trị:
```
01000000000000000000000000000000
```
Tức là:
![image](https://hackmd.io/_uploads/BJtXcuqKMx.png)
Đây là giá trị thực sự được lưu vào biến `max_reservation` là:
```
1073741824/10 = 107374182
```
Tương đương `102.4MiB`

Vậy ý đồ của lập trình viên là muốn tạo một con số cho biến `max_reservation = 966367641` nhưng giá trị thực tế mà nó nhận được lại là: `107374182`


Điều này có nguy cơ gây ra overflow khiến hacker có thể ghi đè lên những vùng không cho phép cho phép quyền arbitrary read, write, open và có thể lên tới RCE nếu máy chủ cấu hình lỏng lẻo


Tiếp tục tìm kiếm xem biến này tiếp tục được sử dụng như thế nào trong engine:


```cpp
...
    size_t max_reservation = RoundUp<kCodeAlignment>(
        v8_flags.wasm_max_code_space_size_mb * MB * 9 / 10);
    size_t code_space_size = std::min(max_reservation, remaining_code_size_);
    std::tie(current_code_space_, current_jump_tables_) =
        native_module_->AllocateForDeserializedCode(code_space_size);
    DCHECK_EQ(current_code_space_.size(), code_space_size);
    CHECK(current_jump_tables_.is_valid());
  }

  DeserializationUnit unit;
  unit.src_code_buffer = reader->ReadVector<byte>(code_size);
  auto reloc_info = reader->ReadVector<byte>(reloc_size);
  auto source_pos = reader->ReadVector<byte>(source_position_size);
  auto protected_instructions =
      reader->ReadVector<byte>(protected_instructions_size);

  base::Vector<uint8_t> instructions =
      current_code_space_.SubVector(0, code_size);
  current_code_space_ += code_size;
  remaining_code_size_ -= code_size;
...
```


Tìm trong mã nguồn của chương trình biến `max_reservation` chỉ tương tác với biến `code_space_size`

Ta tìm hiểu xem chương trình sử dụng biến `code_space_size` ở những đâu:


![image](https://hackmd.io/_uploads/Sk3r8qjYMx.png)

Biến `code_space_size` chỉ được sử dụng ở đây hai dòng đáng chí ý ở đây là
1. `std::tie(current_code_space_, current_jump_tables_) =
        native_module_->AllocateForDeserializedCode(code_space_size);`
2. `size_t code_space_size = std::min(max_reservation, remaining_code_size_);`

Ta có thể thấy ở đây lệnh:
```cpp
size_t code_space_size = std::min(max_reservation, remaining_code_size_);
```
V8 muốn xin một vùng nhớ `code_space_size` đủ để chứa `remaining_code_size_` nhưng bây giờ biến `max_reservation` đã nhỏ đi rất nhiều so với dự định của người viết chương trình.

Lúc đó lệnh:

```cpp
base::Vector<uint8_t> instructions =
      current_code_space_.SubVector(0, code_size);
  current_code_space_ += code_size;
  remaining_code_size_ -= code_size;
```

Cắt một đoạn vùng nhớ từ 0 đến `code_size` trong `current_code_space_` để chuẩn bị chép mã WASM nhưng giả định `code_size` lớn có thể lên tới 20000000 tức lớn hơn giá trị của `current_code_space_` lúc đó nó sẽ gây tràn bộ nhớ và hàng loạt lỗ hổng nguy hiểm khác. Đáng lẽ lúc này các hàm DCHECK sẽ ngăn chặn chuyện này lại nhưng khi ra bản Release tất cả hàm DCHECK đã bị xóa đi khiến chương trình xuất hiện lỗi.

Ở đây ta tiếp tục xem hàm `AllocateForDeserializedCode` làm gì với biến `code_space_size`

Tìm trong chương trình ta tiếp tục mã nguồn của hàm `AllocateForDeserializedCode`:

```cpp
std::pair<base::Vector<uint8_t>, NativeModule::JumpTablesRef>
NativeModule::AllocateForDeserializedCode(size_t total_code_size) {
  base::RecursiveMutexGuard guard{&allocation_mutex_};
  base::Vector<uint8_t> code_space =
      code_allocator_.AllocateForCode(this, total_code_size);
  auto jump_tables =
      FindJumpTablesForRegionLocked(base::AddressRegionOf(code_space));
  return {code_space, jump_tables};
}
```

Ở ngoài ta thấy hàm này sẽ trả hai giá trị `code_space` và `jump_tables` lần lượt cho hai biến là `current_code_space_` và `current_jump_tables_`

Mục đích của hàm này là trả đúng giá trị được yêu cầu = giá trị `code_space_size` nhưng đã giải thích ở trước biến `code_space_size` đã bị bóp nhỏ đi rất nhiều so với dự tính ban đầu của lập trình viên.

Cho nên `code_space` cũng nhỏ theo khi nó trả giá trị đó cho `current_code_space_` cũng bị nhỏ như thế

Tiếp tục đào sâu xem chương trình làm gì tiếp với biến `current_code_space_` không

![image](https://hackmd.io/_uploads/H1Qm_9iYfe.png)


Ta thấy ở ngay dưới đoạn mã có đoạn:


```cpp
  base::Vector<uint8_t> instructions =
      current_code_space_.SubVector(0, code_size);
  current_code_space_ += code_size;
  remaining_code_size_ -= code_size;
```

Đi vào hàm `SubVector` xem nó thực sự làm gì

```cpp
// [*] src/base/vector.h
Vector<T> SubVector(size_t from, size_t to) const {
  DCHECK_LE(from, to);
  DCHECK_LE(to, length_);        oversized span
  return Vector<T>(begin() + from, to - from);
}

Vector<T> operator+=(size_t offset) {
  DCHECK_LE(offset, length_);      
  start_ += offset;
  length_ -= offset;               //tính toán thành giá trị âm
  return *this;
}
```
Chú ý biến `current_code_space_` có kiểu dữ liệu do lập trình viên tự viết ra là `Vector` như đã trình bày bên trên cho nên phép `+=` cũng cần được định nghĩa trong mã nguồn.

Ở đây hàm `SubVector` trả về đối tượng vector từ from đến to:
Ở bản debug được quản lí rất chặt chẽ nhưng khi release tất cả hàm dcheck đã bị xóa khiến cho không kiểm tra `to` có lớn hơn `length_` không khi đó giả sử:
`current_code_space` có `length_ = 100` nhưng `code_size` là `offset` có giá trị tận `200` tức còn lớn hơn cả `length_` cho nên `length_` lúc này bị tính toán thành giá trị âm cụ thể là `-100` vì là biến 64bit nên bị tràn số âm 64bit nên biến có giá trị `2^64-100`,`length_` bây giờ có giá trị cực lớn còn `start_` vượt quá kích thước của `current_code_space`. Làm cho máy tính tưởng rằng `current_code_space` còn rất nhiều.


Phương thức `current_code_space_.size()` luôn trả về một con số siêu lớn.
Lúc này lệnh:
```cpp
if (current_code_space_.size() < static_cast<size_t>(code_size))
```
Sẽ luôn trả lại giá trị false khiến cho hàm `AllocateForDeserializedCode` vĩnh viện không được gọi khiến cho các hàm Wasm phía sau trong tệp serialized sẽ liên tục bị cắt ra nối tiếp nhau vượt xa ranh giới ban đầu ghi đè vào các vùng dữ liệu sau đó


Tại sao lập trình viên lại mắc lỗi sở đẳng như vậy hãy xem đoạn code sau:

```cpp
// [*] src/wasm/wasm-code-manager.cc
  size_t max_code_batch_size = v8_flags.wasm_max_code_space_size_mb * MB / 2;
  size_t total_code_space = 0;
  for (auto& result : results) {
    DCHECK(result.succeeded());
    size_t new_code_space =
        RoundUp<kCodeAlignment>(result.code_desc.instr_size);
    if (total_code_space + new_code_space > max_code_batch_size) {
      // Split off the first part of the {results} vector and process it
      // separately. This method then continues with the rest.
      size_t split_point = &result - results.begin();
      CHECK_WITH_MSG(
          split_point != 0,
          "A single code object needs more than half of the code space size");
      auto first_results = AddCompiledCode(results.SubVector(0, split_point));
```

Cụ thể chú ý lệnh này:

```cpp
size_t max_code_batch_size = v8_flags.wasm_max_code_space_size_mb * MB / 2;
```
tương đương với:
`1024*1024*1024/2 = 536870912`
Tương đương với `512MiB`
![image](https://hackmd.io/_uploads/SykBYKqFzl.png)

Chỉ cần 30bit để biểu diễn `max_code_batch_size` hoàn toàn nằm trong khả năng biểu diễn số nguyên không dấu (giới hạn là 2^32-1)

Ở đây lập trình viên cho giới hạn max không gian của một hàm là xấp xỉ `512MiB` luôn bé hơn con số current_code_space mà lập trình viên dự tính lên đến `912MiB` nhưng lỗi interger overflow đã làm hỏng giá trị này chỉ còn khoảng `102MiB` tức một vùng nhớ `512MiB` có thể được chép vào một vùng nhớ chỉ `102MiB` gây ra lỗi overflow cực kì nguy hiểm.
