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}