LibJS: Materialize decoded bytecode cache blobs

Create parser-free script and module materializers for decoded cache
blobs. Cached functions create SFDs without Rust compile inputs and
attach their precompiled executable immediately, while declaration
metadata is populated from decoded records.

Treat cache blobs as external input from the HTTP disk cache. Run
bytecode validation unconditionally before fixing up cache pointers, and
reject decoded source ranges or metadata indices that would be
out-of-bounds during C++ materialization.

Report executable validation failures as parser errors so callers can
reject corrupt sidecars and fall back to source compilation. LibJS tests
cover corrupt top-level bytecode, declaration bytecode, and declaration
source spans.
This commit is contained in:
Andreas Kling 2026-05-03 19:53:55 +02:00 committed by Andreas Kling
parent b265694f0d
commit afa1f77252
7 changed files with 1076 additions and 10 deletions

View file

@ -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(),

View file

@ -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<DecodedFunctionRecord>,
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<DecodedFunctionRecord>,
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<FFIUtf16Slice>,
values: Vec<FFIUtf16Slice>,
}
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<FFIUtf16Slice> = 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<PendingClassBlueprint> = 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<DecodedClassBlueprintRecord> 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<DecodedClassElementRecord> 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 {

View file

@ -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

View file

@ -6,6 +6,7 @@
#include <LibJS/RustIntegration.h>
#include <AK/TemporaryChange.h>
#include <AK/Utf16String.h>
#include <AK/Utf16View.h>
#include <AK/kmalloc.h>
@ -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<Result<ScriptResult, Vector<ParserError>>> materialize_compiled_script(
return builder.result;
}
Optional<Result<ScriptResult, Vector<ParserError>>> materialize_bytecode_cache_script(DecodedBytecodeCacheBlob* blob, NonnullRefPtr<SourceCode const> 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> { ParserError { "Failed to materialize bytecode cache"_string, {} } };
builder.result.executable = static_cast<Bytecode::Executable*>(exec_ptr);
return builder.result;
}
Optional<Result<ScriptResult, Vector<ParserError>>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset)
{
auto source_code = SourceCode::create(
@ -606,6 +630,56 @@ Optional<Result<ModuleResult, Vector<ParserError>>> materialize_compiled_module(
return builder.result;
}
Optional<Result<ModuleResult, Vector<ParserError>>> materialize_bytecode_cache_module(DecodedBytecodeCacheBlob* blob, NonnullRefPtr<SourceCode const> 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> { ParserError { "Failed to materialize bytecode cache"_string, {} } };
if (tla_executable) {
auto& vm = realm.vm();
auto* tla_exec = static_cast<Bytecode::Executable*>(tla_executable);
builder.result.tla_shared_data = vm.heap().allocate<SharedFunctionInstanceData>(
vm, FunctionKind::Async,
"module code with top-level await"_utf16_fly_string,
0, 0, true, false, true,
Vector<Utf16FlyString> {}, 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<Bytecode::Executable*>(exec_ptr);
}
return builder.result;
}
Optional<Result<ModuleResult, Vector<ParserError>>> 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();

View file

@ -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<Result<ScriptResult, Vector<ParserError>>> materialize_bytecode_cache_script(FFI::DecodedBytecodeCacheBlob*, NonnullRefPtr<SourceCode const> 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<Result<ModuleResult, Vector<ParserError>>> materialize_bytecode_cache_module(FFI::DecodedBytecodeCacheBlob*, NonnullRefPtr<SourceCode const> 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.

View file

@ -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}")

View file

@ -0,0 +1,296 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibCrypto/Hash/SHA2.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/RustIntegration.h>
#include <LibJS/SourceCode.h>
#include <LibTest/TestCase.h>
struct BytecodeCacheTestData {
NonnullRefPtr<JS::SourceCode const> 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<u32>(m_bytes[m_offset])
| (static_cast<u32>(m_bytes[m_offset + 1]) << 8)
| (static_cast<u32>(m_bytes[m_offset + 2]) << 16)
| (static_cast<u32>(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<u8 const*>(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<JS::GlobalObject>(*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<JS::GlobalObject>(*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<JS::GlobalObject>(*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);
}