Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/mrubyedge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ jobs:
git diff
exit $TEST_RESULT
fi
- name: Run extra test cases for "${{ matrix.BUILD_TARGET }}" profile
run: |
export MRUBYEDGE_INSN_LIMIT=10000
cargo test --features insn-limit --test insn_limit --profile ${{ matrix.BUILD_TARGET }}
- name: Build binaries for "${{ matrix.BUILD_TARGET }}${{ matrix.ENABLE_FNV_HASH }}" profile
run: |
cargo build -p mrubyedge \
Expand Down
1 change: 1 addition & 0 deletions mrubyedge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ mruby-hash-fnv = ["dep:fnv"]
mruby-regexp = ["dep:regex"]
mrubyedge-debug = ["wasi"]
mruby-random = ["dep:rand_core", "dep:rand_xorshift"]
insn-limit = []
# mruby-securerandom = ["wasi", "dep:getrandom"]
no-wasi = []
17 changes: 16 additions & 1 deletion mrubyedge/src/yamrb/prelude/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::rc::Rc;
use crate::{
Error,
yamrb::{
helpers::{mrb_define_cmethod, mrb_funcall},
helpers::{mrb_call_block, mrb_define_cmethod, mrb_funcall},
value::*,
vm::VM,
},
Expand Down Expand Up @@ -116,6 +116,7 @@ pub(crate) fn initialize_object(vm: &mut VM) {
"extend",
Box::new(mrb_object_extend),
);
mrb_define_cmethod(vm, object_class.clone(), "loop", Box::new(mrb_object_loop));

// define global consts:
vm.consts.insert(
Expand Down Expand Up @@ -365,6 +366,20 @@ fn mrb_object_class(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, E
Ok(RObject::class_or_module(class.as_module(), vm))
}

fn mrb_object_loop(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
let block = args[0].clone();
if !matches!(block.value, RValue::Proc(_)) {
return Err(Error::ArgumentError(
"Object#loop expects a block".to_string(),
));
}

let this = vm.getself()?;
loop {
mrb_call_block(vm, block.clone(), Some(this.clone()), &[], 0)?;
}
}

fn mrb_object_method_missing(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
let method_name_obj = &args
.first()
Expand Down
47 changes: 47 additions & 0 deletions mrubyedge/src/yamrb/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub struct VM {

pub flag_preemption: Cell<bool>,

#[cfg(feature = "insn-limit")]
pub insn_count: Cell<usize>,
#[cfg(feature = "insn-limit")]
pub insn_limit: usize,

// common class
pub object_class: Rc<RClass>,
pub builtin_class_table: RHashMap<&'static str, Rc<RClass>>,
Expand Down Expand Up @@ -266,6 +271,19 @@ impl VM {
let cur_env = RHashMap::default();
let has_env_ref = RHashMap::default();

#[cfg(feature = "insn-limit")]
let insn_count = Cell::new(0);
#[cfg(feature = "insn-limit")]
let insn_limit = {
let limit_str = env!(
"MRUBYEDGE_INSN_LIMIT",
"MRUBYEDGE_INSN_LIMIT must be set when insn-limit feature is enabled"
);
limit_str
.parse::<usize>()
.expect("MRUBYEDGE_INSN_LIMIT must be a valid number")
};

let mut vm = VM {
id,
bytecode,
Expand All @@ -281,6 +299,10 @@ impl VM {
target_class,
exception,
flag_preemption,
#[cfg(feature = "insn-limit")]
insn_count,
#[cfg(feature = "insn-limit")]
insn_limit,
object_class,
builtin_class_table,
class_object_table,
Expand All @@ -298,6 +320,18 @@ impl VM {
vm
}

/// Resets the instruction counter. Only available when the `insn-limit` feature is enabled.
#[cfg(feature = "insn-limit")]
pub fn reset_insn_count(&mut self) {
self.insn_count.set(0);
}

/// Returns the current instruction count. Only available when the `insn-limit` feature is enabled.
#[cfg(feature = "insn-limit")]
pub fn get_insn_count(&self) -> usize {
self.insn_count.get()
}

/// Executes the current IREP until completion, returning the value in
/// register 0 or propagating any raised exception as an error. The
/// top-level `self` is initialized automatically before evaluation.
Expand Down Expand Up @@ -431,6 +465,19 @@ impl VM {
let operand = op.operand;
self.pc.set(pc + 1);

#[cfg(feature = "insn-limit")]
{
let count = self.insn_count.get();
if count >= self.insn_limit {
return Err(Error::internal(format!(
"instruction limit exceeded: {} instructions",
self.insn_limit
))
.into());
}
self.insn_count.set(count + 1);
}

#[cfg(feature = "mrubyedge-debug")]
if let Ok(v) = env::var("MRUBYEDGE_DEBUG") {
let level: i32 = v.parse().unwrap_or(1);
Expand Down
150 changes: 150 additions & 0 deletions mrubyedge/tests/insn_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#![cfg(feature = "insn-limit")]
extern crate mec_mrbc_sys;
extern crate mrubyedge;

mod helpers;
use helpers::*;

#[test]
fn insn_limit_basic_test() {
let code = r#"
def test_simple
a = 1
b = 2
a + b
end
"#;
let binary = mrbc_compile("insn_limit_basic", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);

// Simple function should complete within limit
let result = vm.run();
assert!(result.is_ok());

let args = vec![];
let result: i32 = mrb_funcall(&mut vm, None, "test_simple", &args)
.unwrap()
.as_ref()
.try_into()
.unwrap();
assert_eq!(result, 3);
}

#[test]
fn insn_limit_exceeded_test() {
let code = r#"
def test_infinite_loop
i = 0
loop do
i += 1
end
end
"#;
let binary = mrbc_compile("insn_limit_exceeded", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);
vm.run().unwrap();

let args = vec![];
let result = mrb_funcall(&mut vm, None, "test_infinite_loop", &args);

// Should fail due to instruction limit
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("instruction limit exceeded"));
}

#[test]
fn insn_limit_reset_test() {
let code = r#"
def test_count
sum = 0
10.times do |i|
sum += i
end
sum
end
"#;
let binary = mrbc_compile("insn_limit_reset", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);
vm.run().unwrap();

// First call
let args = vec![];
let result: i32 = mrb_funcall(&mut vm, None, "test_count", &args)
.unwrap()
.as_ref()
.try_into()
.unwrap();
assert_eq!(result, 45);

let count_before_reset = vm.get_insn_count();
assert!(count_before_reset > 0);

vm.reset_insn_count();
assert_eq!(vm.get_insn_count(), 0);

// Second call should work after reset
let result: i32 = mrb_funcall(&mut vm, None, "test_count", &args)
.unwrap()
.as_ref()
.try_into()
.unwrap();
assert_eq!(result, 45);
}

#[test]
fn insn_limit_while_loop_test() {
let code = r#"
def test_while
i = 0
sum = 0
while i < 100000
i += 1
sum += i
end
sum
end
"#;
let binary = mrbc_compile("insn_limit_while", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);
vm.run().unwrap();

let args = vec![];
let result = mrb_funcall(&mut vm, None, "test_while", &args);

assert!(result.is_err());
assert!(format!("{:?}", result.unwrap_err()).contains("instruction limit exceeded"));
}

#[test]
fn insn_limit_counter_increments_test() {
let code = r#"
def test_increment
a = 1
b = 2
c = 3
a + b + c
end
"#;
let binary = mrbc_compile("insn_limit_increment", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);

let initial_count = vm.get_insn_count();
assert_eq!(initial_count, 0);

vm.run().unwrap();

let count_after_run = vm.get_insn_count();
assert!(count_after_run > 0);

let args = vec![];
mrb_funcall(&mut vm, None, "test_increment", &args).unwrap();

let count_after_call = vm.get_insn_count();
assert!(count_after_call > count_after_run);
}
26 changes: 26 additions & 0 deletions mrubyedge/tests/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,29 @@ fn object_extend_multiple_arguments_priority_test() {
"M2 only"
);
}

#[test]
fn object_loop_basic_test() {
let code = r#"
def test_loop
i = 0
loop do
i += 1
break if i >= 5
end
i
end
"#;
let binary = mrbc_compile_debug("loop_basic", code);
let mut rite = mrubyedge::rite::load(&binary).unwrap();
let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite);
vm.run().unwrap();

let args = vec![];
let result: i32 = mrb_funcall(&mut vm, None, "test_loop", &args)
.unwrap()
.as_ref()
.try_into()
.unwrap();
assert_eq!(result, 5);
}