move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
595
docs/SANDBOX_MILESTONE_1.md
Normal file
595
docs/SANDBOX_MILESTONE_1.md
Normal file
@@ -0,0 +1,595 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user