# Milestone 1: Core Sandbox Foundation **Status**: ✅ Complete **Goal**: Create isolated Lua environments with resource limits and dangerous global removal. --- ## Overview This milestone establishes the foundational security layer for the Mosis app sandbox. Every subsequent milestone builds on this foundation. ### Key Deliverables 1. **LuaSandbox class** - Per-app isolated Lua state 2. **Custom allocator** - Memory tracking and limits 3. **Instruction hook** - CPU limit enforcement 4. **Globals removal** - Remove dangerous functions 5. **Bytecode prevention** - Text-only loading 6. **Metatable protection** - Freeze globals --- ## File Structure ``` src/main/cpp/sandbox/ ├── lua_sandbox.h # Main header ├── lua_sandbox.cpp # Implementation └── sandbox_tests.h # Test declarations (for test runner) sandbox-test/ ├── CMakeLists.txt # Test build config ├── README.md # Test documentation ├── src/ │ ├── main.cpp # Test runner entry │ ├── test_harness.h # Test framework │ └── test_harness.cpp # Test framework impl └── scripts/ ├── test_globals_removed.lua ├── test_bytecode_rejected.lua ├── test_memory_limit.lua ├── test_cpu_limit.lua ├── test_metatable_protected.lua ├── test_safe_operations.lua └── test_string_dump_removed.lua ``` --- ## Implementation Details ### 1. SandboxLimits Structure ```cpp // lua_sandbox.h struct SandboxLimits { size_t memory_bytes = 16 * 1024 * 1024; // 16 MB default size_t max_string_size = 1 * 1024 * 1024; // 1 MB max string size_t max_table_entries = 100000; // Prevent hash DoS uint64_t instructions_per_call = 1000000; // ~10ms execution int stack_depth = 200; // Recursion limit }; ``` ### 2. SandboxContext Structure ```cpp // lua_sandbox.h struct SandboxContext { std::string app_id; std::string app_path; std::vector permissions; bool is_system_app = false; }; ``` ### 3. LuaSandbox Class ```cpp // lua_sandbox.h class LuaSandbox { public: explicit LuaSandbox(const SandboxContext& context, const SandboxLimits& limits = {}); ~LuaSandbox(); // Non-copyable LuaSandbox(const LuaSandbox&) = delete; LuaSandbox& operator=(const LuaSandbox&) = delete; // Load and execute Lua code bool LoadString(const std::string& code, const std::string& chunk_name = "chunk"); bool LoadFile(const std::string& path); // State access lua_State* GetState() const { return m_L; } const std::string& GetLastError() const { return m_last_error; } // Resource usage size_t GetMemoryUsed() const { return m_memory_used; } uint64_t GetInstructionsUsed() const { return m_instructions_used; } // Context access const SandboxContext& GetContext() const { return m_context; } const std::string& app_id() const { return m_context.app_id; } // Reset instruction counter (call before each event handler) void ResetInstructionCount(); private: void SetupSandbox(); void RemoveDangerousGlobals(); void ProtectBuiltinTables(); void SetupInstructionHook(); static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize); static void InstructionHook(lua_State* L, lua_Debug* ar); lua_State* m_L = nullptr; SandboxContext m_context; SandboxLimits m_limits; size_t m_memory_used = 0; uint64_t m_instructions_used = 0; std::string m_last_error; }; ``` ### 4. Custom Memory Allocator ```cpp // lua_sandbox.cpp void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { auto* sandbox = static_cast(ud); // Calculate new usage 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) { return nullptr; // Allocation fails, triggers Lua memory error } // Update tracking sandbox->m_memory_used = new_usage; // Free if (nsize == 0) { free(ptr); return nullptr; } // Alloc or realloc return realloc(ptr, nsize); } ``` ### 5. Instruction Hook ```cpp // lua_sandbox.cpp void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) { // Get sandbox from registry lua_getfield(L, LUA_REGISTRYINDEX, "__sandbox"); auto* sandbox = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); if (!sandbox) return; // Increment by hook interval (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 (%llu instructions)", sandbox->m_instructions_used); } } void LuaSandbox::SetupInstructionHook() { lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000); } ``` ### 6. Dangerous Globals Removal ```cpp // lua_sandbox.cpp void LuaSandbox::RemoveDangerousGlobals() { const char* dangerous_globals[] = { // Code execution "dofile", "loadfile", "load", "loadstring", // Raw access (bypass metatables) "rawget", "rawset", "rawequal", "rawlen", // Metatable manipulation (removed for protection) "getmetatable", "setmetatable", // GC manipulation "collectgarbage", // Dangerous libraries "os", "io", "debug", "package", // LuaJIT/FFI (if present) "ffi", "jit", "newproxy", nullptr }; for (const char** p = dangerous_globals; *p; ++p) { lua_pushnil(m_L); lua_setglobal(m_L, *p); } // Remove require (we'll add safe version later) lua_pushnil(m_L); lua_setglobal(m_L, "require"); // Remove string.dump (creates bytecode) 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); } ``` ### 7. Metatable Protection ```cpp // lua_sandbox.cpp void LuaSandbox::ProtectBuiltinTables() { // Protect string 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 with protective metatable lua_pushglobaltable(m_L); lua_newtable(m_L); // metatable for _G // __metatable prevents access via getmetatable lua_pushstring(m_L, "globals"); lua_setfield(m_L, -2, "__metatable"); // __newindex prevents modification 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"); lua_setmetatable(m_L, -2); lua_pop(m_L, 1); // pop _G } ``` ### 8. Load with Bytecode Prevention ```cpp // lua_sandbox.cpp bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) { // Reset instruction count for this execution ResetInstructionCount(); // Load as TEXT ONLY - "t" mode rejects bytecode 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 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; } return true; } ``` --- ## Test Cases Each test has a corresponding Lua script and C++ test function. ### Test 1: Dangerous Globals Removed **Script**: `scripts/test_globals_removed.lua` ```lua -- Test that dangerous globals are nil local dangerous = { "os", "io", "debug", "package", "ffi", "jit", "dofile", "loadfile", "load", "loadstring", "rawget", "rawset", "rawequal", "rawlen", "collectgarbage", "newproxy", "require" } for _, name in ipairs(dangerous) do local value = _G[name] if value ~= nil then error("FAIL: " .. name .. " should be nil but is " .. type(value)) end end print("PASS: All dangerous globals removed") ``` **C++ Test**: ```cpp TEST(LuaSandbox, DangerousGlobalsRemoved) { LuaSandbox sandbox(TestContext()); EXPECT_TRUE(sandbox.LoadFile("scripts/test_globals_removed.lua")); } ``` ### Test 2: Bytecode Rejected **Script**: `scripts/test_bytecode_rejected.lua` ```lua -- This script tests that bytecode loading is blocked -- The actual bytecode test is done from C++ side print("PASS: Text loading works") ``` **C++ Test**: ```cpp TEST(LuaSandbox, BytecodeRejected) { LuaSandbox sandbox(TestContext()); // Lua bytecode starts with signature: \x1bLua std::string bytecode = "\x1bLua\x54\x00\x19\x93"; // Fake bytecode header EXPECT_FALSE(sandbox.LoadString(bytecode, "bytecode_test")); EXPECT_TRUE(sandbox.GetLastError().find("binary") != std::string::npos || sandbox.GetLastError().find("attempt to load") != std::string::npos); } ``` ### Test 3: Memory Limit Enforced **Script**: `scripts/test_memory_limit.lua` ```lua -- This script intentionally tries to exhaust memory -- Should fail with memory error local t = {} local i = 0 while true do i = i + 1 t[i] = string.rep("x", 100000) -- 100KB strings if i > 1000 then error("FAIL: Should have hit memory limit by now") end end ``` **C++ Test**: ```cpp TEST(LuaSandbox, MemoryLimitEnforced) { SandboxLimits limits; limits.memory_bytes = 1024 * 1024; // 1 MB limit LuaSandbox sandbox(TestContext(), limits); EXPECT_FALSE(sandbox.LoadFile("scripts/test_memory_limit.lua")); // Should fail due to memory allocation failure } ``` ### Test 4: CPU Limit Enforced **Script**: `scripts/test_cpu_limit.lua` ```lua -- Infinite loop - should be stopped by instruction limit while true do -- busy loop end error("FAIL: Should never reach here") ``` **C++ Test**: ```cpp TEST(LuaSandbox, CPULimitEnforced) { SandboxLimits limits; limits.instructions_per_call = 10000; // Very low limit LuaSandbox sandbox(TestContext(), limits); EXPECT_FALSE(sandbox.LoadFile("scripts/test_cpu_limit.lua")); EXPECT_TRUE(sandbox.GetLastError().find("instruction") != std::string::npos); } ``` ### Test 5: Metatable Protected **Script**: `scripts/test_metatable_protected.lua` ```lua -- Test 1: String metatable is protected local mt = getmetatable("") if mt ~= "string" then error("FAIL: string metatable should return 'string', got " .. tostring(mt)) end -- Test 2: Cannot modify global environment local ok, err = pcall(function() _G.my_new_global = "test" end) if ok then error("FAIL: Should not be able to add globals") end -- Test 3: Cannot modify existing globals local ok2, err2 = pcall(function() _G.print = nil end) if ok2 then error("FAIL: Should not be able to modify print") end print("PASS: Metatables protected") ``` **C++ Test**: ```cpp TEST(LuaSandbox, MetatableProtected) { LuaSandbox sandbox(TestContext()); EXPECT_TRUE(sandbox.LoadFile("scripts/test_metatable_protected.lua")); } ``` ### Test 6: Safe Operations Work **Script**: `scripts/test_safe_operations.lua` ```lua -- Test that safe operations still work -- Math operations local x = math.sin(1.5) + math.floor(3.7) assert(type(x) == "number", "Math failed") -- String operations local s = string.format("hello %d", 42) assert(s == "hello 42", "String format failed") local upper = string.upper("test") assert(upper == "TEST", "String upper failed") -- Table operations local t = {1, 2, 3} table.insert(t, 4) assert(#t == 4, "Table insert failed") table.sort(t) assert(t[1] == 1 and t[4] == 4, "Table sort failed") -- Iteration local count = 0 for i, v in ipairs(t) do count = count + 1 end assert(count == 4, "ipairs failed") count = 0 for k, v in pairs({a=1, b=2, c=3}) do count = count + 1 end assert(count == 3, "pairs failed") -- Error handling local ok, err = pcall(function() error("test error") end) assert(not ok, "pcall should catch error") assert(err:find("test error"), "Error message wrong") -- Type checks assert(type({}) == "table", "type table failed") assert(type("") == "string", "type string failed") assert(type(123) == "number", "type number failed") assert(type(function() end) == "function", "type function failed") -- tonumber/tostring assert(tonumber("42") == 42, "tonumber failed") assert(tostring(42) == "42", "tostring failed") -- select local a, b = select(2, 1, 2, 3) assert(a == 2 and b == 3, "select failed") -- assert works assert(true, "assert failed") print("PASS: All safe operations work") ``` ### Test 7: string.dump Removed **Script**: `scripts/test_string_dump_removed.lua` ```lua -- string.dump can create bytecode, should be removed if string.dump ~= nil then error("FAIL: string.dump should be nil") end print("PASS: string.dump removed") ``` --- ## Build & Test Commands ### Build ```bash cd sandbox-test cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake cmake --build build --config Debug ``` ### Run All Tests ```bash ./build/Debug/sandbox-test.exe ``` ### Run Single Test ```bash ./build/Debug/sandbox-test.exe --test DangerousGlobalsRemoved ``` ### Test Output Format ```json { "name": "Lua Sandbox Security Tests", "timestamp": "2024-01-15T10:30:00Z", "summary": { "passed": 7, "failed": 0, "total": 7 }, "tests": [ { "name": "DangerousGlobalsRemoved", "status": "passed", "duration_ms": 5 }, ... ] } ``` --- ## Acceptance Criteria All tests must pass: - [x] `TEST(LuaSandbox, DangerousGlobalsRemoved)` - os/io/debug/etc are nil - [x] `TEST(LuaSandbox, BytecodeRejected)` - Binary chunks rejected - [x] `TEST(LuaSandbox, MemoryLimitEnforced)` - OOM before system impact - [x] `TEST(LuaSandbox, CPULimitEnforced)` - Infinite loops stopped - [x] `TEST(LuaSandbox, MetatableProtected)` - _G frozen, string mt protected - [x] `TEST(LuaSandbox, SafeOperationsWork)` - Normal Lua code works - [x] `TEST(LuaSandbox, StringDumpRemoved)` - string.dump is nil --- ## Security Audit Checklist After implementation, verify: - [x] No way to access `os.execute` or equivalent - [x] No way to load bytecode - [x] No way to read arbitrary files - [x] Memory exhaustion causes graceful failure - [x] CPU exhaustion causes graceful failure - [x] Cannot escape sandbox via metatable tricks - [x] Cannot modify protected globals --- ## Next Steps After Milestone 1 passes: 1. Integrate with kernel (replace global lua_State) 2. Add permission system (Milestone 2) 3. Add audit logging (Milestone 3)