ladybird/Libraries/LibJS/Bytecode/Validator.cpp
Andreas Kling 7a246b63c7 LibJS: Infer computed property function names
Use a runtime SetFunctionName bytecode operation when object literal
property keys are not known until evaluation. This lets anonymous
function and class expressions, methods, and accessors receive names
from numeric, computed, and Symbol property keys.

Store inferred ECMAScript function names on each function object instead
of mutating shared function data. That keeps repeated evaluations with
different computed keys from leaking names across closures, while still
using the per-instance name for stack metadata.

Add regression coverage for computed object property names, repeated
computed-key evaluations, and preserving unnamed functions that are only
referenced by a computed property value.
2026-05-22 01:56:57 +02:00

172 lines
8.4 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/Format.h>
#include <AK/NumericLimits.h>
#include <AK/StringView.h>
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Bytecode/Instruction.h>
#include <LibJS/Bytecode/PutKind.h>
#include <LibJS/Bytecode/Validator.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/RustFFI.h>
namespace JS::Bytecode {
static StringView validation_error_kind_to_string(JS::FFI::ValidationErrorKind kind)
{
switch (kind) {
case JS::FFI::ValidationErrorKind::Ok:
return "Ok"sv;
case JS::FFI::ValidationErrorKind::BufferNotAligned:
return "BufferNotAligned"sv;
case JS::FFI::ValidationErrorKind::InstructionMisaligned:
return "InstructionMisaligned"sv;
case JS::FFI::ValidationErrorKind::UnknownOpcode:
return "UnknownOpcode"sv;
case JS::FFI::ValidationErrorKind::TruncatedInstruction:
return "TruncatedInstruction"sv;
case JS::FFI::ValidationErrorKind::InvalidLength:
return "InvalidLength"sv;
case JS::FFI::ValidationErrorKind::OperandOutOfRange:
return "OperandOutOfRange"sv;
case JS::FFI::ValidationErrorKind::OperandInvalid:
return "OperandInvalid"sv;
case JS::FFI::ValidationErrorKind::LabelNotAtInstructionBoundary:
return "LabelNotAtInstructionBoundary"sv;
case JS::FFI::ValidationErrorKind::IdentifierIndexOutOfRange:
return "IdentifierIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::StringIndexOutOfRange:
return "StringIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::PropertyKeyIndexOutOfRange:
return "PropertyKeyIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::RegexIndexOutOfRange:
return "RegexIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::PropertyLookupCacheIndexOutOfRange:
return "PropertyLookupCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::GlobalVariableCacheIndexOutOfRange:
return "GlobalVariableCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::EnvironmentCoordinateCacheIndexOutOfRange:
return "EnvironmentCoordinateCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::TemplateObjectCacheIndexOutOfRange:
return "TemplateObjectCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::ObjectShapeCacheIndexOutOfRange:
return "ObjectShapeCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::ObjectPropertyIteratorCacheIndexOutOfRange:
return "ObjectPropertyIteratorCacheIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::SharedFunctionDataIndexOutOfRange:
return "SharedFunctionDataIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::ClassBlueprintIndexOutOfRange:
return "ClassBlueprintIndexOutOfRange"sv;
case JS::FFI::ValidationErrorKind::EnumOutOfRange:
return "EnumOutOfRange"sv;
case JS::FFI::ValidationErrorKind::BasicBlockOffsetInvalid:
return "BasicBlockOffsetInvalid"sv;
case JS::FFI::ValidationErrorKind::ExceptionHandlerStartInvalid:
return "ExceptionHandlerStartInvalid"sv;
case JS::FFI::ValidationErrorKind::ExceptionHandlerEndInvalid:
return "ExceptionHandlerEndInvalid"sv;
case JS::FFI::ValidationErrorKind::ExceptionHandlerHandlerInvalid:
return "ExceptionHandlerHandlerInvalid"sv;
case JS::FFI::ValidationErrorKind::ExceptionHandlerRangeInvalid:
return "ExceptionHandlerRangeInvalid"sv;
case JS::FFI::ValidationErrorKind::SourceMapOffsetInvalid:
return "SourceMapOffsetInvalid"sv;
}
VERIFY_NOT_REACHED();
}
// Variant counts for the C++ enums referenced by Bytecode.def fields. The
// static_asserts pin the last variant so adding a new one without bumping
// the count here breaks the build instead of silently outdating the
// validator.
static constexpr u32 completion_type_variant_count = to_underlying(Completion::Type::Throw) + 1;
static_assert(completion_type_variant_count == 6);
static constexpr u32 iterator_hint_variant_count = to_underlying(IteratorHint::Async) + 1;
static_assert(iterator_hint_variant_count == 2);
static constexpr u32 environment_mode_variant_count = to_underlying(Op::EnvironmentMode::Var) + 1;
static_assert(environment_mode_variant_count == 2);
static constexpr u32 put_kind_variant_count = to_underlying(PutKind::Own) + 1;
static_assert(put_kind_variant_count == 5);
static constexpr u32 arguments_kind_variant_count = to_underlying(Op::ArgumentsKind::Unmapped) + 1;
static_assert(arguments_kind_variant_count == 2);
static constexpr u32 function_name_prefix_variant_count = to_underlying(Op::FunctionNamePrefix::Set) + 1;
static_assert(function_name_prefix_variant_count == 3);
ErrorOr<void> validate_bytecode(Executable const& executable, ReadonlySpan<u32> basic_block_offsets)
{
JS::FFI::FFIValidatorBounds bounds {
.number_of_registers = executable.number_of_registers,
.number_of_locals = static_cast<u32>(executable.local_variable_names.size()),
.number_of_constants = static_cast<u32>(executable.constants.size()),
.number_of_arguments = executable.number_of_arguments,
.identifier_table_size = static_cast<u32>(executable.identifier_table->identifiers().size()),
.string_table_size = static_cast<u32>(executable.string_table->size()),
.property_key_table_size = static_cast<u32>(executable.property_key_table->property_keys().size()),
// The regex table is not consulted at runtime; m_regex_index fields
// are skipped during validation.
.regex_table_size = 0,
.property_lookup_cache_count = static_cast<u32>(executable.property_lookup_caches.size()),
.global_variable_cache_count = static_cast<u32>(executable.global_variable_caches.size()),
.environment_coordinate_cache_count = static_cast<u32>(executable.environment_coordinate_caches.size()),
.template_object_cache_count = static_cast<u32>(executable.template_object_caches.size()),
.object_shape_cache_count = static_cast<u32>(executable.object_shape_caches.size()),
.object_property_iterator_cache_count = static_cast<u32>(executable.object_property_iterator_caches.size()),
.class_blueprint_count = static_cast<u32>(executable.class_blueprints.size()),
.shared_function_data_count = static_cast<u32>(executable.shared_function_data.size()),
.completion_type_variant_count = completion_type_variant_count,
.iterator_hint_variant_count = iterator_hint_variant_count,
.environment_mode_variant_count = environment_mode_variant_count,
.put_kind_variant_count = put_kind_variant_count,
.arguments_kind_variant_count = arguments_kind_variant_count,
.function_name_prefix_variant_count = function_name_prefix_variant_count,
};
// Project Executable's exception handlers down to plain offsets; the
// structural metadata's source-position parts aren't validated here.
Vector<JS::FFI::FFIExceptionHandlerOffsets> handler_offsets;
handler_offsets.ensure_capacity(executable.exception_handlers.size());
for (auto const& h : executable.exception_handlers) {
handler_offsets.append({
.start = static_cast<u32>(h.start_offset),
.end = static_cast<u32>(h.end_offset),
.handler = static_cast<u32>(h.handler_offset),
});
}
Vector<u32> source_map_offsets;
source_map_offsets.ensure_capacity(executable.source_map.size());
for (auto const& entry : executable.source_map)
source_map_offsets.append(entry.bytecode_offset);
JS::FFI::FFIValidatorExtras extras {
.basic_block_offsets = basic_block_offsets.data(),
.basic_block_count = basic_block_offsets.size(),
.exception_handlers = handler_offsets.data(),
.exception_handler_count = handler_offsets.size(),
.source_map_offsets = source_map_offsets.data(),
.source_map_count = source_map_offsets.size(),
};
JS::FFI::FFIValidationError error {};
auto ok = rust_validate_bytecode(
executable.bytecode.data(),
executable.bytecode.size(),
&bounds,
&extras,
&error);
if (ok)
return {};
auto kind = validation_error_kind_to_string(error.kind);
dbgln("Bytecode validation failed at offset {} (opcode {}): {}",
error.offset, error.opcode, kind);
return AK::Error::from_string_view(kind);
}
}