Files
MosisService/docs/SANDBOX_MILESTONE_1.md

596 lines
15 KiB
Markdown

# 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<std::string> 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<LuaSandbox*>(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<LuaSandbox*>(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)