SteonMod
View as Markdown

0002: One Luau VM per addon, with a time limit instead of an instruction count

  • Date: 2026-10-02
  • Status: accepted (step 0.11)

Context

BUILD.md §3.9 asks for a Luau sandbox with a per-script memory limit and an interrupt callback that stops runaway loops, with addons loaded from a folder and hot-reloaded.

What we found in mlua 0.12.1 (feature luau):

  • Lua::set_memory_limit limits a whole VM, not one script inside it.
  • Luau has no instruction-count hook (no debug.sethook). It has an interrupt callback, which Luau calls at loop back-edges and function calls. Returning an error from it raises a Lua error at that point.
  • A Lua error can be caught by pcall, so a script could catch "out of time" and keep looping.
  • mlua gives Luau a default require that reads files from disk.
  • Lua::sandbox(true) makes the libraries and globals read-only; the script's own globals go into a private table on top.

Decision

  • Each addon (a folder with init.lua) runs in its own Luau VM, with its own memory limit (64 MB) and its own guard.
  • Time limit, not instruction count: before every call into an addon, Fusion sets a deadline (250 ms for loading init.lua, 50 ms for one event across all of that addon's hooks). The interrupt callback reads the clock every 64th call. Once the deadline passes, the guard is tripped: every later interrupt and every API call in that VM fails, so pcall can't rescue the loop.
  • An addon that runs out of time or memory is stopped: its VM is dropped (hooks and memory go with it), the player sees a plain message, and the other addons keep running. Saving the addon's files loads it again.
  • require, loadstring, getfenv and setfenv are removed before sandboxing. Multi-file addons will get a require that only sees the addon's own folder.
  • Lua never touches Godot directly: API calls queue commands (e.g. spawn a prop), which the FusionLua node turns into signals each frame. fusion-lua has no Godot types and is tested on its own.

Consequences

  • Fault isolation like a process boundary, inside one process: one broken addon can't freeze Fusion or break other addons. A test shows an endless loop wrapped in pcall is stopped within the 50 ms budget. In the playground the hitch is one frame of about 90 ms, then everything carries on.
  • Hot reload is simply dropping the VM and making a new one; nothing leaks between versions.
  • Addons don't share globals (unlike Garry's Mod, where all addons share one state). Addons that need to talk to each other will do it through Fusion: events, and later shared values. That's in line with "Lua talks to blocks only through ports, actions and events".
  • Each VM costs a little memory (a fresh Luau state is small, tens of KB). If Devices (step 4.4) create many small scripts, they can share one VM per scenario.
  • Long-running C functions (e.g. a huge string.rep) aren't interrupted mid-call; the memory limit catches the big ones. Good enough for v0.