Skip to main content

lust/vm/
budget.rs

1use super::*;
2use core::mem::size_of;
3
4const MAP_ENTRY_BYTES_ESTIMATE: usize = 64;
5const UPVALUE_BYTES_ESTIMATE: usize = 64;
6
7#[derive(Debug, Clone, Default)]
8pub(crate) struct BudgetState {
9    gas: GasBudget,
10    memory: MemoryBudget,
11}
12
13#[derive(Debug, Clone, Default)]
14struct GasBudget {
15    limit: Option<u64>,
16    used: u64,
17}
18
19#[derive(Debug, Clone, Default)]
20struct MemoryBudget {
21    limit_bytes: Option<usize>,
22    used_bytes: usize,
23}
24
25impl BudgetState {
26    #[inline]
27    pub(super) fn charge_gas(&mut self, amount: u64) -> Result<()> {
28        self.gas.used = self.gas.used.saturating_add(amount);
29        if let Some(limit) = self.gas.limit {
30            if self.gas.used > limit {
31                return Err(LustError::RuntimeError {
32                    message: format!("Out of gas (limit: {}, used: {})", limit, self.gas.used),
33                });
34            }
35        }
36        Ok(())
37    }
38
39    #[inline]
40    pub(super) fn charge_mem_bytes(&mut self, bytes: usize) -> Result<()> {
41        let Some(limit) = self.memory.limit_bytes else {
42            return Ok(());
43        };
44        self.memory.used_bytes = self.memory.used_bytes.saturating_add(bytes);
45        if self.memory.used_bytes > limit {
46            return Err(LustError::RuntimeError {
47                message: format!(
48                    "Out of memory budget (limit: {} bytes, used since reset: {} bytes)",
49                    limit, self.memory.used_bytes
50                ),
51            });
52        }
53        Ok(())
54    }
55
56    #[inline]
57    pub(super) fn try_charge_mem_bytes(&mut self, bytes: usize) -> bool {
58        let Some(limit) = self.memory.limit_bytes else {
59            return true;
60        };
61        let Some(next) = self.memory.used_bytes.checked_add(bytes) else {
62            return false;
63        };
64        if next > limit {
65            return false;
66        }
67        self.memory.used_bytes = next;
68        true
69    }
70
71    #[inline]
72    pub(super) fn mem_budget_enabled(&self) -> bool {
73        self.memory.limit_bytes.is_some()
74    }
75
76    #[inline]
77    pub(super) fn charge_value_vec(&mut self, element_count: usize) -> Result<()> {
78        if element_count == 0 {
79            return Ok(());
80        }
81        self.charge_mem_bytes(element_count.saturating_mul(size_of::<Value>()))
82    }
83
84    #[inline]
85    pub(super) fn try_charge_value_vec(&mut self, element_count: usize) -> bool {
86        if element_count == 0 {
87            return true;
88        }
89        self.try_charge_mem_bytes(element_count.saturating_mul(size_of::<Value>()))
90    }
91
92    #[inline]
93    pub(super) fn charge_vec_growth<T>(&mut self, old_cap: usize, new_cap: usize) -> Result<()> {
94        if new_cap <= old_cap {
95            return Ok(());
96        }
97        let delta = new_cap - old_cap;
98        self.charge_mem_bytes(delta.saturating_mul(size_of::<T>()))
99    }
100
101    #[inline]
102    pub(super) fn try_charge_vec_growth<T>(&mut self, old_cap: usize, new_cap: usize) -> bool {
103        if new_cap <= old_cap {
104            return true;
105        }
106        let delta = new_cap - old_cap;
107        self.try_charge_mem_bytes(delta.saturating_mul(size_of::<T>()))
108    }
109
110    #[inline]
111    pub(super) fn charge_map_entry_estimate(&mut self) -> Result<()> {
112        self.charge_mem_bytes(MAP_ENTRY_BYTES_ESTIMATE)
113    }
114
115    #[inline]
116    pub(super) fn charge_upvalues_estimate(&mut self, upvalue_count: usize) -> Result<()> {
117        if upvalue_count == 0 {
118            return Ok(());
119        }
120        self.charge_mem_bytes(upvalue_count.saturating_mul(UPVALUE_BYTES_ESTIMATE))
121    }
122}
123
124impl VM {
125    pub fn set_gas_budget(&mut self, limit: u64) {
126        self.budgets.gas.limit = Some(limit);
127    }
128
129    pub fn clear_gas_budget(&mut self) {
130        self.budgets.gas.limit = None;
131    }
132
133    pub fn reset_gas_counter(&mut self) {
134        self.budgets.gas.used = 0;
135    }
136
137    pub fn gas_used(&self) -> u64 {
138        self.budgets.gas.used
139    }
140
141    pub fn gas_remaining(&self) -> Option<u64> {
142        self.budgets
143            .gas
144            .limit
145            .map(|limit| limit.saturating_sub(self.budgets.gas.used))
146    }
147
148    pub fn set_memory_budget_bytes(&mut self, limit_bytes: usize) {
149        self.budgets.memory.limit_bytes = Some(limit_bytes);
150    }
151
152    pub fn set_memory_budget_kb(&mut self, limit_kb: u64) {
153        let bytes = limit_kb.saturating_mul(1024);
154        let limit_bytes = usize::try_from(bytes).unwrap_or(usize::MAX);
155        self.set_memory_budget_bytes(limit_bytes);
156    }
157
158    pub fn clear_memory_budget(&mut self) {
159        self.budgets.memory.limit_bytes = None;
160        self.budgets.memory.used_bytes = 0;
161    }
162
163    pub fn reset_memory_counter(&mut self) {
164        self.budgets.memory.used_bytes = 0;
165    }
166
167    pub fn memory_used_bytes(&self) -> usize {
168        self.budgets.memory.used_bytes
169    }
170
171    pub fn memory_remaining_bytes(&self) -> Option<usize> {
172        self.budgets
173            .memory
174            .limit_bytes
175            .map(|limit| limit.saturating_sub(self.budgets.memory.used_bytes))
176    }
177
178    pub(crate) fn try_charge_memory_bytes(&mut self, bytes: usize) -> bool {
179        self.budgets.try_charge_mem_bytes(bytes)
180    }
181
182    pub(crate) fn try_charge_memory_value_vec(&mut self, element_count: usize) -> bool {
183        self.budgets.try_charge_value_vec(element_count)
184    }
185
186    pub(crate) fn try_charge_memory_vec_growth<T>(
187        &mut self,
188        old_cap: usize,
189        new_cap: usize,
190    ) -> bool {
191        self.budgets.try_charge_vec_growth::<T>(old_cap, new_cap)
192    }
193}
194
195#[cfg(all(test, feature = "std"))]
196mod tests {
197    use crate::EmbeddedProgram;
198    use crate::{LustError, Result};
199
200    #[test]
201    fn gas_budget_traps() -> Result<()> {
202        let mut program = EmbeddedProgram::builder()
203            .module(
204                "main",
205                r#"
206                    pub function spin(): ()
207                        while true do
208                        end
209                    end
210                "#,
211            )
212            .entry_module("main")
213            .compile()?;
214
215        program.vm_mut().set_gas_budget(30);
216        program.vm_mut().reset_gas_counter();
217        let err = program.call_raw("main.spin", vec![]).unwrap_err();
218        match err {
219            LustError::RuntimeErrorWithTrace { message, .. }
220            | LustError::RuntimeError { message } => {
221                assert!(message.to_lowercase().contains("out of gas"));
222            }
223            other => panic!("unexpected error: {other:?}"),
224        }
225        Ok(())
226    }
227
228    #[test]
229    fn memory_budget_traps_on_growth() -> Result<()> {
230        let mut program = EmbeddedProgram::builder()
231            .module(
232                "main",
233                r#"
234                    pub function grow(): ()
235                        local arr: Array<int> = []
236                        arr:push(1)
237                    end
238                "#,
239            )
240            .entry_module("main")
241            .compile()?;
242
243        program.vm_mut().set_memory_budget_bytes(32);
244        program.vm_mut().reset_memory_counter();
245        let err = program.call_raw("main.grow", vec![]).unwrap_err();
246        match err {
247            LustError::RuntimeErrorWithTrace { message, .. }
248            | LustError::RuntimeError { message } => {
249                assert!(message.to_lowercase().contains("memory budget"));
250            }
251            other => panic!("unexpected error: {other:?}"),
252        }
253        Ok(())
254    }
255}