From 2d166cbfa5fd16311c806d9f2c3124b9a513fe79 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 22 Jun 2026 19:43:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E5=AE=89=E5=85=A8=E4=B8=8E=E5=86=85=E5=AD=98?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 46 ++++++++ crates/oxide_compiler/src/emitter.rs | 109 ++++++++++++------ .../tests/compile_expr_tests.rs | 8 ++ crates/oxide_kernel/src/kernel.rs | 12 ++ crates/oxide_kernel/src/prop_forge.rs | 12 ++ crates/oxide_kernel/src/shape_forge.rs | 17 +++ crates/oxide_kernel/src/string_forge.rs | 8 ++ crates/oxide_test262/src/main.rs | 3 + crates/oxide_types/src/object.rs | 6 +- crates/oxide_vm/src/builtins/array.rs | 63 ++++++---- crates/oxide_vm/src/builtins/array_buffer.rs | 20 +++- crates/oxide_vm/src/builtins/data_view.rs | 69 ++++++++++- crates/oxide_vm/src/builtins/date.rs | 2 +- crates/oxide_vm/src/builtins/function.rs | 2 +- crates/oxide_vm/src/builtins/iterator.rs | 2 +- crates/oxide_vm/src/builtins/json.rs | 4 +- crates/oxide_vm/src/builtins/map.rs | 2 +- crates/oxide_vm/src/builtins/object.rs | 16 +-- crates/oxide_vm/src/builtins/reflect.rs | 2 +- crates/oxide_vm/src/builtins/regexp.rs | 22 +++- crates/oxide_vm/src/builtins/set.rs | 2 +- crates/oxide_vm/src/builtins/string.rs | 6 + crates/oxide_vm/src/builtins/typed_array.rs | 67 ++++++++++- crates/oxide_vm/src/coercion.rs | 2 +- crates/oxide_vm/src/dispatch/call.rs | 2 +- crates/oxide_vm/src/dispatch/property.rs | 6 +- crates/oxide_vm/src/session_arena.rs | 10 +- crates/oxide_vm/src/session_gc.rs | 30 ++++- crates/oxide_vm/src/vm.rs | 24 +++- crates/oxide_vm/src/vm_dispatch_misc.rs | 17 ++- crates/oxide_vm/src/vm_pool.rs | 15 ++- crates/oxide_vm/src/vm_props.rs | 69 ++++++----- crates/oxide_vm/src/vm_runtime.rs | 9 +- crates/oxide_vm/src/vm_support.rs | 20 +++- crates/oxide_vm/tests/regression_tests.rs | 51 +++++++- 35 files changed, 620 insertions(+), 135 deletions(-) diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 70320540..d024af41 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -158,6 +158,8 @@ pub(crate) struct CompileCtx { /// Set when alloc_reg() overflows into the reserved this/new.target range (≥254). /// Checked after each emit phase to produce a compile error rather than silent corruption. pub(crate) reg_overflow: bool, + pub(crate) const_overflow: bool, + pub(crate) jump_overflow: bool, } #[derive(Clone, Copy)] @@ -222,6 +224,8 @@ impl CompileCtx { after_super_insert: None, after_super_inserted: false, reg_overflow: false, + const_overflow: false, + jump_overflow: false, } } @@ -280,6 +284,10 @@ impl CompileCtx { return idx; } + if self.constants.len() >= u16::MAX as usize { + self.const_overflow = true; + return u16::MAX; + } let idx = self.constants.len() as u16; self.constants.push(c); self.constant_map.insert(key, idx); @@ -287,10 +295,23 @@ impl CompileCtx { } let idx = self.constants.len(); + if idx >= u16::MAX as usize { + self.const_overflow = true; + return u16::MAX; + } self.constants.push(c); idx as u16 } + pub(crate) fn checked_jump_offset(&mut self, offset: isize) -> i16 { + if offset < i16::MIN as isize || offset > i16::MAX as isize { + self.jump_overflow = true; + 0 + } else { + offset as i16 + } + } + pub(crate) fn resolve_label(&self, label: Label) -> Result { self.label_map .get(&label) @@ -652,6 +673,12 @@ impl Compiler { if ctx.reg_overflow { return Err("RangeError: function body uses too many registers (max 253)".into()); } + if ctx.const_overflow { + return Err("RangeError: too many constants".into()); + } + if ctx.jump_overflow { + return Err("RangeError: jump offset out of range".into()); + } Ok(CompiledModule { bytecode: ctx.bytecode, @@ -717,6 +744,12 @@ impl Compiler { if ctx.reg_overflow { return Err("RangeError: function body uses too many registers (max 253)".into()); } + if ctx.const_overflow { + return Err("RangeError: too many constants".into()); + } + if ctx.jump_overflow { + return Err("RangeError: jump offset out of range".into()); + } Ok(CompiledModule { bytecode: ctx.bytecode, @@ -741,3 +774,16 @@ impl Default for Compiler { Self::new() } } + +#[cfg(test)] +mod tests { + use super::CompileCtx; + + #[test] + fn test_jump_offset_overflow_range_error() { + let mut ctx = CompileCtx::new(); + let offset = ctx.checked_jump_offset(i16::MAX as isize + 1); + assert_eq!(offset, 0); + assert!(ctx.jump_overflow); + } +} diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 0c24482f..dded7494 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -18,7 +18,8 @@ impl Compiler { let dup_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, reg, 0)); let offset = (short_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, offset)); Ok(()) } @@ -189,20 +190,24 @@ impl Compiler { LogicalOperator::And => { let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); } LogicalOperator::Or => { let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(test_reg, offset)); } LogicalOperator::Coalesce => { let store_pos = ctx.resolve_label(store_label)?; let offset = (store_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_nullish(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_nullish(test_reg, offset)); let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); } } Ok(()) @@ -337,7 +342,8 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::LOAD_VAR, val_reg, default_reg, 0)); let after = ctx.bytecode.len(); let offset = after as isize - jump_pos as isize; - ctx.bytecode[jump_pos] = opcode::encode_jmp_if_false(eq_reg, offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jump_pos] = opcode::encode_jmp_if_false(eq_reg, offset); Ok(val_reg) } @@ -426,9 +432,12 @@ impl Compiler { let tmp_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::INC_PRE, idx_reg, tmp_reg, tmp_reg)); let back = loop_start as isize - ctx.bytecode.len() as isize; - ctx.emit(opcode::encode_jmp(back as i16)); + let back = ctx.checked_jump_offset(back); + ctx.emit(opcode::encode_jmp(back)); let after = ctx.bytecode.len(); - ctx.bytecode[end_jmp] = opcode::encode_jmp_if_false(has_reg, (after as isize - end_jmp as isize) as i16); + let end_offset = after as isize - end_jmp as isize; + let end_offset = ctx.checked_jump_offset(end_offset); + ctx.bytecode[end_jmp] = opcode::encode_jmp_if_false(has_reg, end_offset); Ok(rest_reg) } @@ -1013,7 +1022,8 @@ impl Compiler { let else_pos = ctx.resolve_label(else_label)?; let offset = (else_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); let cons_reg = self.emit_statement(&ifs.consequent, ctx)?; let result_reg = ctx.alloc_reg(); @@ -1028,7 +1038,8 @@ impl Compiler { if has_alt { let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); } if let Some(alt) = &ifs.alternate { @@ -1054,13 +1065,15 @@ impl Compiler { let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); self.emit_statement(&wh.body, ctx)?; let start_pos = ctx.resolve_label(start_label)?; let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); ctx.pop_loop(); Ok(None) @@ -1078,7 +1091,8 @@ impl Compiler { let start_pos = ctx.resolve_label(start_label)?; let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(test_reg, offset)); ctx.pop_loop(); Ok(None) @@ -1112,7 +1126,8 @@ impl Compiler { let test_reg = self.emit_expression(test, ctx)?; let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); } self.emit_statement(&fr.body, ctx)?; @@ -1123,7 +1138,8 @@ impl Compiler { let start_pos = ctx.resolve_label(start_label)?; let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); ctx.pop_loop(); @@ -1145,7 +1161,8 @@ impl Compiler { let end_pos = ctx.resolve_label(end_label)?; let cleanup_jmp_offset = (end_pos as isize) - (ctx.bytecode.len() as isize); ctx.emit(opcode::encode_jmp_if_false(done_reg, 2)); - ctx.emit(opcode::encode_jmp(cleanup_jmp_offset as i16)); + let cleanup_jmp_offset = ctx.checked_jump_offset(cleanup_jmp_offset); + ctx.emit(opcode::encode_jmp(cleanup_jmp_offset)); let key_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::FOR_IN_NEXT, key_reg, 0, 0)); @@ -1175,7 +1192,8 @@ impl Compiler { let start_pos = ctx.resolve_label(start_label)?; let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0)); @@ -1195,7 +1213,8 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0)); let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(has_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(has_reg, offset)); let val_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0)); @@ -1222,7 +1241,8 @@ impl Compiler { self.emit_statement(&fo.body, ctx)?; let start_pos = ctx.resolve_label(start_label)?; let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0)); ctx.pop_loop(); Ok(None) @@ -1245,7 +1265,8 @@ impl Compiler { let case_label = Label::SwitchCase(id, case_idx as u32); let body_pos = ctx.resolve_label(case_label)?; let offset = (body_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset)); ctx.restore_reg_checkpoint(compare_reg_checkpoint); } } @@ -1254,7 +1275,8 @@ impl Compiler { if !has_default { let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); } for case in cases.iter() { @@ -1275,14 +1297,16 @@ impl Compiler { }; let break_pos = ctx.resolve_label(break_label)?; let offset = (break_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); Ok(None) } Statement::ContinueStatement(_) => { let (_, continue_label) = ctx.current_loop().ok_or("continue outside loop".to_string())?; let continue_pos = ctx.resolve_label(*continue_label)?; let offset = (continue_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); Ok(None) } Statement::FunctionDeclaration(fd) => { @@ -1395,7 +1419,8 @@ impl Compiler { if let Some(try_begin_pc) = try_begin_pos { let offset = catch_label_pc as isize - (try_begin_pc as isize); - ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset); } if let Some(catch) = &ts.handler { @@ -1423,12 +1448,14 @@ impl Compiler { ctx.label_map.insert(finally_label, finally_label_pc); if let Some(fb_pos) = try_finally_begin_pos { let offset = finally_label_pc as isize - (fb_pos as isize); - ctx.bytecode[fb_pos] = opcode::encode_try_finally_begin(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[fb_pos] = opcode::encode_try_finally_begin(offset); } if let Some(jmp_pos) = jmp_skip_pos { let offset = finally_label_pc as isize - (jmp_pos as isize); - ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); } let mut last_finally_result: Option = None; @@ -1446,7 +1473,8 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::TRY_FINALLY_END, 0, 0, 0)); } else if let Some(jmp_pos) = jmp_skip_pos { let offset = ctx.bytecode.len() as isize - (jmp_pos as isize); - ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); } let try_end_pc = ctx.bytecode.len(); @@ -1589,13 +1617,15 @@ impl Compiler { let offset = (short_pos as isize) - (jump_pos as isize); let instr = ctx.bytecode[jump_pos]; let rd = opcode::rd(instr); - ctx.bytecode[jump_pos] = opcode::encode_jmp_if_nullish(rd, offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jump_pos] = opcode::encode_jmp_if_nullish(rd, offset); } let true_idx = ctx.add_constant(Constant::Boolean(true)); ctx.emit_load_const(result_reg, true_idx); let end_pos = ctx.bytecode.len(); let offset = (end_pos as isize) - (end_jump_pos as isize); - ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset); Ok(result_reg) } _ => Err("invalid delete target".into()), @@ -1640,14 +1670,16 @@ impl Compiler { let end_pos = ctx.resolve_label(end_label)?; let offset = (else_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); let cons_reg = self.emit_expression(&cond.consequent, ctx)?; let result_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, cons_reg, 0)); let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); let alt_reg = self.emit_expression(&cond.alternate, ctx)?; ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, alt_reg, 0)); @@ -1682,9 +1714,11 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, right_reg, 0)); let end_pos = ctx.bytecode.len(); let offset = (rhs_pos as isize) - (nullish_jump_pos as isize); - ctx.bytecode[nullish_jump_pos] = opcode::encode_jmp_if_nullish(dup_reg, offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[nullish_jump_pos] = opcode::encode_jmp_if_nullish(dup_reg, offset); let offset = (end_pos as isize) - (end_jump_pos as isize); - ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset); return Ok(dup_reg); } let skip_label = match log.operator { @@ -1700,10 +1734,12 @@ impl Compiler { let offset = (skip_pos as isize) - (ctx.bytecode.len() as isize); match log.operator { LogicalOperator::And => { - ctx.emit(opcode::encode_jmp_if_false(dup_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(dup_reg, offset)); } LogicalOperator::Or => { - ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset)); } LogicalOperator::Coalesce => unreachable!(), } @@ -1768,7 +1804,8 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, value_reg, 0)); let end_pos = ctx.resolve_label(end_label)?; let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - ctx.emit(opcode::encode_jmp(offset as i16)); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); let undefined_idx = ctx.add_constant(Constant::Undefined); ctx.emit_load_const(result_reg, undefined_idx); Ok(result_reg) diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index dc313049..c34a9d98 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -31,6 +31,14 @@ fn compile_simple_expr() { assert_eq!(last, OpCode::HALT); } +#[test] +fn test_constant_pool_overflow_range_error() { + let expr = (0..70_000).map(|i| i.to_string()).collect::>().join(","); + let source = format!("var a=[{expr}]; a"); + let err = compile_source_err(&source); + assert!(err.contains("too many constants"), "unexpected error: {err}"); +} + #[test] fn compile_logical_assignment_vars() { compile_source("let x = 0; x ||= 1;"); diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index a66f9150..e87d1ea5 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -146,6 +146,18 @@ impl KernelCore { pub fn set_session_gc_threshold(&mut self, bytes: usize) { self.config.session_gc_threshold = bytes; } + + pub fn sweep_runner_forges(&self) { + self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1))); + // test262 creates a fresh VM/session per test. At this boundary no JS object + // from prior tests may retain transient shapes/templates. + if self.shape_forge.len() > 50_000 { + self.shape_forge.clear_transient(); + self.prop_forge.clear(); + } else if self.prop_forge.len() > 50_000 { + self.prop_forge.clear(); + } + } } /// Per-session mutable state: builtin prototype objects and the global object. diff --git a/crates/oxide_kernel/src/prop_forge.rs b/crates/oxide_kernel/src/prop_forge.rs index 93b424fe..95192cbd 100644 --- a/crates/oxide_kernel/src/prop_forge.rs +++ b/crates/oxide_kernel/src/prop_forge.rs @@ -43,6 +43,18 @@ impl PropForge { } } } + + pub fn clear(&self) { + self.templates.clear(); + } + + pub fn len(&self) -> usize { + self.templates.len() + } + + pub fn is_empty(&self) -> bool { + self.templates.is_empty() + } } impl Default for PropForge { diff --git a/crates/oxide_kernel/src/shape_forge.rs b/crates/oxide_kernel/src/shape_forge.rs index a55f8e7c..73105e2f 100644 --- a/crates/oxide_kernel/src/shape_forge.rs +++ b/crates/oxide_kernel/src/shape_forge.rs @@ -114,6 +114,23 @@ impl ShapeForge { shapes.get((id - 1) as usize).and_then(|s| s.clone()) } + pub fn len(&self) -> usize { + self.shapes.read().unwrap().len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn clear_transient(&self) { + let mut shapes = self.shapes.write().unwrap(); + if shapes.len() > 1 { + shapes.truncate(1); + } + self.transitions.clear(); + self.next_id.store(2, Ordering::Relaxed); + } + pub fn lookup_position(&self, shape_id: ShapeId, prop_name: StringIndex) -> Option { let shapes = self.shapes.read().unwrap(); let total_depth = Self::compute_depth(shape_id, &shapes); diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index a30f9e29..6c64c0d8 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -126,6 +126,14 @@ impl StringForge { inner.map = new_map; inner.entries = new_entries; } + + pub fn len(&self) -> usize { + self.inner.read().unwrap().entries.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl Default for StringForge { diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index a8d933a2..725763cb 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -783,6 +783,9 @@ fn run_tests() -> bool { out.push((i, result)); let done = progress.fetch_add(1, Ordering::Relaxed) + 1; + if done % 500 == 0 { + kernel.sweep_runner_forges(); + } if done % 500 == 0 || done == total { eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total); } diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index cc88b122..9596ca41 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -40,6 +40,7 @@ unsafe impl Send for NativeFnPtr {} unsafe impl Sync for NativeFnPtr {} pub type ShapeId = u32; +pub const MAX_DENSE_PROPS: usize = 1_000_000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TypedArrayKind { @@ -297,7 +298,7 @@ impl JsObject { captured_this: JsValue::undefined(), home_object: JsValue::undefined(), }; - let vec = Box::new(vec![JsValue::undefined(); n_elements]); + let vec = Box::new(vec![JsValue::undefined(); n_elements.min(MAX_DENSE_PROPS)]); obj.hash_props = Box::into_raw(vec) as *mut u8; obj } @@ -615,6 +616,9 @@ impl JsObject { /// Set property value at position index. Vec auto-grows if needed. pub fn set_prop_at(&mut self, position: impl PropIndex, val: JsValue) { let pos = position.to_u32() as usize; + if pos > MAX_DENSE_PROPS { + return; + } { let vec = self.ensure_hash_props(); if pos < vec.len() { diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index 016d6ca4..0771b84c 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -1,5 +1,5 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, MAX_DENSE_PROPS}; use oxide_types::value::JsValue; use crate::coercion; @@ -40,8 +40,20 @@ fn get_this_array_ref(val: JsValue) -> Result<*mut JsObject, JsValue> { fn create_new_array(vm: &mut Vm, n: usize) -> *mut JsObject { let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - vm.epoch() - .alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())) + vm.alloc_object(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(proto), + n.min(MAX_DENSE_PROPS), + vm.epoch().bump(), + )) +} + +fn array_length_arg(vm: &mut Vm, value: JsValue) -> Result { + let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + if !n.is_finite() || n < 0.0 || n.fract() != 0.0 || n > MAX_DENSE_PROPS as f64 { + return Err(crate::builtins::error::create_range_error(vm, "Invalid array length")); + } + Ok(n as usize) } fn invoke_native_callback(vm: &mut Vm, callback_val: JsValue, this_val: JsValue, cb_args: &[JsValue]) -> NativeResult { @@ -92,26 +104,18 @@ pub fn array_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() == 2 { let val = vm.reg(args[1]); - if val.is_int() { - let n = val.as_int().max(0) as usize; - let arr = vm - .epoch() - .alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump())); - return NativeResult::Ok(JsValue::from_js_object(arr)); - } - if val.is_double() { - let n = val.as_double() as usize; - let arr = vm - .epoch() - .alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump())); + if val.is_int() || val.is_double() { + let n = match array_length_arg(vm, val) { + Ok(n) => n, + Err(err) => return NativeResult::Err(err), + }; + let arr = vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump())); return NativeResult::Ok(JsValue::from_js_object(arr)); } } let n_elems = if args.len() > 1 { args.len() - 1 } else { 0 }; - let arr = vm - .epoch() - .alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n_elems, vm.epoch().bump())); + let arr = vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n_elems, vm.epoch().bump())); for i in 0..n_elems { unsafe { (*arr).set_prop_at(i, vm.reg(args[1 + i])); @@ -373,22 +377,34 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr = unsafe { &*arr_ptr }; let n = arr.prop_count() as usize; let depth = if args.len() > 1 { - (coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32).max(1) as usize + let n = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()); + if !n.is_finite() { + vm.kernel_core.config.max_call_depth + } else { + (n as i32).max(1) as usize + } } else { 1 - }; + } + .min(vm.kernel_core.config.max_call_depth); - fn flatten(items: &[JsValue], remaining_depth: usize) -> Vec { + fn flatten(items: &[JsValue], remaining_depth: usize, seen: &mut Vec<*mut JsObject>) -> Vec { let mut out = Vec::new(); for &v in items { if remaining_depth > 0 && v.is_object() { let ptr = v.as_js_object_ptr(); if !ptr.is_null() { + if seen.iter().any(|seen_ptr| std::ptr::eq(*seen_ptr, ptr)) { + out.push(v); + continue; + } let o = unsafe { &*ptr }; if o.is_array() { + seen.push(ptr); let on = o.prop_count() as usize; let sub: Vec = (0..on).map(|i| o.get_prop_at(i)).collect(); - let flat = flatten(&sub, remaining_depth - 1); + let flat = flatten(&sub, remaining_depth - 1, seen); + seen.pop(); out.extend(flat); continue; } @@ -400,7 +416,8 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult { } let all: Vec = (0..n).map(|i| arr.get_prop_at(i)).collect(); - let flat = flatten(&all, depth); + let mut seen = vec![arr_ptr]; + let flat = flatten(&all, depth, &mut seen); let new_arr = create_new_array(vm, flat.len()); unsafe { for (i, val) in flat.iter().enumerate() { diff --git a/crates/oxide_vm/src/builtins/array_buffer.rs b/crates/oxide_vm/src/builtins/array_buffer.rs index d09e45f2..b5892e41 100644 --- a/crates/oxide_vm/src/builtins/array_buffer.rs +++ b/crates/oxide_vm/src/builtins/array_buffer.rs @@ -66,7 +66,25 @@ pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec) -> *mut JsObject { JsValue::int(len as i32), PropAttributes::new(false, false, false), ); - vm.epoch().alloc(obj) + vm.alloc_object(obj) +} + +pub(crate) fn drop_array_buffer_native(obj: &mut JsObject) -> u64 { + if !obj.is_array_buffer_obj() { + return 0; + } + let Some(ptr) = obj.native_fn() else { + return 0; + }; + let data_ptr = ptr.as_ptr() as *mut Vec; + if data_ptr.is_null() { + return 0; + } + // SAFETY: ArrayBuffer stores `Box>` in native_fn. + let data = unsafe { Box::from_raw(data_ptr) }; + let bytes = std::mem::size_of::>() + data.capacity(); + obj.set_native_fn(None); + bytes as u64 } pub(crate) fn array_buffer_data_ptr(vm: &mut Vm, this_val: JsValue) -> Result<*mut Vec, JsValue> { diff --git a/crates/oxide_vm/src/builtins/data_view.rs b/crates/oxide_vm/src/builtins/data_view.rs index 40b8370d..c642a502 100644 --- a/crates/oxide_vm/src/builtins/data_view.rs +++ b/crates/oxide_vm/src/builtins/data_view.rs @@ -8,7 +8,7 @@ use crate::native::NativeResult; use crate::vm::Vm; #[derive(Clone, Copy)] -struct DataViewData { +pub(crate) struct DataViewData { buffer: JsValue, byte_offset: usize, byte_length: usize, @@ -177,7 +177,72 @@ pub fn data_view_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { JsValue::int(byte_length as i32), PropAttributes::new(false, false, false), ); - NativeResult::Ok(JsValue::from_js_object(vm.epoch().alloc(obj))) + NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) +} + +fn data_view_data_ptr(obj: &JsObject) -> Option<*mut DataViewData> { + if !obj.is_data_view_obj() { + return None; + } + obj.native_fn().map(|ptr| ptr.as_ptr() as *mut DataViewData) +} + +pub(crate) fn data_view_native_edges(obj: &JsObject) -> Vec { + let Some(ptr) = data_view_data_ptr(obj) else { + return Vec::new(); + }; + if ptr.is_null() { + return Vec::new(); + } + let data = unsafe { *ptr }; + if data.buffer.is_object() { + vec![data.buffer] + } else { + Vec::new() + } +} + +pub(crate) fn clone_data_view_native_with_rewrite(old_obj: &JsObject, new_obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + let Some(ptr) = data_view_data_ptr(old_obj) else { + return; + }; + if ptr.is_null() { + return; + } + let mut data = unsafe { *ptr }; + data.buffer = rewrite(data.buffer); + let cloned = Box::into_raw(Box::new(data)); + new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned as *const ()) })); +} + +pub(crate) fn rewrite_data_view_native(obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + let Some(ptr) = data_view_data_ptr(obj) else { + return; + }; + if ptr.is_null() { + return; + } + unsafe { + (*ptr).buffer = rewrite((*ptr).buffer); + } +} + +pub(crate) fn drop_data_view_native(obj: &mut JsObject) -> u64 { + let Some(ptr) = data_view_data_ptr(obj) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + unsafe { drop(Box::from_raw(ptr)) }; + obj.set_native_fn(None); + std::mem::size_of::() as u64 } pub fn data_view_get_int8(vm: &mut Vm, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/builtins/date.rs b/crates/oxide_vm/src/builtins/date.rs index 0d36f525..55f5ced2 100644 --- a/crates/oxide_vm/src/builtins/date.rs +++ b/crates/oxide_vm/src/builtins/date.rs @@ -201,7 +201,7 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { obj.type_tag = JsObject::OBJ_TYPE_DATE; obj.set_prop_at(0, JsValue::float(timestamp)); - let ptr = vm.epoch().alloc(obj); + let ptr = vm.alloc_object(obj); NativeResult::Ok(JsValue::from_js_object(ptr)) } diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index 4e77c83c..dd1470bb 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -91,7 +91,7 @@ pub fn function_bind(vm: &mut Vm, args: &[u8]) -> NativeResult { } let bound_this = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let wrapper = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let wrapper = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); unsafe { (*wrapper).set_function(true); // SAFETY: bind_dispatcher is a NativeFn fn-item; valid to store as NativeFnPtr. diff --git a/crates/oxide_vm/src/builtins/iterator.rs b/crates/oxide_vm/src/builtins/iterator.rs index 7357e98a..81b50396 100644 --- a/crates/oxide_vm/src/builtins/iterator.rs +++ b/crates/oxide_vm/src/builtins/iterator.rs @@ -156,7 +156,7 @@ fn make_native_function(vm: &mut Vm, name: &str, native_fn: *const (), arg_count // SAFETY: native_fn is provided from a NativeFn fn item. func.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(native_fn) })); func.set_native_arg_count(arg_count); - let func = vm.epoch().alloc(func); + let func = vm.alloc_object(func); let name_si = vm.kernel_core().string_forge().intern("name").0; let value = vm.intern(name); let func_ref = unsafe { &mut *func }; diff --git a/crates/oxide_vm/src/builtins/json.rs b/crates/oxide_vm/src/builtins/json.rs index 187228b5..515d2dcb 100644 --- a/crates/oxide_vm/src/builtins/json.rs +++ b/crates/oxide_vm/src/builtins/json.rs @@ -48,7 +48,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { serde_json::Value::Array(arr) => { let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let n = arr.len(); - let array_obj = vm.epoch().alloc(JsObject::new_array( + let array_obj = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, @@ -72,7 +72,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { obj.set_shape_id(new_shape); obj.ensure_hash_props().push(jsv); } - let obj_ptr = vm.epoch().alloc(obj); + let obj_ptr = vm.alloc_object(obj); JsValue::from_js_object(obj_ptr) } } diff --git a/crates/oxide_vm/src/builtins/map.rs b/crates/oxide_vm/src/builtins/map.rs index 7efa54d8..b679350b 100644 --- a/crates/oxide_vm/src/builtins/map.rs +++ b/crates/oxide_vm/src/builtins/map.rs @@ -65,7 +65,7 @@ fn alloc_map(vm: &mut Vm) -> *mut JsObject { obj.set_map(true); let inner = new_map_inner(); obj.set_native_data(inner as *mut u8); - vm.epoch().alloc(obj) + vm.alloc_object(obj) } pub(crate) fn map_native_edges(obj: &JsObject) -> Vec { diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index 0c03555d..e317aad8 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -39,7 +39,7 @@ pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u32)> { } pub fn object_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { - let obj = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); NativeResult::Ok(JsValue::from_js_object(obj)) } @@ -68,7 +68,7 @@ pub fn object_keys(vm: &mut Vm, args: &[u8]) -> NativeResult { let n = key_names.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch().alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, @@ -94,7 +94,7 @@ pub fn object_create(vm: &mut Vm, args: &[u8]) -> NativeResult { if !proto_val.is_null() && !proto_val.is_object() { return NativeResult::Ok(JsValue::undefined()); } - let obj = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); NativeResult::Ok(JsValue::from_js_object(obj)) } @@ -412,7 +412,7 @@ pub fn object_get_own_property_names(vm: &mut Vm, args: &[u8]) -> NativeResult { let n = key_names.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch().alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, @@ -450,7 +450,7 @@ pub fn object_from_entries(vm: &mut Vm, args: &[u8]) -> NativeResult { if !entries.is_object() { return NativeResult::Ok(JsValue::null()); } - let obj_ptr = vm.epoch().alloc(JsObject::new_empty( + let obj_ptr = vm.alloc_object(JsObject::new_empty( EMPTY_SHAPE_ID, JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject), )); @@ -517,7 +517,7 @@ pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult { let keys = walk_own_keys(vm, obj); let n = keys.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch().alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, @@ -527,7 +527,7 @@ pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult { let key_str = vm.kernel_core().string_forge().lookup(*si).unwrap_or_default(); let key_val = vm.intern(&key_str); let val = obj.get_prop_at(*offset); - let pair = vm.epoch().alloc(JsObject::new_array( + let pair = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), 2, @@ -552,7 +552,7 @@ pub fn object_values(vm: &mut Vm, args: &[u8]) -> NativeResult { let keys = walk_own_keys(vm, obj); let n = keys.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch().alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, diff --git a/crates/oxide_vm/src/builtins/reflect.rs b/crates/oxide_vm/src/builtins/reflect.rs index e5d5190f..9d8ada1d 100644 --- a/crates/oxide_vm/src/builtins/reflect.rs +++ b/crates/oxide_vm/src/builtins/reflect.rs @@ -221,7 +221,7 @@ fn own_field(vm: &Vm, obj: &JsObject, prop_si: u32) -> Option { fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch().alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(proto), parts.len(), diff --git a/crates/oxide_vm/src/builtins/regexp.rs b/crates/oxide_vm/src/builtins/regexp.rs index 716e1626..629c2288 100644 --- a/crates/oxide_vm/src/builtins/regexp.rs +++ b/crates/oxide_vm/src/builtins/regexp.rs @@ -103,7 +103,7 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { // SAFETY: re_ptr is a Box pointer stored by the constructor via // NativeFnPtr::from_raw(re_ptr as *const ()). RegExp objects repurpose the native_fn // field to hold the compiled Regex — not a NativeFn pointer. Valid for the object's - // lifetime because the Box is never freed (intentional leak tied to epoch). + // lifetime; VM reset drops the Box through `drop_regexp_native`. obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(re_ptr as *const ()) })); } Err(e) => { @@ -123,10 +123,26 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { set_prop(&mut obj, "unicode", JsValue::bool(false), vm); obj.type_tag = JsObject::OBJ_TYPE_REGEXP; - let obj_ptr = vm.epoch().alloc(obj); + let obj_ptr = vm.alloc_object(obj); NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } +pub(crate) fn drop_regexp_native(obj: &mut JsObject) -> u64 { + if !obj.is_regexp_obj() { + return 0; + } + let Some(ptr) = obj.native_fn() else { + return 0; + }; + let regex_ptr = ptr.as_ptr() as *mut regex::Regex; + if regex_ptr.is_null() { + return 0; + } + unsafe { drop(Box::from_raw(regex_ptr)) }; + obj.set_native_fn(None); + std::mem::size_of::() as u64 +} + pub fn regexp_test(vm: &mut Vm, args: &[u8]) -> NativeResult { let re_ptr = match get_regexp_ptr(vm, args) { Ok(ptr) => ptr, @@ -231,7 +247,7 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult { set_prop_at(re_ptr, 0, JsValue::int(m.end() as i32)); } - let obj_ptr = vm.epoch().alloc(match_obj); + let obj_ptr = vm.alloc_object(match_obj); NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } else { if is_global { diff --git a/crates/oxide_vm/src/builtins/set.rs b/crates/oxide_vm/src/builtins/set.rs index 07e6d90e..05f8e3bd 100644 --- a/crates/oxide_vm/src/builtins/set.rs +++ b/crates/oxide_vm/src/builtins/set.rs @@ -105,7 +105,7 @@ fn alloc_set(vm: &mut Vm) -> *mut JsObject { obj.set_set(true); let inner = new_set_inner(); obj.set_native_data(inner as *mut u8); - vm.epoch().alloc(obj) + vm.alloc_object(obj) } pub(crate) fn set_native_edges(obj: &JsObject) -> Vec { diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index 8438bd11..229e159d 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -288,6 +288,9 @@ pub fn string_pad_start(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { s_len }; + if target > 10000 { + return NativeResult::Err(crate::builtins::error::create_range_error(vm, "Invalid string length")); + } let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() }; if s_len >= target || pad.is_empty() { return NativeResult::Ok(vm.intern(&s)); @@ -308,6 +311,9 @@ pub fn string_pad_end(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { s_len }; + if target > 10000 { + return NativeResult::Err(crate::builtins::error::create_range_error(vm, "Invalid string length")); + } let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() }; if s_len >= target || pad.is_empty() { return NativeResult::Ok(vm.intern(&s)); diff --git a/crates/oxide_vm/src/builtins/typed_array.rs b/crates/oxide_vm/src/builtins/typed_array.rs index b5ba1fe7..b1b9b0ea 100644 --- a/crates/oxide_vm/src/builtins/typed_array.rs +++ b/crates/oxide_vm/src/builtins/typed_array.rs @@ -117,7 +117,72 @@ fn create_typed_array( JsValue::int(length as i32), PropAttributes::new(false, false, false), ); - vm.epoch().alloc(obj) + vm.alloc_object(obj) +} + +fn typed_array_data_ptr(obj: &JsObject) -> Option<*mut TypedArrayData> { + if !obj.is_typed_array_obj() { + return None; + } + obj.native_fn().map(|ptr| ptr.as_ptr() as *mut TypedArrayData) +} + +pub(crate) fn typed_array_native_edges(obj: &JsObject) -> Vec { + let Some(ptr) = typed_array_data_ptr(obj) else { + return Vec::new(); + }; + if ptr.is_null() { + return Vec::new(); + } + let data = unsafe { *ptr }; + if data.buffer.is_object() { + vec![data.buffer] + } else { + Vec::new() + } +} + +pub(crate) fn clone_typed_array_native_with_rewrite(old_obj: &JsObject, new_obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + let Some(ptr) = typed_array_data_ptr(old_obj) else { + return; + }; + if ptr.is_null() { + return; + } + let mut data = unsafe { *ptr }; + data.buffer = rewrite(data.buffer); + let cloned = Box::into_raw(Box::new(data)); + new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned as *const ()) })); +} + +pub(crate) fn rewrite_typed_array_native(obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + let Some(ptr) = typed_array_data_ptr(obj) else { + return; + }; + if ptr.is_null() { + return; + } + unsafe { + (*ptr).buffer = rewrite((*ptr).buffer); + } +} + +pub(crate) fn drop_typed_array_native(obj: &mut JsObject) -> u64 { + let Some(ptr) = typed_array_data_ptr(obj) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + unsafe { drop(Box::from_raw(ptr)) }; + obj.set_native_fn(None); + std::mem::size_of::() as u64 } pub(crate) fn get_typed_array_data(vm: &mut Vm, this_val: JsValue) -> Result { diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index 5fe4316a..3cd83104 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -312,7 +312,7 @@ pub fn to_object(val: JsValue, vm: &mut Vm) -> Result { (&*vm.object_prototype as *const JsObject as *mut JsObject, JsObject::OBJ_TYPE_PLAIN) }; let proto_val = JsValue::from_js_object(proto_ptr); - let obj = vm.epoch.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); let obj_val = JsValue::from_js_object(obj); let obj_ref = unsafe { &mut *obj }; obj_ref.type_tag = type_tag; diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index ca6cad87..0a6437b1 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -21,7 +21,7 @@ impl Vm { &mut self, obj: &JsObject, callee: JsValue, this_reg: u8, first_arg_reg: u8, arg_count: usize, ) -> Result<(), String> { if self.native_call_depth >= self.kernel_core.config.max_call_depth { - return Err("RangeError: Maximum call stack size exceeded".into()); + return self.raise_error_kind("RangeError", "Maximum call stack size exceeded"); } let (args_buf, len) = Self::build_native_args(first_arg_reg, arg_count, this_reg); let args_slice = &args_buf[..len]; diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index ad38d534..e3f61b38 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -5,7 +5,7 @@ use oxide_types::object::JsObject; use oxide_types::private_key::make_private_name_id; use oxide_types::value::JsValue; -use crate::vm::Vm; +use crate::vm::{Vm, MAX_PROTO_CHAIN_DEPTH}; #[inline(always)] fn ic_get_hit(obj: &JsObject, shape_id: u32, slot_index: u32) -> Option { @@ -98,7 +98,9 @@ impl Vm { return Some(obj.get_prop_at(pos)); } let mut proto = obj.proto(); - while proto.is_object() { + let mut depth = 0usize; + while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH { + depth += 1; let proto_obj = unsafe { &*proto.as_js_object_ptr() }; if let Some(pos) = self.private_slot(proto_obj, private_key) { return Some(proto_obj.get_prop_at(pos)); diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index b2d08e82..2c5e9313 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -use crate::builtins::{map, set}; +use crate::builtins::{data_view, map, set, typed_array}; use crate::vm::Vm; impl Vm { @@ -50,6 +50,14 @@ impl Vm { set::clone_set_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); + } else if src_ref.is_typed_array_obj() { + typed_array::clone_typed_array_native_with_rewrite(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); + } else if src_ref.is_data_view_obj() { + data_view::clone_data_view_native_with_rewrite(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); } dst } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 757f227f..ba29d700 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -5,7 +5,7 @@ use std::time::Instant; use oxide_types::object::{JsObject, PropMetaEntry}; use oxide_types::value::JsValue; -use crate::builtins::{map, set}; +use crate::builtins::{array_buffer, data_view, map, regexp, set, typed_array}; use crate::vm::Vm; pub struct SessionGc { @@ -87,6 +87,12 @@ impl SessionGc { if obj.is_set() { edges.extend(set::set_native_edges(obj)); } + if obj.is_typed_array_obj() { + edges.extend(typed_array::typed_array_native_edges(obj)); + } + if obj.is_data_view_obj() { + edges.extend(data_view::data_view_native_edges(obj)); + } edges } @@ -141,7 +147,7 @@ impl SessionGc { } } - fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 { + pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject, require_session: bool) -> u64 { if obj_ptr.is_null() { return 0; } @@ -150,7 +156,9 @@ impl SessionGc { // `JsObject::ensure_hash_props`/`ensure_prop_meta` and then drop them once here. unsafe { let obj = &mut *obj_ptr; - debug_assert!(obj.is_session_epoch()); + if require_session { + debug_assert!(obj.is_session_epoch()); + } let mut freed_bytes = 0u64; let hash_ptr = obj.hash_props_raw() as *mut Vec; @@ -170,11 +178,19 @@ impl SessionGc { freed_bytes += map::drop_map_native(obj); freed_bytes += set::drop_set_native(obj); + freed_bytes += array_buffer::drop_array_buffer_native(obj); + freed_bytes += regexp::drop_regexp_native(obj); + freed_bytes += typed_array::drop_typed_array_native(obj); + freed_bytes += data_view::drop_data_view_native(obj); freed_bytes } } + fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 { + Self::drop_object_heap_data(obj_ptr, true) + } + fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 { Self::drop_session_object_heap_data(obj_ptr) + size_of::() as u64 } @@ -204,6 +220,10 @@ impl SessionGc { map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_set() { set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_typed_array_obj() { + typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_data_view_obj() { + data_view::clone_data_view_native_with_rewrite(old_ref, new_ref, |value| value); } forwarding.insert(old_ptr, new_ptr); freed_bytes += Self::drop_session_object_heap_data(old_ptr); @@ -229,6 +249,10 @@ impl SessionGc { map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); } else if obj.is_set() { set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); + } else if obj.is_typed_array_obj() { + typed_array::rewrite_typed_array_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); + } else if obj.is_data_view_obj() { + data_view::rewrite_data_view_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 8b35400a..e975e1f0 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -18,6 +18,8 @@ use oxide_types::mem::{Epoch, P}; use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; +pub(crate) const MAX_PROTO_CHAIN_DEPTH: usize = 1024; + /// Convert a `NativeFnPtr` to a callable `NativeFn`. /// /// # Safety @@ -145,6 +147,7 @@ pub struct Vm { pub epoch: Epoch, pub(crate) session_epoch: bumpalo::Bump, pub(crate) session_gc: SessionGc, + pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, pub(crate) session_object_ptrs: Vec<*mut JsObject>, pub(crate) session_bytes_allocated: usize, pub object_prototype: P, @@ -180,6 +183,12 @@ impl Vm { unsafe { (*obj_ptr).is_session_epoch() } } + pub(crate) fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject { + let ptr = self.epoch.alloc(obj); + self.epoch_object_ptrs.push(ptr); + ptr + } + pub(crate) fn gc_roots(&self) -> Vec { let mut roots = Vec::new(); roots.extend_from_slice(&self.regs); @@ -374,7 +383,9 @@ impl Vm { } } let mut proto = obj.proto(); - while proto.is_object() { + let mut depth = 0usize; + while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH { + depth += 1; let proto_obj = unsafe { &*proto.as_js_object_ptr() }; if let Some(pos) = self .kernel_core @@ -429,7 +440,8 @@ impl Vm { if let Some(native_fn) = callee_obj.native_fn() { if self.native_call_depth >= self.kernel_core.config.max_call_depth { - return Err("RangeError: Maximum call stack size exceeded".into()); + self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?; + return Ok(JsValue::undefined()); } let saved_regs = self.regs; let saved_this = self.regs[254]; @@ -488,7 +500,7 @@ impl Vm { )); } if self.frames.len() >= self.kernel_core.config.max_call_depth { - return Err("RangeError: Maximum call stack size exceeded".into()); + return self.raise_error_kind("RangeError", "Maximum call stack size exceeded"); } let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); @@ -940,7 +952,7 @@ impl Vm { let proto_ptr = self.session.builtin_world().array_proto.as_ptr() as *mut JsObject; let n = opcode::imm16(instr) as usize; let bump = self.epoch.bump(); - let obj = self.epoch.alloc(JsObject::new_array( + let obj = self.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr), n, @@ -1226,13 +1238,13 @@ mod tests { obj.set_function(true); // SAFETY: f is a NativeFn fn-item; valid to store as NativeFnPtr. obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(f as *const ()) })); - JsValue::object(vm.epoch.alloc(obj) as *mut u8) + JsValue::object(vm.alloc_object(obj) as *mut u8) } fn plain_object(vm: &mut Vm) -> JsValue { let proto = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; let obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); - JsValue::object(vm.epoch.alloc(obj) as *mut u8) + JsValue::object(vm.alloc_object(obj) as *mut u8) } fn add_accessor(vm: &mut Vm, obj_val: JsValue, name: &str, get: JsValue, set: JsValue) { diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 66865494..b62cbb50 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -1,5 +1,5 @@ use crate::native::{NativeFn, NativeResult}; -use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm}; +use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; @@ -37,7 +37,7 @@ impl Vm { let arg_count = (ext & 0xFF) as usize; let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; - let new_obj = self.epoch.alloc(JsObject::new_empty( + let new_obj = self.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr), )); @@ -225,11 +225,17 @@ impl Vm { }; let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto(); + let mut depth = 0usize; loop { if !proto.is_object() { self.regs[rd] = JsValue::bool(false); break; } + if depth >= MAX_PROTO_CHAIN_DEPTH { + self.regs[rd] = JsValue::bool(false); + break; + } + depth += 1; let proto_ptr = proto.as_js_object_ptr(); if proto_ptr == ctor_proto_ptr { self.regs[rd] = JsValue::bool(true); @@ -262,11 +268,16 @@ impl Vm { let mut keys_vec = bumpalo::collections::Vec::new_in(self.epoch.bump()); let mut seen = std::collections::HashSet::new(); let mut current = obj_val; + let mut depth = 0usize; loop { if !current.is_object() { break; } + if depth >= MAX_PROTO_CHAIN_DEPTH { + break; + } + depth += 1; let cur = unsafe { &*current.as_js_object_ptr() }; let mut cursor = Some(cur.shape_id()); while let Some(id) = cursor { @@ -425,7 +436,7 @@ impl Vm { let excluded: std::collections::HashSet<&str> = excluded.split('\0').filter(|s| !s.is_empty()).collect(); let proto_ptr = self.session.builtin_world().object_proto.as_ptr() as *mut JsObject; - let rest_ptr = self.epoch.alloc(JsObject::new_empty( + let rest_ptr = self.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr), )); diff --git a/crates/oxide_vm/src/vm_pool.rs b/crates/oxide_vm/src/vm_pool.rs index 2aa01ed2..7e5b48bb 100644 --- a/crates/oxide_vm/src/vm_pool.rs +++ b/crates/oxide_vm/src/vm_pool.rs @@ -1,6 +1,7 @@ #![allow(clippy::arc_with_non_send_sync)] use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; use crate::vm::Vm; use oxide_kernel::kernel::KernelCore; @@ -91,7 +92,19 @@ impl VmPool { }; } - inner = self.condvar.wait(inner).unwrap(); + let (guard, wait) = self.condvar.wait_timeout(inner, Duration::from_secs(5)).unwrap(); + inner = guard; + if wait.timed_out() { + inner.total_count += 1; + drop(inner); + let vm = Self::new_vm(&self.kernel_core); + return VmGuard { + vm: Some(vm), + pool: Arc::clone(self), + dirty: false, + interned_strings: Vec::new(), + }; + } } } } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index a404cc62..b49eb59a 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -1,5 +1,5 @@ use crate::coercion; -use crate::vm::{FrameContinuation, Vm}; +use crate::vm::{FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; use oxide_types::value::JsValue; @@ -20,39 +20,44 @@ impl Vm { &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option, ) -> Result { let length_si = self.kernel_core.string_forge().intern("length").0; - if obj.is_array() && prop_name_si == length_si { - return Ok(JsValue::int(obj.prop_count() as i32)); - } - if obj.is_array() { - if let Some(index) = self.array_index_from_property_key(prop_name_si) { - if index < obj.prop_vec_len() as u32 { - return Ok(obj.get_prop_at(index)); + let mut current = Some(obj); + let mut depth = 0usize; + while let Some(obj) = current { + if obj.is_array() && prop_name_si == length_si { + return Ok(JsValue::int(obj.prop_count() as i32)); + } + if obj.is_array() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if index < obj.prop_vec_len() as u32 { + return Ok(obj.get_prop_at(index)); + } } } - } - if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { - if let Some(meta) = obj.prop_meta_at(pos) { - if meta.is_accessor { - return if meta.get.is_undefined() { - Ok(JsValue::undefined()) - } else if let Some(tr) = target_reg { - let getter = meta.get; - let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?; - if pushed { - return Ok(JsValue::undefined()); - } - Ok(self.regs[tr as usize]) - } else { - self.call_function_sync(meta.get, receiver, &[]) - }; + if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { + if let Some(meta) = obj.prop_meta_at(pos) { + if meta.is_accessor { + return if meta.get.is_undefined() { + Ok(JsValue::undefined()) + } else if let Some(tr) = target_reg { + let getter = meta.get; + let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?; + if pushed { + return Ok(JsValue::undefined()); + } + Ok(self.regs[tr as usize]) + } else { + self.call_function_sync(meta.get, receiver, &[]) + }; + } } + return Ok(obj.get_prop_at(pos)); } - return Ok(obj.get_prop_at(pos)); - } - let proto = obj.proto(); - if proto.is_object() { - let proto_obj = unsafe { &*proto.as_js_object_ptr() }; - return self.ordinary_get_inner(proto_obj, prop_name_si, receiver, target_reg); + if depth >= MAX_PROTO_CHAIN_DEPTH { + break; + } + depth += 1; + let proto = obj.proto(); + current = proto.is_object().then(|| unsafe { &*proto.as_js_object_ptr() }); } Ok(JsValue::undefined()) } @@ -89,7 +94,9 @@ impl Vm { fn inherited_property_meta(&self, obj: &JsObject, prop_name_si: u32) -> Option { let mut proto = obj.proto(); - while proto.is_object() { + let mut depth = 0usize; + while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH { + depth += 1; let proto_obj = unsafe { &*proto.as_js_object_ptr() }; if let Some(pos) = self.get_own_property_slot(proto_obj, prop_name_si) { return proto_obj.prop_meta_at(pos); diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 1367df63..bbfa564d 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -21,8 +21,14 @@ impl Vm { )); } if self.frames.len() >= self.kernel_core.config.max_call_depth { - return Err("RangeError: Maximum call stack size exceeded".into()); + self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?; + return Ok(JsValue::undefined()); } + if self.native_call_depth >= self.kernel_core.config.max_call_depth { + self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?; + return Ok(JsValue::undefined()); + } + self.native_call_depth += 1; let sub = self.sub_modules[sub_idx].clone(); let converted_constants = self.convert_constants(&sub.constants).map_err(String::from)?; @@ -67,6 +73,7 @@ impl Vm { let _ = callee; let result = self.dispatch(); + self.native_call_depth -= 1; self.regs = *saved.regs; self.pc = saved.pc; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index df41ce89..b092f91b 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -32,6 +32,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0, object_prototype: obj_proto, @@ -73,6 +74,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0, object_prototype: obj_proto, @@ -124,7 +126,9 @@ impl Vm { self.clear_execution_state(); self.bytecode.clear(); self.constants.clear(); + self.free_epoch_object_heap_data(); self.epoch.reset(); + self.epoch_object_ptrs.clear(); self.session_epoch.reset(); self.session_object_ptrs.clear(); self.session_bytes_allocated = 0; @@ -137,6 +141,17 @@ impl Vm { self.active_reg_limit = 0; } + fn free_epoch_object_heap_data(&mut self) { + let mut freed = 0u64; + for ptr in self.epoch_object_ptrs.drain(..) { + freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, false); + } + if freed > 0 { + self.session_gc.total_bytes_freed = self.session_gc.total_bytes_freed.saturating_add(freed); + self.session_gc.last_collection_bytes_freed = freed; + } + } + pub(crate) fn clear_execution_state(&mut self) { // Reset contract: // - Clears register file, pc, frame/iterator stacks, saved execution stacks, @@ -163,7 +178,9 @@ impl Vm { self.maybe_collect_session_gc(); self.bytecode.clear(); self.constants.clear(); + self.free_epoch_object_heap_data(); self.epoch.reset(); + self.epoch_object_ptrs.clear(); self.interned_strings.clear(); self.root_reg_limit = 0; self.active_reg_limit = 0; @@ -194,7 +211,7 @@ impl Vm { obj.set_arrow(true); obj.set_captured_this(self.regs[254]); } - let obj_ptr = self.epoch.alloc(obj); + let obj_ptr = self.alloc_object(obj); let func_val = JsValue::object(obj_ptr as *mut u8); if !is_arrow { @@ -202,6 +219,7 @@ impl Vm { let prototype_obj = self .epoch .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto_ptr))); + self.epoch_object_ptrs.push(prototype_obj); let prototype_val = JsValue::from_js_object(prototype_obj); let constructor_si = self.kernel_core.string_forge().intern("constructor").0; diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index 08adeb08..100e86a2 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -67,7 +67,12 @@ fn regression_rerun_clears_ic_cache() { #[test] fn regression_recursion_depth_limit() { - assert_eq!(eval("function f(){f()} f()"), "vm error: RangeError: Maximum call stack size exceeded"); + let result = eval("function f(){f()} f()"); + assert!(result.contains("RangeError"), "expected RangeError, got: {result}"); + assert!( + result.contains("Maximum call stack size exceeded"), + "expected stack limit message, got: {result}" + ); } #[test] @@ -164,3 +169,47 @@ fn regression_many_declarations_keep_stable_registers() { fn regression_method_call_receiver_survives_register_reuse() { assert_eq!(eval("var o = { x: 41, f: function() { return this.x + 1; } }; o.f()"), "42"); } + +#[test] +fn test_recursive_getter_throws_range_error() { + let result = eval("function f(){ return f(); } f()"); + assert!( + result.contains("Maximum call stack size exceeded"), + "expected catchable RangeError, got: {result}" + ); +} + +#[test] +fn test_array_length_range_error() { + let result = eval("new Array(4294967295)"); + assert!(result.contains("Invalid array length"), "expected invalid length, got: {result}"); + let result = eval("new Array(-1)"); + assert!(result.contains("Invalid array length"), "expected invalid length, got: {result}"); +} + +#[test] +fn test_huge_array_index_bounded() { + let result = eval("var a=[]; a[2147483648]=1; 1"); + assert_eq!(result, "1"); +} + +#[test] +fn test_string_pad_bounded() { + let result = eval("''.padStart(2147483648)"); + assert!(result.contains("Invalid string length"), "expected invalid string length, got: {result}"); + assert_eq!(eval("'x'.padStart(5,'0') == '0000x'"), "true"); +} + +#[test] +fn test_typed_array_survives_promote_reset() { + assert_eq!(eval("var a=new Int32Array(4); a.fill(7); a.at(0)"), "7"); + assert_eq!( + eval("var b=new ArrayBuffer(8); var d=new DataView(b); d.setInt32(0, 42); d.getInt32(0)"), + "42" + ); +} + +#[test] +fn test_flat_infinity_bounded() { + assert_eq!(eval("[1,[2,[3,[4]]]].flat(Infinity).length"), "4"); +}