Skip to main content

rune/runtime/
budget.rs

1//! Budgeting module for Runestick.
2//!
3//! This module contains methods which allows for limiting the execution of the
4//! virtual machine to abide by the specified budget.
5//!
6//! By default the budget is disabled, but can be enabled by wrapping your
7//! function call in [with].
8
9#[cfg_attr(feature = "std", path = "budget/std.rs")]
10mod no_std;
11
12use core::future::Future;
13use core::pin::Pin;
14use core::task::{Context, Poll};
15
16use pin_project::pin_project;
17use rune_alloc::callable::Callable;
18
19/// Wrapper for something being [budgeted].
20///
21/// See [with].
22///
23/// [budgeted]: self
24#[pin_project]
25pub struct Budget<T> {
26    /// Instruction budget.
27    budget: usize,
28    /// The thing being budgeted.
29    #[pin]
30    value: T,
31}
32
33/// Wrap the given value with a budget.
34///
35/// Budgeting is only performed on a per-instruction basis in the virtual
36/// machine. What exactly constitutes an instruction might be a bit vague. But
37/// important to note is that without explicit co-operation from native
38/// functions the budget cannot be enforced. So care must be taken with the
39/// native functions that you provide to Rune to ensure that the limits you
40/// impose cannot be circumvented.
41///
42/// The following things can be wrapped:
43/// * A [`FnOnce`] closure, like `with(|| println!("Hello World")).call()`.
44/// * A [`Future`], like `with(async { /* async work */ }).await`;
45///
46/// It's also possible to wrap other wrappers which implement [`Callable`].
47///
48/// # Examples
49///
50/// ```no_run
51/// use rune::runtime::budget;
52/// use rune::Vm;
53///
54/// let mut vm: Vm = todo!();
55/// // The virtual machine and any tasks associated with it is only allowed to execute 100 budget.
56/// budget::with(100, || vm.call(&["main"], ())).call()?;
57/// # Ok::<(), rune::support::Error>(())
58/// ```
59///
60/// This budget can be conveniently combined with the memory [`limit`] module
61/// due to both wrappers implementing [`Callable`].
62///
63/// [`limit`]: crate::alloc::limit
64///
65/// ```
66/// use rune::runtime::budget;
67/// use rune::alloc::{limit, Vec};
68///
69/// #[derive(Debug, PartialEq)]
70/// struct Marker;
71///
72/// // Limit the given closure to run one instruction and allocate 1024 bytes.
73/// let f = budget::with(1, limit::with(1024, || {
74///     let mut budget = budget::acquire();
75///     assert!(budget.take());
76///     assert!(!budget.take());
77///     assert!(Vec::<u8>::try_with_capacity(1).is_ok());
78///     assert!(Vec::<u8>::try_with_capacity(1024).is_ok());
79///     assert!(Vec::<u8>::try_with_capacity(1025).is_err());
80///     Marker
81/// }));
82///
83/// assert_eq!(f.call(), Marker);
84/// ```
85pub fn with<T>(budget: usize, value: T) -> Budget<T> {
86    tracing::trace!(?budget);
87    Budget { budget, value }
88}
89
90/// Replace the current budget returning a guard that will restore the one which
91/// was in effect when it is dropped.
92#[inline(never)]
93pub fn replace(budget: usize) -> BudgetGuard {
94    BudgetGuard(self::no_std::rune_budget_replace(budget))
95}
96
97/// Acquire the current budget, leaving no budget in effect for the duration of
98/// the returned guard.
99///
100/// Use [`BudgetGuard::take`] to take permits from the returned budget.
101///
102/// Note that while the guard is alive nothing else can see the budget, so
103/// anything called in the meantime - including a nested execution of the
104/// virtual machine - runs unbudgeted. Prefer [`take`] unless that is what is
105/// wanted.
106#[inline(never)]
107pub fn acquire() -> BudgetGuard {
108    BudgetGuard(self::no_std::rune_budget_replace(usize::MAX))
109}
110
111/// Take a single permit from the ambient budget, if one is set.
112///
113/// Returns `false` if the budget has been exhausted.
114///
115/// This is the primitive every budgeted loop uses, the machine's own included.
116/// The permit is taken out of the budget where it lives rather than out of a
117/// local copy of it, so anything which is called while a loop is running - a
118/// native function, or a nested execution of the machine - draws from the same
119/// budget and cannot escape it.
120///
121/// # Examples
122///
123/// ```
124/// use rune::runtime::budget;
125///
126/// budget::with(2, || {
127///     assert!(budget::take());
128///     assert!(budget::take());
129///     assert!(!budget::take());
130/// })
131/// .call();
132/// ```
133#[inline]
134pub fn take() -> bool {
135    let budget = self::no_std::rune_budget_get();
136
137    // No budget is in effect.
138    if budget == usize::MAX {
139        return true;
140    }
141
142    if budget == 0 {
143        return false;
144    }
145
146    self::no_std::rune_budget_replace(budget - 1);
147    true
148}
149
150/// Take a permit from the ambient budget on behalf of a natively driven loop.
151///
152/// The machine only takes a permit for the instructions it executes itself, so
153/// a native loop which walks a value has to take one per step or a host which
154/// set a budget has no way to interrupt it. Without this, a script can spend an
155/// unbounded amount of time inside a single call such as
156/// `(0..).iter().count()`.
157///
158/// The error raised is the same one the machine's own callers raise when the
159/// budget runs out, so a host sees one shape either way.
160#[inline]
161pub(crate) fn permit() -> Result<(), crate::runtime::VmError> {
162    use crate::runtime::{VmError, VmErrorKind, VmHaltInfo};
163
164    if !take() {
165        return Err(VmError::new(VmErrorKind::Halted {
166            halt: VmHaltInfo::Limited,
167        }));
168    }
169
170    Ok(())
171}
172
173/// A locally acquired budget.
174///
175/// This guard is acquired by calling [`take`] and can be used to take permits.
176///
177/// [`take`]: BudgetGuard::take
178#[repr(transparent)]
179pub struct BudgetGuard(usize);
180
181impl BudgetGuard {
182    /// Take a ticker from the budget.
183    #[inline]
184    pub fn take(&mut self) -> bool {
185        if self.0 == usize::MAX {
186            return true;
187        }
188
189        if self.0 == 0 {
190            return false;
191        }
192
193        self.0 -= 1;
194        true
195    }
196}
197
198impl Drop for BudgetGuard {
199    #[inline]
200    fn drop(&mut self) {
201        let _ = self::no_std::rune_budget_replace(self.0);
202    }
203}
204
205impl<T> Budget<T>
206where
207    T: Callable,
208{
209    /// Call the budgeted function.
210    #[inline]
211    pub fn call(self) -> T::Output {
212        Callable::call(self)
213    }
214}
215
216impl<T> Callable for Budget<T>
217where
218    T: Callable,
219{
220    type Output = T::Output;
221
222    #[inline]
223    fn call(self) -> Self::Output {
224        let _guard = BudgetGuard(self::no_std::rune_budget_replace(self.budget));
225        self.value.call()
226    }
227}
228
229impl<T> Future for Budget<T>
230where
231    T: Future,
232{
233    type Output = T::Output;
234
235    #[inline]
236    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
237        let this = self.project();
238
239        let _guard = BudgetGuard(self::no_std::rune_budget_replace(*this.budget));
240        let poll = this.value.poll(cx);
241        *this.budget = self::no_std::rune_budget_get();
242        poll
243    }
244}