LibJS: Avoid revalidating cached bytecode executables

Cache blobs already validate decoded bytecode before rebuilding C++
Bytecode::Executable objects. Keep that as the only cache validation
pass and mark the decoded cache state once it completes.

Materialization now asserts that cache blobs and lazy cached function
records passed through validation before they can be installed or
decoded. This keeps the invariant without re-running the same validator
from rust_create_executable().
This commit is contained in:
Andreas Kling 2026-06-03 22:56:24 +02:00 committed by Andreas Kling
parent ef58631dd5
commit 9143ad4c96
3 changed files with 136 additions and 51 deletions

View file

@ -712,6 +712,7 @@ impl CacheBlob<'_> {
is_strict_mode: bool::decode(decoder)?,
metadata: DeclarationMetadataRecord::decode(decoder)?,
program: ProgramRecord::decode(decoder)?,
has_been_validated_for_materialization: false,
})
}
}
@ -723,6 +724,12 @@ pub(crate) struct DecodedCacheBlob {
is_strict_mode: bool,
metadata: DecodedDeclarationMetadata,
program: DecodedProgramRecord,
has_been_validated_for_materialization: bool,
}
#[derive(Clone, Copy)]
enum CachedBytecodeValidation {
Validated,
}
impl DecodedCacheBlob {
@ -738,18 +745,34 @@ impl DecodedCacheBlob {
self.source_len
}
pub(crate) fn source_ranges_are_valid(&self, source_len: usize) -> bool {
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) fn validate_cached_bytecode(&self) -> Result<(), ValidationErrorKind> {
fn validate_cached_bytecode(&self) -> Result<(), ValidationErrorKind> {
self.metadata.validate_cached_bytecode()?;
self.program.validate_cached_bytecode()
}
pub(crate) fn validate_for_materialization(&mut self, source_len: usize) -> Result<(), ValidationErrorKind> {
if !self.source_ranges_are_valid(source_len) {
return Err(ValidationErrorKind::InvalidLength);
}
self.validate_cached_bytecode()?;
self.has_been_validated_for_materialization = true;
Ok(())
}
fn verify_has_been_validated_for_materialization(&self) {
assert!(
self.has_been_validated_for_materialization,
"decoded bytecode cache blob must be validated before materialization"
);
}
pub(crate) unsafe fn materialize_script(
self,
vm_ptr: *mut c_void,
@ -758,6 +781,7 @@ impl DecodedCacheBlob {
gdi_context: *mut c_void,
) -> *mut c_void {
unsafe {
self.verify_has_been_validated_for_materialization();
let Self {
program_type,
is_strict_mode,
@ -795,7 +819,13 @@ impl DecodedCacheBlob {
) {
return std::ptr::null_mut();
}
materialize_executable(program.executable, vm_ptr, source_code_ptr, shared_function_data_owner)
materialize_executable(
program.executable,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
)
}
}
@ -809,6 +839,7 @@ impl DecodedCacheBlob {
tla_executable_out: *mut *mut c_void,
) -> *mut c_void {
unsafe {
self.verify_has_been_validated_for_materialization();
if callbacks.is_null() {
return std::ptr::null_mut();
}
@ -851,8 +882,13 @@ impl DecodedCacheBlob {
match program.kind {
ProgramKind::AsyncModule => {
let exec_ptr =
materialize_executable(program.executable, vm_ptr, source_code_ptr, shared_function_data_owner);
let exec_ptr = materialize_executable(
program.executable,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
);
if !tla_executable_out.is_null() {
*tla_executable_out = exec_ptr;
}
@ -862,7 +898,13 @@ impl DecodedCacheBlob {
if !tla_executable_out.is_null() {
*tla_executable_out = std::ptr::null_mut();
}
materialize_executable(program.executable, vm_ptr, source_code_ptr, shared_function_data_owner)
materialize_executable(
program.executable,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
)
}
}
}
@ -876,6 +918,7 @@ impl DecodedCacheBlob {
existing_shared_function_data_ptrs: &[*mut c_void],
) -> *mut c_void {
unsafe {
self.verify_has_been_validated_for_materialization();
if existing_executable_ptr.is_null() {
return std::ptr::null_mut();
}
@ -921,6 +964,7 @@ impl DecodedCacheBlob {
vm_ptr,
source_code_ptr,
&mut pending_function_installs,
CachedBytecodeValidation::Validated,
);
if executable_ptr.is_null() {
return std::ptr::null_mut();
@ -945,6 +989,7 @@ impl DecodedCacheBlob {
tla_executable_out: *mut *mut c_void,
) -> *mut c_void {
unsafe {
self.verify_has_been_validated_for_materialization();
let Self {
program_type,
has_top_level_await,
@ -988,6 +1033,7 @@ impl DecodedCacheBlob {
vm_ptr,
source_code_ptr,
&mut pending_function_installs,
CachedBytecodeValidation::Validated,
);
if exec_ptr.is_null() {
return std::ptr::null_mut();
@ -1017,6 +1063,7 @@ impl DecodedCacheBlob {
vm_ptr,
source_code_ptr,
&mut pending_function_installs,
CachedBytecodeValidation::Validated,
);
if executable_ptr.is_null() {
return std::ptr::null_mut();
@ -1056,6 +1103,7 @@ unsafe fn prepare_declaration_function_installs(
vm_ptr,
source_code_ptr,
pending_function_installs,
CachedBytecodeValidation::Validated,
)
.is_null()
{
@ -1097,6 +1145,7 @@ unsafe fn materialize_script_declaration_metadata(
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
);
if sfd_ptr.is_null() {
return false;
@ -1181,7 +1230,14 @@ unsafe fn materialize_module_declaration_metadata(
(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, shared_function_data_owner);
let sfd_ptr = materialize_function(
function,
true,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
);
if sfd_ptr.is_null() {
return false;
}
@ -1319,6 +1375,7 @@ impl PendingFunctionInstall {
unsafe {
match self.replacement {
PendingFunctionInstallReplacement::CachedBytecode(cached_executable) => {
cached_executable.verify_has_been_validated_for_materialization();
let cached_executable_ptr = Box::into_raw(Box::new(cached_executable)) as *mut c_void;
crate::bytecode::ffi::rust_sfd_install_cached_bytecode_executable(
self.existing_sfd_ptr,
@ -1351,11 +1408,12 @@ impl PendingFunctionInstall {
}
unsafe fn materialize_function(
function: DecodedFunctionRecord,
mut function: DecodedFunctionRecord,
outer_strict: bool,
vm_ptr: *mut c_void,
source_code_ptr: *const c_void,
shared_function_data_owner: crate::bytecode::ffi::SharedFunctionDataOwner,
validation: CachedBytecodeValidation,
) -> *mut c_void {
unsafe {
let (_parameter_name_storage, parameter_names): (Vec<Vec<u16>>, Vec<FFIUtf16Slice>) = function
@ -1412,6 +1470,7 @@ unsafe fn materialize_function(
crate::bytecode::ffi::rust_sfd_set_class_field_initializer_name(sfd_ptr, name, name_len, *is_private);
}
function.precompiled.mark_as_validated(validation);
let cached_executable_ptr = Box::into_raw(Box::new(function.precompiled)) as *mut c_void;
crate::bytecode::ffi::rust_sfd_set_cached_bytecode_executable(
sfd_ptr,
@ -1430,12 +1489,13 @@ unsafe fn materialize_function(
}
unsafe fn prepare_function_install(
function: DecodedFunctionRecord,
mut function: DecodedFunctionRecord,
outer_strict: bool,
existing_shared_function_data: &mut ExistingSharedFunctionData<'_>,
vm_ptr: *mut c_void,
source_code_ptr: *const c_void,
pending_function_installs: &mut Vec<PendingFunctionInstall>,
validation: CachedBytecodeValidation,
) -> *mut c_void {
unsafe {
let (_parameter_name_storage, parameter_names): (Vec<Vec<u16>>, Vec<FFIUtf16Slice>) = function
@ -1479,6 +1539,7 @@ unsafe fn prepare_function_install(
}
if crate::bytecode::ffi::rust_sfd_executable(existing_sfd_ptr).is_null() {
function.precompiled.mark_as_validated(validation);
pending_function_installs.push(PendingFunctionInstall {
existing_sfd_ptr,
replacement: PendingFunctionInstallReplacement::CachedBytecode(function.precompiled),
@ -1487,6 +1548,7 @@ unsafe fn prepare_function_install(
return existing_sfd_ptr;
}
function.precompiled.mark_as_validated(validation);
let Some(executable) = function.precompiled.decode_executable() else {
return std::ptr::null_mut();
};
@ -1497,6 +1559,7 @@ unsafe fn prepare_function_install(
vm_ptr,
source_code_ptr,
pending_function_installs,
validation,
);
if executable_ptr.is_null() {
return std::ptr::null_mut();
@ -1531,7 +1594,13 @@ pub(crate) unsafe fn materialize_cached_function(
} else {
crate::bytecode::ffi::SharedFunctionDataOwner::List(shared_function_data_list_ptr)
};
materialize_executable(executable, vm_ptr, source_code_ptr, shared_function_data_owner)
materialize_executable(
executable,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
CachedBytecodeValidation::Validated,
)
}
}
@ -1550,6 +1619,7 @@ unsafe fn materialize_executable(
vm_ptr: *mut c_void,
source_code_ptr: *const c_void,
shared_function_data_owner: crate::bytecode::ffi::SharedFunctionDataOwner,
validation: CachedBytecodeValidation,
) -> *mut c_void {
unsafe {
let mut pending_function_installs = Vec::new();
@ -1560,6 +1630,7 @@ unsafe fn materialize_executable(
vm_ptr,
source_code_ptr,
&mut pending_function_installs,
validation,
)
}
}
@ -1571,6 +1642,7 @@ unsafe fn materialize_executable_for_install(
vm_ptr: *mut c_void,
source_code_ptr: *const c_void,
pending_function_installs: &mut Vec<PendingFunctionInstall>,
validation: CachedBytecodeValidation,
) -> *mut c_void {
unsafe {
let DecodedExecutableRecord {
@ -1630,10 +1702,17 @@ unsafe fn materialize_executable_for_install(
vm_ptr,
source_code_ptr,
pending_function_installs,
validation,
) as *const c_void
} else {
materialize_function(function, strict, vm_ptr, source_code_ptr, shared_function_data_owner)
as *const c_void
materialize_function(
function,
strict,
vm_ptr,
source_code_ptr,
shared_function_data_owner,
validation,
) as *const c_void
};
sfd_ptrs.push(sfd_ptr);
}
@ -2918,28 +2997,48 @@ impl DecodedExecutableRecord {
struct DecodedCachedExecutableRecord {
bytes: DecodedBytecodeBytes,
has_been_validated_for_materialization: bool,
}
impl DecodedCachedExecutableRecord {
fn decode_executable(&self) -> Option<DecodedExecutableRecord> {
self.verify_has_been_validated_for_materialization();
let mut decoder = self.bytes.decoder();
let executable = ExecutableRecord::decode(&mut decoder)?;
decoder.is_empty().then_some(executable)
}
fn mark_as_validated(&mut self, _: CachedBytecodeValidation) {
self.has_been_validated_for_materialization = true;
}
fn verify_has_been_validated_for_materialization(&self) {
assert!(
self.has_been_validated_for_materialization,
"cached bytecode executable must be validated before materialization"
);
}
fn validate_cached_bytecode(&self) -> Result<(), ValidationErrorKind> {
self.decode_executable()
.ok_or(ValidationErrorKind::InvalidLength)?
.validate_cached_bytecode()
let mut decoder = self.bytes.decoder();
let executable = ExecutableRecord::decode(&mut decoder).ok_or(ValidationErrorKind::InvalidLength)?;
if !decoder.is_empty() {
return Err(ValidationErrorKind::InvalidLength);
}
executable.validate_cached_bytecode()
}
fn source_ranges_are_valid(&self, source_len: usize) -> bool {
self.decode_executable()
.is_some_and(|executable| executable.source_ranges_are_valid(source_len))
let mut decoder = self.bytes.decoder();
let Some(executable) = ExecutableRecord::decode(&mut decoder) else {
return false;
};
decoder.is_empty() && executable.source_ranges_are_valid(source_len)
}
fn validate(&self) {
let _ = self.bytes.len();
let _ = self.has_been_validated_for_materialization;
}
}
@ -3672,6 +3771,7 @@ impl PrecompiledFunctionRecord<'_> {
decoder.align_bytes_payload_to(BYTECODE_ALIGNMENT)?;
Some(DecodedCachedExecutableRecord {
bytes: DecodedBytecodeBytes::decode(decoder)?,
has_been_validated_for_materialization: false,
})
}
}
@ -3953,6 +4053,7 @@ mod tests {
DecodedCachedExecutableRecord {
bytes: DecodedBytecodeBytes::Owned(encoder.finish()),
has_been_validated_for_materialization: false,
}
}

View file

@ -955,11 +955,8 @@ pub unsafe extern "C" fn rust_materialize_bytecode_cache_script(
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();
}
if blob._blob.validate_cached_bytecode().is_err() {
let mut blob = Box::from_raw(blob);
if blob._blob.validate_for_materialization(source_len).is_err() {
return std::ptr::null_mut();
}
blob._blob
@ -992,11 +989,8 @@ pub unsafe extern "C" fn rust_materialize_bytecode_cache_module(
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();
}
if blob._blob.validate_cached_bytecode().is_err() {
let mut blob = Box::from_raw(blob);
if blob._blob.validate_for_materialization(source_len).is_err() {
return std::ptr::null_mut();
}
blob._blob.materialize_module(
@ -1037,7 +1031,7 @@ pub unsafe extern "C" fn rust_install_bytecode_cache_script(
if blob.is_null() {
return std::ptr::null_mut();
}
let blob = Box::from_raw(blob);
let mut blob = Box::from_raw(blob);
let existing_declaration_functions = if existing_declaration_function_count == 0 {
&[]
} else {
@ -1046,10 +1040,7 @@ pub unsafe extern "C" fn rust_install_bytecode_cache_script(
}
std::slice::from_raw_parts(existing_declaration_function_ptrs, existing_declaration_function_count)
};
if !blob._blob.source_ranges_are_valid(source_len) {
return std::ptr::null_mut();
}
if blob._blob.validate_cached_bytecode().is_err() {
if blob._blob.validate_for_materialization(source_len).is_err() {
return std::ptr::null_mut();
}
blob._blob.install_script(
@ -1091,7 +1082,7 @@ pub unsafe extern "C" fn rust_install_bytecode_cache_module(
if blob.is_null() {
return std::ptr::null_mut();
}
let blob = Box::from_raw(blob);
let mut blob = Box::from_raw(blob);
let existing_declaration_functions = if existing_declaration_function_count == 0 {
&[]
} else {
@ -1100,10 +1091,7 @@ pub unsafe extern "C" fn rust_install_bytecode_cache_module(
}
std::slice::from_raw_parts(existing_declaration_function_ptrs, existing_declaration_function_count)
};
if !blob._blob.source_ranges_are_valid(source_len) {
return std::ptr::null_mut();
}
if blob._blob.validate_cached_bytecode().is_err() {
if blob._blob.validate_for_materialization(source_len).is_err() {
return std::ptr::null_mut();
}
blob._blob.install_module(

View file

@ -40,10 +40,9 @@ 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;
// Bytecode cache materialization validates decoded cache bytecode before rebuilding Executables. Materialization paths
// flip this flag for the duration of their work so rust_create_executable() does not run the same validator again.
static thread_local bool s_skip_bytecode_validation_for_prevalidated_cache = false;
static Utf16View utf16_view_from_bytes(uint16_t const* data, size_t len)
{
@ -506,7 +505,7 @@ Optional<Result<ScriptResult, Vector<ParserError>>> materialize_bytecode_cache_s
return {};
GC::DeferGC defer_gc(realm.vm().heap());
TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true };
TemporaryChange skip_cache_executable_validation { s_skip_bytecode_validation_for_prevalidated_cache, true };
ScriptGdiBuilder builder;
void* exec_ptr = rust_materialize_bytecode_cache_script(blob, &realm.vm(), source_code.ptr(), source_code->length_in_code_units(), &builder.shared_function_data, &builder);
@ -683,7 +682,7 @@ Optional<Result<ModuleResult, Vector<ParserError>>> materialize_bytecode_cache_m
return {};
GC::DeferGC defer_gc(realm.vm().heap());
TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true };
TemporaryChange skip_cache_executable_validation { s_skip_bytecode_validation_for_prevalidated_cache, true };
ModuleBuilder builder;
ModuleCallbacks callbacks {
.set_has_top_level_await = module_set_has_top_level_await,
@ -741,7 +740,7 @@ GC::Ptr<Bytecode::Executable> try_install_bytecode_cache_script(DecodedBytecodeC
GC::Root<Bytecode::Executable> executable;
{
GC::DeferGC defer_gc(realm.vm().heap());
TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true };
TemporaryChange skip_cache_executable_validation { s_skip_bytecode_validation_for_prevalidated_cache, true };
executable = static_cast<Bytecode::Executable*>(rust_install_bytecode_cache_script(
blob, &realm.vm(), source_code.ptr(), source_code->length_in_code_units(), &existing_executable,
@ -771,7 +770,7 @@ Optional<ModuleBytecodeCacheInstallResult> try_install_bytecode_cache_module(Dec
existing_shared_function_data_ptrs.unchecked_append(function);
GC::DeferGC defer_gc(realm.vm().heap());
TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true };
TemporaryChange skip_cache_executable_validation { s_skip_bytecode_validation_for_prevalidated_cache, true };
void* top_level_await_executable = nullptr;
auto* exec = static_cast<Bytecode::Executable*>(rust_install_bytecode_cache_module(
@ -899,7 +898,7 @@ GC::Ptr<Bytecode::Executable> compile_function(VM& vm, SharedFunctionInstanceDat
if (shared_data.m_cached_bytecode_executable) {
GC::DeferGC defer_gc(vm.heap());
TemporaryChange validate_cache_executables { s_validate_materialized_bytecode_cache_executables, true };
TemporaryChange skip_cache_executable_validation { s_skip_bytecode_validation_for_prevalidated_cache, true };
auto* exec = static_cast<Bytecode::Executable*>(rust_materialize_bytecode_cache_function(
shared_data.m_cached_bytecode_executable,
&vm,
@ -1258,16 +1257,13 @@ 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)
auto const should_validate_bytecode = true;
auto const should_validate_bytecode = !JS::RustIntegration::s_skip_bytecode_validation_for_prevalidated_cache;
#else
auto const should_validate_bytecode = is_materializing_bytecode_cache;
auto const should_validate_bytecode = false;
#endif
if (should_validate_bytecode) {
if (auto validation = JS::Bytecode::validate_bytecode(*executable, basic_block_offsets.span()); validation.is_error()) {
if (is_materializing_bytecode_cache)
return nullptr;
#if !defined(NDEBUG) || defined(HAS_ADDRESS_SANITIZER)
VERIFY_NOT_REACHED();
#else