#include "lua_sandbox.h" #include #include #include #include #include #include #include namespace mosis { //============================================================================= // ALLOCATOR //============================================================================= void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { auto* sandbox = static_cast(ud); // Calculate new usage // osize is the old size (0 for new allocations) // nsize is the new size (0 for frees) size_t new_usage = sandbox->m_memory_used - osize + nsize; // Check limit (only when allocating, not freeing) if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) { // Allocation would exceed limit - return nullptr to signal failure // Lua will raise a memory error return nullptr; } // Update tracking sandbox->m_memory_used = new_usage; // Free operation if (nsize == 0) { free(ptr); return nullptr; } // Alloc or realloc return realloc(ptr, nsize); } //============================================================================= // INSTRUCTION HOOK //============================================================================= void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) { (void)ar; // Unused // Get sandbox pointer from registry lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox"); auto* sandbox = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); if (!sandbox) return; // Increment by hook interval (called every 1000 instructions) sandbox->m_instructions_used += 1000; // Check limit if (sandbox->m_instructions_used > sandbox->m_limits.instructions_per_call) { luaL_error(L, "instruction limit exceeded"); } } //============================================================================= // SAFE PRINT //============================================================================= int LuaSandbox::SafePrint(lua_State* L) { int n = lua_gettop(L); // number of arguments lua_getglobal(L, "tostring"); for (int i = 1; i <= n; i++) { if (i > 1) std::cout << "\t"; lua_pushvalue(L, -1); // push tostring lua_pushvalue(L, i); // push argument lua_call(L, 1, 1); // call tostring const char* s = lua_tostring(L, -1); if (s) { std::cout << s; } lua_pop(L, 1); } std::cout << std::endl; return 0; } //============================================================================= // CONSTRUCTOR / DESTRUCTOR //============================================================================= LuaSandbox::LuaSandbox(const SandboxContext& context, const SandboxLimits& limits) : m_context(context), m_limits(limits) { // Create Lua state with custom allocator m_L = lua_newstate(SandboxAlloc, this); if (!m_L) { m_last_error = "Failed to create Lua state"; return; } // Store sandbox pointer in registry for hooks to access lua_pushlightuserdata(m_L, this); lua_setfield(m_L, LUA_REGISTRYINDEX, "__mosis_sandbox"); // Setup the sandbox SetupSandbox(); } LuaSandbox::~LuaSandbox() { if (m_L) { lua_close(m_L); m_L = nullptr; } } //============================================================================= // SETUP //============================================================================= void LuaSandbox::SetupSandbox() { // Open safe standard libraries luaL_openlibs(m_L); // Remove dangerous globals FIRST RemoveDangerousGlobals(); // Setup safe replacements SetupSafeGlobals(); // Protect metatables ProtectBuiltinTables(); // Setup instruction hook for CPU limiting SetupInstructionHook(); } void LuaSandbox::RemoveDangerousGlobals() { // List of dangerous globals to remove const char* dangerous_globals[] = { // Code execution from files/strings "dofile", "loadfile", "load", "loadstring", // Lua 5.1 compat // Raw access (bypasses metatables) "rawget", "rawset", "rawequal", "rawlen", // Metatable manipulation // Note: We keep getmetatable but protect the actual metatables // setmetatable is removed to prevent modifications "setmetatable", // GC manipulation "collectgarbage", // Dangerous libraries "os", "io", "debug", "package", // LuaJIT / FFI (if present) "ffi", "jit", "newproxy", // Module system (we'll add safe version later) "require", nullptr }; for (const char** p = dangerous_globals; *p; ++p) { lua_pushnil(m_L); lua_setglobal(m_L, *p); } // Remove string.dump (can create bytecode from functions) lua_getglobal(m_L, "string"); if (lua_istable(m_L, -1)) { lua_pushnil(m_L); lua_setfield(m_L, -2, "dump"); } lua_pop(m_L, 1); } void LuaSandbox::SetupSafeGlobals() { // Replace print with safe version lua_pushcfunction(m_L, SafePrint); lua_setglobal(m_L, "print"); // Setup safe require if app_path is set if (!m_context.app_path.empty()) { SetupSafeRequire(); } } //============================================================================= // SAFE REQUIRE //============================================================================= // Registry key for loaded modules cache static const char* LOADED_KEY = "mosis.loaded_modules"; // Validate module name for require() - alphanumeric, underscore, dots only static bool IsValidModuleName(const std::string& name) { if (name.empty()) return false; for (size_t i = 0; i < name.length(); i++) { char c = name[i]; if (std::isalnum(static_cast(c))) continue; if (c == '_') continue; if (c == '.') { if (i == 0 || i == name.length() - 1) return false; if (i > 0 && name[i-1] == '.') return false; continue; } return false; } if (name.find("..") != std::string::npos) return false; return true; } // Convert module name to path (e.g., "ui.button" -> "scripts/ui/button.lua") static std::string ModuleToPath(const std::string& module_name) { std::string path = module_name; std::replace(path.begin(), path.end(), '.', '/'); return "scripts/" + path + ".lua"; } int LuaSandbox::SafeRequire(lua_State* L) { // Get module name const char* module_name = luaL_checkstring(L, 1); // Validate module name if (!IsValidModuleName(module_name)) { return luaL_error(L, "invalid module name: %s", module_name); } // Check cache first lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY); if (lua_istable(L, -1)) { lua_getfield(L, -1, module_name); if (!lua_isnil(L, -1)) { return 1; // Return cached module } lua_pop(L, 1); } lua_pop(L, 1); // Get sandbox pointer from registry lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox"); auto* sandbox = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); if (!sandbox) { return luaL_error(L, "require not properly initialized"); } // Build full path std::string relative_path = ModuleToPath(module_name); std::string full_path = sandbox->m_context.app_path; if (!full_path.empty() && full_path.back() != '/' && full_path.back() != '\\') { full_path += '/'; } full_path += relative_path; // Read the file std::ifstream file(full_path); if (!file.is_open()) { return luaL_error(L, "module '%s' not found at '%s'", module_name, full_path.c_str()); } std::stringstream buffer; buffer << file.rdbuf(); std::string source = buffer.str(); file.close(); // Load as text only (no bytecode) std::string chunk_name = "@" + std::string(module_name); int status = luaL_loadbufferx(L, source.c_str(), source.size(), chunk_name.c_str(), "t"); if (status != LUA_OK) { return lua_error(L); } // Execute the chunk lua_call(L, 0, 1); // If chunk returned nil, use true as the module value if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushboolean(L, 1); } // Cache the result lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY); if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY); } lua_pushvalue(L, -2); lua_setfield(L, -2, module_name); lua_pop(L, 1); return 1; } void LuaSandbox::SetupSafeRequire() { // Create loaded modules cache lua_newtable(m_L); lua_setfield(m_L, LUA_REGISTRYINDEX, LOADED_KEY); // Register require function lua_pushcfunction(m_L, SafeRequire); lua_setglobal(m_L, "require"); } void LuaSandbox::ProtectBuiltinTables() { // Protect string metatable // When someone calls getmetatable(""), they get "string" instead of the real metatable lua_pushstring(m_L, ""); if (lua_getmetatable(m_L, -1)) { lua_pushstring(m_L, "string"); lua_setfield(m_L, -2, "__metatable"); lua_pop(m_L, 1); // pop metatable } lua_pop(m_L, 1); // pop string // Freeze _G using a proxy pattern // This is needed because __newindex only fires for NEW keys, not existing ones // We create: empty_proxy -> metatable { __index = real_G, __newindex = error } // Get the current _G (with all our safe functions) lua_pushglobaltable(m_L); // stack: real_G // Create a new empty table to be the proxy lua_newtable(m_L); // stack: real_G, proxy // Create metatable for proxy lua_newtable(m_L); // stack: real_G, proxy, mt // __metatable - prevent access to real metatable lua_pushstring(m_L, "globals"); lua_setfield(m_L, -2, "__metatable"); // __index - read from real_G lua_pushvalue(m_L, -3); // push real_G lua_setfield(m_L, -2, "__index"); // __newindex - block all writes lua_pushcfunction(m_L, [](lua_State* L) -> int { const char* key = lua_tostring(L, 2); return luaL_error(L, "cannot modify global '%s'", key ? key : "(unknown)"); }); lua_setfield(m_L, -2, "__newindex"); // Set metatable on proxy: setmetatable(proxy, mt) lua_setmetatable(m_L, -2); // stack: real_G, proxy // Now we need to replace _G with proxy // In Lua 5.2+, we use lua_rawseti on the registry lua_pushvalue(m_L, -1); // stack: real_G, proxy, proxy lua_rawseti(m_L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); // stack: real_G, proxy // Also update _G variable in real_G to point to proxy // This is critical: when code does _G.foo = bar, it accesses _G variable lua_setfield(m_L, -2, "_G"); // stack: real_G (sets real_G["_G"] = proxy) lua_pop(m_L, 1); // pop real_G } void LuaSandbox::SetupInstructionHook() { // Set hook to fire every 1000 VM instructions lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000); } //============================================================================= // LOAD AND EXECUTE //============================================================================= bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) { if (!m_L) { m_last_error = "Lua state not initialized"; return false; } // Reset instruction count for this execution ResetInstructionCount(); // Load as TEXT ONLY - "t" mode rejects bytecode (starts with \x1bLua) int result = luaL_loadbufferx(m_L, code.c_str(), code.size(), chunk_name.c_str(), "t"); if (result != LUA_OK) { m_last_error = lua_tostring(m_L, -1); lua_pop(m_L, 1); return false; } // Execute the loaded chunk result = lua_pcall(m_L, 0, 0, 0); if (result != LUA_OK) { m_last_error = lua_tostring(m_L, -1); lua_pop(m_L, 1); return false; } m_last_error.clear(); return true; } bool LuaSandbox::LoadFile(const std::string& path) { // Read file contents std::ifstream f(path); if (!f) { m_last_error = "Cannot open file: " + path; return false; } std::stringstream ss; ss << f.rdbuf(); std::string code = ss.str(); // Load as string return LoadString(code, "@" + path); } void LuaSandbox::ResetInstructionCount() { m_instructions_used = 0; } } // namespace mosis