Skip to content
Sandboxing and Environments
step 1/4

Reading — step 1 of 4

Learn

~1 min readSandboxing and Embedding

Lua's small surface makes it easy to embed AND sandbox. Both Roblox and Redis run untrusted Lua in restricted environments.

load(chunk, name, mode, env) — compile Lua code with a custom environment:

local sandbox_env = {
    print = print,
    string = string,
    math = math,
    -- omit io, os, debug, package — restrict access
}

local code = [[
    print("hello from sandbox")
    print(string.upper("hi"))
]]

local chunk, err = load(code, "sandbox", "t", sandbox_env)
if chunk then
    chunk()
else
    print("error: " .. err)
end
  • The chunk only sees what's in sandbox_env. No io.open, no os.execute.
  • mode = "t" allows text but not bytecode (bytecode could exploit VM bugs).
  • Errors from compilation come back as a string.

Tighter sandbox with intercepting metatables:

local env = setmetatable({}, {
    __index = function(t, key)
        error("access denied: " .. key)
    end
})

-- Now any global lookup throws
local chunk = load(code, "sandbox", "t", env)

Resource limits — Lua doesn't have built-in CPU/memory limits, so:

  • Set debug.sethook to interrupt long-running scripts:
local limit = 100000
local count = 0
debug.sethook(function()
    count = count + 1
    if count > limit then error("timeout") end
end, "", 1)

The "", 1 means "call the hook every 1 instruction." Slow but reliable.

pcall / xpcall for error containment:

local ok, err = pcall(function()
    chunk()
end)
if not ok then
    print("sandbox error: " .. tostring(err))
end

Real production:

  • Roblox: custom Luau (typed Lua) with extensive sandboxing
  • Redis: Lua scripts with KEYS/ARGV restrictions, no globals, deterministic-only ops
  • Neovim: full Lua, trusted (configs run with full power)

The principle: enumerate what's ALLOWED, not what's blocked. Default-deny.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…