// Copyright 2026 the V8 project authors. All rights reserved.
// Use of this source code is goValidatened by a BSD-style license that can be
// found in the LICENSE file.
#include "src/sandbox/generated-code-validator.h"
#ifdef V8_ENABLE_GENERATED_CODE_VALIDATOR
#include "src/base/logging.h"
#include "src/builtins/builtins.h"
#include "src/codegen/arm64/instructions-arm64.h"
#include "src/codegen/arm64/register-arm64.h"
#include "src/objects/code-inl.h"
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
#endif
#include "third_party/disarm/src/disarm64.h"
#ifdef __clang__
#pragma clang diagnostic pop
#endif
namespace v8::internal {
namespace {
struct Da64InstFormatter {
static std::string Format(const Da64Inst& instr) {
char formatted_instruction[128] = {'\0'};
da64_format(&instr, formatted_instruction);
return formatted_instruction;
}
};
static bool Da64OpEquals(const Da64Op& op1, const Da64Op& op2) {
if (op1.type != op2.type) {
return false;
}
switch (op1.type) {
case DA_OP_NONE:
return true;
case DA_OP_IMMLARGE:
case DA_OP_RELADDR:
case DA_OP_IMMFLOAT:
// These operand types keep their values in the Da64Inst struct. Comparing
// such operand should not use this method.
UNREACHABLE();
case DA_OP_REGGP:
case DA_OP_REGGPINC:
case DA_OP_REGSP:
return op1.reg == op2.reg && op1.reggp.sf == op2.reggp.sf;
case DA_OP_REGGPEXT:
return op1.reg == op2.reg && op1.reggpext.sf == op2.reggpext.sf &&
op1.reggpext.ext == op2.reggpext.ext &&
op1.reggpext.shift == op2.reggpext.shift;
case DA_OP_REGFP:
return op1.reg == op2.reg && op1.regfp.size == op2.regfp.size;
case DA_OP_REGVEC:
return op1.reg == op2.reg && op1.regvec.va == op2.regvec.va;
case DA_OP_REGVTBL:
return op1.reg == op2.reg && op1.regvtbl.va == op2.regvtbl.va &&
op1.regvtbl.cnt == op2.regvtbl.cnt;
case DA_OP_REGVIDX:
return op1.reg == op2.reg && op1.regvidx.esize == op2.regvidx.esize &&
op1.regvidx.elem == op2.regvidx.elem;
case DA_OP_REGVTBLIDX:
return op1.reg == op2.reg &&
op1.regvtblidx.esize == op2.regvtblidx.esize &&
op1.regvtblidx.elem == op2.regvtblidx.elem &&
op1.regvtblidx.cnt == op2.regvtblidx.cnt;
case DA_OP_MEMUOFF:
return op1.reg == op2.reg && op1.uimm16 == op2.uimm16;
case DA_OP_MEMSOFF:
case DA_OP_MEMSOFFPRE:
case DA_OP_MEMSOFFPOST:
return op1.reg == op2.reg && op1.simm16 == op2.simm16;
case DA_OP_MEMREG:
case DA_OP_MEMREGPOST:
return op1.reg == op2.reg && op1.memreg.sc == op2.memreg.sc &&
op1.memreg.ext == op2.memreg.ext &&
op1.memreg.shift == op2.memreg.shift &&
op1.memreg.offreg == op2.memreg.offreg;
case DA_OP_MEMINC:
return op1.reg == op2.reg;
case DA_OP_COND:
return op1.cond == op2.cond;
case DA_OP_PRFOP:
return op1.prfop == op2.prfop;
case DA_OP_SYSREG:
return op1.sysreg == op2.sysreg;
case DA_OP_IMMSMALL:
case DA_OP_UIMM:
return op1.uimm16 == op2.uimm16;
case DA_OP_SIMM:
return op1.simm16 == op2.simm16;
case DA_OP_UIMMSHIFT:
return op1.uimm16 == op2.uimm16 &&
op1.immshift.mask == op2.immshift.mask &&
op1.immshift.shift == op2.immshift.shift;
}
UNREACHABLE();
}
} // namespace
#define VALIDATOR_CHECK(condition, message) \
do { \
if (V8_UNLIKELY(!(condition))) { \
violations_reporter_.ReportViolationWithInstruction( \
pc, Da64InstFormatter::Format(instr), (message)); \
} \
} while (false)
class InstructionChecker {
static_assert(kPtrComprCageBaseRegister != no_reg);
static constexpr int cage_base_register = kPtrComprCageBaseRegister.code();
static_assert(kRootRegister != no_reg);
static constexpr int root_register = kRootRegister.code();
static constexpr int kMaxOperands = 5;
using ViolationsReporter = GeneratedCodeValidator::ViolationsReporter;
using Utils = GeneratedCodeValidator::Utils;
using State = GeneratedCodeValidator::State;
public:
InstructionChecker(Isolate* isolate, Tagged<Code> code,
ViolationsReporter& violations_reporter)
: isolate_(isolate),
code_(code),
violations_reporter_(violations_reporter),
state_(code_),
root_reg_init_state_(isolate_) {}
void Check(const uint8_t* pc, const Da64Inst& instr) {
static_assert(kMaxOperands == (sizeof(instr.ops) / sizeof(Da64Op)));
// REGEXP code doesn't follow the V8 ABI and instead uses standard C ABI.
if (code_->kind() != CodeKind::REGEXP) {
CheckNoWritesToCageBaseRegister(pc, instr);
CheckNoWritesToRootRegister(pc, instr);
}
CheckNoSystemRegisterWrites(pc, instr);
}
private:
// Verifies whether the instruction accesses the pointer compression cage base
// register (x28) directly or modifies it via writeback. The cage base
// register must remain read-only across all generated code to preserve
// sandbox integrity and prevent pointer corruption. This will not block using
// the cage base register as a base address for a memory operand without
// writeback.
void CheckNoWritesToCageBaseRegister(const uint8_t* pc,
const Da64Inst& instr) {
switch (instr.mnem) {
// Subs is used as a comparison to assert that a register holds a heap
// object in the main cage.
case DA64I_SUBS_SHIFT:
// Add and sub instructions are used for decompressing and compressing
// sandboxed pointers.
case DA64I_ADD_SHIFT:
case DA64I_SUB_SHIFT:
// Or and Add instructions are used for decompressing tagged pointers.
case DA64I_ADD_IMM:
case DA64I_ORR_IMM:
case DA64I_ORR_SHIFT:
// Add extended register is used for decompressing tagged pointers with
// zero-extension: add xd, x28, ws, uxtw #0.
case DA64I_ADD_EXT:
// Check that the target is not the cage base register.
if (!IsCageBaseReg(instr.ops[0])) {
// Arithmetic and logical instructions do not support writeback
// operands.
return;
}
break;
// Stp is used by entry and deopt builtins to save the cage base
// register's value in C++ code.
case DA64I_STPX:
case DA64I_STPX_PRE:
case DA64I_STPX_POST:
// Stp isn't updating any registers.
DCHECK_IMPLIES(
IsCageBaseReg(instr.ops[0]) || IsCageBaseReg(instr.ops[1]),
Utils::IsEntryCode(code_) || Utils::IsDeoptCode(code_));
if (!IsCageBaseWritebackReg(instr.ops[2])) {
return;
}
break;
// Ldp is used by entry and deopt builtins to restore the cage base
// register's value in C++ code.
case DA64I_LDPX:
case DA64I_LDPX_PRE:
case DA64I_LDPX_POST:
// TODO(523128533): verify the popped value is the same as the
// previously pushed value. E.g. track changes to stack register and
// check that push and pop operate on the same offset (assuming calls
// don't violate it).
// TODO(523128533): Deopt builtins save and restore the cage base
// register so that the deoptimizer can update register values as
// needed. Since the cage base register is callee saved and should not
// change, can we avoid saving and restoring it?
if (Utils::IsEntryCode(code_) || Utils::IsDeoptCode(code_)) {
if ((IsCageBaseReg(instr.ops[0]) || IsCageBaseReg(instr.ops[1]))) {
state_.is_cage_base_reg_valid_ = false;
}
if (!IsCageBaseWritebackReg(instr.ops[2])) {
return;
}
}
break;
// Ldur is used by entry builtins to initialize the cage base register.
case DA64I_LDURX:
if (!IsCageBaseReg(instr.ops[0])) {
return;
}
VALIDATOR_CHECK(!state_.is_cage_base_reg_valid_,
"Instruction overwrites a valid cage base register");
if (Utils::IsEntryCode(code_) &&
IsExpectedOperand(instr.ops[1],
{.type = DA_OP_MEMSOFF,
.reg = root_register,
.simm16 = IsolateData::cage_base_offset()})) {
VALIDATOR_CHECK(
state_.is_root_reg_valid_,
"Cage base register initialization uses invalid root register");
state_.is_cage_base_reg_valid_ = true;
return;
}
break;
default:
// All other cases are not expected to accesses the cage base register
// directly and fall through to the generic handling below.
break;
}
// Check that no operand is the cage base register.
for (int i = 0; i < kMaxOperands; i++) {
VALIDATOR_CHECK(
!IsCageBaseReg(instr.ops[i]) && !IsCageBaseWritebackReg(instr.ops[i]),
std::format("Instruction accesses cage bage register at operand {0}",
i));
}
}
// Verifies whether the instruction accesses the root register (x26) directly
// or modifies it via writeback. The root register must remain read-only
// across all generated code to preserve sandbox integrity and prevent pointer
// corruption. This will not block using the root register as a base address
// for a memory operand without writeback.
void CheckNoWritesToRootRegister(const uint8_t* pc, const Da64Inst& instr) {
const bool expecting_init = root_reg_init_state_.in_progress;
bool was_init = false;
// Flag to skip the generic validation at the end of this method. Used to
// prevent accidentally skipping the `expecting_init && !was_init` check
// below.
bool is_validated = false;
switch (instr.mnem) {
// Add and sub are also used for root relative indexing and accessing
// isolate fields (or the isolate itself) via the root register.
case DA64I_ADD_SHIFT:
case DA64I_ADD_IMM:
case DA64I_ADD_EXT:
case DA64I_SUB_IMM:
// Orr is used for copying the root register value and by entry builtins
// to initialize the root register.
case DA64I_ORR_SHIFT:
// Check that the target is not the cage base register or root register.
if (!IsRootReg(instr.ops[0])) {
// Arithmetic and logical instructions do not support writeback
// operands.
is_validated = true;
break;
}
VALIDATOR_CHECK(!state_.is_root_reg_valid_,
"Instruction overwrites a valid root register");
if (Utils::IsEntryCode(code_) && IsValidRootRegInitialization(instr)) {
is_validated = true;
}
break;
// Stp is used by entry and deopt builtins to save the root register's
// value in C++ code.
case DA64I_STPX:
case DA64I_STPX_PRE:
case DA64I_STPX_POST:
// Stp isn't updating any registers.
DCHECK_IMPLIES(IsRootReg(instr.ops[0]) || IsRootReg(instr.ops[1]),
Utils::IsEntryCode(code_) || Utils::IsDeoptCode(code_));
if (!IsRootWritebackReg(instr.ops[2])) {
is_validated = true;
}
break;
// Ldp is used by entry and deopt builtins to restore the root registers'
// value in C++ code.
case DA64I_LDPX:
case DA64I_LDPX_PRE:
case DA64I_LDPX_POST:
// TODO(523128533): verify the popped value is the same as the
// previously pushed value. E.g. track changes to stack register and
// check that push and pop operate on the same offset (assuming calls
// don't violate it).
// TODO(523128533): Deopt builtins save and restore the root
// register so that the deoptimizer can update register values as
// needed. Since the root register is callee saved and should not
// change, can we avoid saving and restoring it?
if (Utils::IsEntryCode(code_) || Utils::IsDeoptCode(code_)) {
if ((IsRootReg(instr.ops[0]) || IsRootReg(instr.ops[1]))) {
state_.is_root_reg_valid_ = false;
}
if (!IsRootWritebackReg(instr.ops[2])) {
is_validated = true;
}
}
break;
// Mov is used by entry builtins to initialize the root register.
case DA64I_MOVZ:
case DA64I_MOVK:
if (!IsRootReg(instr.ops[0])) {
is_validated = true;
break;
}
VALIDATOR_CHECK(!state_.is_root_reg_valid_,
"Instruction overwrites a valid root register");
was_init = true;
if (Utils::IsEntryCode(code_) && IsValidRootRegInitialization(instr)) {
is_validated = true;
}
break;
default:
// All other cases are not expected to accesses the cage base register
// or root register directly and fall through to the generic handling
// below.
break;
}
VALIDATOR_CHECK(!(expecting_init && !was_init),
"Root register initialization interrupted");
if (is_validated) {
return;
}
// Check that no operand is the cage base register or the root register.
for (int i = 0; i < kMaxOperands; i++) {
VALIDATOR_CHECK(
!IsRootReg(instr.ops[i]) && !IsRootWritebackReg(instr.ops[i]),
std::format("Instruction accesses root register at operand {0}", i));
}
}
// Verifies that system registers are never updated. This prevents
// unintended modification of CPU control flags or floating-point execution
// state.
void CheckNoSystemRegisterWrites(const uint8_t* pc, const Da64Inst& instr) {
static constexpr Da64InstKind prohibited_instructions[] = {
DA64I_SYS, DA64I_SYSL, DA64I_MSR,
DA64I_MSR_UAO, DA64I_MSR_PAN, DA64I_MSR_SPSel,
DA64I_MSR_ALLINT, DA64I_MSR_PM, DA64I_MSR_SSBS,
DA64I_MSR_DIT, DA64I_MSR_TCO, DA64I_MSR_DAIFSet,
DA64I_MSR_DAIFClr, DA64I_MSR_SVCRSM, DA64I_MSR_SVCRZA,
DA64I_MSR_SVCRSMZA};
VALIDATOR_CHECK(std::find(std::begin(prohibited_instructions),
std::end(prohibited_instructions),
instr.mnem) == std::end(prohibited_instructions),
"Instruction writes to prohibited system registers");
}
static bool IsExpectedReg(const Da64Op& op, int expected_reg) {
return ((op.type == DA_OP_REGGP) || (op.type == DA_OP_REGGPEXT) ||
(op.type == DA_OP_REGGPINC)) &&
(op.reg == expected_reg);
}
static bool IsCageBaseReg(const Da64Op& op) {
return IsExpectedReg(op, cage_base_register);
}
static bool IsRootReg(const Da64Op& op) {
return IsExpectedReg(op, root_register);
}
static bool IsExpectedWritebackReg(const Da64Op& op, int expected_reg) {
return ((op.type == DA_OP_MEMSOFFPRE) || (op.type == DA_OP_MEMSOFFPOST) ||
(op.type == DA_OP_MEMREGPOST) || (op.type == DA_OP_MEMINC)) &&
(op.reg == expected_reg);
}
static bool IsCageBaseWritebackReg(const Da64Op& op) {
return IsExpectedWritebackReg(op, cage_base_register);
}
static bool IsRootWritebackReg(const Da64Op& op) {
return IsExpectedWritebackReg(op, root_register);
}
static bool IsExpectedOperand(const Da64Op& op, const Da64Op& expected) {
return Da64OpEquals(op, expected);
}
bool IsValidRootRegInitialization(const Da64Inst& instr) {
DCHECK(IsRootReg(instr.ops[0]));
switch (instr.mnem) {
case DA64I_MOVZ:
// Movz is the first in a sequence of movs to initialize the root
// register.
DCHECK_EQ(root_reg_init_state_.current_value, kNullAddress);
DCHECK(!root_reg_init_state_.in_progress);
[[fallthrough]];
case DA64I_MOVK: {
// Movk and Movz are used to construct the expected root register value.
const Da64Op& op = instr.ops[1];
DCHECK_EQ(DA_OP_UIMMSHIFT, op.type);
DCHECK_EQ(0, op.immshift.mask);
const Address previous_value = root_reg_init_state_.current_value;
root_reg_init_state_.current_value |=
(static_cast<uint64_t>(op.uimm16) << op.immshift.shift);
state_.is_root_reg_valid_ = root_reg_init_state_.current_value ==
root_reg_init_state_.expected_value_;
root_reg_init_state_.in_progress = !state_.is_root_reg_valid_;
// This is a valid initialization as long as it's making progress
// towards the expected value.
return (previous_value != root_reg_init_state_.current_value) &&
((root_reg_init_state_.current_value &
root_reg_init_state_.expected_value_) ==
root_reg_init_state_.current_value);
}
case DA64I_ORR_SHIFT:
DCHECK(!root_reg_init_state_.in_progress);
// Assumes builtins receive the correct value as their first
// argument.
state_.is_root_reg_valid_ = (code_->kind() == CodeKind::BUILTIN) &&
IsExpectedReg(instr.ops[2], x0.code()) &&
IsExpectedReg(instr.ops[1], xzr.code());
break;
default:
break;
}
return state_.is_root_reg_valid_;
}
Isolate* const isolate_;
const Tagged<Code> code_;
ViolationsReporter& violations_reporter_;
State state_;
struct RootRegisterInitializationState {
explicit RootRegisterInitializationState(Isolate* isolate)
: expected_value_(ExternalReference::isolate_root(isolate).raw()) {
CHECK_NE(expected_value_, kNullAddress);
}
const Address expected_value_;
Address current_value = kNullAddress;
bool in_progress = false;
} root_reg_init_state_;
};
void GeneratedCodeValidator::ValidateImpl(Isolate* isolate, Tagged<Code> code) {
DCHECK_EQ(code->instruction_size() % kInstrSize, 0);
ViolationsReporter reporter(code);
InstructionChecker instruction_checker(isolate, code, reporter);
InstructionIteratorSkippingData it(code);
while (!it.IsDone()) {
const uint8_t* pc = it.GetCurrent();
Da64Inst instr;
da64_decode(*reinterpret_cast<const uint32_t*>(pc), &instr);
if (instr.mnem == DA64I_UNKNOWN) {
reporter.ReportDisassemblyFailed(pc, kInstrSize);
return;
}
if (v8_flags.validate_generated_code_include_code) {
reporter.RecordDisassembledInstruction(pc, kInstrSize,
Da64InstFormatter::Format(instr));
}
instruction_checker.Check(pc, instr);
it.Advance(kInstrSize);
}
}
} // namespace v8::internal
#undef VALIDATOR_CHECK
#endif // V8_ENABLE_GENERATED_CODE_VALIDATOR