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}