Skip to content
Open
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
1 change: 1 addition & 0 deletions nova_cli/src/lib/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ pub fn initialize_global_object_with_internals(agent: &mut Agent, global: Object
print_internals: false,
// Always allow children to block.
no_block: false,
execution_timeout: None,
},
child_hooks,
);
Expand Down
1 change: 1 addition & 0 deletions nova_cli/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ impl Instance {
disable_gc: !config.enable_gc,
print_internals: config.verbose,
no_block: !config.block,
execution_timeout: None,
},
// SAFETY: We keep the host hooks alive for at least as long as the agent
unsafe { extend_lifetime(&*host_hooks) as &'static _ },
Expand Down
11 changes: 11 additions & 0 deletions nova_vm/src/ecmascript/execution/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ pub struct AgentOptions {
/// calling `Atomics.wait()` will throw an error to signal that blocking the
/// main thread is not allowed.
pub no_block: bool,
/// Wall-clock time limit for a single script execution. When set, any
/// script that runs longer than this duration will be terminated with a
/// JavaScript `Error`.
Comment on lines +70 to +72
pub execution_timeout: Option<std::time::Duration>,
}

/// Result of methods that may throw a JavaScript error.
Expand Down Expand Up @@ -894,6 +898,10 @@ pub struct Agent {
/// \[\[AsyncEvaluationOrder]] field of modules that are asynchronous or
/// have asynchronous dependencies.
module_async_evaluation_count: u32,
/// Per-execution deadline derived from `options.execution_timeout`.
/// Set at the start of `script_evaluation` and cleared when it returns.
Comment on lines +901 to +902
/// `None` means no deadline is active.
pub(crate) execution_deadline: Option<std::time::Instant>,
}

impl Agent {
Expand All @@ -912,6 +920,7 @@ impl Agent {
kept_alive: false,
private_names_counter: 0,
module_async_evaluation_count: 0,
execution_deadline: None,
}
}

Expand Down Expand Up @@ -1625,6 +1634,7 @@ impl HeapMarkAndSweep for Agent {
kept_alive: _,
private_names_counter: _,
module_async_evaluation_count: _,
execution_deadline: _,
} = self;

execution_context_stack.iter().for_each(|ctx| {
Expand Down Expand Up @@ -1676,6 +1686,7 @@ impl HeapMarkAndSweep for Agent {
kept_alive: _,
private_names_counter: _,
module_async_evaluation_count: _,
execution_deadline: _,
} = self;

execution_context_stack
Expand Down
51 changes: 51 additions & 0 deletions nova_vm/src/ecmascript/scripts_and_modules/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,16 @@ pub fn script_evaluation<'a>(
// 10. Push scriptContext onto the execution context stack; scriptContext is now the running execution context.
agent.push_execution_context(script_context);

// NOTE: Nova extension: if a wall-clock execution timeout is configured
// and no deadline is already active (i.e. this is the outermost script
// call), record the absolute deadline now so the bytecode dispatch loop
// can enforce it. Nested re-entrant calls intentionally inherit the
// already-running deadline rather than resetting it.
let set_deadline = agent.execution_deadline.is_none();
if set_deadline && let Some(timeout) = agent.options.execution_timeout {
agent.execution_deadline = std::time::Instant::now().checked_add(timeout);
}

// 11. Let script be scriptRecord.[[ECMAScriptCode]].
// NOTE: We cannot define the script here due to reference safety.

Expand Down Expand Up @@ -376,6 +386,12 @@ pub fn script_evaluation<'a>(
// 14. Suspend scriptContext and remove it from the execution context stack.
_ = agent.pop_execution_context();

// NOTE: Nova extension: clear the deadline we set so it does not leak
// into future executions.
if set_deadline {
agent.execution_deadline = None;
}

// TODO: 15. Assert: The execution context stack is not empty.
// This is not currently true as we do not push an "empty" context stack to the root before running script evaluation.
// debug_assert!(!agent.execution_context_stack.is_empty());
Expand Down Expand Up @@ -1971,4 +1987,39 @@ mod test {
.bind(gc.nogc());
assert_eq!(result, Value::from_static_str(&mut agent, "c", gc.nogc()));
}

/// Verify that an infinite loop is interrupted once the configured
/// `execution_timeout` has elapsed.
#[test]
fn execution_timeout_kills_infinite_loop() {
let (mut gc, mut scope) = unsafe { GcScope::create_root() };
let mut gc = GcScope::new(&mut gc, &mut scope);
let mut agent = Agent::new(
AgentOptions {
execution_timeout: Some(std::time::Duration::from_millis(100)),
..AgentOptions::default()
},
&DefaultHostHooks,
);
initialize_default_realm(&mut agent, gc.reborrow());

let start = std::time::Instant::now();
let source_text = String::from_static_str(&mut agent, "while(true){}", gc.nogc());
let result = agent.run_script(source_text.unbind(), gc.reborrow());
let elapsed = start.elapsed();

// The script must have thrown an error.
assert!(result.is_err(), "expected a timeout error");
let err_value = result.unwrap_err().value();
let Value::Error(err) = err_value else {
panic!("expected an Error value, got {:?}", err_value);
};
assert_eq!(err.get(&agent).kind, ExceptionType::Error);

// Execution should have been cut off well within a few seconds.
assert!(
elapsed < std::time::Duration::from_secs(5),
"execution took too long: {elapsed:?}"
);
}
}
14 changes: 14 additions & 0 deletions nova_vm/src/engine/bytecode/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ impl Vm {
if agent.check_gc() {
self.trigger_gc(agent, gc.reborrow());
}
// NOTE: Nova extension: check the wall-clock deadline on every
// instruction. `Instant::now()` is only called when a deadline is
// active; in the common case (`execution_deadline` is `None`) the
// short-circuit prevents the syscall entirely.
if let Some(deadline) = agent.execution_deadline
&& std::time::Instant::now() >= deadline
{
let err = agent.throw_exception_with_static_message(
ExceptionType::Error,
"Script execution timed out",
gc.into_nogc(),
);
return ExecutionResult::Throw(err);
}
if agent.options.print_internals {
Self::print_executing(instr.kind);
}
Expand Down
Loading