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    #[allow(dead_code)]
103    pub(super) fn try_charge_vec_growth<T>(&mut self, old_cap: usize, new_cap: usize) -> bool {
104        if new_cap <= old_cap {
105            return true;
106        }
107        let delta = new_cap - old_cap;
108        self.try_charge_mem_bytes(delta.saturating_mul(size_of::<T>()))
109    }
110
111    #[inline]
112    pub(super) fn charge_map_entry_estimate(&mut self) -> Result<()> {
113        self.charge_mem_bytes(MAP_ENTRY_BYTES_ESTIMATE)
114    }
115
116    #[inline]
117    pub(super) fn charge_upvalues_estimate(&mut self, upvalue_count: usize) -> Result<()> {
118        if upvalue_count == 0 {
119            return Ok(());
120        }
121        self.charge_mem_bytes(upvalue_count.saturating_mul(UPVALUE_BYTES_ESTIMATE))
122    }
123}
124
125impl VM {
126    pub fn set_gas_budget(&mut self, limit: u64) {
127        self.budgets.gas.limit = Some(limit);
128    }
129
130    pub fn clear_gas_budget(&mut self) {
131        self.budgets.gas.limit = None;
132    }
133
134    pub fn reset_gas_counter(&mut self) {
135        self.budgets.gas.used = 0;
136    }
137
138    pub fn gas_used(&self) -> u64 {
139        self.budgets.gas.used
140    }
141
142    pub fn gas_remaining(&self) -> Option<u64> {
143        self.budgets
144            .gas
145            .limit
146            .map(|limit| limit.saturating_sub(self.budgets.gas.used))
147    }
148
149    pub fn set_memory_budget_bytes(&mut self, limit_bytes: usize) {
150        self.budgets.memory.limit_bytes = Some(limit_bytes);
151    }
152
153    pub fn set_memory_budget_kb(&mut self, limit_kb: u64) {
154        let bytes = limit_kb.saturating_mul(1024);
155        let limit_bytes = usize::try_from(bytes).unwrap_or(usize::MAX);
156        self.set_memory_budget_bytes(limit_bytes);
157    }
158
159    pub fn clear_memory_budget(&mut self) {
160        self.budgets.memory.limit_bytes = None;
161        self.budgets.memory.used_bytes = 0;
162    }
163
164    pub fn reset_memory_counter(&mut self) {
165        self.budgets.memory.used_bytes = 0;
166    }
167
168    pub fn memory_used_bytes(&self) -> usize {
169        self.budgets.memory.used_bytes
170    }
171
172    pub fn memory_remaining_bytes(&self) -> Option<usize> {
173        self.budgets
174            .memory
175            .limit_bytes
176            .map(|limit| limit.saturating_sub(self.budgets.memory.used_bytes))
177    }
178
179    pub(crate) fn try_charge_memory_bytes(&mut self, bytes: usize) -> bool {
180        self.budgets.try_charge_mem_bytes(bytes)
181    }
182
183    pub(crate) fn try_charge_memory_value_vec(&mut self, element_count: usize) -> bool {
184        self.budgets.try_charge_value_vec(element_count)
185    }
186
187    #[allow(dead_code)]
188    pub(crate) fn try_charge_memory_vec_growth<T>(
189        &mut self,
190        old_cap: usize,
191        new_cap: usize,
192    ) -> bool {
193        self.budgets.try_charge_vec_growth::<T>(old_cap, new_cap)
194    }
195}
196
197#[cfg(all(test, feature = "std"))]
198mod tests {
199    use crate::EmbeddedProgram;
200    use crate::{LustError, Result};
201
202    #[test]
203    fn gas_budget_traps() -> Result<()> {
204        let mut program = EmbeddedProgram::builder()
205            .module(
206                "main",
207                r#"
208                    pub function spin(): ()
209                        while true do
210                        end
211                    end
212                "#,
213            )
214            .entry_module("main")
215            .compile()?;
216
217        program.vm_mut().set_gas_budget(30);
218        program.vm_mut().reset_gas_counter();
219        let err = program.call_raw("main.spin", vec![]).unwrap_err();
220        match err {
221            LustError::RuntimeErrorWithTrace { message, .. }
222            | LustError::RuntimeError { message } => {
223                assert!(message.to_lowercase().contains("out of gas"));
224            }
225            other => panic!("unexpected error: {other:?}"),
226        }
227        Ok(())
228    }
229
230    #[test]
231    fn memory_budget_traps_on_growth() -> Result<()> {
232        let mut program = EmbeddedProgram::builder()
233            .module(
234                "main",
235                r#"
236                    pub function grow(): ()
237                        local arr: Array<int> = []
238                        arr:push(1)
239                    end
240                "#,
241            )
242            .entry_module("main")
243            .compile()?;
244
245        program.vm_mut().set_memory_budget_bytes(32);
246        program.vm_mut().reset_memory_counter();
247        let err = program.call_raw("main.grow", vec![]).unwrap_err();
248        match err {
249            LustError::RuntimeErrorWithTrace { message, .. }
250            | LustError::RuntimeError { message } => {
251                assert!(message.to_lowercase().contains("memory budget"));
252            }
253            other => panic!("unexpected error: {other:?}"),
254        }
255        Ok(())
256    }
257}