// Copyright 2025 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/sandbox/bytecode-verifier.h"
#include "src/base/numerics/checked_math.h"
#include "src/codegen/handler-table.h"
#include "src/flags/flags.h"
#include "src/interpreter/bytecode-array-iterator.h"
#include "src/interpreter/bytecode-decoder.h"
#include "src/interpreter/bytecodes.h"
#include "src/objects/bytecode-array-inl.h"
#include "src/runtime/runtime.h"
#include "src/utils/bit-vector.h"
namespace v8::internal {
// static
void BytecodeVerifier::Verify(IsolateForSandbox isolate,
Handle<BytecodeArray> bytecode, Zone* zone) {
if (v8_flags.verify_bytecode_full) {
VerifyFull(isolate, bytecode, zone);
} else if (v8_flags.verify_bytecode_light) {
VerifyLight(isolate, bytecode, zone);
}
bytecode->MarkVerified(isolate);
}
// static
void BytecodeVerifier::VerifyLight(IsolateForSandbox isolate,
Handle<BytecodeArray> bytecode, Zone* zone) {
// VerifyLight is meant to catch the most important issues (in particular,
// ones that we've seen in the past) and should be lightweight enough to be
// enabled by default.
//
// In particular, the lightweight verification ensures basic control-flow
// integrity (CFI) by validating that jump targets are valid.
unsigned bytecode_length = bytecode->length();
BitVector valid_offsets(bytecode_length, zone);
BitVector seen_jumps(bytecode_length, zone);
interpreter::BytecodeArrayIterator iterator(bytecode);
for (; !iterator.done(); iterator.Advance()) {
unsigned current_offset = iterator.current_offset();
valid_offsets.Add(current_offset);
interpreter::Bytecode current_bytecode = iterator.current_bytecode();
if (interpreter::Bytecodes::IsJump(current_bytecode)) {
unsigned target_offset = iterator.GetJumpTargetOffset();
Check(target_offset < bytecode_length, "Invalid jump offset");
if (interpreter::Bytecodes::IsForwardJump(current_bytecode)) {
// We're specifically disallowing a forward jump with offset zero (i.e.
// to itself here) as that may cause our compilers to become confused.
Check(target_offset > current_offset, "Invalid jump offset");
} else {
Check(target_offset <= current_offset, "Invalid jump offset");
}
seen_jumps.Add(target_offset);
} else if (interpreter::Bytecodes::IsSwitch(current_bytecode)) {
for (const auto entry : iterator.GetJumpTableTargetOffsets()) {
unsigned target_offset = entry.target_offset;
Check(target_offset < bytecode_length, "Invalid switch offset");
Check(target_offset > current_offset, "Invalid switch offset");
seen_jumps.Add(target_offset);
}
}
}
Check(seen_jumps.IsSubsetOf(valid_offsets), "Invalid control-flow");
HandlerTable table(*bytecode);
for (uint32_t i = 0; i < table.NumberOfRangeEntries(); ++i) {
unsigned start = table.GetRangeStart(i);
unsigned end = table.GetRangeEnd(i);
unsigned handler = table.GetRangeHandler(i);
Check(end <= bytecode_length && start <= end,
"Invalid exception handler range");
Check(handler >= end, "Exception handler must be after the try-range");
Check(handler < bytecode_length && valid_offsets.Contains(handler),
"Invalid exception handler offset");
int data = table.GetRangeData(i);
Check((data >= 0 && data < bytecode->register_count()),
"Invalid exception handler data");
}
}
// static
void BytecodeVerifier::VerifyFull(IsolateForSandbox isolate,
Handle<BytecodeArray> bytecode, Zone* zone) {
// VerifyFull does full verification and is for now just used during fuzzing
// (to test the verifier). However, in the future it may also (sometimes)
// be enabled in production as well.
//
// Full verification includes the lightweight verification for CFI but also
// ensures that registers accesses are valid (i.e. don't read/write
// out-of-bounds on the stack). In addition, we also need to check a few more
// invariants, for example that various IDs embedded in the bytecode are
// valid, but that is mostly for fuzzer cleanliness.
auto VerifyRegister = [&](interpreter::Register reg, bool is_output) {
if (reg.is_parameter()) {
int param_index = reg.ToParameterIndex();
int parameter_count = bytecode->parameter_count();
if (!base::IsInHalfOpenRange(param_index, 0, parameter_count)) {
// Access to these special objects is fine as they are inside the
// sandbox and so anyway untrusted. However, we must not access trusted
// objects/values such as the BytecodeArray itself
// (is_bytecode_array()) or the bytecode offset (is_bytecode_offset()).
if (!reg.is_current_context() && !reg.is_function_closure()) {
FailVerification("Parameter index out of bounds");
} else if (is_output) {
// However, we should not write to these special registers as they
// are not "real" parameters. This would be fine if we were only
// executing the bytecode (these objects are inside the sandbox, so
// it doesn't matter if they or pointers to them get corrupted), but
// for example our compilers sometimes assume that writes only go to
// "normal" parameters or registers and would otherwise crash in
// potentially unsafe ways. See e.g. https://crbug.com/467843950.
FailVerification("Invalid write to special register");
}
}
} else {
Check(base::IsInHalfOpenRange(reg.index(), 0, bytecode->register_count()),
"Register index out of bounds");
}
};
interpreter::Register incoming_new_target_or_generator =
bytecode->incoming_new_target_or_generator_register();
if (incoming_new_target_or_generator.is_valid()) {
VerifyRegister(incoming_new_target_or_generator, true);
}
uint32_t constant_pool_length = bytecode->constant_pool()->ulength().value();
interpreter::BytecodeArrayIterator iterator(bytecode);
interpreter::Bytecode previous_bytecode = interpreter::Bytecode::kIllegal;
int previous_bytecode_end = 0;
for (; !iterator.done(); iterator.Advance()) {
interpreter::Bytecode current_bytecode = iterator.current_bytecode();
previous_bytecode = current_bytecode;
Check(current_bytecode <= interpreter::Bytecode::kLast, "Invalid bytecode");
// We don't really care about this one as it will terminate the process
// upon execution, but it will terminate it in a way that we detect as
// a bug, so we have to disallow it here for fuzzer cleanliness.
Check(current_bytecode != interpreter::Bytecode::kIllegal,
"Illegal bytecode");
int current_bytecode_start = iterator.current_offset();
int current_bytecode_end =
current_bytecode_start + iterator.current_bytecode_size();
previous_bytecode_end = current_bytecode_end;
if (iterator.current_operand_scale() > interpreter::OperandScale::kSingle) {
bool bytecode_can_be_scaled =
interpreter::Bytecodes::IsBytecodeWithScalableOperands(
current_bytecode);
Check(bytecode_can_be_scaled, "Invalid scaled bytecode");
}
int operand_count =
interpreter::Bytecodes::NumberOfOperands(current_bytecode);
const interpreter::OperandType* operand_types =
interpreter::Bytecodes::GetOperandTypes(current_bytecode);
for (int i = 0; i < operand_count; ++i) {
interpreter::OperandType operand_type = operand_types[i];
switch (operand_type) {
case interpreter::OperandType::kReg:
case interpreter::OperandType::kRegOut:
case interpreter::OperandType::kRegInOut:
case interpreter::OperandType::kRegPair:
case interpreter::OperandType::kRegOutPair:
case interpreter::OperandType::kRegOutTriple:
case interpreter::OperandType::kRegList:
case interpreter::OperandType::kRegOutList: {
bool is_output =
interpreter::Bytecodes::IsRegisterOutputOperandType(operand_type);
uint32_t count = iterator.GetRegisterOperandRange(i);
if (count > 0) {
interpreter::Register start = iterator.GetRegisterOperand(i);
VerifyRegister(start, is_output);
if (count > 1) {
base::internal::CheckedNumeric<int> end_index = start.index();
end_index += count - 1;
Check(end_index.IsValid(), "Register range end overflows");
interpreter::Register end(end_index.ValueOrDie());
VerifyRegister(end, is_output);
Check(start.is_parameter() == end.is_parameter(),
"Register range crosses parameter/local boundary");
}
}
break;
}
case interpreter::OperandType::kConstantPoolIndex: {
uint32_t index = iterator.GetConstantPoolIndexOperand(i);
Check(index < constant_pool_length,
"Constant pool index out of bounds");
break;
}
case interpreter::OperandType::kRuntimeId: {
Runtime::FunctionId id = iterator.GetRuntimeIdOperand(i);
Check(id < Runtime::kNumFunctions, "Invalid runtime function id");
Check(IsAllowedRuntimeFunction(id), "Disallowed runtime function");
break;
}
case interpreter::OperandType::kAbortReason: {
// This is mostly just needed for fuzzer cleanliness. Otherwise we'll
// perform some OOB memory reads in the abort handler.
int reason = static_cast<int>(iterator.GetAbortReasonOperand(i));
Check(IsValidAbortReason(reason), "Invalid abort reason");
break;
}
case interpreter::OperandType::kFlag8:
case interpreter::OperandType::kFlag16:
case interpreter::OperandType::kEmbeddedFeedback:
case interpreter::OperandType::kIntrinsicId:
case interpreter::OperandType::kNativeContextIndex:
case interpreter::OperandType::kUImm:
case interpreter::OperandType::kImm:
case interpreter::OperandType::kFeedbackSlot:
case interpreter::OperandType::kContextSlot:
case interpreter::OperandType::kCoverageSlot:
case interpreter::OperandType::kRegCount:
break;
case interpreter::OperandType::kNone:
UNREACHABLE();
}
}
if (interpreter::Bytecodes::WritesImplicitRegister(current_bytecode)) {
interpreter::Register reg = iterator.GetStarTargetRegister();
VerifyRegister(reg, true);
}
}
Check(previous_bytecode_end == bytecode->length(),
"Final instruction does not end at the end of the bytecode array");
if (!interpreter::Bytecodes::Returns(previous_bytecode) &&
!interpreter::Bytecodes::UnconditionallyThrows(previous_bytecode) &&
!interpreter::Bytecodes::IsUnconditionalJump(previous_bytecode)) {
FailVerification(
"Bytecode does not end with a control-flow terminating instruction");
}
Check(bytecode->frame_size() >= 0, "Invalid bytecode array frame size");
Check(bytecode->parameter_count() >= kJSArgcReceiverSlots,
"Invalid bytecode array parameter count");
// Finally perform lightweight verification for CFI. If we ever enable full
// verification in production then we'd most likely want to avoid iterating
// over the bytecode twice, so we'd have to embed the CFI checks also here.
//
// It would probably be more "natural" to perform the lightweight
// verification at the start instead of at the end. However, lightweight
// verification doesn't ensure that the opcodes are valid (only full
// verification does) and so we would get into weird states if we observe
// invalid bytecode opcodes. Therefore we do this at the end, once we're
// certain that the bytecode is otherwise well-structured.
VerifyLight(isolate, bytecode, zone);
}
// static
bool BytecodeVerifier::IsAllowedRuntimeFunction(Runtime::FunctionId id) {
// This is currently purely for fuzzer-cleanliness and so we only add
// functions to this blocklist if we see them cause crashes during fuzzing.
// It's unclear if we'll ever need this in production, but if we decide to
// use it, we should do a more thorough audit to determine which functions
// should be disallowed here.
if (!v8_flags.fuzzing) return true;
switch (id) {
case Runtime::kLoadLookupSlotForCall_Baseline:
case Runtime::kBytecodeBudgetInterruptWithStackCheck_Sparkplug:
case Runtime::kBytecodeBudgetInterrupt_Sparkplug:
case Runtime::kBytecodeBudgetInterrupt_Maglev:
case Runtime::kBytecodeBudgetLoopInterrupt_Maglev:
case Runtime::kPatchLoadICUninitializedBaseline:
case Runtime::kLoadIC_Miss_FromBaseline:
case Runtime::kInstallBaselineCode:
case Runtime::kSwissTableAdd:
case Runtime::kSwissTableAllocate:
case Runtime::kSwissTableDelete:
case Runtime::kSwissTableDetailsAt:
case Runtime::kSwissTableElementsCount:
case Runtime::kSwissTableEquals:
case Runtime::kSwissTableFindEntry:
case Runtime::kSwissTableUpdate:
case Runtime::kSwissTableValueAt:
case Runtime::kSwissTableKeyAt:
case Runtime::kDebugPrintPtr:
case Runtime::kCopyDataPropertiesWithExcludedPropertiesOnStack:
#if V8_ENABLE_SPARKPLUG_PLUS
case Runtime::kPatchCompareOpBaselineCode:
case Runtime::kPatchCompareOpBaselineCodeAndThrow:
case Runtime::kPatchBinopBaselineCode:
case Runtime::kPatchBinopBaselineCodeAndThrow:
case Runtime::kPatchUnaryOpBaselineCode:
case Runtime::kPatchUnaryOpBaselineCodeAndThrow:
#endif // V8_ENABLE_SPARKPLUG_PLUS
return false;
#if V8_ENABLE_WEBASSEMBLY
#define CASE_WASM_INTRINSIC(Name, ...) case Runtime::k##Name:
#define CASE_WASM_INTRINSIC_INLINE(Name, ...) case Runtime::kInline##Name:
// Only allow fuzzing-safe Wasm intrinsics because these intrinsics are
// never called by JS bytecode in production and inappropriate calls often
// lead to uninteresting fuzzer crashes.
FOR_EACH_INTRINSIC_WASM(CASE_WASM_INTRINSIC, CASE_WASM_INTRINSIC_INLINE)
FOR_EACH_INTRINSIC_WASM_TEST(CASE_WASM_INTRINSIC,
CASE_WASM_INTRINSIC_INLINE)
#undef CASE_WASM_INTRINSIC_INLINE
#undef CASE_WASM_INTRINSIC
return Runtime::IsEnabledForFuzzing(id);
#endif // V8_ENABLE_WEBASSEMBLY
default:
return true;
}
}
} // namespace v8::internal