luaur_rt/memory.rs
1//! Memory limit + memory-category control. Mirrors mlua's `Lua::set_memory_limit`
2//! and `Lua::set_memory_category`.
3//!
4//! ## Memory limit
5//!
6//! Luau routes every allocation through `global_State::frealloc(ud, ...)`. We
7//! install a limit-enforcing allocator ([`limited_alloc`]) over the default one
8//! by overwriting `frealloc`/`ud` on the global state, keyed by a heap-boxed
9//! [`MemoryControl`] that holds the cap and a pointer back to the global state
10//! (so the allocator can compare the would-be `totalbytes` to the cap). When a
11//! growing allocation would exceed the cap the allocator returns null, which the
12//! VM turns into a `LUA_ERRMEM` longjmp — surfaced by luaur-rt as
13//! [`Error::MemoryError`](crate::Error::MemoryError).
14//!
15//! A limit of `0` means "unlimited" (mlua's convention).
16//!
17//! ## Memory categories
18//!
19//! Luau tags allocations with an 8-bit *category* (`global_State::activememcat`,
20//! `memcatbytes[256]`). mlua exposes named categories; we keep a per-VM
21//! name→id table (max 255 user categories: id 0 is reserved for `"main"`),
22//! validate names (`[A-Za-z0-9_]+`), and call `lua_setmemcat`.
23
24use std::cell::RefCell;
25use std::collections::HashMap;
26
27use crate::error::{Error, Result};
28use crate::state::Lua;
29use crate::sys::*;
30
31/// The per-VM allocator control block, pointed to by `global_State::ud` once a
32/// memory limit is installed.
33struct MemoryControl {
34 /// The byte cap (`0` = unlimited).
35 limit: usize,
36 /// The global state, so the allocator can read the live `totalbytes`.
37 global: *mut core::ffi::c_void,
38 /// The original allocator we delegate to (libc realloc/free).
39 base: unsafe extern "C" fn(
40 *mut core::ffi::c_void,
41 *mut core::ffi::c_void,
42 usize,
43 usize,
44 ) -> *mut core::ffi::c_void,
45 /// The original allocator's userdata.
46 base_ud: *mut core::ffi::c_void,
47}
48
49thread_local! {
50 /// One `MemoryControl` per VM, keyed by the global-state pointer. Boxed so
51 /// its address (handed to the VM as `ud`) is stable.
52 static MEMORY_CONTROLS: RefCell<HashMap<*mut core::ffi::c_void, Box<MemoryControl>>> =
53 RefCell::new(HashMap::new());
54
55 /// Per-VM memory-category name→id table (id 0 is reserved for `"main"`).
56 static MEMORY_CATEGORIES: RefCell<HashMap<*mut core::ffi::c_void, HashMap<String, u8>>> =
57 RefCell::new(HashMap::new());
58}
59
60/// The global-state pointer for `state` — the per-VM key.
61unsafe fn global_key(state: *mut lua_State) -> *mut core::ffi::c_void {
62 unsafe { (*state).global as *mut core::ffi::c_void }
63}
64
65/// The limit-enforcing allocator. Reads the live `totalbytes` from the global
66/// state and refuses any growing allocation that would push it past the cap.
67unsafe extern "C" fn limited_alloc(
68 ud: *mut core::ffi::c_void,
69 ptr: *mut core::ffi::c_void,
70 osize: usize,
71 nsize: usize,
72) -> *mut core::ffi::c_void {
73 let ctrl = unsafe { &*(ud as *const MemoryControl) };
74 if ctrl.limit != 0 && nsize > osize {
75 let g = ctrl.global as *const luaur_vm::records::global_state::global_State;
76 let used = unsafe { (*g).totalbytes };
77 // The would-be new total once this (re)allocation is accounted for.
78 let projected = used.saturating_sub(osize).saturating_add(nsize);
79 if projected > ctrl.limit {
80 return core::ptr::null_mut();
81 }
82 }
83 unsafe { (ctrl.base)(ctrl.base_ud, ptr, osize, nsize) }
84}
85
86impl Lua {
87 /// Set the VM's memory limit in bytes (`0` = unlimited). Mirrors
88 /// `mlua::Lua::set_memory_limit`.
89 ///
90 /// Once installed, an allocation that would exceed the cap fails with
91 /// [`Error::MemoryError`](crate::Error::MemoryError), both during execution
92 /// and during chunk loading.
93 pub fn set_memory_limit(&self, limit: usize) -> Result<usize> {
94 let state = self.state();
95 unsafe {
96 let key = global_key(state);
97 let g = (*state).global;
98 let prev = MEMORY_CONTROLS.with(|m| {
99 let mut map = m.borrow_mut();
100 if let Some(ctrl) = map.get_mut(&key) {
101 // Already installed: just update the cap.
102 let prev = ctrl.limit;
103 ctrl.limit = limit;
104 Some(prev)
105 } else {
106 None
107 }
108 });
109 if let Some(prev) = prev {
110 return Ok(prev);
111 }
112 // First install: capture the existing allocator and wrap it.
113 let base = (*g).frealloc.expect("VM allocator must be set");
114 let base_ud = (*g).ud;
115 let ctrl = Box::new(MemoryControl {
116 limit,
117 global: g as *mut core::ffi::c_void,
118 base,
119 base_ud,
120 });
121 let ctrl_ptr = (&*ctrl) as *const MemoryControl as *mut core::ffi::c_void;
122 MEMORY_CONTROLS.with(|m| {
123 m.borrow_mut().insert(key, ctrl);
124 });
125 (*g).ud = ctrl_ptr;
126 (*g).frealloc = Some(limited_alloc);
127 Ok(0)
128 }
129 }
130
131 /// Set the active memory category by name. Mirrors
132 /// `mlua::Lua::set_memory_category`.
133 ///
134 /// Category names must be non-empty and consist only of `[A-Za-z0-9_]`.
135 /// At most 255 distinct user categories can be created (id 0 is reserved
136 /// for the implicit `"main"` category); creating a 256th fails.
137 pub fn set_memory_category(&self, name: &str) -> Result<()> {
138 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
139 return Err(Error::runtime(format!(
140 "invalid memory category name: {name:?}"
141 )));
142 }
143 let state = self.state();
144 let key = unsafe { global_key(state) };
145 let id = MEMORY_CATEGORIES.with(|m| -> Result<u8> {
146 let mut map = m.borrow_mut();
147 let cats = map.entry(key).or_insert_with(|| {
148 let mut h = HashMap::new();
149 // id 0 is the implicit "main" category.
150 h.insert("main".to_string(), 0u8);
151 h
152 });
153 if let Some(&id) = cats.get(name) {
154 return Ok(id);
155 }
156 // Assign the next free id. Luau has 256 category slots (ids
157 // 0..=255); id 0 is the implicit "main" and the top slot (255) is
158 // reserved, so at most 255 distinct categories exist (ids 0..=254 —
159 // "main" plus 254 user categories). Creating a 256th fails.
160 let next = cats.len();
161 if next >= 255 {
162 return Err(Error::runtime(
163 "too many memory categories (limit 255)".to_string(),
164 ));
165 }
166 let id = next as u8;
167 cats.insert(name.to_string(), id);
168 Ok(id)
169 })?;
170 lua_setmemcat(state, id as c_int);
171 Ok(())
172 }
173
174 /// The number of bytes accounted to the named memory category, or `None` if
175 /// the category was never set on this VM. A luaur-rt extension (mlua tracks
176 /// this only via `heap_dump`, which luaur cannot back — see the module).
177 pub fn memory_category_bytes(&self, name: &str) -> Option<usize> {
178 let state = self.state();
179 let key = unsafe { global_key(state) };
180 let id =
181 MEMORY_CATEGORIES.with(|m| m.borrow().get(&key).and_then(|c| c.get(name).copied()))?;
182 unsafe {
183 let g = (*state).global;
184 Some((*g).memcatbytes[id as usize])
185 }
186 }
187}