fix: 修复全局边界安全与内存稳定性问题

This commit is contained in:
爱特每羊队
2026-06-22 19:43:00 +08:00
parent 1016f432f1
commit 933612c89d
35 changed files with 620 additions and 135 deletions
+46
View File
@@ -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<usize, String> {
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);
}
}
+73 -36
View File
@@ -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<u8> = 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)
@@ -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::<Vec<_>>().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;");
+12
View File
@@ -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.
+12
View File
@@ -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 {
+17
View File
@@ -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<u32> {
let shapes = self.shapes.read().unwrap();
let total_depth = Self::compute_depth(shape_id, &shapes);
+8
View File
@@ -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 {
+3
View File
@@ -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);
}
+5 -1
View File
@@ -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() {
+40 -23
View File
@@ -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<usize, JsValue> {
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<JsValue> {
fn flatten(items: &[JsValue], remaining_depth: usize, seen: &mut Vec<*mut JsObject>) -> Vec<JsValue> {
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<JsValue> = (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<JsValue> = (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() {
+19 -1
View File
@@ -66,7 +66,25 @@ pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec<u8>) -> *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<u8>;
if data_ptr.is_null() {
return 0;
}
// SAFETY: ArrayBuffer stores `Box<Vec<u8>>` in native_fn.
let data = unsafe { Box::from_raw(data_ptr) };
let bytes = std::mem::size_of::<Vec<u8>>() + 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<u8>, JsValue> {
+67 -2
View File
@@ -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<JsValue> {
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<F>(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<F>(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::<DataViewData>() as u64
}
pub fn data_view_get_int8(vm: &mut Vm, args: &[u8]) -> NativeResult {
+1 -1
View File
@@ -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))
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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 };
+2 -2
View File
@@ -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)
}
}
+1 -1
View File
@@ -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<JsValue> {
+8 -8
View File
@@ -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,
+1 -1
View File
@@ -221,7 +221,7 @@ fn own_field(vm: &Vm, obj: &JsObject, prop_si: u32) -> Option<JsValue> {
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(),
+19 -3
View File
@@ -103,7 +103,7 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
// SAFETY: re_ptr is a Box<regex::Regex> 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::<regex::Regex>() 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 {
+1 -1
View File
@@ -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<JsValue> {
+6
View File
@@ -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));
+66 -1
View File
@@ -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<JsValue> {
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<F>(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<F>(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::<TypedArrayData>() as u64
}
pub(crate) fn get_typed_array_data(vm: &mut Vm, this_val: JsValue) -> Result<TypedArrayData, JsValue> {
+1 -1
View File
@@ -312,7 +312,7 @@ pub fn to_object(val: JsValue, vm: &mut Vm) -> Result<JsValue, &'static str> {
(&*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;
+1 -1
View File
@@ -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];
+4 -2
View File
@@ -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<JsValue> {
@@ -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));
+9 -1
View File
@@ -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
}
+27 -3
View File
@@ -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<JsValue>;
@@ -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::<JsObject>() 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));
}
}
+18 -6
View File
@@ -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<JsObject>,
@@ -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<JsValue> {
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) {
+14 -3
View File
@@ -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),
));
+14 -1
View File
@@ -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(),
};
}
}
}
}
+38 -31
View File
@@ -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<u8>,
) -> Result<JsValue, String> {
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<PropMetaEntry> {
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);
+8 -1
View File
@@ -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;
+19 -1
View File
@@ -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;
+50 -1
View File
@@ -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");
}