Store compact cache indexes in bytecode instructions instead of raw pointers to the executable cache vectors. This keeps the instruction stream independent from heap addresses and removes pointer fixups when materializing cached bytecode. Resolve the mutable cache pointers at execution time from the current Executable. Bytecode test expectations are updated for the smaller cache operands and resulting instruction offsets.
707 lines
24 KiB
Rust
707 lines
24 KiB
Rust
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
//! Bytecode validator.
|
|
//!
|
|
//! Walks a packed bytecode buffer and verifies its structural integrity
|
|
//! against a set of bounds (table sizes, register/local/constant/argument
|
|
//! counts, cache-array sizes). Intended for two purposes:
|
|
//! 1. A debug/sanitizer-build sanity check that runs after every successful
|
|
//! bytecode compilation.
|
|
//! 2. A safety net for loading bytecode from on-disk caches.
|
|
//!
|
|
//! Operates on the wire-format bytes only, without round-tripping through
|
|
//! the Rust `Instruction` enum, so it works on freshly-encoded as well as
|
|
//! freshly-deserialized bytecode.
|
|
|
|
use super::instruction::{NUM_OPCODES, instruction_length_from_bytes, validate_instruction};
|
|
|
|
/// Sentinel u32 used by `Operand::INVALID` and by `Optional<*TableIndex>` for
|
|
/// "no value". Mirrors the C++ `0xFFFFFFFF` constant used throughout
|
|
/// `Bytecode/Operand.h` and the per-table index types.
|
|
const INVALID_INDEX_U32: u32 = 0xFFFF_FFFF;
|
|
|
|
/// Sentinel u32 written into cache fields by the bytecode encoder when no
|
|
/// cache slot was reserved.
|
|
const NO_CACHE_INDEX: u32 = u32::MAX;
|
|
|
|
/// Bounds against which bytecode references are checked.
|
|
#[repr(C)]
|
|
pub struct FFIValidatorBounds {
|
|
pub number_of_registers: u32,
|
|
pub number_of_locals: u32,
|
|
pub number_of_constants: u32,
|
|
pub number_of_arguments: u32,
|
|
pub identifier_table_size: u32,
|
|
pub string_table_size: u32,
|
|
pub property_key_table_size: u32,
|
|
pub regex_table_size: u32,
|
|
pub property_lookup_cache_count: u32,
|
|
pub global_variable_cache_count: u32,
|
|
pub template_object_cache_count: u32,
|
|
pub object_shape_cache_count: u32,
|
|
pub object_property_iterator_cache_count: u32,
|
|
pub class_blueprint_count: u32,
|
|
pub shared_function_data_count: u32,
|
|
/// Variant counts for the C++ enum types referenced by Bytecode.def
|
|
/// fields. Plumbed across the FFI rather than hardcoded so that adding
|
|
/// or removing a variant in the C++ enum can't silently outdate the
|
|
/// Rust validator.
|
|
pub completion_type_variant_count: u32,
|
|
pub iterator_hint_variant_count: u32,
|
|
pub environment_mode_variant_count: u32,
|
|
pub put_kind_variant_count: u32,
|
|
pub arguments_kind_variant_count: u32,
|
|
}
|
|
|
|
/// Categorization of validation failures, mirrored to C++ as an enum class.
|
|
#[repr(u32)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ValidationErrorKind {
|
|
Ok = 0,
|
|
BufferNotAligned = 1,
|
|
InstructionMisaligned = 2,
|
|
UnknownOpcode = 3,
|
|
TruncatedInstruction = 4,
|
|
InvalidLength = 5,
|
|
OperandOutOfRange = 6,
|
|
OperandInvalid = 7,
|
|
LabelNotAtInstructionBoundary = 8,
|
|
IdentifierIndexOutOfRange = 9,
|
|
StringIndexOutOfRange = 10,
|
|
PropertyKeyIndexOutOfRange = 11,
|
|
RegexIndexOutOfRange = 12,
|
|
PropertyLookupCacheIndexOutOfRange = 13,
|
|
GlobalVariableCacheIndexOutOfRange = 14,
|
|
TemplateObjectCacheIndexOutOfRange = 15,
|
|
ObjectShapeCacheIndexOutOfRange = 16,
|
|
ObjectPropertyIteratorCacheIndexOutOfRange = 17,
|
|
SharedFunctionDataIndexOutOfRange = 18,
|
|
ClassBlueprintIndexOutOfRange = 19,
|
|
EnumOutOfRange = 20,
|
|
BasicBlockOffsetInvalid = 21,
|
|
ExceptionHandlerStartInvalid = 22,
|
|
ExceptionHandlerEndInvalid = 23,
|
|
ExceptionHandlerHandlerInvalid = 24,
|
|
ExceptionHandlerRangeInvalid = 25,
|
|
SourceMapOffsetInvalid = 26,
|
|
}
|
|
|
|
/// Detail returned to the C++ caller on validation failure.
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct FFIValidationError {
|
|
pub kind: ValidationErrorKind,
|
|
pub offset: u32,
|
|
pub opcode: u32,
|
|
}
|
|
|
|
/// Layout-compatible mirror of `Bytecode::Executable::ExceptionHandlers`,
|
|
/// flattened to plain offsets for FFI.
|
|
#[repr(C)]
|
|
pub struct FFIExceptionHandlerOffsets {
|
|
pub start: u32,
|
|
pub end: u32,
|
|
pub handler: u32,
|
|
}
|
|
|
|
/// Structural metadata that lives on `Executable` alongside the bytecode
|
|
/// itself. Every offset must point at an instruction boundary; some endpoints
|
|
/// (handler ranges, source map entries) may also be one-past-the-last
|
|
/// instruction so that "end of bytecode" is representable.
|
|
#[repr(C)]
|
|
pub struct FFIValidatorExtras {
|
|
pub basic_block_offsets: *const u32,
|
|
pub basic_block_count: usize,
|
|
pub exception_handlers: *const FFIExceptionHandlerOffsets,
|
|
pub exception_handler_count: usize,
|
|
pub source_map_offsets: *const u32,
|
|
pub source_map_count: usize,
|
|
}
|
|
|
|
impl FFIValidationError {
|
|
pub const fn new(kind: ValidationErrorKind, offset: usize, opcode: u8) -> Self {
|
|
Self {
|
|
kind,
|
|
offset: offset as u32,
|
|
opcode: opcode as u32,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Borrowed state passed to the generated per-instruction validator.
|
|
pub struct ValidationContext<'a> {
|
|
pub bounds: &'a FFIValidatorBounds,
|
|
pub bytes: &'a [u8],
|
|
/// Sorted byte offsets of valid instruction starts, populated by Pass 1.
|
|
pub valid_offsets: &'a [u32],
|
|
}
|
|
|
|
#[inline]
|
|
pub fn read_u32(bytes: &[u8], at: usize) -> u32 {
|
|
u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn read_u64(bytes: &[u8], at: usize) -> u64 {
|
|
u64::from_ne_bytes(bytes[at..at + 8].try_into().unwrap())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_operand(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
// After the assembler runs, operands in the encoded instruction stream
|
|
// are flat indices into the runtime [registers | locals | constants |
|
|
// arguments] array; the original 3-bit type tag has been zeroed out by
|
|
// Operand::offset_index_by. The runtime indexes the combined array
|
|
// directly with raw, so the validator just needs to keep raw inside that
|
|
// array's bounds.
|
|
if raw == INVALID_INDEX_U32 {
|
|
return Err(ValidationErrorKind::OperandInvalid);
|
|
}
|
|
let max = ctx
|
|
.bounds
|
|
.number_of_registers
|
|
.saturating_add(ctx.bounds.number_of_locals)
|
|
.saturating_add(ctx.bounds.number_of_constants)
|
|
.saturating_add(ctx.bounds.number_of_arguments);
|
|
if raw >= max {
|
|
return Err(ValidationErrorKind::OperandOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_optional_operand(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 {
|
|
return Ok(());
|
|
}
|
|
validate_operand(raw, ctx)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_label(addr: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if ctx.valid_offsets.binary_search(&addr).is_err() {
|
|
return Err(ValidationErrorKind::LabelNotAtInstructionBoundary);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_identifier_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 || raw >= ctx.bounds.identifier_table_size {
|
|
return Err(ValidationErrorKind::IdentifierIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_optional_identifier_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.identifier_table_size {
|
|
return Err(ValidationErrorKind::IdentifierIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_string_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 || raw >= ctx.bounds.string_table_size {
|
|
return Err(ValidationErrorKind::StringIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_optional_string_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.string_table_size {
|
|
return Err(ValidationErrorKind::StringIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_property_key_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == INVALID_INDEX_U32 || raw >= ctx.bounds.property_key_table_size {
|
|
return Err(ValidationErrorKind::PropertyKeyIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_property_lookup_cache_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == NO_CACHE_INDEX {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.property_lookup_cache_count {
|
|
return Err(ValidationErrorKind::PropertyLookupCacheIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_global_variable_cache_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == NO_CACHE_INDEX {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.global_variable_cache_count {
|
|
return Err(ValidationErrorKind::GlobalVariableCacheIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_template_object_cache_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == NO_CACHE_INDEX {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.template_object_cache_count {
|
|
return Err(ValidationErrorKind::TemplateObjectCacheIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_object_shape_cache_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw == NO_CACHE_INDEX {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.object_shape_cache_count {
|
|
return Err(ValidationErrorKind::ObjectShapeCacheIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_object_property_iterator_cache_index(
|
|
raw: u32,
|
|
ctx: &ValidationContext,
|
|
) -> Result<(), ValidationErrorKind> {
|
|
if raw == NO_CACHE_INDEX {
|
|
return Ok(());
|
|
}
|
|
if raw >= ctx.bounds.object_property_iterator_cache_count {
|
|
return Err(ValidationErrorKind::ObjectPropertyIteratorCacheIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_shared_function_data_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.shared_function_data_count {
|
|
return Err(ValidationErrorKind::SharedFunctionDataIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_class_blueprint_index(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.class_blueprint_count {
|
|
return Err(ValidationErrorKind::ClassBlueprintIndexOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_completion_type(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.completion_type_variant_count {
|
|
return Err(ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_iterator_hint(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.iterator_hint_variant_count {
|
|
return Err(ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_environment_mode(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.environment_mode_variant_count {
|
|
return Err(ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_put_kind(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.put_kind_variant_count {
|
|
return Err(ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn validate_arguments_kind(raw: u32, ctx: &ValidationContext) -> Result<(), ValidationErrorKind> {
|
|
if raw >= ctx.bounds.arguments_kind_variant_count {
|
|
return Err(ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Walk `bytes` and verify the structural integrity of every instruction.
|
|
///
|
|
/// Pass 1 verifies that the buffer is a tight sequence of well-formed
|
|
/// instructions: each opcode is in range, each instruction is properly
|
|
/// aligned, the reported length is sane, and the buffer ends exactly on
|
|
/// an instruction boundary. The set of valid instruction-start offsets
|
|
/// is collected for use in Pass 2.
|
|
///
|
|
/// Pass 2 dispatches per-opcode field validation.
|
|
///
|
|
/// Pass 3 validates the structural metadata that travels alongside the
|
|
/// bytecode buffer in `Executable` (basic block start offsets, exception
|
|
/// handler ranges, source map entries) against the offset set.
|
|
pub fn validate_bytecode(
|
|
bytes: &[u8],
|
|
bounds: &FFIValidatorBounds,
|
|
basic_block_offsets: &[u32],
|
|
exception_handlers: &[FFIExceptionHandlerOffsets],
|
|
source_map_offsets: &[u32],
|
|
) -> Result<(), FFIValidationError> {
|
|
let mut valid_offsets: Vec<u32> = Vec::with_capacity(bytes.len() / 16);
|
|
|
|
let mut at: usize = 0;
|
|
while at < bytes.len() {
|
|
if !at.is_multiple_of(8) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::InstructionMisaligned,
|
|
at,
|
|
0,
|
|
));
|
|
}
|
|
|
|
let opcode = bytes[at];
|
|
if (opcode as u32) >= NUM_OPCODES {
|
|
return Err(FFIValidationError::new(ValidationErrorKind::UnknownOpcode, at, opcode));
|
|
}
|
|
|
|
let size = instruction_length_from_bytes(opcode, bytes, at)
|
|
.map_err(|kind| FFIValidationError::new(kind, at, opcode))?;
|
|
|
|
if size == 0 || !size.is_multiple_of(8) {
|
|
return Err(FFIValidationError::new(ValidationErrorKind::InvalidLength, at, opcode));
|
|
}
|
|
|
|
let end = at
|
|
.checked_add(size)
|
|
.ok_or_else(|| FFIValidationError::new(ValidationErrorKind::InvalidLength, at, opcode))?;
|
|
if end > bytes.len() {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::TruncatedInstruction,
|
|
at,
|
|
opcode,
|
|
));
|
|
}
|
|
|
|
valid_offsets.push(at as u32);
|
|
at = end;
|
|
}
|
|
|
|
let ctx = ValidationContext {
|
|
bounds,
|
|
bytes,
|
|
valid_offsets: &valid_offsets,
|
|
};
|
|
for &off_u32 in &valid_offsets {
|
|
let off = off_u32 as usize;
|
|
let opcode = bytes[off];
|
|
validate_instruction(opcode, &ctx, off).map_err(|kind| FFIValidationError::new(kind, off, opcode))?;
|
|
}
|
|
|
|
// Pass 3: structural metadata.
|
|
let bytecode_end = bytes.len() as u32;
|
|
let is_instruction_boundary = |offset: u32| -> bool { valid_offsets.binary_search(&offset).is_ok() };
|
|
let is_instruction_or_end = |offset: u32| -> bool { offset == bytecode_end || is_instruction_boundary(offset) };
|
|
|
|
for &off in basic_block_offsets {
|
|
if !is_instruction_boundary(off) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::BasicBlockOffsetInvalid,
|
|
off as usize,
|
|
0,
|
|
));
|
|
}
|
|
}
|
|
|
|
for handler in exception_handlers {
|
|
if !is_instruction_or_end(handler.start) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::ExceptionHandlerStartInvalid,
|
|
handler.start as usize,
|
|
0,
|
|
));
|
|
}
|
|
if !is_instruction_or_end(handler.end) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::ExceptionHandlerEndInvalid,
|
|
handler.end as usize,
|
|
0,
|
|
));
|
|
}
|
|
if handler.start > handler.end {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::ExceptionHandlerRangeInvalid,
|
|
handler.start as usize,
|
|
0,
|
|
));
|
|
}
|
|
if !is_instruction_boundary(handler.handler) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::ExceptionHandlerHandlerInvalid,
|
|
handler.handler as usize,
|
|
0,
|
|
));
|
|
}
|
|
}
|
|
|
|
for &off in source_map_offsets {
|
|
if !is_instruction_or_end(off) {
|
|
return Err(FFIValidationError::new(
|
|
ValidationErrorKind::SourceMapOffsetInvalid,
|
|
off as usize,
|
|
0,
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::super::instruction::OpCode;
|
|
use super::*;
|
|
|
|
fn permissive_bounds() -> FFIValidatorBounds {
|
|
FFIValidatorBounds {
|
|
number_of_registers: 8,
|
|
number_of_locals: 4,
|
|
number_of_constants: 4,
|
|
number_of_arguments: 4,
|
|
identifier_table_size: 4,
|
|
string_table_size: 4,
|
|
property_key_table_size: 4,
|
|
regex_table_size: 0,
|
|
property_lookup_cache_count: 4,
|
|
global_variable_cache_count: 4,
|
|
template_object_cache_count: 4,
|
|
object_shape_cache_count: 4,
|
|
object_property_iterator_cache_count: 4,
|
|
class_blueprint_count: 4,
|
|
shared_function_data_count: 4,
|
|
completion_type_variant_count: 6,
|
|
iterator_hint_variant_count: 2,
|
|
environment_mode_variant_count: 2,
|
|
put_kind_variant_count: 5,
|
|
arguments_kind_variant_count: 2,
|
|
}
|
|
}
|
|
|
|
fn validate(bytes: &[u8], bounds: &FFIValidatorBounds) -> Result<(), FFIValidationError> {
|
|
validate_bytecode(bytes, bounds, &[], &[], &[])
|
|
}
|
|
|
|
fn put_u32(bytes: &mut [u8], at: usize, v: u32) {
|
|
bytes[at..at + 4].copy_from_slice(&v.to_ne_bytes());
|
|
}
|
|
|
|
/// Build the smallest possible valid bytecode buffer: a single `End` with
|
|
/// `m_value` set to register 0. Useful as a baseline that callers can then
|
|
/// corrupt to trigger a specific error.
|
|
fn minimal_end_buffer() -> [u8; 8] {
|
|
let mut bytes = [0u8; 8];
|
|
bytes[0] = OpCode::End as u8;
|
|
// m_value at offset 4 stays as 0, which encodes register 0.
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_minimal_valid_buffer() {
|
|
let bytes = minimal_end_buffer();
|
|
validate(&bytes, &permissive_bounds()).expect("valid buffer should pass");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_opcode() {
|
|
let mut bytes = minimal_end_buffer();
|
|
bytes[0] = 0xFF;
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::UnknownOpcode);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_truncated_instruction() {
|
|
// End is 8 bytes; lop one off so the walker can't fit the trailing
|
|
// operand inside the buffer.
|
|
let bytes = &minimal_end_buffer()[..7];
|
|
let err = validate(bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::TruncatedInstruction);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_misaligned_second_instruction() {
|
|
// First instruction is End (8 bytes), but we trail a 1-byte payload that
|
|
// can't possibly start an instruction at an 8-aligned offset.
|
|
let mut bytes = [0u8; 9];
|
|
bytes[..8].copy_from_slice(&minimal_end_buffer());
|
|
bytes[8] = OpCode::End as u8;
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
// The walker stops when it can't fit a whole 8-byte End at offset 8.
|
|
assert_eq!(err.kind, ValidationErrorKind::TruncatedInstruction);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_undersized_variable_length_instruction() {
|
|
// NewArray's fixed prefix is 16 bytes. A shorter m_length must be
|
|
// rejected before pass 2 reads fixed fields beyond the buffer.
|
|
let mut bytes = [0u8; 8];
|
|
bytes[0] = OpCode::NewArray as u8;
|
|
put_u32(&mut bytes, 4, 8);
|
|
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::InvalidLength);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_operand_invalid() {
|
|
let mut bytes = minimal_end_buffer();
|
|
put_u32(&mut bytes, 4, 0xFFFF_FFFF);
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::OperandInvalid);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_operand_out_of_range() {
|
|
let mut bytes = minimal_end_buffer();
|
|
// Bounds total = 8+4+4+4 = 20, so flat index 20 is out of range.
|
|
put_u32(&mut bytes, 4, 20);
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::OperandOutOfRange);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_label_not_at_instruction_boundary() {
|
|
// Build [Jump @4, End]: the Jump's target points into the middle of
|
|
// its own instruction, which is not an instruction-start offset.
|
|
let mut bytes = [0u8; 16];
|
|
bytes[0] = OpCode::Jump as u8;
|
|
// m_target at offset 4 (the only field on Jump after the header).
|
|
put_u32(&mut bytes, 4, 4);
|
|
bytes[8] = OpCode::End as u8;
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::LabelNotAtInstructionBoundary);
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_label_at_instruction_boundary() {
|
|
let mut bytes = [0u8; 16];
|
|
bytes[0] = OpCode::Jump as u8;
|
|
put_u32(&mut bytes, 4, 8); // points at the End below
|
|
bytes[8] = OpCode::End as u8;
|
|
validate(&bytes, &permissive_bounds()).expect("forward jump to End should pass");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_basic_block_offset_invalid() {
|
|
let bytes = minimal_end_buffer();
|
|
let bad_basic_blocks: [u32; 1] = [4]; // mid-instruction
|
|
let err = validate_bytecode(&bytes, &permissive_bounds(), &bad_basic_blocks, &[], &[]).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::BasicBlockOffsetInvalid);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_exception_handler_handler_invalid() {
|
|
let bytes = minimal_end_buffer();
|
|
let handlers = [FFIExceptionHandlerOffsets {
|
|
start: 0,
|
|
end: 8,
|
|
handler: 4, // mid-instruction
|
|
}];
|
|
let err = validate_bytecode(&bytes, &permissive_bounds(), &[], &handlers, &[]).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::ExceptionHandlerHandlerInvalid);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_exception_handler_range_invalid() {
|
|
let bytes = minimal_end_buffer();
|
|
let handlers = [FFIExceptionHandlerOffsets {
|
|
start: 8,
|
|
end: 0,
|
|
handler: 0,
|
|
}];
|
|
let err = validate_bytecode(&bytes, &permissive_bounds(), &[], &handlers, &[]).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::ExceptionHandlerRangeInvalid);
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_exception_handler_at_end_of_buffer() {
|
|
let bytes = minimal_end_buffer();
|
|
let handlers = [FFIExceptionHandlerOffsets {
|
|
start: 0,
|
|
end: 8, // one-past-last is OK for end
|
|
handler: 0,
|
|
}];
|
|
validate_bytecode(&bytes, &permissive_bounds(), &[], &handlers, &[])
|
|
.expect("end-of-buffer handler end should pass");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_source_map_offset_invalid() {
|
|
let bytes = minimal_end_buffer();
|
|
let bad_source_map: [u32; 1] = [4];
|
|
let err = validate_bytecode(&bytes, &permissive_bounds(), &[], &[], &bad_source_map).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::SourceMapOffsetInvalid);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_cache_index_out_of_range() {
|
|
// NewObject layout: header(2) + pad(2) + m_dst(4) + m_cache(4) -> 16 bytes.
|
|
let mut bytes = [0u8; 16];
|
|
bytes[0] = OpCode::NewObject as u8;
|
|
// m_dst at offset 4 stays 0 (register 0).
|
|
// m_cache at offset 8: an index well past object_shape_cache_count.
|
|
put_u32(&mut bytes, 8, 999_999);
|
|
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::ObjectShapeCacheIndexOutOfRange);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_object_literal_shape_cache_index_out_of_range() {
|
|
// InitObjectLiteralProperty layout: header(2) + pad(2) + m_object(4)
|
|
// + m_property(4) + m_src(4) + m_shape_cache_index(4)
|
|
// + m_property_slot(4) = 24 bytes.
|
|
let mut bytes = [0u8; 24];
|
|
bytes[0] = OpCode::InitObjectLiteralProperty as u8;
|
|
// m_object, m_property, and m_src stay at 0.
|
|
// m_shape_cache_index at offset 16: an index well past
|
|
// object_shape_cache_count.
|
|
put_u32(&mut bytes, 16, 999_999);
|
|
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::ObjectShapeCacheIndexOutOfRange);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_enum_out_of_range() {
|
|
// SetCompletionType layout: header(2) + pad(2) + m_completion(4) + m_completion_type(4) = 12, padded to 16.
|
|
let mut bytes = [0u8; 16];
|
|
bytes[0] = OpCode::SetCompletionType as u8;
|
|
// m_completion at offset 4 stays as register 0.
|
|
// m_completion_type at offset 8: 99 is well past the 6 valid variants.
|
|
put_u32(&mut bytes, 8, 99);
|
|
let err = validate(&bytes, &permissive_bounds()).unwrap_err();
|
|
assert_eq!(err.kind, ValidationErrorKind::EnumOutOfRange);
|
|
}
|
|
}
|