Skip to content

Commit 26cfade

Browse files
Limits for interpreter depth (#1890)
* Set up max depth limit for interpreter * Add debug-level warnings if interp runs too long
1 parent 9fd437e commit 26cfade

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

source/rust_verify_test/tests/assert_by_compute.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,43 @@ test_verify_one_file! {
780780
}
781781
} => Ok(())
782782
}
783+
784+
test_verify_one_file! {
785+
#[test] depth_tracking_infinite_recursion verus_code! {
786+
use vstd::prelude::*;
787+
788+
spec fn infinite_recursion(n: int) -> nat
789+
decreases n,
790+
{
791+
infinite_recursion(1) // Infinite recursion - should trigger depth limit
792+
}
793+
794+
proof fn test_depth_limit() {
795+
assert(infinite_recursion(42) >= 0) by(compute);
796+
}
797+
} => Err(err) => {
798+
assert_eq!(err.errors.len(), 1);
799+
assert!(err.errors[0].rendered.contains("assert_by_compute exceeded maximum recursion depth"));
800+
}
801+
}
802+
803+
test_verify_one_file! {
804+
#[test] depth_tracking_deep_but_finite verus_code! {
805+
use vstd::prelude::*;
806+
807+
spec fn count_down(n: nat) -> nat
808+
decreases n,
809+
{
810+
if n == 0 {
811+
0
812+
} else {
813+
count_down((n - 1) as nat)
814+
}
815+
}
816+
817+
proof fn test_deep_recursion() {
818+
// This should succeed - finite recursion within reasonable depth
819+
assert(count_down(50) == 0) by(compute);
820+
}
821+
} => Ok(())
822+
}

source/vir/src/interpreter.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,22 @@ use std::iter::FromIterator;
3131
use std::ops::ControlFlow;
3232
use std::sync::Arc;
3333
use std::thread;
34+
use std::time::Instant;
3435
use vir_macros::ToDebugSNode;
3536

3637
// An approximation of how many interpreter invocations we can do in 1 second (in release mode)
3738
const RLIMIT_MULTIPLIER: u64 = 400_000;
3839

40+
// Depth limit multiplier: max recursion depth as a multiplier of rlimit.
41+
//
42+
// This is set to be generous enough for legitimate use cases (like complex mathematical
43+
// computations) while still preventing stack overflow. Stack overflow typically occurs
44+
// around 50,000+ recursion levels, so this gives us good protection.
45+
const DEPTH_LIMIT_MULTIPLIER: u64 = 500;
46+
47+
// Time-based warning interval in seconds
48+
const WARNING_INTERVAL_SECS: u64 = 2;
49+
3950
type Env = ScopeMap<UniqueIdent, Exp>;
4051
type TypeEnv = ScopeMap<Ident, Typ>;
4152

@@ -98,7 +109,7 @@ impl<T> PtrSet<T> {
98109

99110
/// Mutable interpreter state
100111
struct State {
101-
/// Depth of our current recursion; used for formatting log output
112+
/// Depth of our current recursion; used for formatting log output and recursion control
102113
depth: usize,
103114
/// Symbol table mapping bound variables to their values
104115
env: Env,
@@ -127,6 +138,10 @@ struct State {
127138
ptr_misses: u64,
128139
/// Number of calls for each function
129140
fun_calls: HashMap<Fun, u64>,
141+
142+
/// Time tracking for warnings
143+
start_time: Instant,
144+
last_warning_time: Instant,
130145
}
131146

132147
// Define the function-call cache's API
@@ -137,6 +152,22 @@ impl State {
137152
}
138153
}
139154

155+
/// Check time-based warnings and emit warning if enough time has passed. Do so only in debug mode.
156+
fn check_time_warning(&mut self) {
157+
if !cfg!(debug_assertions) {
158+
return;
159+
}
160+
let now = Instant::now();
161+
if now.duration_since(self.last_warning_time).as_secs() >= WARNING_INTERVAL_SECS {
162+
let total_time = now.duration_since(self.start_time).as_secs();
163+
eprintln!(
164+
"note: assert_by_compute has been running for {} seconds (depth: {}, iterations: {})",
165+
total_time, self.depth, self.iterations
166+
);
167+
self.last_warning_time = now;
168+
}
169+
}
170+
140171
fn lookup_call(&mut self, f: &Fun, args: &Exps, memoize: bool) -> Option<Exp> {
141172
if self.enable_cache && memoize {
142173
if self.perf {
@@ -163,6 +194,8 @@ struct Ctx<'a> {
163194
fun_ssts: &'a HashMap<Fun, FunctionSst>,
164195
/// We avoid infinite loops by running for a fixed number of intervals
165196
max_iterations: u64,
197+
/// Maximum recursion depth to prevent stack overflow
198+
max_depth: usize,
166199
arch: ArchWordBits,
167200
global: &'a GlobalCtx,
168201
}
@@ -1057,6 +1090,11 @@ fn eval_expr_internal(ctx: &Ctx, state: &mut State, exp: &Exp) -> Result<Exp, Vi
10571090
if state.iterations > ctx.max_iterations {
10581091
return Err(error(&exp.span, "assert_by_compute timed out"));
10591092
}
1093+
if state.depth > ctx.max_depth {
1094+
return Err(error(&exp.span, "assert_by_compute exceeded maximum recursion depth"));
1095+
}
1096+
state.check_time_warning();
1097+
10601098
state.log(format!(
10611099
"{}Evaluating {:}",
10621100
"\t".repeat(state.depth),
@@ -1860,6 +1898,7 @@ fn eval_expr_launch(
18601898
let cache = HashMap::new();
18611899
let logging = log.is_some();
18621900
let msgs = Vec::new();
1901+
let now = Instant::now();
18631902
let mut state = State {
18641903
depth: 0,
18651904
env,
@@ -1877,10 +1916,14 @@ fn eval_expr_launch(
18771916
ptr_hits: 0,
18781917
ptr_misses: 0,
18791918
fun_calls: HashMap::new(),
1919+
start_time: now,
1920+
last_warning_time: now,
18801921
};
18811922
// Don't run for too long
18821923
let max_iterations = (rlimit as f64 * RLIMIT_MULTIPLIER as f64) as u64;
1883-
let ctx = Ctx { fun_ssts: &fun_ssts, max_iterations, arch, global };
1924+
// Calculate max recursion depth as a fraction of rlimit
1925+
let max_depth = (rlimit as f64 * DEPTH_LIMIT_MULTIPLIER as f64) as usize;
1926+
let ctx = Ctx { fun_ssts: &fun_ssts, max_iterations, max_depth, arch, global };
18841927
let result = eval_expr_top(&ctx, &mut state, &exp)?;
18851928
display_perf_stats(&state);
18861929
if state.log.is_some() {

0 commit comments

Comments
 (0)