Files
MosisService/SANDBOX_MILESTONE_1.md

15 KiB

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

-- 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:

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

-- This script tests that bytecode loading is blocked
-- The actual bytecode test is done from C++ side
print("PASS: Text loading works")

C++ Test:

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

-- 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:

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

-- Infinite loop - should be stopped by instruction limit
while true do
    -- busy loop
end
error("FAIL: Should never reach here")

C++ Test:

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

-- 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:

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

-- 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

-- 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

cd sandbox-test
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
cmake --build build --config Debug

Run All Tests

./build/Debug/sandbox-test.exe

Run Single Test

./build/Debug/sandbox-test.exe --test DangerousGlobalsRemoved

Test Output Format

{
  "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:

  • TEST(LuaSandbox, DangerousGlobalsRemoved) - os/io/debug/etc are nil
  • TEST(LuaSandbox, BytecodeRejected) - Binary chunks rejected
  • TEST(LuaSandbox, MemoryLimitEnforced) - OOM before system impact
  • TEST(LuaSandbox, CPULimitEnforced) - Infinite loops stopped
  • TEST(LuaSandbox, MetatableProtected) - _G frozen, string mt protected
  • TEST(LuaSandbox, SafeOperationsWork) - Normal Lua code works
  • TEST(LuaSandbox, StringDumpRemoved) - string.dump is nil

Security Audit Checklist

After implementation, verify:

  • No way to access os.execute or equivalent
  • No way to load bytecode
  • No way to read arbitrary files
  • Memory exhaustion causes graceful failure
  • CPU exhaustion causes graceful failure
  • Cannot escape sandbox via metatable tricks
  • 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)