Skip to main content

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}