449 lines
12 KiB
C++
449 lines
12 KiB
C++
#include "lua_sandbox.h"
|
|
|
|
#include <lua.hpp>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <iostream>
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
|
|
namespace mosis {
|
|
|
|
//=============================================================================
|
|
// ALLOCATOR
|
|
//=============================================================================
|
|
|
|
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
|
|
auto* sandbox = static_cast<LuaSandbox*>(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<LuaSandbox*>(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<unsigned char>(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<LuaSandbox*>(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
|