diff --git a/Libraries/LibJS/Rust/src/bytecode/ffi.rs b/Libraries/LibJS/Rust/src/bytecode/ffi.rs index 4915e2be3b..b2766ee4bf 100644 --- a/Libraries/LibJS/Rust/src/bytecode/ffi.rs +++ b/Libraries/LibJS/Rust/src/bytecode/ffi.rs @@ -491,7 +491,11 @@ unsafe fn materialize_class_blueprints( } } -unsafe fn materialize_class_blueprint( +/// Create a C++ ClassBlueprint from a pending blueprint record. +/// +/// # Safety +/// `vm_ptr` and `source_code_ptr` must be valid pointers. +pub unsafe fn materialize_class_blueprint( blueprint: &PendingClassBlueprint, vm_ptr: *mut c_void, source_code_ptr: *const c_void, @@ -616,6 +620,29 @@ pub unsafe fn create_executable( assembled: &AssembledBytecode, vm_ptr: *mut c_void, source_code_ptr: *const c_void, +) -> ExecutableHandle { + unsafe { + let sfd_ptrs = materialize_shared_function_data(generator, vm_ptr, source_code_ptr); + let bp_ptrs = materialize_class_blueprints(generator, vm_ptr, source_code_ptr); + create_executable_with_dependencies(generator, assembled, vm_ptr, source_code_ptr, &sfd_ptrs, &bp_ptrs) + } +} + +/// Create a C++ Executable from already materialized dependency objects. +/// +/// This is used by bytecode cache materialization, where the cache blob +/// contains precompiled nested functions and class blueprints instead of +/// AST-backed `PendingSharedFunctionData` records. +/// +/// # Safety +/// `vm_ptr`, `source_code_ptr`, and all dependency pointers must be valid. +pub unsafe fn create_executable_with_dependencies( + generator: &Generator, + assembled: &AssembledBytecode, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + sfd_ptrs: &[*const c_void], + bp_ptrs: &[*mut c_void], ) -> ExecutableHandle { unsafe { // Build FFI slices for tables @@ -673,9 +700,6 @@ pub unsafe fn create_executable( .map(|v| FFIUtf16Slice::from(v.name.as_ref())) .collect(); - let sfd_ptrs = materialize_shared_function_data(generator, vm_ptr, source_code_ptr); - let bp_ptrs = materialize_class_blueprints(generator, vm_ptr, source_code_ptr); - let ffi_data = FFIExecutableData { bytecode: assembled.bytecode.as_ptr(), bytecode_length: assembled.bytecode.len(), diff --git a/Libraries/LibJS/Rust/src/bytecode_cache.rs b/Libraries/LibJS/Rust/src/bytecode_cache.rs index 8fbc0520ac..8baf15b745 100644 --- a/Libraries/LibJS/Rust/src/bytecode_cache.rs +++ b/Libraries/LibJS/Rust/src/bytecode_cache.rs @@ -11,20 +11,34 @@ //! of growing a separate procedural parser. use std::collections::HashMap; +use std::ffi::c_void; use crate::bytecode::basic_block::SourceMapEntry; -use crate::bytecode::ffi::{AbstractOperationKind, ConstantTag, WellKnownSymbolKind}; +use crate::bytecode::ffi::{ + AbstractOperationKind, ConstantTag, FFISharedFunctionData, FFIUtf16Slice, WellKnownSymbolKind, +}; use crate::bytecode::generator::{ AssembledBytecode, ConstantValue, ExceptionHandler, FunctionSfdMetadata, Generator, LocalVariable, PendingClassBlueprint, PendingClassElement, PendingLiteralValueKind, PendingSharedFunctionData, PrecompiledFunction, }; -use crate::{CompiledProgram, CompiledProgramBytecode, ast, u32_from_usize}; +use crate::bytecode::operand::PropertyKeyTableIndex; +use crate::{CompiledProgram, CompiledProgramBytecode, ModuleCallbacks, ast, u32_from_usize}; const MAGIC: &[u8; 8] = b"LBJSBC\0\0"; const FORMAT_VERSION: u32 = 1; const SOURCE_HASH_SIZE: usize = 32; +fn source_span_is_valid(start: u32, end: u32, source_len: usize) -> bool { + let start = start as usize; + let end = end as usize; + start <= end && end <= source_len +} + +fn source_range_is_valid(offset: usize, length: usize, source_len: usize) -> bool { + offset <= source_len && length <= source_len - offset +} + pub fn serialize_compiled_program( compiled: &CompiledProgram, program_type: ast::ProgramType, @@ -355,6 +369,453 @@ impl DecodedCacheBlob { self.metadata.validate(); self.program.validate(); } + + pub(crate) fn source_ranges_are_valid(&self, source_len: usize) -> bool { + self.metadata.source_ranges_are_valid(source_len) + && self.metadata.indices_are_valid() + && self.program.source_ranges_are_valid(source_len) + && self.program.indices_are_valid() + } + + pub(crate) unsafe fn materialize_script( + self, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + gdi_context: *mut c_void, + ) -> *mut c_void { + unsafe { + let Self { + program_type, + is_strict_mode, + metadata, + program, + .. + } = self; + if program_type != ast::ProgramType::Script { + return std::ptr::null_mut(); + } + let DecodedDeclarationMetadata::Script { + metadata, + declaration_functions, + } = metadata + else { + return std::ptr::null_mut(); + }; + let ProgramKind::ScriptOrModule = program.kind else { + return std::ptr::null_mut(); + }; + if declaration_functions.len() != metadata.function_names.len() { + return std::ptr::null_mut(); + } + + if !materialize_script_declaration_metadata( + metadata, + declaration_functions, + is_strict_mode, + vm_ptr, + source_code_ptr, + gdi_context, + ) { + return std::ptr::null_mut(); + } + materialize_executable(program.executable, vm_ptr, source_code_ptr) + } + } + + pub(crate) unsafe fn materialize_module( + self, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + module_context: *mut c_void, + callbacks: *const ModuleCallbacks, + tla_executable_out: *mut *mut c_void, + ) -> *mut c_void { + unsafe { + if callbacks.is_null() { + return std::ptr::null_mut(); + } + let cb = &*callbacks; + let Self { + program_type, + has_top_level_await, + metadata, + program, + .. + } = self; + if program_type != ast::ProgramType::Module { + return std::ptr::null_mut(); + } + let DecodedDeclarationMetadata::Module { + metadata, + declaration_functions, + } = metadata + else { + return std::ptr::null_mut(); + }; + if declaration_functions.len() != metadata.function_names.len() { + return std::ptr::null_mut(); + } + + (cb.set_has_top_level_await)(module_context, has_top_level_await); + if !materialize_module_declaration_metadata( + metadata, + declaration_functions, + vm_ptr, + source_code_ptr, + module_context, + cb, + ) { + return std::ptr::null_mut(); + } + + match program.kind { + ProgramKind::AsyncModule => { + let exec_ptr = materialize_executable(program.executable, vm_ptr, source_code_ptr); + if !tla_executable_out.is_null() { + *tla_executable_out = exec_ptr; + } + std::ptr::null_mut() + } + ProgramKind::ScriptOrModule => { + if !tla_executable_out.is_null() { + *tla_executable_out = std::ptr::null_mut(); + } + materialize_executable(program.executable, vm_ptr, source_code_ptr) + } + } + } + } +} + +unsafe fn materialize_script_declaration_metadata( + metadata: ScriptDeclarationMetadata, + declaration_functions: Vec, + is_strict_mode: bool, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + gdi_context: *mut c_void, +) -> bool { + unsafe { + use crate::bytecode::ffi::{ + script_gdi_push_annex_b_name, script_gdi_push_function, script_gdi_push_lexical_binding, + script_gdi_push_lexical_name, script_gdi_push_var_name, script_gdi_push_var_scoped_name, + }; + + for name in &metadata.lexical_names { + script_gdi_push_lexical_name(gdi_context, name.as_ptr(), name.len()); + } + for name in &metadata.var_names { + script_gdi_push_var_name(gdi_context, name.as_ptr(), name.len()); + } + for (function, name) in declaration_functions.into_iter().zip(metadata.function_names.iter()) { + let sfd_ptr = materialize_function(function, is_strict_mode, vm_ptr, source_code_ptr); + if sfd_ptr.is_null() { + return false; + } + script_gdi_push_function(gdi_context, sfd_ptr, name.as_ptr(), name.len()); + } + for name in &metadata.var_scoped_names { + script_gdi_push_var_scoped_name(gdi_context, name.as_ptr(), name.len()); + } + for name in &metadata.annex_b_candidate_names { + script_gdi_push_annex_b_name(gdi_context, name.as_ptr(), name.len()); + } + for binding in &metadata.lexical_bindings { + script_gdi_push_lexical_binding( + gdi_context, + binding.name.as_ptr(), + binding.name.len(), + binding.is_constant, + ); + } + + true + } +} + +unsafe fn materialize_module_declaration_metadata( + metadata: ModuleDeclarationMetadata, + declaration_functions: Vec, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + module_context: *mut c_void, + cb: &ModuleCallbacks, +) -> bool { + unsafe { + for entry in &metadata.import_entries { + let (import_name, import_name_len, is_namespace) = entry + .import_name + .as_ref() + .map(|name| (name.as_ptr(), name.len(), false)) + .unwrap_or((std::ptr::null(), 0, true)); + let attributes = import_attributes_to_ffi(&entry.module_request.attributes); + (cb.push_import_entry)( + module_context, + import_name, + import_name_len, + is_namespace, + entry.local_name.as_ptr(), + entry.local_name.len(), + entry.module_request.specifier.as_ptr(), + entry.module_request.specifier.len(), + attributes.keys.as_ptr(), + attributes.values.as_ptr(), + attributes.keys.len(), + ); + } + + for entry in &metadata.local_exports { + push_module_export_entry(module_context, cb.push_local_export, entry); + } + for entry in &metadata.indirect_exports { + push_module_export_entry(module_context, cb.push_indirect_export, entry); + } + for entry in &metadata.star_exports { + push_module_export_entry(module_context, cb.push_star_export, entry); + } + for request in &metadata.requested_modules { + let attributes = import_attributes_to_ffi(&request.attributes); + (cb.push_requested_module)( + module_context, + request.specifier.as_ptr(), + request.specifier.len(), + attributes.keys.as_ptr(), + attributes.values.as_ptr(), + attributes.keys.len(), + ); + } + if let Some(name) = &metadata.default_export_binding_name { + (cb.set_default_export_binding)(module_context, name.as_ptr(), name.len()); + } + for name in &metadata.var_declared_names { + (cb.push_var_name)(module_context, name.as_ptr(), name.len()); + } + for (function, name) in declaration_functions.into_iter().zip(metadata.function_names.iter()) { + let sfd_ptr = materialize_function(function, true, vm_ptr, source_code_ptr); + if sfd_ptr.is_null() { + return false; + } + (cb.push_function)(module_context, sfd_ptr, name.as_ptr(), name.len()); + } + for binding in &metadata.lexical_bindings { + (cb.push_lexical_binding)( + module_context, + binding.name.as_ptr(), + binding.name.len(), + binding.is_constant, + binding.function_index, + ); + } + + true + } +} + +struct ImportAttributesFfi { + keys: Vec, + values: Vec, +} + +fn import_attributes_to_ffi(attributes: &[ast::ImportAttribute]) -> ImportAttributesFfi { + ImportAttributesFfi { + keys: attributes + .iter() + .map(|attribute| FFIUtf16Slice::from(attribute.key.as_ref())) + .collect(), + values: attributes + .iter() + .map(|attribute| FFIUtf16Slice::from(attribute.value.as_ref())) + .collect(), + } +} + +unsafe fn push_module_export_entry( + module_context: *mut c_void, + callback: crate::ModuleExportEntryCallback, + entry: &ModuleExportEntryRecord, +) { + unsafe { + let (export_name, export_name_len) = entry + .export_name + .as_ref() + .map(|name| (name.as_ptr(), name.len())) + .unwrap_or((std::ptr::null(), 0)); + let (local_or_import_name, local_or_import_name_len) = entry + .local_or_import_name + .as_ref() + .map(|name| (name.as_ptr(), name.len())) + .unwrap_or((std::ptr::null(), 0)); + let (module_specifier, module_specifier_len, attributes) = entry + .module_request + .as_ref() + .map(|request| { + ( + request.specifier.as_ptr(), + request.specifier.len(), + import_attributes_to_ffi(&request.attributes), + ) + }) + .unwrap_or(( + std::ptr::null(), + 0, + ImportAttributesFfi { + keys: Vec::new(), + values: Vec::new(), + }, + )); + + callback( + module_context, + entry.kind as u8, + export_name, + export_name_len, + local_or_import_name, + local_or_import_name_len, + module_specifier, + module_specifier_len, + attributes.keys.as_ptr(), + attributes.values.as_ptr(), + attributes.keys.len(), + ); + } +} + +unsafe fn materialize_function( + function: DecodedFunctionRecord, + outer_strict: bool, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, +) -> *mut c_void { + unsafe { + let parameter_names: Vec = function + .parameter_names + .as_ref() + .map(|names| names.iter().map(|name| FFIUtf16Slice::from(name.as_ref())).collect()) + .unwrap_or_default(); + let (name, name_len) = function + .name + .as_ref() + .map(|name| (name.as_ptr(), name.len())) + .unwrap_or((std::ptr::null(), 0)); + let source_text_offset = function.source_text_start as usize; + let source_text_length = function + .source_text_end + .checked_sub(function.source_text_start) + .map(|length| length as usize) + .unwrap_or(0); + + let data = FFISharedFunctionData { + name, + name_len, + function_kind: function.kind as u8, + function_length: function.function_length, + formal_parameter_count: function.formal_parameter_count, + strict: function.is_strict_mode || outer_strict, + is_arrow: function.is_arrow_function, + has_simple_parameter_list: function.parameter_names.is_some(), + parameter_names: parameter_names.as_ptr(), + parameter_name_count: parameter_names.len(), + source_text_offset, + source_text_length, + rust_function_ast: std::ptr::null_mut(), + uses_this: function.uses_this, + uses_this_from_environment: function.uses_this_from_environment, + }; + + let sfd_ptr = crate::bytecode::ffi::rust_create_sfd(vm_ptr, source_code_ptr, &raw const data); + if sfd_ptr.is_null() { + return std::ptr::null_mut(); + } + + if let Some((name, is_private)) = &function.class_field_initializer_name { + crate::bytecode::ffi::rust_sfd_set_class_field_initializer_name( + sfd_ptr, + name.as_ptr(), + name.len(), + *is_private, + ); + } + + let executable_ptr = materialize_executable(function.precompiled, vm_ptr, source_code_ptr); + if executable_ptr.is_null() { + return std::ptr::null_mut(); + } + crate::bytecode::ffi::rust_sfd_set_precompiled_executable( + sfd_ptr, + executable_ptr, + function.metadata.uses_this, + function.metadata.this_value_needs_environment_resolution, + function.metadata.function_environment_needed, + function.metadata.function_environment_bindings_count, + function.metadata.might_need_arguments, + function.metadata.contains_eval, + ); + + sfd_ptr + } +} + +unsafe fn materialize_executable( + executable: DecodedExecutableRecord, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, +) -> *mut c_void { + unsafe { + let mut generator = Generator::new(); + generator.strict = executable.strict; + generator.this_value_needs_environment_resolution = executable.this_value_needs_environment_resolution; + generator.next_property_lookup_cache = executable.cache_counters.property_lookup_cache_count; + generator.next_global_variable_cache = executable.cache_counters.global_variable_cache_count; + generator.next_template_object_cache = executable.cache_counters.template_object_cache_count; + generator.next_object_shape_cache = executable.cache_counters.object_shape_cache_count; + generator.next_object_property_iterator_cache = executable.cache_counters.object_property_iterator_cache_count; + generator.identifier_table = executable.identifier_table; + generator.property_key_table = executable.property_key_table; + generator.string_table = executable.string_table; + generator.constants = executable.constants; + generator.local_variables = executable.local_variables; + generator.length_identifier = executable.length_identifier.map(PropertyKeyTableIndex); + + let sfd_ptrs: Vec<*const c_void> = executable + .shared_functions + .into_iter() + .map(|function| materialize_function(function, generator.strict, vm_ptr, source_code_ptr) as *const c_void) + .collect(); + if sfd_ptrs.iter().any(|ptr| ptr.is_null()) { + return std::ptr::null_mut(); + } + + let class_blueprints: Vec = executable + .class_blueprints + .into_iter() + .map(PendingClassBlueprint::from) + .collect(); + let bp_ptrs: Vec<*mut c_void> = class_blueprints + .iter() + .map(|blueprint| crate::bytecode::ffi::materialize_class_blueprint(blueprint, vm_ptr, source_code_ptr)) + .collect(); + if bp_ptrs.iter().any(|ptr| ptr.is_null()) { + return std::ptr::null_mut(); + } + + let assembled = AssembledBytecode { + bytecode: executable.bytecode, + source_map: executable.source_map, + exception_handlers: executable.exception_handlers, + basic_block_start_offsets: executable.basic_block_start_offsets, + number_of_registers: executable.number_of_registers, + number_of_arguments: executable.number_of_arguments, + }; + + crate::bytecode::ffi::create_executable_with_dependencies( + &generator, + &assembled, + vm_ptr, + source_code_ptr, + &sfd_ptrs, + &bp_ptrs, + ) + } } impl Encode for ast::ProgramType { @@ -465,6 +926,36 @@ impl DecodedDeclarationMetadata { } } } + + fn source_ranges_are_valid(&self, source_len: usize) -> bool { + match self { + Self::Script { + declaration_functions, .. + } + | Self::Module { + declaration_functions, .. + } => declaration_functions + .iter() + .all(|function| function.source_ranges_are_valid(source_len)), + } + } + + fn indices_are_valid(&self) -> bool { + match self { + Self::Script { .. } => true, + Self::Module { + metadata, + declaration_functions, + } => { + declaration_functions.len() == metadata.function_names.len() + && metadata.lexical_bindings.iter().all(|binding| { + binding.function_index < 0 + || usize::try_from(binding.function_index) + .is_ok_and(|index| index < declaration_functions.len()) + }) + } + } + } } #[repr(u8)] @@ -1321,6 +1812,14 @@ impl DecodedProgramRecord { let _ = self.kind as u8; self.executable.validate(); } + + fn source_ranges_are_valid(&self, source_len: usize) -> bool { + self.executable.source_ranges_are_valid(source_len) + } + + fn indices_are_valid(&self) -> bool { + self.executable.indices_are_valid() + } } #[repr(u8)] @@ -1443,6 +1942,25 @@ impl DecodedExecutableRecord { blueprint.validate(); } } + + fn source_ranges_are_valid(&self, source_len: usize) -> bool { + self.shared_functions + .iter() + .all(|function| function.source_ranges_are_valid(source_len)) + && self + .class_blueprints + .iter() + .all(|blueprint| blueprint.source_range_is_valid(source_len)) + } + + fn indices_are_valid(&self) -> bool { + self.length_identifier + .is_none_or(|index| (index as usize) < self.property_key_table.len()) + && self + .class_blueprints + .iter() + .all(|blueprint| blueprint.indices_are_valid(self.shared_functions.len())) + } } struct CacheCounters<'a>(&'a Generator); @@ -1795,7 +2313,9 @@ struct DecodedFunctionRecord { impl DecodedFunctionRecord { fn validate(&self) { let _ = self.name.as_ref().map(|name| name.as_slice().len()); - let _ = self.source_text_start + self.source_text_end + self.formal_parameter_count; + let _ = self.source_text_start; + let _ = self.source_text_end; + let _ = self.formal_parameter_count; let _ = self.function_length; let _ = self.kind as u8; let _ = self.is_strict_mode || self.is_arrow_function || self.uses_this || self.uses_this_from_environment; @@ -1807,6 +2327,11 @@ impl DecodedFunctionRecord { self.precompiled.validate(); validate_function_metadata(&self.metadata); } + + fn source_ranges_are_valid(&self, source_len: usize) -> bool { + source_span_is_valid(self.source_text_start, self.source_text_end, source_len) + && self.precompiled.source_ranges_are_valid(source_len) + } } impl<'a> FunctionRecord<'a> { @@ -2008,13 +2533,40 @@ struct DecodedClassBlueprintRecord { impl DecodedClassBlueprintRecord { fn validate(&self) { let _ = self.name.as_ref().map(|name| name.as_slice().len()); - let _ = self.source_text_offset + self.source_text_length; + let _ = self.source_text_offset; + let _ = self.source_text_length; let _ = self.constructor_sfd_index; let _ = self.has_super_class || self.has_name; for element in &self.elements { element.validate(); } } + + fn source_range_is_valid(&self, source_len: usize) -> bool { + source_range_is_valid(self.source_text_offset, self.source_text_length, source_len) + } + + fn indices_are_valid(&self, shared_function_count: usize) -> bool { + (self.constructor_sfd_index as usize) < shared_function_count + && self + .elements + .iter() + .all(|element| element.indices_are_valid(shared_function_count)) + } +} + +impl From for PendingClassBlueprint { + fn from(record: DecodedClassBlueprintRecord) -> Self { + Self { + name: record.name, + source_text_offset: record.source_text_offset, + source_text_length: record.source_text_length, + constructor_sfd_index: record.constructor_sfd_index, + has_super_class: record.has_super_class, + has_name: record.has_name, + elements: record.elements.into_iter().map(PendingClassElement::from).collect(), + } + } } struct ClassElementRecord<'a>(&'a PendingClassElement); @@ -2071,6 +2623,42 @@ impl DecodedClassElementRecord { let _ = self.literal_value_number; let _ = self.literal_value_string.as_ref().map(|value| value.as_slice().len()); } + + fn indices_are_valid(&self, shared_function_count: usize) -> bool { + let shared_function_data_index_is_valid = || { + self.shared_function_data_index + .is_some_and(|index| (index as usize) < shared_function_count) + }; + + match self.kind { + 0 | 1 | 2 | 4 => shared_function_data_index_is_valid(), + 3 => { + if self.has_initializer && matches!(self.literal_value_kind, PendingLiteralValueKind::None) { + shared_function_data_index_is_valid() + } else { + self.shared_function_data_index + .is_none_or(|index| (index as usize) < shared_function_count) + } + } + _ => false, + } + } +} + +impl From for PendingClassElement { + fn from(record: DecodedClassElementRecord) -> Self { + Self { + kind: record.kind, + is_static: record.is_static, + is_private: record.is_private, + private_identifier: record.private_identifier, + shared_function_data_index: record.shared_function_data_index, + has_initializer: record.has_initializer, + literal_value_kind: record.literal_value_kind, + literal_value_number: record.literal_value_number, + literal_value_string: record.literal_value_string, + } + } } impl Decode for PendingLiteralValueKind { diff --git a/Libraries/LibJS/Rust/src/lib.rs b/Libraries/LibJS/Rust/src/lib.rs index f9e72c6370..c8eb8190bf 100644 --- a/Libraries/LibJS/Rust/src/lib.rs +++ b/Libraries/LibJS/Rust/src/lib.rs @@ -963,6 +963,68 @@ pub unsafe extern "C" fn rust_free_decoded_bytecode_cache_blob(blob: *mut Decode } } +/// Materialize a decoded script bytecode cache blob. Consumes and frees the blob. +/// +/// # Safety +/// - `blob` must be a valid pointer from `rust_decode_bytecode_cache_blob()`. +/// - `vm_ptr` must be a valid `JS::VM*`. +/// - `source_code_ptr` must be a valid `JS::SourceCode const*`. +/// - `gdi_context` must be a valid pointer to a C++ ScriptGdiBuilder. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_materialize_bytecode_cache_script( + blob: *mut DecodedBytecodeCacheBlob, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + source_len: usize, + gdi_context: *mut c_void, +) -> *mut c_void { + unsafe { + abort_on_panic(|| { + if blob.is_null() { + return std::ptr::null_mut(); + } + let blob = Box::from_raw(blob); + if !blob._blob.source_ranges_are_valid(source_len) { + return std::ptr::null_mut(); + } + blob._blob.materialize_script(vm_ptr, source_code_ptr, gdi_context) + }) + } +} + +/// Materialize a decoded module bytecode cache blob. Consumes and frees the blob. +/// +/// # Safety +/// - `blob` must be a valid pointer from `rust_decode_bytecode_cache_blob()`. +/// - `vm_ptr` must be a valid `JS::VM*`. +/// - `source_code_ptr` must be a valid `JS::SourceCode const*`. +/// - `module_context` must be a valid `ModuleBuilder*`. +/// - `callbacks` must point to a valid `ModuleCallbacks`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_materialize_bytecode_cache_module( + blob: *mut DecodedBytecodeCacheBlob, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + source_len: usize, + module_context: *mut c_void, + callbacks: *const ModuleCallbacks, + tla_executable_out: *mut *mut c_void, +) -> *mut c_void { + unsafe { + abort_on_panic(|| { + if blob.is_null() { + return std::ptr::null_mut(); + } + let blob = Box::from_raw(blob); + if !blob._blob.source_ranges_are_valid(source_len) { + return std::ptr::null_mut(); + } + blob._blob + .materialize_module(vm_ptr, source_code_ptr, module_context, callbacks, tla_executable_out) + }) + } +} + /// Get the AST dump string from a ParsedProgram. /// /// Generates the dump on first call and caches it. Writes the pointer diff --git a/Libraries/LibJS/RustIntegration.cpp b/Libraries/LibJS/RustIntegration.cpp index e2651a9d40..4e45d08da6 100644 --- a/Libraries/LibJS/RustIntegration.cpp +++ b/Libraries/LibJS/RustIntegration.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -38,6 +39,11 @@ namespace JS::RustIntegration { // --- Shared helpers --- +// Bytecode cache materialization rebuilds executables from disk, which is untrusted input. Materialization paths flip +// this flag for the duration of their work so that the in-process bytecode validator runs even in release builds; the +// normal Rust pipeline path leaves it off and keeps the existing debug/sanitizer-only behavior. +static thread_local bool s_validate_materialized_bytecode_cache_executables = false; + static Utf16View utf16_view_from_bytes(uint16_t const* data, size_t len) { if (len == 0) @@ -450,6 +456,24 @@ Optional>> materialize_compiled_script( return builder.result; } +Optional>> materialize_bytecode_cache_script(DecodedBytecodeCacheBlob* blob, NonnullRefPtr source_code, Realm& realm) +{ + if (!blob) + return {}; + + GC::DeferGC defer_gc(realm.vm().heap()); + TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true }; + ScriptGdiBuilder builder; + + void* exec_ptr = rust_materialize_bytecode_cache_script(blob, &realm.vm(), source_code.ptr(), source_code->length_in_code_units(), &builder); + + if (!exec_ptr) + return Vector { ParserError { "Failed to materialize bytecode cache"_string, {} } }; + + builder.result.executable = static_cast(exec_ptr); + return builder.result; +} + Optional>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset) { auto source_code = SourceCode::create( @@ -606,6 +630,56 @@ Optional>> materialize_compiled_module( return builder.result; } +Optional>> materialize_bytecode_cache_module(DecodedBytecodeCacheBlob* blob, NonnullRefPtr source_code, Realm& realm) +{ + if (!blob) + return {}; + + GC::DeferGC defer_gc(realm.vm().heap()); + TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true }; + ModuleBuilder builder; + ModuleCallbacks callbacks { + .set_has_top_level_await = module_set_has_top_level_await, + .push_import_entry = module_push_import_entry, + .push_local_export = module_push_local_export, + .push_indirect_export = module_push_indirect_export, + .push_star_export = module_push_star_export, + .push_requested_module = module_push_requested_module, + .set_default_export_binding = module_set_default_export_binding, + .push_var_name = module_push_var_name, + .push_function = module_push_function, + .push_lexical_binding = module_push_lexical_binding, + }; + + void* tla_executable = nullptr; + + void* exec_ptr = rust_materialize_bytecode_cache_module(blob, &realm.vm(), source_code.ptr(), source_code->length_in_code_units(), + &builder, &callbacks, &tla_executable); + + if (!exec_ptr && !tla_executable) + return Vector { ParserError { "Failed to materialize bytecode cache"_string, {} } }; + + if (tla_executable) { + auto& vm = realm.vm(); + auto* tla_exec = static_cast(tla_executable); + + builder.result.tla_shared_data = vm.heap().allocate( + vm, FunctionKind::Async, + "module code with top-level await"_utf16_fly_string, + 0, 0, true, false, true, + Vector {}, nullptr); + builder.result.tla_shared_data->m_is_module_wrapper = true; + builder.result.tla_shared_data->m_uses_this = true; + builder.result.tla_shared_data->m_function_environment_needed = true; + builder.result.tla_shared_data->update_asm_call_metadata(); + builder.result.tla_shared_data->set_executable(tla_exec); + } else { + builder.result.executable = static_cast(exec_ptr); + } + + return builder.result; +} + Optional>> compile_module(StringView source_text, Realm& realm, StringView filename) { auto source_code = SourceCode::create(String::from_utf8(filename).release_value_but_fixme_should_propagate_errors(), Utf16String::from_utf8(source_text)); @@ -987,10 +1061,23 @@ extern "C" void* rust_create_executable( delete bp; } + auto const is_materializing_bytecode_cache = JS::RustIntegration::s_validate_materialized_bytecode_cache_executables; #if !defined(NDEBUG) || defined(HAS_ADDRESS_SANITIZER) - if (auto validation = JS::Bytecode::validate_bytecode(*executable, JS::Bytecode::CacheState::BeforeFixup); validation.is_error()) - VERIFY_NOT_REACHED(); + auto const should_validate_bytecode = true; +#else + auto const should_validate_bytecode = is_materializing_bytecode_cache; #endif + if (should_validate_bytecode) { + if (auto validation = JS::Bytecode::validate_bytecode(*executable, JS::Bytecode::CacheState::BeforeFixup); validation.is_error()) { + if (is_materializing_bytecode_cache) + return nullptr; +#if !defined(NDEBUG) || defined(HAS_ADDRESS_SANITIZER) + VERIFY_NOT_REACHED(); +#else + return nullptr; +#endif + } + } executable->fixup_cache_pointers(); diff --git a/Libraries/LibJS/RustIntegration.h b/Libraries/LibJS/RustIntegration.h index d2ad9a0a8d..09af099041 100644 --- a/Libraries/LibJS/RustIntegration.h +++ b/Libraries/LibJS/RustIntegration.h @@ -117,6 +117,14 @@ JS_API FFI::DecodedBytecodeCacheBlob* decode_bytecode_cache_blob(ReadonlyBytes, // Free a decoded bytecode cache blob. JS_API void free_decoded_bytecode_cache_blob(FFI::DecodedBytecodeCacheBlob*); +// Materialize a decoded script bytecode cache blob. Must be called on the main thread. +// Consumes and frees the decoded blob. +JS_API Optional>> materialize_bytecode_cache_script(FFI::DecodedBytecodeCacheBlob*, NonnullRefPtr source_code, Realm&); + +// Materialize a decoded module bytecode cache blob. Must be called on the main thread. +// Consumes and frees the decoded blob. +JS_API Optional>> materialize_bytecode_cache_module(FFI::DecodedBytecodeCacheBlob*, NonnullRefPtr source_code, Realm&); + // Compile a previously parsed script. Must be called on the main thread. // Consumes and frees the Rust ParsedProgram. // Returns nullopt if Rust is not available. diff --git a/Tests/LibJS/CMakeLists.txt b/Tests/LibJS/CMakeLists.txt index 013cea3365..ae589e3e74 100644 --- a/Tests/LibJS/CMakeLists.txt +++ b/Tests/LibJS/CMakeLists.txt @@ -1,5 +1,6 @@ ladybird_test(test-value-js.cpp LibJS LIBS LibJS LibUnicode) ladybird_test(test-primitive-string.cpp LibJS LIBS LibJS LibGC) +ladybird_test(test-bytecode-cache.cpp LibJS LIBS LibCrypto LibGC LibJS) ladybird_testjs_test(test-js.cpp test-js LIBS LibGC) set_tests_properties(test-js PROPERTIES ENVIRONMENT "LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR}") diff --git a/Tests/LibJS/test-bytecode-cache.cpp b/Tests/LibJS/test-bytecode-cache.cpp new file mode 100644 index 0000000000..36e8d093a3 --- /dev/null +++ b/Tests/LibJS/test-bytecode-cache.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +struct BytecodeCacheTestData { + NonnullRefPtr source_code; + ByteBuffer blob; + Crypto::Hash::SHA256::DigestType source_hash; +}; + +class BytecodeCacheBlobReader { +public: + explicit BytecodeCacheBlobReader(ReadonlyBytes bytes) + : m_bytes(bytes) + { + } + + void skip(size_t length) + { + VERIFY(m_offset + length <= m_bytes.size()); + m_offset += length; + } + + bool read_bool() + { + return read_u8() != 0; + } + + u8 read_u8() + { + VERIFY(m_offset < m_bytes.size()); + return m_bytes[m_offset++]; + } + + u32 read_u32() + { + VERIFY(m_offset + sizeof(u32) <= m_bytes.size()); + auto value = static_cast(m_bytes[m_offset]) + | (static_cast(m_bytes[m_offset + 1]) << 8) + | (static_cast(m_bytes[m_offset + 2]) << 16) + | (static_cast(m_bytes[m_offset + 3]) << 24); + m_offset += sizeof(u32); + return value; + } + + void skip_utf16() + { + auto length = read_u32(); + skip(length * sizeof(u16)); + } + + void skip_utf16_vector() + { + auto length = read_u32(); + for (u32 i = 0; i < length; ++i) + skip_utf16(); + } + + void skip_optional_utf16() + { + if (read_bool()) + skip_utf16(); + } + + void skip_optional_u32() + { + if (read_bool()) + skip(sizeof(u32)); + } + + size_t offset() const { return m_offset; } + +private: + ReadonlyBytes m_bytes; + size_t m_offset { 0 }; +}; + +static void write_u32(ByteBuffer& bytes, size_t offset, u32 value) +{ + VERIFY(offset + sizeof(u32) <= bytes.size()); + bytes[offset] = value & 0xff; + bytes[offset + 1] = (value >> 8) & 0xff; + bytes[offset + 2] = (value >> 16) & 0xff; + bytes[offset + 3] = (value >> 24) & 0xff; +} + +static BytecodeCacheTestData create_bytecode_cache_blob(StringView source) +{ + auto source_code = JS::SourceCode::create("test.js"_string, Utf16String::from_utf8(source)); + auto source_hash = Crypto::Hash::SHA256::hash(reinterpret_cast(source_code->utf16_data()), source_code->length_in_code_units() * sizeof(u16)); + + auto* parsed = JS::RustIntegration::parse_program(source_code->utf16_data(), source_code->length_in_code_units(), JS::RustIntegration::ProgramType::Script); + VERIFY(parsed); + ArmedScopeGuard free_parsed = [&] { + JS::RustIntegration::free_parsed_program(parsed); + }; + EXPECT(!JS::RustIntegration::parsed_program_has_errors(parsed)); + + auto* compiled = JS::RustIntegration::compile_parsed_program_fully_off_thread(parsed, source_code->length_in_code_units()); + VERIFY(compiled); + free_parsed.disarm(); + ScopeGuard free_compiled = [&] { + JS::RustIntegration::free_compiled_program(compiled); + }; + + auto blob = JS::RustIntegration::serialize_compiled_program_for_bytecode_cache(*compiled, JS::RustIntegration::ProgramType::Script, source_hash.bytes()); + VERIFY(!blob.is_empty()); + + return { + .source_code = source_code, + .blob = move(blob), + .source_hash = source_hash, + }; +} + +static size_t first_declaration_function_bytecode_payload_offset(ReadonlyBytes blob) +{ + BytecodeCacheBlobReader reader { blob }; + + reader.skip(8); // Magic. + reader.skip(sizeof(u32)); // Format version. + reader.skip(1); // Program type. + reader.skip(32); // Source hash. + reader.skip(1); // Has top-level await. + reader.skip(1); // Is strict mode. + + EXPECT_EQ(reader.read_u8(), 0); // Script declaration metadata. + reader.skip_utf16_vector(); // Lexical names. + reader.skip_utf16_vector(); // Var names. + reader.skip_utf16_vector(); // Function names. + reader.skip_utf16_vector(); // Var-scoped names. + reader.skip_utf16_vector(); // Annex B candidate names. + + auto lexical_binding_count = reader.read_u32(); + for (u32 i = 0; i < lexical_binding_count; ++i) { + reader.skip_utf16(); + reader.skip(1); + } + + auto declaration_function_count = reader.read_u32(); + VERIFY(declaration_function_count > 0); + + reader.skip_optional_utf16(); // Function name. + reader.skip(sizeof(u32)); // Source text start. + reader.skip(sizeof(u32)); // Source text end. + reader.skip(sizeof(i32)); // Function length. + reader.skip(sizeof(u32)); // Formal parameter count. + reader.skip(1); // Function kind. + reader.skip(1); // Strict mode. + reader.skip(1); // Arrow function. + + if (reader.read_bool()) + reader.skip_utf16_vector(); + + reader.skip(1); // Uses this. + reader.skip(1); // Uses this from environment. + if (reader.read_bool()) { + reader.skip_utf16(); + reader.skip(1); + } + + reader.skip(3); // Function metadata bools. + reader.skip(sizeof(u64)); // Function environment bindings count. + reader.skip(sizeof(u64)); // Var environment bindings count. + reader.skip(2); // Function metadata bools. + reader.skip(1); // Executable strict mode. + reader.skip(sizeof(u32)); // Number of registers. + reader.skip(sizeof(u32)); // Number of arguments. + reader.skip(5 * sizeof(u32)); // Cache counters. + reader.skip(1); // This value needs environment resolution. + reader.skip_optional_u32(); // Length identifier. + + auto bytecode_length = reader.read_u32(); + VERIFY(bytecode_length > 0); + return reader.offset(); +} + +static size_t first_declaration_function_source_text_start_offset(ReadonlyBytes blob) +{ + BytecodeCacheBlobReader reader { blob }; + + reader.skip(8); // Magic. + reader.skip(sizeof(u32)); // Format version. + reader.skip(1); // Program type. + reader.skip(32); // Source hash. + reader.skip(1); // Has top-level await. + reader.skip(1); // Is strict mode. + + EXPECT_EQ(reader.read_u8(), 0); // Script declaration metadata. + reader.skip_utf16_vector(); // Lexical names. + reader.skip_utf16_vector(); // Var names. + reader.skip_utf16_vector(); // Function names. + reader.skip_utf16_vector(); // Var-scoped names. + reader.skip_utf16_vector(); // Annex B candidate names. + + auto lexical_binding_count = reader.read_u32(); + for (u32 i = 0; i < lexical_binding_count; ++i) { + reader.skip_utf16(); + reader.skip(1); + } + + auto declaration_function_count = reader.read_u32(); + VERIFY(declaration_function_count > 0); + + reader.skip_optional_utf16(); // Function name. + return reader.offset(); +} + +TEST_CASE(bytecode_cache_materialization_failure_has_parser_error) +{ + auto vm = JS::VM::create(); + auto root_execution_context = JS::create_simple_execution_context(*vm); + auto& realm = *root_execution_context->realm; + + auto test_data = create_bytecode_cache_blob("1;"sv); + + auto corrupted_blob = MUST(ByteBuffer::copy(test_data.blob.bytes())); + + // For this minimal script, the encoded top-level bytecode payload begins + // after the cache header, empty declaration metadata, and executable + // metadata. Corrupting the payload keeps the blob structurally decodable + // while causing executable validation to reject it during materialization. + constexpr size_t bytecode_payload_offset_for_empty_script = 112; + VERIFY(corrupted_blob.size() > bytecode_payload_offset_for_empty_script); + corrupted_blob[bytecode_payload_offset_for_empty_script] ^= 0xff; + + // Structural decode still succeeds because the blob is internally well-formed; the corruption is in the bytecode + // payload, which is only checked by the validator that runs during materialization. + auto* decoded_blob = JS::RustIntegration::decode_bytecode_cache_blob(corrupted_blob.bytes(), JS::RustIntegration::ProgramType::Script, test_data.source_hash.bytes()); + VERIFY(decoded_blob); + + auto materialized = JS::RustIntegration::materialize_bytecode_cache_script(decoded_blob, test_data.source_code, realm); + EXPECT(materialized.has_value()); + EXPECT(materialized->is_error()); + EXPECT(!materialized->error().is_empty()); + EXPECT_EQ(materialized->error().first().message, "Failed to materialize bytecode cache"_string); +} + +TEST_CASE(bytecode_cache_rejects_corrupt_declaration_function) +{ + auto vm = JS::VM::create(); + auto root_execution_context = JS::create_simple_execution_context(*vm); + auto& realm = *root_execution_context->realm; + + auto test_data = create_bytecode_cache_blob("function f() { return 1; } f();"_string); + auto corrupted_blob = MUST(ByteBuffer::copy(test_data.blob.bytes())); + + auto declaration_function_bytecode_offset = first_declaration_function_bytecode_payload_offset(corrupted_blob.bytes()); + VERIFY(declaration_function_bytecode_offset < corrupted_blob.size()); + corrupted_blob[declaration_function_bytecode_offset] ^= 0xff; + + // Structural decode still succeeds (the blob layout is intact); the corruption is in a function's bytecode payload + // and is only caught when the materializer asks the validator to check it. + auto* decoded_blob = JS::RustIntegration::decode_bytecode_cache_blob(corrupted_blob.bytes(), JS::RustIntegration::ProgramType::Script, test_data.source_hash.bytes()); + VERIFY(decoded_blob); + + auto materialized = JS::RustIntegration::materialize_bytecode_cache_script(decoded_blob, test_data.source_code, realm); + EXPECT(materialized.has_value()); + EXPECT(materialized->is_error()); + EXPECT(!materialized->error().is_empty()); + EXPECT_EQ(materialized->error().first().message, "Failed to materialize bytecode cache"_string); +} + +TEST_CASE(bytecode_cache_rejects_out_of_range_declaration_function_source_span) +{ + auto vm = JS::VM::create(); + auto root_execution_context = JS::create_simple_execution_context(*vm); + auto& realm = *root_execution_context->realm; + + auto test_data = create_bytecode_cache_blob("function f() { return 1; } f();"_string); + auto corrupted_blob = MUST(ByteBuffer::copy(test_data.blob.bytes())); + + auto source_text_start_offset = first_declaration_function_source_text_start_offset(corrupted_blob.bytes()); + write_u32(corrupted_blob, source_text_start_offset, test_data.source_code->length_in_code_units() + 1); + + // The source hash still matches and the blob layout is intact, but the cached function source span points outside + // the current SourceCode. Materialization should reject it as a cache miss instead of handing the range to C++. + auto* decoded_blob = JS::RustIntegration::decode_bytecode_cache_blob(corrupted_blob.bytes(), JS::RustIntegration::ProgramType::Script, test_data.source_hash.bytes()); + VERIFY(decoded_blob); + + auto materialized = JS::RustIntegration::materialize_bytecode_cache_script(decoded_blob, test_data.source_code, realm); + EXPECT(materialized.has_value()); + EXPECT(materialized->is_error()); + EXPECT(!materialized->error().is_empty()); + EXPECT_EQ(materialized->error().first().message, "Failed to materialize bytecode cache"_string); +}