From a4ecb0f132750204d88423b8eb36771ddc9c32c1 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sun, 18 Jan 2026 14:28:44 +0100 Subject: [PATCH] add Lua sandbox with timer system (milestones 1-5 complete) --- CLAUDE.md | 23 + SANDBOX.md | 3569 +++++++++++++++++ SANDBOX_MILESTONES.md | 964 +++++ SANDBOX_MILESTONE_1.md | 595 +++ SANDBOX_MILESTONE_2.md | 488 +++ SANDBOX_MILESTONE_3.md | 357 ++ SANDBOX_MILESTONE_4.md | 241 ++ SANDBOX_MILESTONE_5.md | 333 ++ sandbox-test/CMakeLists.txt | 54 + sandbox-test/README.md | 132 + sandbox-test/run_tests.bat | 59 + sandbox-test/run_tests.ps1 | 67 + sandbox-test/scripts/scripts/test_module.lua | 11 + .../scripts/test_bytecode_rejected.lua | 5 + sandbox-test/scripts/test_cpu_limit.lua | 12 + sandbox-test/scripts/test_globals_removed.lua | 26 + sandbox-test/scripts/test_memory_limit.lua | 20 + .../scripts/test_metatable_protected.lua | 33 + sandbox-test/scripts/test_safe_operations.lua | 158 + .../scripts/test_string_dump_removed.lua | 18 + sandbox-test/src/main.cpp | 984 +++++ sandbox-test/src/test_harness.cpp | 128 + sandbox-test/src/test_harness.h | 85 + sandbox-test/test_results.json | 221 + src/main/cpp/sandbox/audit_log.cpp | 188 + src/main/cpp/sandbox/audit_log.h | 94 + src/main/cpp/sandbox/lua_sandbox.cpp | 448 +++ src/main/cpp/sandbox/lua_sandbox.h | 101 + src/main/cpp/sandbox/path_sandbox.cpp | 344 ++ src/main/cpp/sandbox/path_sandbox.h | 52 + src/main/cpp/sandbox/permission_gate.cpp | 197 + src/main/cpp/sandbox/permission_gate.h | 73 + src/main/cpp/sandbox/rate_limiter.cpp | 209 + src/main/cpp/sandbox/rate_limiter.h | 68 + src/main/cpp/sandbox/timer_manager.cpp | 440 ++ src/main/cpp/sandbox/timer_manager.h | 87 + 36 files changed, 10884 insertions(+) create mode 100644 SANDBOX.md create mode 100644 SANDBOX_MILESTONES.md create mode 100644 SANDBOX_MILESTONE_1.md create mode 100644 SANDBOX_MILESTONE_2.md create mode 100644 SANDBOX_MILESTONE_3.md create mode 100644 SANDBOX_MILESTONE_4.md create mode 100644 SANDBOX_MILESTONE_5.md create mode 100644 sandbox-test/CMakeLists.txt create mode 100644 sandbox-test/README.md create mode 100644 sandbox-test/run_tests.bat create mode 100644 sandbox-test/run_tests.ps1 create mode 100644 sandbox-test/scripts/scripts/test_module.lua create mode 100644 sandbox-test/scripts/test_bytecode_rejected.lua create mode 100644 sandbox-test/scripts/test_cpu_limit.lua create mode 100644 sandbox-test/scripts/test_globals_removed.lua create mode 100644 sandbox-test/scripts/test_memory_limit.lua create mode 100644 sandbox-test/scripts/test_metatable_protected.lua create mode 100644 sandbox-test/scripts/test_safe_operations.lua create mode 100644 sandbox-test/scripts/test_string_dump_removed.lua create mode 100644 sandbox-test/src/main.cpp create mode 100644 sandbox-test/src/test_harness.cpp create mode 100644 sandbox-test/src/test_harness.h create mode 100644 sandbox-test/test_results.json create mode 100644 src/main/cpp/sandbox/audit_log.cpp create mode 100644 src/main/cpp/sandbox/audit_log.h create mode 100644 src/main/cpp/sandbox/lua_sandbox.cpp create mode 100644 src/main/cpp/sandbox/lua_sandbox.h create mode 100644 src/main/cpp/sandbox/path_sandbox.cpp create mode 100644 src/main/cpp/sandbox/path_sandbox.h create mode 100644 src/main/cpp/sandbox/permission_gate.cpp create mode 100644 src/main/cpp/sandbox/permission_gate.h create mode 100644 src/main/cpp/sandbox/rate_limiter.cpp create mode 100644 src/main/cpp/sandbox/rate_limiter.h create mode 100644 src/main/cpp/sandbox/timer_manager.cpp create mode 100644 src/main/cpp/sandbox/timer_manager.h diff --git a/CLAUDE.md b/CLAUDE.md index 060d6a4..028ae87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,8 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides | Android Service | `src/main/` | Native service running RmlUi renderer | | Desktop Designer | `designer/` | UI development with hot-reload | | Designer Tests | `designer-test/` | Automated UI testing framework | +| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests | +| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation | | UI Assets | `src/main/assets/` | Shared RML/RCSS/Lua assets | ## Build Commands @@ -94,6 +96,27 @@ cmake --build build --config Debug ./build/Debug/designer-test.exe ``` +### Sandbox Security Tests (CMake) + +```bash +# Configure (from sandbox-test/ folder) +cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake + +# Build +cmake --build build --config Debug + +# Run all tests (uber command) +./run_tests.bat + +# Or run directly +./build/Debug/sandbox-test.exe + +# Run specific test +./build/Debug/sandbox-test.exe --test DangerousGlobals +./build/Debug/sandbox-test.exe --test Memory +./build/Debug/sandbox-test.exe --test CPU +``` + ## Environment Requirements Required environment variables: diff --git a/SANDBOX.md b/SANDBOX.md new file mode 100644 index 0000000..464ca81 --- /dev/null +++ b/SANDBOX.md @@ -0,0 +1,3569 @@ +# Mosis Lua Sandbox Security + +**Status**: Design Phase +**Goal**: Secure app isolation with defense-in-depth approach. + +--- + +## Overview + +Third-party apps run in isolated Lua environments with restricted access to system resources. Each app gets its own `lua_State` with carefully controlled APIs. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Mosis Kernel │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ LuaSandboxManager │ │ +│ │ - Creates per-app lua_State with custom allocator │ │ +│ │ - Enforces memory/CPU limits │ │ +│ │ - Routes permission-gated API calls │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ App A State │ │ App B State │ │ App C State │ │ +│ │ (isolated) │ │ (isolated) │ │ (isolated) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ │ │ +│ └───────────────────┴───────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Permission Gate │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌──────────────────────────▼──────────────────────────────────┐ │ +│ │ System Services │ │ +│ │ Camera │ Network │ Storage │ Contacts │ Messages │ ... │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Threat Model + +### Threat Categories + +| Category | Threat | Severity | Mitigation | +|----------|--------|----------|------------| +| **Code Execution** | `os.execute()`, `io.popen()` | Critical | Remove globals | +| **File Access** | `io.open()`, `loadfile()` | Critical | Remove globals | +| **Bytecode Injection** | `load()` with binary chunks | Critical | Text-only loading | +| **Memory Exhaustion** | Infinite tables/strings | High | Custom allocator with limits | +| **CPU Exhaustion** | Infinite loops | High | Instruction count hook | +| **Sandbox Escape** | `debug.getregistry()` | Critical | Remove debug library | +| **Global Pollution** | Modifying `_G`, `string` | Medium | Frozen globals | +| **Path Traversal** | `require("../../etc/passwd")` | Critical | Path validation | +| **Data Exfiltration** | Unauthorized network access | High | Permission-gated network | +| **Privilege Escalation** | Access other app's data | High | Per-app storage isolation | +| **Timing Attacks** | High-resolution timers | Low | Limit timer precision | +| **Side Channels** | Memory/CPU usage patterns | Low | Rate limiting | + +### Attacker Capabilities + +We assume a malicious app developer can: +- Write arbitrary Lua code within the sandbox +- Attempt to exploit any exposed API +- Try to escape the sandbox via Lua language features +- Attempt DoS via resource exhaustion +- Try to access other apps' data +- Attempt to exfiltrate user data + +--- + +## Security Layers + +### Layer 1: Dangerous Globals Removal + +**Status**: Designed + +Remove all dangerous functions before any app code runs. + +```cpp +void RemoveDangerousGlobals(lua_State* L) { + const char* dangerous[] = { + // Execution + "dofile", "loadfile", "load", "loadstring", + + // Raw access (bypass metatables) + "rawget", "rawset", "rawequal", "rawlen", + + // Metatable manipulation + "getmetatable", "setmetatable", + + // GC manipulation + "collectgarbage", + + // Must not exist + "os", "io", "debug", "package", "require", + "ffi", "jit", "newproxy", + + nullptr + }; + + for (const char** p = dangerous; *p; ++p) { + lua_pushnil(L); + lua_setglobal(L, *p); + } + + // Remove string.dump (creates bytecode) + lua_getglobal(L, "string"); + lua_pushnil(L); + lua_setfield(L, -2, "dump"); + lua_pop(L, 1); +} +``` + +**Safe globals retained**: +- `print` (redirected to logger) +- `type`, `tonumber`, `tostring` +- `pairs`, `ipairs`, `next` +- `pcall`, `xpcall` +- `assert`, `error` +- `select`, `unpack` +- `string.*` (except `dump`) +- `table.*` +- `math.*` +- `utf8.*` + +### Layer 2: Bytecode Prevention + +**Status**: Designed + +Prevent loading of binary Lua chunks which can bypass sandbox checks. + +```cpp +// Only allow text chunks, reject bytecode +int result = luaL_loadbufferx(L, code, len, name, "t"); +// mode: "t" = text only +``` + +Bytecode starts with `\27Lua` signature - the "t" mode rejects this. + +### Layer 3: Memory Limits + +**Status**: Designed + +Custom allocator tracks and limits memory usage per app. + +```cpp +void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { + auto* sandbox = static_cast(ud); + + size_t new_usage = sandbox->m_memory_used - osize + nsize; + + if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) { + return nullptr; // Allocation fails + } + + sandbox->m_memory_used = new_usage; + + if (nsize == 0) { + free(ptr); + return nullptr; + } + return realloc(ptr, nsize); +} + +lua_State* L = lua_newstate(SandboxAlloc, sandbox); +``` + +**Default limits**: +| Resource | Limit | Rationale | +|----------|-------|-----------| +| Memory | 16 MB | Enough for UI, prevents DoS | +| String size | 1 MB | Prevent single huge allocation | +| Table entries | 100,000 | Prevent hash DoS | + +### Layer 4: CPU Limits + +**Status**: Designed + +Instruction count hook prevents infinite loops and excessive computation. + +```cpp +void InstructionHook(lua_State* L, lua_Debug* ar) { + auto* sandbox = GetSandbox(L); + sandbox->m_instructions += 1000; + + if (sandbox->m_instructions > sandbox->m_limits.instructions) { + luaL_error(L, "instruction limit exceeded"); + } +} + +lua_sethook(L, InstructionHook, LUA_MASKCOUNT, 1000); +``` + +**Default limits**: +| Context | Limit | Approx Time | +|---------|-------|-------------| +| Event handler | 1,000,000 | ~10ms | +| Frame update | 100,000 | ~1ms | +| App initialization | 10,000,000 | ~100ms | + +### Layer 5: Path Sandboxing + +**Status**: Designed + +All file access restricted to app's own directory. + +```cpp +bool ValidatePath(const std::string& app_path, const std::string& requested) { + // Reject obvious traversal attempts + if (requested.find("..") != std::string::npos) return false; + if (requested.find(':') != std::string::npos) return false; // Windows drive + if (requested[0] == '/' || requested[0] == '\\') return false; // Absolute + + // Canonicalize and verify within app directory + std::filesystem::path base = std::filesystem::canonical(app_path); + std::filesystem::path full = std::filesystem::weakly_canonical( + base / requested + ); + + // Must be within app directory + auto [base_end, full_it] = std::mismatch( + base.begin(), base.end(), full.begin() + ); + return base_end == base.end(); +} +``` + +### Layer 6: Safe `require` Replacement + +**Status**: Designed + +Custom module loader only loads from app's scripts directory. + +```cpp +int SafeRequire(lua_State* L) { + const char* module = luaL_checkstring(L, 1); + auto* sandbox = GetSandbox(L); + + // Validate module name + std::string name(module); + if (!IsValidModuleName(name)) { + return luaL_error(L, "invalid module name"); + } + + // Check cache + lua_getfield(L, LUA_REGISTRYINDEX, "__loaded"); + lua_getfield(L, -1, module); + if (!lua_isnil(L, -1)) return 1; + lua_pop(L, 2); + + // Load from app/scripts/name.lua + std::string path = sandbox->app_path + "/scripts/" + name + ".lua"; + if (!ValidatePath(sandbox->app_path, "scripts/" + name + ".lua")) { + return luaL_error(L, "path validation failed"); + } + + // Load as text only + if (luaL_loadfilex(L, path.c_str(), "t") != LUA_OK) { + return lua_error(L); + } + + // Execute and cache + lua_call(L, 0, 1); + lua_getfield(L, LUA_REGISTRYINDEX, "__loaded"); + lua_pushvalue(L, -2); + lua_setfield(L, -2, module); + lua_pop(L, 1); + + return 1; +} +``` + +### Layer 7: Metatable Protection + +**Status**: Designed + +Prevent modification of built-in metatables and sandbox escape via metatables. + +```cpp +void ProtectBuiltinTables(lua_State* L) { + // Protect string metatable + lua_pushstring(L, ""); + lua_getmetatable(L, -1); + lua_pushstring(L, "string"); + lua_setfield(L, -2, "__metatable"); // Prevents getmetatable + lua_pop(L, 2); + + // Freeze _G + lua_pushglobaltable(L); + lua_newtable(L); + lua_pushstring(L, "globals"); + lua_setfield(L, -2, "__metatable"); + lua_pushcfunction(L, [](lua_State* L) -> int { + return luaL_error(L, "cannot modify global environment"); + }); + lua_setfield(L, -2, "__newindex"); + lua_setmetatable(L, -2); + lua_pop(L, 1); +} +``` + +**Safe metatable access**: +```cpp +// getmetatable returns __metatable field (not real metatable) +// setmetatable blocked on protected tables +``` + +### Layer 8: Permission-Gated APIs + +**Status**: Designed + +Sensitive APIs check permissions before execution. + +```cpp +class PermissionGate { +public: + static bool Check(lua_State* L, const std::string& permission) { + auto* sandbox = GetSandbox(L); + auto& perms = sandbox->m_context.permissions; + return std::find(perms.begin(), perms.end(), permission) != perms.end(); + } + + static int RequirePermission(lua_State* L, const char* perm) { + if (!Check(L, perm)) { + return luaL_error(L, "permission denied: %s", perm); + } + return 0; + } +}; + +// Usage in API +int CameraCapture(lua_State* L) { + PermissionGate::RequirePermission(L, "camera"); + // ... actual implementation +} +``` + +**Permission categories**: + +| Permission | Required For | Risk Level | +|------------|--------------|------------| +| `storage` | App-private file access | Normal (auto-granted) | +| `camera` | Camera access | Dangerous | +| `microphone` | Audio recording | Dangerous | +| `location` | GPS/location | Dangerous | +| `contacts.read` | Read contacts | Dangerous | +| `contacts.write` | Modify contacts | Dangerous | +| `network` | HTTP requests | Dangerous | +| `network.websocket` | WebSocket connections | Dangerous | +| `bluetooth` | Bluetooth access | Dangerous | +| `system.notifications` | Show notifications | Normal | + +--- + +## Additional Security Measures + +### Network Request Sandboxing + +**Status**: Planned + +Network requests are proxied through the kernel with domain filtering. + +```cpp +struct NetworkPolicy { + std::vector allowed_domains; // Whitelist + std::vector blocked_domains; // Blacklist + bool allow_localhost = false; + bool allow_private_ips = false; + size_t max_request_size = 10 * 1024 * 1024; // 10 MB + size_t max_response_size = 50 * 1024 * 1024; // 50 MB + int timeout_ms = 30000; +}; + +class NetworkSandbox { +public: + bool ValidateRequest(const HttpRequest& req) { + // Parse URL + auto url = ParseUrl(req.url); + + // Block private IPs + if (!m_policy.allow_private_ips && IsPrivateIP(url.host)) { + return false; + } + + // Block localhost + if (!m_policy.allow_localhost && IsLocalhost(url.host)) { + return false; + } + + // Check domain whitelist/blacklist + if (!m_policy.allowed_domains.empty()) { + if (!MatchesDomain(url.host, m_policy.allowed_domains)) { + return false; + } + } + if (MatchesDomain(url.host, m_policy.blocked_domains)) { + return false; + } + + // Size limits + if (req.body.size() > m_policy.max_request_size) { + return false; + } + + return true; + } + +private: + bool IsPrivateIP(const std::string& host) { + // 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x + // Also check for IPv6 private ranges + } +}; +``` + +**Manifest declaration**: +```json +{ + "permissions": ["network"], + "network": { + "allowed_domains": [ + "api.example.com", + "*.cdn.example.com" + ] + } +} +``` + +### Timer and Callback Security + +**Status**: Planned + +Timers and callbacks are managed by the kernel, not exposed directly. + +```cpp +class TimerManager { +public: + uint32_t SetTimeout(LuaSandbox* sandbox, int callback_ref, int ms) { + // Validate delay + if (ms < 0) return 0; + if (ms < 10) ms = 10; // Minimum 10ms granularity + + // Limit active timers per app + if (m_app_timers[sandbox->app_id()].size() >= MAX_TIMERS_PER_APP) { + return 0; + } + + Timer timer{ + .id = ++m_next_id, + .sandbox = sandbox, + .callback_ref = callback_ref, + .fire_time = Now() + ms, + .repeat = false + }; + + m_timers.push(timer); + m_app_timers[sandbox->app_id()].insert(timer.id); + return timer.id; + } + + void ClearAppTimers(const std::string& app_id) { + // Called when app is stopped + for (uint32_t id : m_app_timers[app_id]) { + // Mark timer as cancelled + } + m_app_timers.erase(app_id); + } + +private: + static constexpr size_t MAX_TIMERS_PER_APP = 100; +}; +``` + +**Lua API**: +```lua +-- Safe timer APIs (kernel-managed) +local id = setTimeout(function() + print("fired!") +end, 1000) + +clearTimeout(id) + +local id = setInterval(function() + print("tick") +end, 1000) + +clearInterval(id) +``` + +### Inter-App Communication + +**Status**: Planned + +Apps communicate through kernel-mediated message passing. + +```cpp +struct AppMessage { + std::string from_app; + std::string to_app; + std::string action; + std::string data; // JSON + std::vector required_permissions; +}; + +class MessageBus { +public: + bool Send(const AppMessage& msg) { + // Validate sender + if (!IsAppRunning(msg.from_app)) return false; + + // Check if target accepts this action + if (!CanReceive(msg.to_app, msg.action)) return false; + + // Check permissions + for (const auto& perm : msg.required_permissions) { + if (!HasPermission(msg.from_app, perm)) return false; + } + + // Deliver (async) + QueueMessage(msg); + return true; + } +}; +``` + +**Manifest declaration**: +```json +{ + "intents": { + "receive": [ + { + "action": "share", + "types": ["image/*", "text/plain"] + } + ], + "send": [ + "share", "view" + ] + } +} +``` + +### Audit Logging + +**Status**: Planned + +All security-relevant operations are logged for debugging and forensics. + +```cpp +enum class AuditEvent { + AppStart, + AppStop, + PermissionCheck, + PermissionDenied, + NetworkRequest, + FileAccess, + StorageAccess, + TimerCreated, + MessageSent, + SandboxViolation, + ResourceLimitHit +}; + +struct AuditEntry { + std::chrono::system_clock::time_point timestamp; + std::string app_id; + AuditEvent event; + std::string detail; + bool success; +}; + +class AuditLog { +public: + void Log(const AuditEntry& entry) { + // Thread-safe append + std::lock_guard lock(m_mutex); + m_entries.push_back(entry); + + // Trim old entries + if (m_entries.size() > MAX_ENTRIES) { + m_entries.erase(m_entries.begin(), + m_entries.begin() + TRIM_COUNT); + } + + // Log security events to system log + if (entry.event == AuditEvent::SandboxViolation || + entry.event == AuditEvent::PermissionDenied) { + Logger::Warn("[SECURITY] {} - {} - {}", + entry.app_id, EventName(entry.event), entry.detail); + } + } + + std::vector GetAppLogs(const std::string& app_id, size_t limit); +}; +``` + +### Coroutine Safety + +**Status**: Planned + +Coroutines are allowed but resource-tracked. + +```cpp +void SetupSafeCoroutines(lua_State* L) { + // Allow coroutine.* but track + luaL_requiref(L, "coroutine", luaopen_coroutine, 1); + lua_pop(L, 1); + + // Wrap coroutine.create to track + lua_getglobal(L, "coroutine"); + lua_getfield(L, -1, "create"); + lua_pushcclosure(L, [](lua_State* L) -> int { + auto* sandbox = GetSandbox(L); + + // Limit active coroutines + if (sandbox->m_coroutine_count >= MAX_COROUTINES) { + return luaL_error(L, "too many coroutines"); + } + + // Call original create + lua_pushvalue(L, lua_upvalueindex(1)); + lua_pushvalue(L, 1); + lua_call(L, 1, 1); + + sandbox->m_coroutine_count++; + return 1; + }, 1); + lua_setfield(L, -2, "create"); + lua_pop(L, 1); +} +``` + +### String Pattern DoS Prevention + +**Status**: Planned + +Complex regex patterns can cause catastrophic backtracking. + +```cpp +int SafeStringMatch(lua_State* L) { + size_t slen, plen; + const char* s = luaL_checklstring(L, 1, &slen); + const char* p = luaL_checklstring(L, 2, &plen); + + // Limit pattern complexity + if (plen > 256) { + return luaL_error(L, "pattern too long"); + } + + // Count pattern operators + int complexity = 0; + for (size_t i = 0; i < plen; i++) { + if (p[i] == '*' || p[i] == '+' || p[i] == '-' || p[i] == '?') { + complexity++; + } + } + + // Reject overly complex patterns + if (complexity > 10) { + return luaL_error(L, "pattern too complex"); + } + + // Also limit based on string length * complexity + if (slen * complexity > 1000000) { + return luaL_error(L, "operation too expensive"); + } + + // Call original + return original_match(L); +} +``` + +--- + +## Runtime Security Audit + +### Audit Function + +```cpp +std::vector AuditSandbox(lua_State* L) { + std::vector issues; + + // Check dangerous globals + const char* dangerous[] = { + "os", "io", "debug", "package", "ffi", "jit", + "dofile", "loadfile", "load", "loadstring", + "rawget", "rawset", "rawequal", "rawlen", + "collectgarbage", "newproxy", + nullptr + }; + + for (const char** p = dangerous; *p; ++p) { + lua_getglobal(L, *p); + if (!lua_isnil(L, -1)) { + issues.push_back({ + .severity = Severity::Critical, + .type = "dangerous_global", + .detail = *p + }); + } + lua_pop(L, 1); + } + + // Check string.dump + lua_getglobal(L, "string"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "dump"); + if (!lua_isnil(L, -1)) { + issues.push_back({ + .severity = Severity::Critical, + .type = "bytecode_dump", + .detail = "string.dump exists" + }); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Check instruction hook + if (lua_gethook(L) == nullptr) { + issues.push_back({ + .severity = Severity::High, + .type = "no_cpu_limit", + .detail = "instruction hook not set" + }); + } + + // Check allocator (verify it's our sandbox allocator) + // ... + + return issues; +} +``` + +### Startup Audit + +```cpp +bool LuaSandbox::Initialize() { + SetupSandbox(); + + // Audit before running any app code + auto issues = AuditSandbox(L); + for (const auto& issue : issues) { + if (issue.severity == Severity::Critical) { + Logger::Error("Sandbox audit failed: {} - {}", + issue.type, issue.detail); + return false; + } + } + + return true; +} +``` + +--- + +## Testing Strategy + +### Unit Tests + +```cpp +// test_sandbox_security.cpp + +TEST(LuaSandbox, DangerousGlobalsRemoved) { + LuaSandbox sandbox(TestContext()); + + EXPECT_FALSE(sandbox.LoadString("return os")); + EXPECT_FALSE(sandbox.LoadString("return io")); + EXPECT_FALSE(sandbox.LoadString("return debug")); + EXPECT_FALSE(sandbox.LoadString("os.execute('ls')")); + EXPECT_FALSE(sandbox.LoadString("io.open('/etc/passwd')")); +} + +TEST(LuaSandbox, BytecodeRejected) { + LuaSandbox sandbox(TestContext()); + + // Try to load bytecode + EXPECT_FALSE(sandbox.LoadString("\x1bLuaS\x00...")); + EXPECT_TRUE(sandbox.GetLastError().find("binary") != std::string::npos); +} + +TEST(LuaSandbox, MemoryLimitEnforced) { + SandboxLimits limits; + limits.memory_limit_bytes = 1024 * 1024; // 1 MB + + LuaSandbox sandbox(TestContext(limits)); + + EXPECT_FALSE(sandbox.LoadString(R"( + local t = {} + while true do + t[#t+1] = string.rep('x', 100000) + end + )")); +} + +TEST(LuaSandbox, CPULimitEnforced) { + SandboxLimits limits; + limits.instruction_limit = 10000; + + LuaSandbox sandbox(TestContext(limits)); + + EXPECT_FALSE(sandbox.LoadString("while true do end")); + EXPECT_TRUE(sandbox.GetLastError().find("instruction") != std::string::npos); +} + +TEST(LuaSandbox, PathTraversalBlocked) { + LuaSandbox sandbox(TestContext()); + + EXPECT_FALSE(sandbox.LoadString("require('../../../etc/passwd')")); + EXPECT_FALSE(sandbox.LoadString("require('..\\\\..\\\\..\\\\windows\\\\system32\\\\config')")); + EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')")); + EXPECT_FALSE(sandbox.LoadString("require('C:\\\\Windows\\\\System32\\\\config')")); +} + +TEST(LuaSandbox, MetatableProtected) { + LuaSandbox sandbox(TestContext()); + + // Cannot access string metatable internals + EXPECT_TRUE(sandbox.LoadString(R"( + local mt = getmetatable("") + assert(mt == "string", "metatable should be protected") + )")); + + // Cannot modify globals + EXPECT_FALSE(sandbox.LoadString("_G.print = nil")); +} + +TEST(LuaSandbox, SafeOperationsWork) { + LuaSandbox sandbox(TestContext()); + + EXPECT_TRUE(sandbox.LoadString(R"( + -- Math + local x = math.sin(1.5) + math.floor(3.7) + + -- String + local s = string.format("hello %d", 42) + local upper = string.upper("test") + + -- Table + local t = {1, 2, 3} + table.insert(t, 4) + table.sort(t) + + -- Iteration + for i, v in ipairs(t) do end + for k, v in pairs({a=1, b=2}) do end + + -- Error handling + local ok, err = pcall(function() error("test") end) + assert(not ok) + + -- Type checks + assert(type({}) == "table") + assert(type("") == "string") + )")); +} +``` + +### Fuzzing + +```cpp +// Fuzz the sandbox with random Lua code +void FuzzSandbox() { + std::random_device rd; + std::mt19937 gen(rd()); + + for (int i = 0; i < 10000; i++) { + std::string code = GenerateRandomLuaCode(gen); + + LuaSandbox sandbox(TestContext()); + sandbox.LoadString(code); // Should never crash + + // Verify sandbox integrity after each run + auto issues = AuditSandbox(sandbox.GetState()); + ASSERT(issues.empty()) << "Sandbox compromised by: " << code; + } +} +``` + +--- + +## Implementation Status + +### Core Sandbox (Milestone 1) + +| Component | Status | File | +|-----------|--------|------| +| LuaSandbox class | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.h/cpp` | +| Custom allocator | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | +| Instruction hook | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | +| Globals removal | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | +| Bytecode prevention | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | +| Safe require | ❌ Planned | Milestone 4 | +| Metatable protection | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | +| Path validation | ❌ Planned | Milestone 4 | + +### APIs + +| Component | Status | File | +|-----------|--------|------| +| Storage API | ❌ Planned | Milestone 7 | +| Timer API | ❌ Planned | Milestone 5 | +| Network API | ❌ Planned | Milestone 9-10 | +| RmlUi bridge | ❌ Planned | Milestone 20 | + +### Security + +| Component | Status | File | +|-----------|--------|------| +| Permission gate | ❌ Planned | Milestone 2 | +| Audit logging | ❌ Planned | Milestone 3 | +| Network sandbox | ❌ Planned | Milestone 9 | +| Message bus | ❌ Planned | Milestone 18 | + +### Testing + +| Component | Status | File | +|-----------|--------|------| +| Unit tests | ✅ Implemented | `sandbox-test/` (11 tests) | +| Integration tests | ❌ Planned | Milestone 19 | +| Fuzzing | ❌ Planned | Milestone 19 | + +--- + +## Additional Security Measures (Continued) + +### JSON Parsing Security + +**Status**: Planned + +Untrusted JSON from network/storage must be safely parsed. + +```cpp +struct JsonLimits { + size_t max_depth = 32; + size_t max_string_length = 1024 * 1024; // 1 MB + size_t max_array_elements = 100000; + size_t max_object_keys = 10000; +}; + +int SafeJsonDecode(lua_State* L) { + size_t len; + const char* json = luaL_checklstring(L, 1, &len); + + // Size check + if (len > 10 * 1024 * 1024) { // 10 MB max + return luaL_error(L, "JSON too large"); + } + + auto* sandbox = GetSandbox(L); + + try { + auto parsed = ParseJsonWithLimits(json, len, sandbox->json_limits); + PushJsonValue(L, parsed); + return 1; + } catch (const JsonDepthError&) { + return luaL_error(L, "JSON nesting too deep"); + } catch (const JsonSizeError&) { + return luaL_error(L, "JSON element too large"); + } +} +``` + +**Lua API**: +```lua +local data = json.decode(json_string) -- Safe parsing +local str = json.encode(table) -- Safe encoding +``` + +### Cryptographic APIs + +**Status**: Planned + +Provide safe crypto primitives (apps shouldn't implement their own). + +```cpp +void SetupCryptoAPI(lua_State* L) { + lua_newtable(L); + + // crypto.randomBytes(n) - cryptographically secure random + lua_pushcfunction(L, [](lua_State* L) -> int { + int n = luaL_checkinteger(L, 1); + if (n < 1 || n > 1024) { + return luaL_error(L, "invalid byte count (1-1024)"); + } + + std::vector bytes(n); + if (!CSPRNG(bytes.data(), n)) { + return luaL_error(L, "random generation failed"); + } + + lua_pushlstring(L, reinterpret_cast(bytes.data()), n); + return 1; + }); + lua_setfield(L, -2, "randomBytes"); + + // crypto.hash(algorithm, data) - SHA-256, SHA-512 + lua_pushcfunction(L, [](lua_State* L) -> int { + const char* algo = luaL_checkstring(L, 1); + size_t len; + const char* data = luaL_checklstring(L, 2, &len); + + if (strcmp(algo, "sha256") == 0) { + auto hash = SHA256(data, len); + lua_pushlstring(L, hash.data(), hash.size()); + return 1; + } else if (strcmp(algo, "sha512") == 0) { + auto hash = SHA512(data, len); + lua_pushlstring(L, hash.data(), hash.size()); + return 1; + } + + return luaL_error(L, "unknown algorithm: %s", algo); + }); + lua_setfield(L, -2, "hash"); + + // crypto.hmac(algorithm, key, data) + // crypto.encrypt(algorithm, key, iv, plaintext) - AES-256-GCM only + // crypto.decrypt(algorithm, key, iv, ciphertext) + + lua_setglobal(L, "crypto"); +} +``` + +### Random Number Security + +**Status**: Planned + +`math.random()` uses a per-app seed to prevent cross-app prediction. + +```cpp +void SetupSafeRandom(lua_State* L) { + auto* sandbox = GetSandbox(L); + + // Seed with cryptographic random + app ID hash + uint64_t seed; + CSPRNG(&seed, sizeof(seed)); + seed ^= std::hash{}(sandbox->app_id); + + // Store RNG state in registry + auto* rng = new std::mt19937_64(seed); + lua_pushlightuserdata(L, rng); + lua_setfield(L, LUA_REGISTRYINDEX, "__rng"); + + // Replace math.random + lua_getglobal(L, "math"); + lua_pushcfunction(L, [](lua_State* L) -> int { + lua_getfield(L, LUA_REGISTRYINDEX, "__rng"); + auto* rng = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + int nargs = lua_gettop(L); + if (nargs == 0) { + // [0, 1) + std::uniform_real_distribution dist(0.0, 1.0); + lua_pushnumber(L, dist(*rng)); + } else if (nargs == 1) { + // [1, n] + int n = luaL_checkinteger(L, 1); + std::uniform_int_distribution dist(1, n); + lua_pushinteger(L, dist(*rng)); + } else { + // [m, n] + int m = luaL_checkinteger(L, 1); + int n = luaL_checkinteger(L, 2); + std::uniform_int_distribution dist(m, n); + lua_pushinteger(L, dist(*rng)); + } + return 1; + }); + lua_setfield(L, -2, "random"); + + // Remove math.randomseed (prevent manipulation) + lua_pushnil(L); + lua_setfield(L, -2, "randomseed"); + + lua_pop(L, 1); +} +``` + +### Environment Fingerprinting Prevention + +**Status**: Planned + +Limit information leakage about device/user. + +```cpp +void SetupEnvironmentAPI(lua_State* L) { + lua_newtable(L); + + // Only expose safe, non-identifying info + lua_pushstring(L, "1.0.0"); + lua_setfield(L, -2, "mosis_version"); + + lua_pushinteger(L, 540); + lua_setfield(L, -2, "screen_width"); + + lua_pushinteger(L, 960); + lua_setfield(L, -2, "screen_height"); + + // NOT exposed: + // - Device model + // - OS version + // - Unique identifiers + // - Installed apps list + // - Precise time (only second precision) + // - Memory/CPU info + // - Network interfaces + + lua_setglobal(L, "env"); +} + +// Time with reduced precision +int SafeTime(lua_State* L) { + // Round to seconds (no milliseconds) + time_t now = time(nullptr); + lua_pushinteger(L, static_cast(now)); + return 1; +} +``` + +### Rate Limiting + +**Status**: Planned + +Prevent abuse of expensive operations. + +```cpp +class RateLimiter { +public: + struct Limit { + int max_calls; + std::chrono::milliseconds window; + }; + + bool Check(const std::string& operation) { + auto now = std::chrono::steady_clock::now(); + auto& bucket = m_buckets[operation]; + + // Remove old entries + while (!bucket.empty() && + now - bucket.front() > m_limits[operation].window) { + bucket.pop_front(); + } + + // Check limit + if (bucket.size() >= m_limits[operation].max_calls) { + return false; + } + + bucket.push_back(now); + return true; + } + +private: + std::unordered_map m_limits = { + {"network.request", {100, std::chrono::minutes(1)}}, + {"storage.write", {1000, std::chrono::minutes(1)}}, + {"crypto.hash", {1000, std::chrono::seconds(1)}}, + {"timer.create", {100, std::chrono::seconds(1)}}, + }; + + std::unordered_map> m_buckets; +}; + +// Usage +int NetworkRequest(lua_State* L) { + auto* sandbox = GetSandbox(L); + if (!sandbox->rate_limiter.Check("network.request")) { + return luaL_error(L, "rate limit exceeded"); + } + // ... proceed with request +} +``` + +### Resource Cleanup on Termination + +**Status**: Planned + +Ensure all resources are released when app stops. + +```cpp +class LuaSandbox { +public: + ~LuaSandbox() { + Cleanup(); + } + + void Cleanup() { + // Cancel all timers + m_timer_manager->CancelAll(m_context.app_id); + + // Close all network connections + m_network_manager->CloseAll(m_context.app_id); + + // Release all file handles + m_file_manager->CloseAll(m_context.app_id); + + // Clear message queue + m_message_bus->ClearPending(m_context.app_id); + + // Release Lua callbacks + for (int ref : m_callback_refs) { + luaL_unref(L, LUA_REGISTRYINDEX, ref); + } + + // Close Lua state (releases memory) + if (L) { + lua_close(L); + L = nullptr; + } + + // Log cleanup + AuditLog::Log({ + .app_id = m_context.app_id, + .event = AuditEvent::AppStop, + .detail = "cleanup completed" + }); + } + +private: + std::vector m_callback_refs; +}; +``` + +### App Signature Verification + +**Status**: Planned + +Verify app packages haven't been tampered with. + +```cpp +struct AppSignature { + std::string algorithm; // "ed25519" + std::string public_key; // Base64 + std::string signature; // Base64 +}; + +class PackageVerifier { +public: + bool Verify(const std::string& mpkg_path) { + // Read manifest + auto manifest = ReadManifest(mpkg_path); + if (!manifest) return false; + + // Check signature exists + if (!manifest->signature) { + // Unsigned packages allowed in dev mode only + return IsDevMode(); + } + + // Get developer's public key from store/registry + auto pub_key = GetDeveloperKey(manifest->author_id); + if (!pub_key) return false; + + // Compute package hash (all files except signature) + auto hash = ComputePackageHash(mpkg_path); + + // Verify signature + return VerifyEd25519( + pub_key->data(), + hash.data(), hash.size(), + Base64Decode(manifest->signature->signature) + ); + } + +private: + std::vector ComputePackageHash(const std::string& path) { + // Hash all files in deterministic order + SHA256_CTX ctx; + SHA256_Init(&ctx); + + for (const auto& file : ListFilesRecursive(path)) { + if (file.name == "signature.json") continue; + + // Include filename in hash + SHA256_Update(&ctx, file.name.data(), file.name.size()); + + // Include content + auto content = ReadFile(file.path); + SHA256_Update(&ctx, content.data(), content.size()); + } + + std::vector hash(32); + SHA256_Final(hash.data(), &ctx); + return hash; + } +}; +``` + +### Debug Mode Restrictions + +**Status**: Planned + +Development features disabled in production. + +```cpp +class DebugMode { +public: + static bool IsEnabled() { + #ifdef NDEBUG + return false; + #else + return s_debug_enabled; + #endif + } + + // Only available in debug mode + static void EnableHotReload(LuaSandbox* sandbox) { + if (!IsEnabled()) return; + // ... + } + + static void EnableInspector(LuaSandbox* sandbox) { + if (!IsEnabled()) return; + // ... + } + + // Log debug state for security audit + static void LogDebugState() { + if (IsEnabled()) { + Logger::Warn("[SECURITY] Debug mode enabled - " + "sandbox restrictions relaxed"); + } + } + +private: + static bool s_debug_enabled; +}; + +// In production builds, these are completely compiled out +#ifdef NDEBUG + #define DEBUG_ONLY(x) ((void)0) +#else + #define DEBUG_ONLY(x) x +#endif +``` + +### Clipboard Isolation + +**Status**: Planned + +Apps can only access clipboard with permission and user gesture. + +```cpp +class ClipboardManager { +public: + // Requires permission + recent user gesture + std::optional Read(LuaSandbox* sandbox) { + if (!sandbox->HasPermission("clipboard.read")) { + return std::nullopt; + } + + // Must be within 1 second of user gesture (click/tap) + if (!sandbox->HasRecentUserGesture(1000)) { + Logger::Warn("Clipboard read blocked - no recent user gesture"); + return std::nullopt; + } + + return GetSystemClipboard(); + } + + bool Write(LuaSandbox* sandbox, const std::string& text) { + if (!sandbox->HasPermission("clipboard.write")) { + return false; + } + + // Size limit + if (text.size() > 1024 * 1024) { + return false; + } + + SetSystemClipboard(text); + return true; + } +}; +``` + +### Stack Depth Limiting + +**Status**: Planned + +Prevent stack overflow via deep recursion. + +```cpp +void SetupStackLimit(lua_State* L, int max_depth) { + // Set C stack limit + // Note: Lua 5.4 has LUAI_MAXCSTACK + + // Also use debug hook to check Lua call depth + lua_sethook(L, [](lua_State* L, lua_Debug* ar) { + lua_Debug info; + int depth = 0; + + while (lua_getstack(L, depth, &info)) { + depth++; + } + + auto* sandbox = GetSandbox(L); + if (depth > sandbox->m_limits.stack_depth) { + luaL_error(L, "stack overflow (depth %d)", depth); + } + }, LUA_MASKCALL, 0); +} +``` + +--- + +## Security Testing Checklist + +### Before Release + +- [ ] All dangerous globals removed +- [ ] Bytecode loading blocked +- [ ] Memory limits enforced +- [ ] CPU limits enforced +- [ ] Path traversal blocked +- [ ] Metatable protection working +- [ ] Permission checks on all APIs +- [ ] Network domain filtering working +- [ ] Rate limiting active +- [ ] Audit logging enabled +- [ ] Debug mode disabled in release +- [ ] Fuzzing completed with no crashes +- [ ] Security audit by external party + +### Per-App Review (Store Submission) + +- [ ] Manifest permissions justified +- [ ] No obfuscated code +- [ ] Network domains declared +- [ ] Package signature valid +- [ ] No known vulnerability patterns +- [ ] Resource usage reasonable + +--- + +## Virtual Hardware Interfaces + +Mosis provides virtualized hardware interfaces that apps can access through permission-gated APIs. Each interface has specific security requirements based on the sensitivity of the data it exposes. + +### Hardware Permission Model + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Permission Categories │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ NORMAL (auto-granted) DANGEROUS (user prompt required) │ +│ ├── storage.app ├── camera │ +│ ├── network.metadata ├── microphone │ +│ ├── system.vibrate ├── location.fine │ +│ └── audio.playback ├── location.coarse │ +│ ├── contacts.read │ +│ SIGNATURE (system apps only) ├── contacts.write │ +│ ├── system.settings ├── storage.shared │ +│ ├── app.install ├── network.internet │ +│ └── hardware.control ├── bluetooth │ +│ ├── phone.call │ +│ ├── sms.send │ +│ └── sensors.body │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Hardware Security Threat Model + +| Interface | Threat | Impact | Mitigation | +|-----------|--------|--------|------------| +| **Camera** | Silent recording | Critical privacy | Indicator, gesture gate | +| **Microphone** | Eavesdropping | Critical privacy | Indicator, gesture gate | +| **Location** | Tracking | High privacy | Precision reduction, rate limit | +| **Network** | Data exfiltration | High privacy | Domain filtering, inspection | +| **Filesystem** | Data theft/corruption | High integrity | Path sandboxing | +| **Database** | SQL injection | High integrity | Prepared statements only | +| **Bluetooth** | Device tracking | Medium privacy | Pairing consent | +| **Sensors** | Fingerprinting | Medium privacy | Reduced precision | +| **Speaker** | Audio spam | Low annoyance | Volume limits, rate limit | + +--- + +## Network Interface Security + +### Network Sandbox Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ App Lua Code │ +│ │ │ +│ network.request(url, opts) │ +│ │ │ +├──────────────────────────────▼──────────────────────────────────────┤ +│ NetworkSandbox │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │ +│ │ Permission │ │ Domain │ │ Content │ │ +│ │ Check │→ │ Validation │→ │ Inspection │ │ +│ └─────────────┘ └──────────────┘ └────────────────────────────┘ │ +│ │ │ +│ ┌─────────▼──────────┐ │ +│ │ Rate Limiter │ │ +│ └─────────┬──────────┘ │ +│ │ │ +├──────────────────────────────▼──────────────────────────────────────┤ +│ System HTTP Client │ +│ (libcurl / Android HttpClient) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### HTTP Request Security + +**Status**: Designed + +```cpp +struct HttpRequestPolicy { + // Domain restrictions + std::vector allowed_domains; // Whitelist from manifest + std::vector blocked_domains; // System blacklist + bool allow_localhost = false; // Block 127.0.0.1, ::1 + bool allow_private_ips = false; // Block 10.x, 192.168.x, etc. + bool allow_metadata_ips = false; // Block 169.254.x (cloud metadata) + + // Protocol restrictions + bool require_https = true; // Block plain HTTP + std::vector allowed_schemes = {"https"}; + + // Size limits + size_t max_request_body = 10 * 1024 * 1024; // 10 MB + size_t max_response_body = 50 * 1024 * 1024; // 50 MB + size_t max_header_size = 64 * 1024; // 64 KB + + // Timeout + int connect_timeout_ms = 10000; + int read_timeout_ms = 30000; + int total_timeout_ms = 60000; + + // Rate limits + int max_concurrent_requests = 6; + int max_requests_per_minute = 100; +}; + +class HttpRequestValidator { +public: + enum class Result { + Allowed, + PermissionDenied, + DomainBlocked, + PrivateIPBlocked, + MetadataIPBlocked, + LocalhostBlocked, + InsecureProtocol, + RequestTooLarge, + RateLimitExceeded + }; + + Result Validate(const HttpRequest& req, const AppContext& ctx) { + // 1. Permission check + if (!ctx.HasPermission("network.internet")) { + return Result::PermissionDenied; + } + + // 2. Parse URL + auto url = ParseUrl(req.url); + if (!url) return Result::DomainBlocked; + + // 3. Protocol check + if (m_policy.require_https && url->scheme != "https") { + return Result::InsecureProtocol; + } + + // 4. Resolve hostname to IP (detect IP-based bypass) + auto ips = ResolveHostname(url->host); + + // 5. Block private IPs + for (const auto& ip : ips) { + if (IsPrivateIP(ip) && !m_policy.allow_private_ips) { + return Result::PrivateIPBlocked; + } + if (IsMetadataIP(ip) && !m_policy.allow_metadata_ips) { + return Result::MetadataIPBlocked; + } + if (IsLocalhost(ip) && !m_policy.allow_localhost) { + return Result::LocalhostBlocked; + } + } + + // 6. Domain whitelist/blacklist + if (!m_policy.allowed_domains.empty()) { + if (!MatchesDomainList(url->host, m_policy.allowed_domains)) { + return Result::DomainBlocked; + } + } + if (MatchesDomainList(url->host, m_policy.blocked_domains)) { + return Result::DomainBlocked; + } + + // 7. Size check + if (req.body.size() > m_policy.max_request_body) { + return Result::RequestTooLarge; + } + + // 8. Rate limit + if (!m_rate_limiter.Check(ctx.app_id, "http_request")) { + return Result::RateLimitExceeded; + } + + return Result::Allowed; + } + +private: + bool IsPrivateIP(const std::string& ip) { + // IPv4 private ranges + if (ip.starts_with("10.")) return true; + if (ip.starts_with("172.")) { + int second_octet = std::stoi(ip.substr(4, ip.find('.', 4) - 4)); + if (second_octet >= 16 && second_octet <= 31) return true; + } + if (ip.starts_with("192.168.")) return true; + + // Link-local + if (ip.starts_with("169.254.")) return true; + + // IPv6 private ranges + if (ip.starts_with("fc") || ip.starts_with("fd")) return true; // ULA + if (ip.starts_with("fe80:")) return true; // Link-local + + return false; + } + + bool IsMetadataIP(const std::string& ip) { + // AWS/GCP/Azure metadata endpoints + return ip == "169.254.169.254" || + ip == "metadata.google.internal" || + ip.starts_with("fd00:ec2::"); + } +}; +``` + +### WebSocket Security + +**Status**: Designed + +```cpp +struct WebSocketPolicy { + bool enabled = true; + int max_connections_per_app = 5; + size_t max_message_size = 1 * 1024 * 1024; // 1 MB + int ping_interval_ms = 30000; + int idle_timeout_ms = 300000; // 5 minutes + + // Same domain validation as HTTP + std::vector allowed_origins; +}; + +class WebSocketManager { +public: + struct Connection { + uint32_t id; + std::string app_id; + std::string url; + ConnectionState state; + std::chrono::steady_clock::time_point created_at; + std::chrono::steady_clock::time_point last_activity; + size_t bytes_sent = 0; + size_t bytes_received = 0; + }; + + std::optional Connect(LuaSandbox* sandbox, const std::string& url) { + // Permission check + if (!sandbox->HasPermission("network.websocket")) { + return std::nullopt; + } + + // Validate URL (same as HTTP) + auto result = m_validator.Validate({.url = url}, sandbox->context()); + if (result != HttpRequestValidator::Result::Allowed) { + return std::nullopt; + } + + // Connection limit + if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) { + return std::nullopt; + } + + // Create connection + auto conn = CreateConnection(url); + conn.app_id = sandbox->app_id(); + + m_connections[conn.id] = conn; + return conn.id; + } + + bool Send(LuaSandbox* sandbox, uint32_t conn_id, const std::string& data) { + auto it = m_connections.find(conn_id); + if (it == m_connections.end()) return false; + + // Verify ownership + if (it->second.app_id != sandbox->app_id()) return false; + + // Size limit + if (data.size() > m_policy.max_message_size) return false; + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "ws_send")) return false; + + // Send + it->second.bytes_sent += data.size(); + it->second.last_activity = std::chrono::steady_clock::now(); + return DoSend(conn_id, data); + } + + void CloseAll(const std::string& app_id) { + // Called when app stops + for (auto it = m_connections.begin(); it != m_connections.end(); ) { + if (it->second.app_id == app_id) { + DoClose(it->first); + it = m_connections.erase(it); + } else { + ++it; + } + } + } + +private: + std::unordered_map m_connections; + WebSocketPolicy m_policy; + HttpRequestValidator m_validator; + RateLimiter m_rate_limiter; +}; +``` + +### Network Lua API + +```lua +-- HTTP Requests (requires "network.internet" permission) +local response = network.request({ + url = "https://api.example.com/data", + method = "POST", -- GET, POST, PUT, DELETE, PATCH + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer " .. token + }, + body = json.encode({key = "value"}), + timeout = 30000 -- milliseconds +}) + +-- Response structure +-- response.status: number (200, 404, etc.) +-- response.headers: table +-- response.body: string +-- response.error: string (nil if success) + +-- WebSocket (requires "network.websocket" permission) +local ws = network.websocket("wss://api.example.com/ws") + +ws:on("open", function() + ws:send(json.encode({type = "hello"})) +end) + +ws:on("message", function(data) + local msg = json.decode(data) + print("Received:", msg.type) +end) + +ws:on("close", function(code, reason) + print("Closed:", code, reason) +end) + +ws:on("error", function(err) + print("Error:", err) +end) + +ws:close() +``` + +### Manifest Declaration + +```json +{ + "permissions": ["network.internet", "network.websocket"], + "network": { + "allowed_domains": [ + "api.example.com", + "*.cdn.example.com", + "wss://realtime.example.com" + ], + "allow_http": false, + "max_connections": 10 + } +} +``` + +--- + +## Filesystem Interface Security + +### Virtual Filesystem Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Physical Storage │ +│ /data/data/com.omixlab.mosis/ │ +│ ├── apps/ │ +│ │ ├── com.example.app1/ │ +│ │ │ ├── data/ ← App-private storage │ +│ │ │ ├── cache/ ← Clearable cache │ +│ │ │ └── temp/ ← Session-only │ +│ │ └── com.example.app2/ │ +│ ├── shared/ │ +│ │ ├── photos/ ← Shared media (permission required) │ +│ │ ├── downloads/ │ +│ │ └── documents/ │ +│ └── system/ ← System apps only │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌───────▼───────┐ + │ VirtualFS API │ + └───────┬───────┘ + │ +┌───────────────────────────────────▼─────────────────────────────────┐ +│ App's Virtual View │ +│ / │ +│ ├── data/ → /apps//data/ (auto-granted) │ +│ ├── cache/ → /apps//cache/ (auto-granted) │ +│ ├── temp/ → /apps//temp/ (auto-granted) │ +│ ├── shared/ → /shared/ (permission required) │ +│ └── (nothing else visible) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Filesystem Security Implementation + +**Status**: Designed + +```cpp +struct FilesystemPolicy { + // Storage quotas + size_t max_app_storage = 100 * 1024 * 1024; // 100 MB per app + size_t max_cache_storage = 50 * 1024 * 1024; // 50 MB cache + size_t max_file_size = 50 * 1024 * 1024; // 50 MB single file + + // File limits + size_t max_files_per_directory = 10000; + size_t max_path_depth = 32; + size_t max_filename_length = 255; + + // Allowed extensions (whitelist approach) + std::vector allowed_extensions = { + ".txt", ".json", ".xml", ".html", ".css", ".js", ".lua", + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", + ".mp3", ".ogg", ".wav", + ".mp4", ".webm", + ".db", ".sqlite" + }; + + // Blocked patterns + std::vector blocked_patterns = { + "*.exe", "*.dll", "*.so", "*.dylib", // Executables + "*.sh", "*.bat", "*.cmd", "*.ps1", // Scripts + ".*", // Hidden files + }; +}; + +class VirtualFilesystem { +public: + VirtualFilesystem(const std::string& app_id, const std::string& base_path) + : m_app_id(app_id) + , m_base_path(base_path) + , m_app_root(base_path + "/apps/" + app_id) {} + + // Resolve virtual path to physical path with security checks + std::optional ResolvePath(const std::string& virtual_path, + AccessMode mode) { + // 1. Validate path format + if (!IsValidPathFormat(virtual_path)) { + AuditLog::Log({m_app_id, AuditEvent::FileAccess, + "invalid path format: " + virtual_path, false}); + return std::nullopt; + } + + // 2. Parse virtual path + auto parts = SplitPath(virtual_path); + if (parts.empty()) return std::nullopt; + + // 3. Determine physical root based on virtual root + std::string physical_root; + bool requires_permission = false; + + if (parts[0] == "data") { + physical_root = m_app_root + "/data"; + } else if (parts[0] == "cache") { + physical_root = m_app_root + "/cache"; + } else if (parts[0] == "temp") { + physical_root = m_app_root + "/temp"; + } else if (parts[0] == "shared") { + physical_root = m_base_path + "/shared"; + requires_permission = true; + } else { + // Unknown root - access denied + return std::nullopt; + } + + // 4. Permission check for shared storage + if (requires_permission) { + std::string perm = (mode == AccessMode::Read) + ? "storage.shared.read" + : "storage.shared.write"; + if (!HasPermission(perm)) { + AuditLog::Log({m_app_id, AuditEvent::PermissionDenied, perm, false}); + return std::nullopt; + } + } + + // 5. Build full path + std::filesystem::path full_path = physical_root; + for (size_t i = 1; i < parts.size(); ++i) { + full_path /= parts[i]; + } + + // 6. Canonicalize and verify containment + std::filesystem::path canonical; + try { + if (mode == AccessMode::Read) { + // Must exist for read + canonical = std::filesystem::canonical(full_path); + } else { + // May not exist for write - canonicalize parent + canonical = std::filesystem::weakly_canonical(full_path); + } + } catch (const std::filesystem::filesystem_error&) { + return std::nullopt; + } + + // 7. Verify path is within allowed root (prevent traversal) + std::string canonical_str = canonical.string(); + if (!canonical_str.starts_with(physical_root)) { + AuditLog::Log({m_app_id, AuditEvent::SandboxViolation, + "path traversal attempt: " + virtual_path, false}); + return std::nullopt; + } + + // 8. Check file extension + if (!IsAllowedExtension(canonical.extension().string())) { + return std::nullopt; + } + + return canonical_str; + } + + // Quota enforcement + bool HasStorageQuota(size_t bytes_needed) { + size_t current = CalculateDirectorySize(m_app_root + "/data"); + return (current + bytes_needed) <= m_policy.max_app_storage; + } + + std::optional GetUsedStorage() { + return CalculateDirectorySize(m_app_root + "/data"); + } + + std::optional GetAvailableStorage() { + auto used = GetUsedStorage(); + if (!used) return std::nullopt; + return m_policy.max_app_storage - *used; + } + +private: + bool IsValidPathFormat(const std::string& path) { + // Must start with / + if (path.empty() || path[0] != '/') return false; + + // No .. components + if (path.find("..") != std::string::npos) return false; + + // No double slashes + if (path.find("//") != std::string::npos) return false; + + // No null bytes + if (path.find('\0') != std::string::npos) return false; + + // Length check + if (path.length() > 4096) return false; + + // Check each component + for (const auto& part : SplitPath(path)) { + if (part.length() > m_policy.max_filename_length) return false; + if (part.empty()) return false; + if (MatchesBlockedPattern(part)) return false; + } + + return true; + } + + bool IsAllowedExtension(const std::string& ext) { + if (ext.empty()) return true; // Directories + std::string lower_ext = ToLower(ext); + return std::find(m_policy.allowed_extensions.begin(), + m_policy.allowed_extensions.end(), + lower_ext) != m_policy.allowed_extensions.end(); + } + + std::string m_app_id; + std::string m_base_path; + std::string m_app_root; + FilesystemPolicy m_policy; +}; +``` + +### File Operations with Security Gates + +```cpp +class SecureFileAPI { +public: + // Read file + std::optional ReadFile(LuaSandbox* sandbox, + const std::string& path) { + auto physical = m_vfs.ResolvePath(path, AccessMode::Read); + if (!physical) return std::nullopt; + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "file_read")) { + return std::nullopt; + } + + // Read with size limit + std::ifstream file(*physical, std::ios::binary); + if (!file) return std::nullopt; + + file.seekg(0, std::ios::end); + size_t size = file.tellg(); + if (size > m_policy.max_file_size) return std::nullopt; + + file.seekg(0, std::ios::beg); + std::string content(size, '\0'); + file.read(content.data(), size); + + AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, + "read: " + path + " (" + std::to_string(size) + " bytes)", true}); + + return content; + } + + // Write file + bool WriteFile(LuaSandbox* sandbox, const std::string& path, + const std::string& content) { + auto physical = m_vfs.ResolvePath(path, AccessMode::Write); + if (!physical) return false; + + // Size check + if (content.size() > m_policy.max_file_size) return false; + + // Quota check + if (!m_vfs.HasStorageQuota(content.size())) { + AuditLog::Log({sandbox->app_id(), AuditEvent::ResourceLimitHit, + "storage quota exceeded", false}); + return false; + } + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "file_write")) { + return false; + } + + // Create parent directories + std::filesystem::create_directories( + std::filesystem::path(*physical).parent_path()); + + // Write atomically (temp file + rename) + std::string temp_path = *physical + ".tmp." + GenerateRandomId(); + { + std::ofstream file(temp_path, std::ios::binary); + if (!file) return false; + file.write(content.data(), content.size()); + } + + std::error_code ec; + std::filesystem::rename(temp_path, *physical, ec); + if (ec) { + std::filesystem::remove(temp_path); + return false; + } + + AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, + "write: " + path + " (" + std::to_string(content.size()) + " bytes)", + true}); + + return true; + } + + // List directory + std::optional> ListDirectory(LuaSandbox* sandbox, + const std::string& path) { + auto physical = m_vfs.ResolvePath(path, AccessMode::Read); + if (!physical) return std::nullopt; + + std::vector entries; + for (const auto& entry : std::filesystem::directory_iterator(*physical)) { + if (entries.size() >= m_policy.max_files_per_directory) break; + + FileInfo info; + info.name = entry.path().filename().string(); + info.is_directory = entry.is_directory(); + info.size = entry.is_regular_file() ? entry.file_size() : 0; + info.modified = GetModificationTime(entry.path()); + entries.push_back(info); + } + + return entries; + } + + // Delete file + bool Delete(LuaSandbox* sandbox, const std::string& path) { + auto physical = m_vfs.ResolvePath(path, AccessMode::Write); + if (!physical) return false; + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "file_delete")) { + return false; + } + + std::error_code ec; + bool removed = std::filesystem::remove(*physical, ec); + + AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, + "delete: " + path, removed}); + + return removed; + } + +private: + VirtualFilesystem m_vfs; + FilesystemPolicy m_policy; + RateLimiter m_rate_limiter; +}; +``` + +### Filesystem Lua API + +```lua +-- File operations (app-private storage is auto-granted) +local content = storage.read("/data/config.json") +local success = storage.write("/data/config.json", json.encode(config)) + +-- Directory operations +local files = storage.list("/data/saves/") +for _, file in ipairs(files) do + print(file.name, file.size, file.is_directory) +end + +-- File info +local info = storage.info("/data/save.json") +-- info.exists, info.size, info.modified, info.is_directory + +-- Delete +storage.delete("/data/old_file.json") + +-- Create directory +storage.mkdir("/data/saves/slot1/") + +-- Check storage quota +local quota = storage.quota() +-- quota.used, quota.available, quota.total + +-- Shared storage (requires "storage.shared.read" / "storage.shared.write") +local photos = storage.list("/shared/photos/") +storage.write("/shared/documents/report.txt", "Hello") +``` + +--- + +## Database Interface Security + +### SQLite Sandbox + +**Status**: Designed + +Apps can use SQLite databases with security restrictions. + +```cpp +struct DatabasePolicy { + size_t max_database_size = 50 * 1024 * 1024; // 50 MB + size_t max_databases_per_app = 5; + size_t max_query_time_ms = 5000; + size_t max_result_rows = 10000; + bool allow_attach = false; // Prevent ATTACH to other DBs + bool allow_load_extension = false; // No extensions +}; + +class SecureDatabase { +public: + SecureDatabase(const std::string& app_id, const std::string& db_name) + : m_app_id(app_id) + , m_db_name(db_name) { + + // Validate database name + if (!IsValidDatabaseName(db_name)) { + throw std::invalid_argument("invalid database name"); + } + + // Open database in app's data directory + std::string path = GetAppDataPath(app_id) + "/databases/" + db_name + ".db"; + + int rc = sqlite3_open_v2(path.c_str(), &m_db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_NOMUTEX, // Single-threaded access per app + nullptr); + + if (rc != SQLITE_OK) { + throw std::runtime_error("failed to open database"); + } + + // Security configuration + ConfigureSecurity(); + } + + ~SecureDatabase() { + if (m_db) sqlite3_close(m_db); + } + + // Execute query with prepared statement (prevents SQL injection) + QueryResult Execute(const std::string& sql, + const std::vector& params) { + // 1. Validate SQL (basic sanity checks) + if (!ValidateSql(sql)) { + return {.error = "invalid SQL"}; + } + + // 2. Prepare statement + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + return {.error = sqlite3_errmsg(m_db)}; + } + + // 3. Bind parameters + for (size_t i = 0; i < params.size(); ++i) { + BindParameter(stmt, i + 1, params[i]); + } + + // 4. Execute with timeout + QueryResult result; + auto start = std::chrono::steady_clock::now(); + + while (true) { + rc = sqlite3_step(stmt); + + // Check timeout + auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed > std::chrono::milliseconds(m_policy.max_query_time_ms)) { + sqlite3_finalize(stmt); + return {.error = "query timeout"}; + } + + if (rc == SQLITE_ROW) { + // Check row limit + if (result.rows.size() >= m_policy.max_result_rows) { + result.truncated = true; + break; + } + result.rows.push_back(ExtractRow(stmt)); + } else if (rc == SQLITE_DONE) { + break; + } else { + result.error = sqlite3_errmsg(m_db); + break; + } + } + + result.changes = sqlite3_changes(m_db); + result.last_insert_id = sqlite3_last_insert_rowid(m_db); + + sqlite3_finalize(stmt); + return result; + } + +private: + void ConfigureSecurity() { + // Disable dangerous features + sqlite3_db_config(m_db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, nullptr); + + // Set authorizer to block dangerous operations + sqlite3_set_authorizer(m_db, Authorizer, this); + + // Set database size limit + sqlite3_soft_heap_limit64(m_policy.max_database_size); + + // Set busy timeout + sqlite3_busy_timeout(m_db, 1000); + } + + static int Authorizer(void* user_data, int action, const char* arg1, + const char* arg2, const char* arg3, const char* arg4) { + auto* self = static_cast(user_data); + + switch (action) { + case SQLITE_ATTACH: + case SQLITE_DETACH: + // Block ATTACH/DETACH to prevent accessing other databases + return SQLITE_DENY; + + case SQLITE_PRAGMA: + // Allow only safe pragmas + if (arg1 && !IsSafePragma(arg1)) { + return SQLITE_DENY; + } + break; + + case SQLITE_FUNCTION: + // Block dangerous functions + if (arg2 && IsDangerousFunction(arg2)) { + return SQLITE_DENY; + } + break; + } + + return SQLITE_OK; + } + + static bool IsSafePragma(const char* pragma) { + // Whitelist of safe pragmas + static const std::unordered_set safe = { + "table_info", "index_list", "foreign_keys", + "journal_mode", "synchronous", "cache_size" + }; + return safe.count(pragma) > 0; + } + + static bool IsDangerousFunction(const char* func) { + // Block functions that can access filesystem or execute code + static const std::unordered_set dangerous = { + "load_extension", "readfile", "writefile", "edit", + "fts3_tokenizer", "sqlite_compileoption_get" + }; + return dangerous.count(func) > 0; + } + + bool ValidateSql(const std::string& sql) { + std::string upper = ToUpper(sql); + + // Block dangerous statements + if (upper.find("ATTACH") != std::string::npos) return false; + if (upper.find("DETACH") != std::string::npos) return false; + if (upper.find("LOAD_EXTENSION") != std::string::npos) return false; + + return true; + } + + sqlite3* m_db = nullptr; + std::string m_app_id; + std::string m_db_name; + DatabasePolicy m_policy; +}; +``` + +### Database Lua API + +```lua +-- Open database (stored in /data/databases/) +local db = database.open("myapp") + +-- Create table +db:execute([[ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE, + created_at INTEGER DEFAULT (strftime('%s', 'now')) + ) +]]) + +-- Insert with parameters (SQL injection safe) +db:execute("INSERT INTO users (name, email) VALUES (?, ?)", {"John", "john@example.com"}) + +-- Query with parameters +local result = db:query("SELECT * FROM users WHERE name LIKE ?", {"%John%"}) +for _, row in ipairs(result.rows) do + print(row.id, row.name, row.email) +end + +-- Transaction +db:transaction(function() + db:execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", {100, 1}) + db:execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", {100, 2}) +end) + +-- Close +db:close() +``` + +--- + +## Camera Interface Security + +### Camera Security Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ App Lua Code │ +│ camera.capture(callback) │ +│ │ │ +├──────────────────────────────▼──────────────────────────────────────┤ +│ CameraManager │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │ +│ │ Permission │ │ Active │ │ Frame Rate │ │ +│ │ Check │→ │ Indicator │→ │ Limiter │ │ +│ └─────────────┘ └──────────────┘ └────────────────────────────┘ │ +│ │ │ +├──────────────────────────────▼──────────────────────────────────────┤ +│ Game Engine Camera │ +│ (Unity RenderTexture / Unreal SceneCapture) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Camera Security Implementation + +**Status**: Designed + +```cpp +struct CameraPolicy { + // Access control + bool require_user_gesture = true; // Must be triggered by tap/click + int max_active_sessions = 1; // Only one app can use camera + + // Frame limits + int max_fps = 30; + int max_resolution_width = 1920; + int max_resolution_height = 1080; + + // Privacy + bool show_indicator = true; // Always show recording indicator + int indicator_min_duration_ms = 500; // Indicator shows at least 500ms + bool allow_silent_capture = false; // Require shutter sound + + // Storage + bool auto_save_to_gallery = false; // App must explicitly save + std::string watermark_text = ""; // Optional watermark +}; + +class CameraManager { +public: + struct CameraSession { + uint32_t id; + std::string app_id; + CameraFacing facing; + Resolution resolution; + bool active; + std::chrono::steady_clock::time_point started_at; + }; + + // Start camera session + std::optional StartSession(LuaSandbox* sandbox, + const CameraOptions& opts) { + // 1. Permission check + if (!sandbox->HasPermission("camera")) { + AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, + "camera", false}); + return std::nullopt; + } + + // 2. User gesture check + if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) { + AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, + "blocked - no user gesture", false}); + return std::nullopt; + } + + // 3. Check exclusive access + if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) { + return std::nullopt; // Another app is using camera + } + + // 4. Validate resolution + Resolution res = opts.resolution; + res.width = std::min(res.width, m_policy.max_resolution_width); + res.height = std::min(res.height, m_policy.max_resolution_height); + + // 5. Start session + CameraSession session{ + .id = ++m_next_session_id, + .app_id = sandbox->app_id(), + .facing = opts.facing, + .resolution = res, + .active = true, + .started_at = std::chrono::steady_clock::now() + }; + + m_active_session = session; + + // 6. Show indicator + if (m_policy.show_indicator) { + ShowCameraIndicator(true); + } + + // 7. Request frames from game engine + m_hardware_camera->Start(res, [this, session_id = session.id](Frame frame) { + OnFrameReceived(session_id, frame); + }); + + AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, + "session started", true}); + + return session.id; + } + + // Capture single frame + std::optional CaptureFrame(LuaSandbox* sandbox, uint32_t session_id) { + // Verify session ownership + if (!m_active_session || m_active_session->id != session_id || + m_active_session->app_id != sandbox->app_id()) { + return std::nullopt; + } + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "camera_capture")) { + return std::nullopt; + } + + // Get latest frame + auto frame = m_latest_frame; + if (!frame) return std::nullopt; + + // Play shutter sound (if configured) + if (!m_policy.allow_silent_capture) { + PlayShutterSound(); + } + + // Apply watermark if configured + if (!m_policy.watermark_text.empty()) { + ApplyWatermark(frame, m_policy.watermark_text); + } + + return frame; + } + + // Stop session + void StopSession(LuaSandbox* sandbox, uint32_t session_id) { + if (!m_active_session || m_active_session->id != session_id || + m_active_session->app_id != sandbox->app_id()) { + return; + } + + m_hardware_camera->Stop(); + + // Keep indicator visible for minimum duration + auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; + if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) { + std::this_thread::sleep_for( + std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed); + } + + ShowCameraIndicator(false); + m_active_session.reset(); + + AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, + "session stopped", true}); + } + + // Stop all sessions for app (called on app termination) + void StopAllSessions(const std::string& app_id) { + if (m_active_session && m_active_session->app_id == app_id) { + m_hardware_camera->Stop(); + ShowCameraIndicator(false); + m_active_session.reset(); + } + } + +private: + void OnFrameReceived(uint32_t session_id, Frame frame) { + if (!m_active_session || m_active_session->id != session_id) { + return; + } + + // Apply frame rate limit + auto now = std::chrono::steady_clock::now(); + auto elapsed = now - m_last_frame_time; + if (elapsed < std::chrono::milliseconds(1000 / m_policy.max_fps)) { + return; + } + m_last_frame_time = now; + + m_latest_frame = frame; + + // Notify app (if callback registered) + if (m_frame_callback) { + m_frame_callback(frame); + } + } + + std::optional m_active_session; + std::optional m_latest_frame; + std::chrono::steady_clock::time_point m_last_frame_time; + CameraPolicy m_policy; + IHardwareCamera* m_hardware_camera; + RateLimiter m_rate_limiter; + uint32_t m_next_session_id = 0; +}; +``` + +### Camera Lua API + +```lua +-- Request camera permission (shows system dialog) +local granted = permissions.request("camera") +if not granted then return end + +-- Start camera session +local session = camera.start({ + facing = "back", -- "back" or "front" + resolution = {width = 1280, height = 720} +}) + +-- Preview frames +session:onFrame(function(frame) + -- frame.width, frame.height, frame.data (base64 JPEG) + ui.setImage("preview", frame.data) +end) + +-- Capture photo +local photo = session:capture() +if photo then + -- Save to app storage + storage.write("/data/photos/photo_" .. os.time() .. ".jpg", + base64.decode(photo.data)) + + -- Or save to shared gallery (requires storage.shared.write) + storage.write("/shared/photos/MyApp/photo.jpg", base64.decode(photo.data)) +end + +-- Stop session +session:stop() +``` + +--- + +## Microphone Interface Security + +### Microphone Security Implementation + +**Status**: Designed + +```cpp +struct MicrophonePolicy { + // Access control + bool require_user_gesture = true; + int max_active_sessions = 1; + + // Audio limits + int max_sample_rate = 48000; + int max_duration_ms = 300000; // 5 minutes max recording + int max_buffer_size = 4096; + + // Privacy + bool show_indicator = true; + int indicator_min_duration_ms = 500; + bool allow_background_recording = false; +}; + +class MicrophoneManager { +public: + struct AudioSession { + uint32_t id; + std::string app_id; + int sample_rate; + int channels; + bool active; + std::chrono::steady_clock::time_point started_at; + size_t samples_recorded = 0; + }; + + std::optional StartRecording(LuaSandbox* sandbox, + const AudioOptions& opts) { + // 1. Permission check + if (!sandbox->HasPermission("microphone")) { + AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, + "microphone", false}); + return std::nullopt; + } + + // 2. User gesture check + if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) { + return std::nullopt; + } + + // 3. Exclusive access + if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) { + return std::nullopt; + } + + // 4. Background recording check + if (!m_policy.allow_background_recording && !sandbox->IsAppForeground()) { + return std::nullopt; + } + + // 5. Validate options + int sample_rate = std::min(opts.sample_rate, m_policy.max_sample_rate); + + AudioSession session{ + .id = ++m_next_session_id, + .app_id = sandbox->app_id(), + .sample_rate = sample_rate, + .channels = opts.channels, + .active = true, + .started_at = std::chrono::steady_clock::now() + }; + + m_active_session = session; + + // 6. Show indicator + if (m_policy.show_indicator) { + ShowMicrophoneIndicator(true); + } + + // 7. Start hardware recording + m_hardware_mic->Start(sample_rate, opts.channels, + [this, session_id = session.id](AudioBuffer buffer) { + OnAudioReceived(session_id, buffer); + }); + + AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess, + "recording started", true}); + + return session.id; + } + + void StopRecording(LuaSandbox* sandbox, uint32_t session_id) { + if (!m_active_session || m_active_session->id != session_id || + m_active_session->app_id != sandbox->app_id()) { + return; + } + + m_hardware_mic->Stop(); + + // Minimum indicator duration + auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; + if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) { + std::this_thread::sleep_for( + std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed); + } + + ShowMicrophoneIndicator(false); + m_active_session.reset(); + + AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess, + "recording stopped", true}); + } + +private: + void OnAudioReceived(uint32_t session_id, AudioBuffer buffer) { + if (!m_active_session || m_active_session->id != session_id) { + return; + } + + // Check duration limit + auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; + if (elapsed > std::chrono::milliseconds(m_policy.max_duration_ms)) { + // Auto-stop after max duration + StopRecordingInternal(session_id); + return; + } + + m_active_session->samples_recorded += buffer.samples; + + // Deliver to app callback + if (m_audio_callback) { + m_audio_callback(buffer); + } + } + + std::optional m_active_session; + MicrophonePolicy m_policy; + IHardwareMicrophone* m_hardware_mic; + uint32_t m_next_session_id = 0; +}; +``` + +### Microphone Lua API + +```lua +-- Request permission +local granted = permissions.request("microphone") +if not granted then return end + +-- Start recording +local session = microphone.start({ + sample_rate = 44100, + channels = 1 +}) + +-- Receive audio data +session:onAudio(function(buffer) + -- buffer.samples: number of samples + -- buffer.data: base64 encoded PCM data +end) + +-- Stop recording +session:stop() + +-- Get recorded audio +local audio = session:getRecording() +storage.write("/data/recordings/voice.wav", audio) +``` + +--- + +## Speaker/Audio Output Security + +### Audio Output Policy + +**Status**: Designed + +```cpp +struct AudioOutputPolicy { + float max_volume = 1.0f; + int max_concurrent_sounds = 8; + int max_sound_duration_ms = 300000; // 5 minutes + size_t max_audio_data_size = 50 * 1024 * 1024; // 50 MB + bool require_audio_focus = true; // Respect other apps +}; + +class AudioOutputManager { +public: + std::optional PlaySound(LuaSandbox* sandbox, + const AudioData& audio, + const PlaybackOptions& opts) { + // No dangerous permission needed for audio output, + // but still track and limit + + // 1. Validate audio data + if (audio.size() > m_policy.max_audio_data_size) { + return std::nullopt; + } + + // 2. Check concurrent sounds limit + int app_sounds = CountAppSounds(sandbox->app_id()); + if (app_sounds >= m_policy.max_concurrent_sounds) { + return std::nullopt; + } + + // 3. Clamp volume + float volume = std::clamp(opts.volume, 0.0f, m_policy.max_volume); + + // 4. Request audio focus + if (m_policy.require_audio_focus) { + if (!RequestAudioFocus(sandbox->app_id())) { + return std::nullopt; + } + } + + // 5. Create sound instance + Sound sound{ + .id = ++m_next_id, + .app_id = sandbox->app_id(), + .volume = volume, + .looping = opts.loop, + .started_at = std::chrono::steady_clock::now() + }; + + m_sounds[sound.id] = sound; + m_hardware_audio->Play(audio, volume, opts.loop); + + return sound.id; + } + + void StopSound(LuaSandbox* sandbox, uint32_t sound_id) { + auto it = m_sounds.find(sound_id); + if (it == m_sounds.end() || it->second.app_id != sandbox->app_id()) { + return; + } + + m_hardware_audio->Stop(sound_id); + m_sounds.erase(it); + } + + void StopAllSounds(const std::string& app_id) { + for (auto it = m_sounds.begin(); it != m_sounds.end(); ) { + if (it->second.app_id == app_id) { + m_hardware_audio->Stop(it->first); + it = m_sounds.erase(it); + } else { + ++it; + } + } + } + +private: + std::unordered_map m_sounds; + AudioOutputPolicy m_policy; + IHardwareAudio* m_hardware_audio; + uint32_t m_next_id = 0; +}; +``` + +### Audio Lua API + +```lua +-- Play sound (no permission required) +local sound = audio.play("/data/sounds/notification.wav", { + volume = 0.8, + loop = false +}) + +-- Control playback +sound:pause() +sound:resume() +sound:stop() + +-- System sounds +audio.playSystem("notification") +audio.playSystem("click") + +-- Vibration (requires nothing or system.vibrate on some platforms) +audio.vibrate(100) -- 100ms +audio.vibrate({100, 50, 100}) -- pattern: vibrate, pause, vibrate +``` + +--- + +## Location Interface Security + +### Location Security Implementation + +**Status**: Designed + +```cpp +struct LocationPolicy { + // Precision control + int coarse_accuracy_meters = 1000; // City-level + int fine_accuracy_meters = 10; // GPS precision + + // Rate limiting + int min_update_interval_ms = 1000; // Max 1 update/sec + int background_update_interval_ms = 60000; // 1/min in background + + // Privacy + bool require_user_gesture_for_first = true; + bool allow_background_updates = false; + int max_cached_locations = 100; +}; + +class LocationManager { +public: + std::optional GetLocation(LuaSandbox* sandbox, LocationAccuracy accuracy) { + // 1. Permission check + std::string perm = (accuracy == LocationAccuracy::Fine) + ? "location.fine" : "location.coarse"; + + if (!sandbox->HasPermission(perm)) { + AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, + perm, false}); + return std::nullopt; + } + + // 2. Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "location")) { + return std::nullopt; + } + + // 3. Get location from hardware + auto location = m_hardware_location->GetCurrent(); + if (!location) return std::nullopt; + + // 4. Apply precision reduction for coarse + if (accuracy == LocationAccuracy::Coarse) { + location = ReducePrecision(*location, m_policy.coarse_accuracy_meters); + } + + AuditLog::Log({sandbox->app_id(), AuditEvent::LocationAccess, + "single location request", true}); + + return location; + } + + std::optional WatchLocation(LuaSandbox* sandbox, + LocationAccuracy accuracy, + LocationCallback callback) { + std::string perm = (accuracy == LocationAccuracy::Fine) + ? "location.fine" : "location.coarse"; + + if (!sandbox->HasPermission(perm)) { + return std::nullopt; + } + + // Background check + if (!sandbox->IsAppForeground() && !m_policy.allow_background_updates) { + return std::nullopt; + } + + uint32_t watch_id = ++m_next_watch_id; + + LocationWatch watch{ + .id = watch_id, + .app_id = sandbox->app_id(), + .accuracy = accuracy, + .callback = callback, + .last_update = std::chrono::steady_clock::time_point::min() + }; + + m_watches[watch_id] = watch; + + // Start hardware updates if first watch + if (m_watches.size() == 1) { + m_hardware_location->StartUpdates([this](Location loc) { + OnLocationUpdate(loc); + }); + } + + return watch_id; + } + + void ClearWatch(LuaSandbox* sandbox, uint32_t watch_id) { + auto it = m_watches.find(watch_id); + if (it == m_watches.end() || it->second.app_id != sandbox->app_id()) { + return; + } + + m_watches.erase(it); + + if (m_watches.empty()) { + m_hardware_location->StopUpdates(); + } + } + +private: + Location ReducePrecision(const Location& loc, int meters) { + // Round coordinates to reduce precision + // 0.01 degree ≈ 1.1km at equator + double precision = meters / 111000.0; // degrees per meter + + Location reduced = loc; + reduced.latitude = std::round(loc.latitude / precision) * precision; + reduced.longitude = std::round(loc.longitude / precision) * precision; + reduced.accuracy = std::max(loc.accuracy, static_cast(meters)); + return reduced; + } + + void OnLocationUpdate(Location loc) { + auto now = std::chrono::steady_clock::now(); + + for (auto& [id, watch] : m_watches) { + // Check update interval + int interval = m_policy.min_update_interval_ms; + auto elapsed = now - watch.last_update; + if (elapsed < std::chrono::milliseconds(interval)) { + continue; + } + + // Apply precision reduction + Location delivered = loc; + if (watch.accuracy == LocationAccuracy::Coarse) { + delivered = ReducePrecision(loc, m_policy.coarse_accuracy_meters); + } + + watch.last_update = now; + watch.callback(delivered); + } + } + + std::unordered_map m_watches; + LocationPolicy m_policy; + IHardwareLocation* m_hardware_location; + RateLimiter m_rate_limiter; + uint32_t m_next_watch_id = 0; +}; +``` + +### Location Lua API + +```lua +-- Request permission +local granted = permissions.request("location.fine") -- or "location.coarse" + +-- Get current location +local loc = location.get() +if loc then + print(loc.latitude, loc.longitude, loc.accuracy) +end + +-- Watch location changes +local watch_id = location.watch(function(loc) + print("Moved to:", loc.latitude, loc.longitude) +end, { + accuracy = "fine", -- "fine" or "coarse" + interval = 5000 -- minimum ms between updates +}) + +-- Stop watching +location.clearWatch(watch_id) +``` + +--- + +## Sensor Interface Security + +### Sensor Security (Accelerometer, Gyroscope, etc.) + +**Status**: Designed + +```cpp +struct SensorPolicy { + // Rate limiting (prevent fingerprinting via sensor noise patterns) + int max_sample_rate_hz = 60; + + // Precision reduction + float accelerometer_precision = 0.01f; // m/s² + float gyroscope_precision = 0.001f; // rad/s + + // Background access + bool allow_background_sensors = false; + + // Body sensors (heart rate, etc.) require special permission + std::vector body_sensors = {"heart_rate", "blood_pressure"}; +}; + +class SensorManager { +public: + std::optional RegisterListener(LuaSandbox* sandbox, + SensorType type, + SensorCallback callback) { + // Body sensors require dangerous permission + if (IsBodySensor(type) && !sandbox->HasPermission("sensors.body")) { + return std::nullopt; + } + + // Background check + if (!sandbox->IsAppForeground() && !m_policy.allow_background_sensors) { + return std::nullopt; + } + + uint32_t listener_id = ++m_next_listener_id; + + SensorListener listener{ + .id = listener_id, + .app_id = sandbox->app_id(), + .type = type, + .callback = callback, + .last_reading = std::chrono::steady_clock::time_point::min() + }; + + m_listeners[listener_id] = listener; + + // Start sensor if first listener + EnableSensorIfNeeded(type); + + return listener_id; + } + + void UnregisterListener(LuaSandbox* sandbox, uint32_t listener_id) { + auto it = m_listeners.find(listener_id); + if (it == m_listeners.end() || it->second.app_id != sandbox->app_id()) { + return; + } + + SensorType type = it->second.type; + m_listeners.erase(it); + + // Stop sensor if no more listeners + DisableSensorIfUnneeded(type); + } + +private: + void OnSensorReading(SensorType type, SensorReading reading) { + auto now = std::chrono::steady_clock::now(); + int min_interval_ms = 1000 / m_policy.max_sample_rate_hz; + + for (auto& [id, listener] : m_listeners) { + if (listener.type != type) continue; + + // Rate limit + auto elapsed = now - listener.last_reading; + if (elapsed < std::chrono::milliseconds(min_interval_ms)) { + continue; + } + + // Apply precision reduction + SensorReading reduced = ReducePrecision(reading, type); + + listener.last_reading = now; + listener.callback(reduced); + } + } + + SensorReading ReducePrecision(const SensorReading& reading, SensorType type) { + SensorReading reduced = reading; + + float precision = 0.01f; + switch (type) { + case SensorType::Accelerometer: + precision = m_policy.accelerometer_precision; + break; + case SensorType::Gyroscope: + precision = m_policy.gyroscope_precision; + break; + default: + break; + } + + reduced.x = std::round(reading.x / precision) * precision; + reduced.y = std::round(reading.y / precision) * precision; + reduced.z = std::round(reading.z / precision) * precision; + + return reduced; + } + + std::unordered_map m_listeners; + SensorPolicy m_policy; + uint32_t m_next_listener_id = 0; +}; +``` + +### Sensor Lua API + +```lua +-- Motion sensors (no special permission) +local accel_id = sensors.listen("accelerometer", function(reading) + print("Acceleration:", reading.x, reading.y, reading.z) +end) + +local gyro_id = sensors.listen("gyroscope", function(reading) + print("Rotation:", reading.x, reading.y, reading.z) +end) + +-- Stop listening +sensors.unlisten(accel_id) +sensors.unlisten(gyro_id) + +-- Body sensors (requires "sensors.body" permission) +local granted = permissions.request("sensors.body") +if granted then + sensors.listen("heart_rate", function(reading) + print("Heart rate:", reading.value, "bpm") + end) +end +``` + +--- + +## Bluetooth Interface Security + +### Bluetooth Security Implementation + +**Status**: Designed + +```cpp +struct BluetoothPolicy { + // Discovery + bool allow_discovery = true; + int discovery_timeout_seconds = 30; + int max_discovered_devices = 50; + + // Connections + int max_connections_per_app = 3; + bool require_pairing_consent = true; + + // Data + size_t max_transfer_size = 1 * 1024 * 1024; // 1 MB +}; + +class BluetoothManager { +public: + // Discover nearby devices + std::optional StartDiscovery(LuaSandbox* sandbox, + DiscoveryCallback callback) { + if (!sandbox->HasPermission("bluetooth")) { + return std::nullopt; + } + + // Only one discovery at a time per app + if (HasActiveDiscovery(sandbox->app_id())) { + return std::nullopt; + } + + uint32_t discovery_id = ++m_next_discovery_id; + + Discovery discovery{ + .id = discovery_id, + .app_id = sandbox->app_id(), + .callback = callback, + .devices = {}, + .started_at = std::chrono::steady_clock::now() + }; + + m_discoveries[discovery_id] = discovery; + + // Start hardware discovery with timeout + m_hardware_bt->StartDiscovery( + m_policy.discovery_timeout_seconds, + [this, discovery_id](BluetoothDevice device) { + OnDeviceDiscovered(discovery_id, device); + }); + + return discovery_id; + } + + // Connect to device + std::optional Connect(LuaSandbox* sandbox, + const std::string& device_address) { + if (!sandbox->HasPermission("bluetooth")) { + return std::nullopt; + } + + // Connection limit + if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) { + return std::nullopt; + } + + // Require user consent for pairing + if (m_policy.require_pairing_consent) { + if (!ShowPairingDialog(device_address)) { + return std::nullopt; + } + } + + // Connect + auto conn = m_hardware_bt->Connect(device_address); + if (!conn) return std::nullopt; + + uint32_t conn_id = ++m_next_conn_id; + + BluetoothConnection connection{ + .id = conn_id, + .app_id = sandbox->app_id(), + .device_address = device_address, + .hardware_handle = *conn + }; + + m_connections[conn_id] = connection; + return conn_id; + } + +private: + void OnDeviceDiscovered(uint32_t discovery_id, BluetoothDevice device) { + auto it = m_discoveries.find(discovery_id); + if (it == m_discoveries.end()) return; + + // Limit discovered devices + if (it->second.devices.size() >= m_policy.max_discovered_devices) { + return; + } + + it->second.devices.push_back(device); + it->second.callback(device); + } + + std::unordered_map m_discoveries; + std::unordered_map m_connections; + BluetoothPolicy m_policy; + IHardwareBluetooth* m_hardware_bt; + uint32_t m_next_discovery_id = 0; + uint32_t m_next_conn_id = 0; +}; +``` + +### Bluetooth Lua API + +```lua +-- Request permission +local granted = permissions.request("bluetooth") +if not granted then return end + +-- Discover devices +local discovery = bluetooth.discover(function(device) + print("Found:", device.name, device.address) +end) + +-- Stop discovery +discovery:stop() + +-- Connect to device (shows pairing dialog) +local conn = bluetooth.connect("AA:BB:CC:DD:EE:FF") +if conn then + -- Send data + conn:send("Hello") + + -- Receive data + conn:onData(function(data) + print("Received:", data) + end) + + -- Disconnect + conn:close() +end +``` + +--- + +## Contacts Interface Security + +### Contacts Security Implementation + +**Status**: Designed + +```cpp +struct ContactsPolicy { + // Access scope + bool allow_read_all = true; + bool allow_write = true; + bool allow_delete = true; + + // Rate limits + int max_contacts_per_query = 1000; + int max_writes_per_minute = 100; + + // Privacy + bool require_purpose_string = true; // App must explain why + bool log_all_access = true; +}; + +class ContactsManager { +public: + std::optional> GetContacts(LuaSandbox* sandbox, + const ContactQuery& query) { + if (!sandbox->HasPermission("contacts.read")) { + return std::nullopt; + } + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_read")) { + return std::nullopt; + } + + // Query contacts + auto contacts = m_contacts_store->Query(query); + + // Limit results + if (contacts.size() > m_policy.max_contacts_per_query) { + contacts.resize(m_policy.max_contacts_per_query); + } + + AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, + "read " + std::to_string(contacts.size()) + " contacts", true}); + + return contacts; + } + + bool CreateContact(LuaSandbox* sandbox, const Contact& contact) { + if (!sandbox->HasPermission("contacts.write")) { + return false; + } + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) { + return false; + } + + // Validate contact data + if (!ValidateContact(contact)) { + return false; + } + + bool success = m_contacts_store->Create(contact); + + AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, + "create contact: " + contact.name, success}); + + return success; + } + + bool DeleteContact(LuaSandbox* sandbox, const std::string& contact_id) { + if (!sandbox->HasPermission("contacts.write")) { + return false; + } + + // Rate limit + if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) { + return false; + } + + bool success = m_contacts_store->Delete(contact_id); + + AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, + "delete contact: " + contact_id, success}); + + return success; + } + +private: + bool ValidateContact(const Contact& contact) { + // Name required + if (contact.name.empty()) return false; + if (contact.name.length() > 256) return false; + + // Validate phone numbers + for (const auto& phone : contact.phones) { + if (phone.number.length() > 50) return false; + } + + // Validate emails + for (const auto& email : contact.emails) { + if (!IsValidEmail(email.address)) return false; + } + + return true; + } + + ContactsPolicy m_policy; + IContactsStore* m_contacts_store; + RateLimiter m_rate_limiter; +}; +``` + +### Contacts Lua API + +```lua +-- Request permission +local granted = permissions.request("contacts.read") + +-- Query contacts +local contacts = contacts.getAll() +for _, c in ipairs(contacts) do + print(c.name, c.phones[1], c.emails[1]) +end + +-- Search contacts +local results = contacts.search("John") + +-- Get single contact +local contact = contacts.get(contact_id) + +-- Create contact (requires contacts.write) +contacts.create({ + name = "John Doe", + phones = {{type = "mobile", number = "+1234567890"}}, + emails = {{type = "home", address = "john@example.com"}} +}) + +-- Update contact +contacts.update(contact_id, { + name = "John Smith" +}) + +-- Delete contact +contacts.delete(contact_id) +``` + +--- + +## Implementation Status - Virtual Hardware + +| Interface | Design | Implementation | Testing | +|-----------|--------|----------------|---------| +| Network HTTP | ✅ Complete | ❌ Not started | ❌ | +| Network WebSocket | ✅ Complete | ❌ Not started | ❌ | +| Filesystem | ✅ Complete | ❌ Not started | ❌ | +| Database | ✅ Complete | ❌ Not started | ❌ | +| Camera | ✅ Complete | ❌ Not started | ❌ | +| Microphone | ✅ Complete | ❌ Not started | ❌ | +| Speaker/Audio | ✅ Complete | ❌ Not started | ❌ | +| Location | ✅ Complete | ❌ Not started | ❌ | +| Sensors | ✅ Complete | ❌ Not started | ❌ | +| Bluetooth | ✅ Complete | ❌ Not started | ❌ | +| Contacts | ✅ Complete | ❌ Not started | ❌ | + +--- + +## References + +- [Lua 5.4 Manual - Sandboxing](https://www.lua.org/manual/5.4/manual.html#4.6) +- [Sandboxing Lua](http://lua-users.org/wiki/SandBoxes) +- [Lapis Lua Sandbox](https://github.com/leafo/lapis/blob/master/lapis/util/sandbox.moon) +- [Cloudflare Workers Lua Isolation](https://blog.cloudflare.com/cloudflare-workers-unleashed/) +- [OWASP Sandboxing Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Sandboxing_Cheat_Sheet.html) +- [Android Permission Model](https://developer.android.com/guide/topics/permissions/overview) +- [iOS App Sandbox Design Guide](https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/) +- [SQLite Security](https://www.sqlite.org/security.html) + +--- + +*Last updated: 2026-01-18* diff --git a/SANDBOX_MILESTONES.md b/SANDBOX_MILESTONES.md new file mode 100644 index 0000000..56c97f0 --- /dev/null +++ b/SANDBOX_MILESTONES.md @@ -0,0 +1,964 @@ +# Sandbox Implementation Milestones + +Based on SANDBOX.md design document. Implementation order optimized for dependencies. + +--- + +## Milestone 1: Core Sandbox Foundation ✅ + +**Status**: Complete +**Goal**: Create isolated Lua environments with resource limits. +**Estimated Files**: 3 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| LuaSandbox class | `src/main/cpp/sandbox/lua_sandbox.h` | Main sandbox container | +| Custom allocator | `src/main/cpp/sandbox/lua_sandbox.cpp` | Per-app memory tracking/limits | +| Instruction hook | `src/main/cpp/sandbox/lua_sandbox.cpp` | CPU limit enforcement | +| Globals removal | `src/main/cpp/sandbox/lua_sandbox.cpp` | Remove os, io, debug, etc. | +| Bytecode prevention | `src/main/cpp/sandbox/lua_sandbox.cpp` | Text-only loading mode | +| Metatable protection | `src/main/cpp/sandbox/lua_sandbox.cpp` | Freeze _G and string metatable | + +### Implementation Tasks + +1. Create `LuaSandbox` class with: + - `lua_State* m_L` - isolated state + - `SandboxLimits m_limits` - memory/CPU/stack limits + - `SandboxContext m_context` - app_id, permissions, etc. + - Custom allocator that tracks `m_memory_used` + +2. Implement `RemoveDangerousGlobals()`: + - Remove: `os`, `io`, `debug`, `package`, `require`, `ffi`, `jit` + - Remove: `dofile`, `loadfile`, `load`, `loadstring` + - Remove: `rawget`, `rawset`, `rawequal`, `rawlen` + - Remove: `collectgarbage`, `newproxy` + - Remove: `string.dump` + +3. Implement instruction hook: + - `lua_sethook(L, InstructionHook, LUA_MASKCOUNT, 1000)` + - Throw error when limit exceeded + +4. Implement `ProtectBuiltinTables()`: + - Set `__metatable` on string metatable + - Freeze `_G` with `__newindex` error + +### Dependencies + +- Lua library already linked (via RmlUi) + +### Test Criteria + +```cpp +TEST(LuaSandbox, DangerousGlobalsRemoved); +TEST(LuaSandbox, BytecodeRejected); +TEST(LuaSandbox, MemoryLimitEnforced); +TEST(LuaSandbox, CPULimitEnforced); +TEST(LuaSandbox, MetatableProtected); +``` + +--- + +## Milestone 2: Permission System ✅ + +**Status**: Complete +**Goal**: Gate API access based on app permissions. +**Estimated Files**: 2 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| PermissionGate | `src/main/cpp/sandbox/permission_gate.h` | Permission checking | +| User gesture tracking | `src/main/cpp/sandbox/permission_gate.cpp` | Recent tap/click detection | +| Permission categories | `src/main/cpp/sandbox/permission_gate.cpp` | Normal/Dangerous/Signature | + +### Implementation Tasks + +1. Define permission categories: + ```cpp + enum class PermissionCategory { Normal, Dangerous, Signature }; + ``` + +2. Create `PermissionGate` class: + - `bool Check(lua_State* L, const std::string& permission)` + - `int RequirePermission(lua_State* L, const char* perm)` - throws Lua error + +3. Implement user gesture tracking: + - `void RecordUserGesture()` - called on touch down + - `bool HasRecentUserGesture(int ms)` - check within time window + +4. Define permission manifest parsing: + - Read `manifest.json` from app package + - Extract `permissions` array + +### Dependencies + +- Milestone 1 (LuaSandbox) + +### Test Criteria + +```cpp +TEST(PermissionGate, NormalPermissionAutoGranted); +TEST(PermissionGate, DangerousPermissionRequiresGrant); +TEST(PermissionGate, SignaturePermissionSystemOnly); +TEST(PermissionGate, UserGestureRequired); +``` + +--- + +## Milestone 3: Audit Logging & Rate Limiting ✅ + +**Status**: Complete +**Goal**: Track security events and prevent API abuse. +**Estimated Files**: 2 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| AuditLog | `src/main/cpp/sandbox/audit_log.h/cpp` | Security event logging | +| RateLimiter | `src/main/cpp/sandbox/rate_limiter.h/cpp` | Token bucket rate limiting | + +### Implementation Tasks + +1. Create `AuditLog` class: + ```cpp + enum class AuditEvent { + AppStart, AppStop, PermissionCheck, PermissionDenied, + NetworkRequest, FileAccess, SandboxViolation, ResourceLimitHit + }; + ``` + +2. Implement thread-safe logging: + - Ring buffer with max entries + - Log critical events to Android logcat + +3. Create `RateLimiter` class: + - Token bucket algorithm + - Per-operation limits: `network.request`, `storage.write`, etc. + - `bool Check(const std::string& app_id, const std::string& operation)` + +### Dependencies + +- Milestone 1 (LuaSandbox) + +### Test Criteria + +```cpp +TEST(AuditLog, LogsSecurityEvents); +TEST(AuditLog, ThreadSafe); +TEST(RateLimiter, EnforcesLimits); +TEST(RateLimiter, RefillsOverTime); +``` + +--- + +## Milestone 4: Safe Path & Require ✅ + +**Status**: Complete +**Goal**: Secure file access within app sandbox. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| Path validation | `src/main/cpp/sandbox/path_sandbox.h/cpp` | Canonical path checking | +| Safe require | `src/main/cpp/sandbox/path_sandbox.cpp` | Module loader from app/scripts/ | + +### Implementation Tasks + +1. Implement `ValidatePath()`: + - Reject `..` traversal + - Reject absolute paths + - Canonicalize and verify prefix match + +2. Implement `SafeRequire()`: + - Validate module name (alphanumeric + underscore) + - Load from `app_path/scripts/name.lua` + - Cache in registry `__loaded` + - Text-only mode (`"t"`) + +3. Register as global `require`: + ```cpp + lua_pushcfunction(L, SafeRequire); + lua_setglobal(L, "require"); + ``` + +### Dependencies + +- Milestone 1 (LuaSandbox) + +### Test Criteria + +```cpp +TEST(PathSandbox, RejectsTraversal); +TEST(PathSandbox, RejectsAbsolutePaths); +TEST(SafeRequire, LoadsFromScriptsDir); +TEST(SafeRequire, CachesModules); +``` + +--- + +## Milestone 5: Timer & Callback System + +**Goal**: Safe timer APIs managed by kernel. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| TimerManager | `src/main/cpp/sandbox/timer_manager.h/cpp` | setTimeout/setInterval | + +### Implementation Tasks + +1. Create `TimerManager` class: + - Priority queue of pending timers + - Per-app timer limits (max 100) + - Minimum 10ms granularity + +2. Implement Lua APIs: + ```lua + setTimeout(callback, ms) -> id + clearTimeout(id) + setInterval(callback, ms) -> id + clearInterval(id) + ``` + +3. Integrate with kernel main loop: + - Check and fire timers each frame + - Reset instruction count before callback + +4. Implement cleanup: + - `ClearAppTimers(app_id)` on app stop + +### Dependencies + +- Milestone 1 (LuaSandbox) +- Milestone 3 (RateLimiter) + +### Test Criteria + +```cpp +TEST(TimerManager, FiresTimeout); +TEST(TimerManager, FiresInterval); +TEST(TimerManager, LimitsPerApp); +TEST(TimerManager, CleansUpOnStop); +``` + +--- + +## Milestone 6: JSON & Crypto APIs + +**Goal**: Safe data parsing and cryptographic primitives. +**Estimated Files**: 2 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| JSON API | `src/main/cpp/sandbox/json_api.cpp` | Safe encode/decode | +| Crypto API | `src/main/cpp/sandbox/crypto_api.cpp` | Hash, random, HMAC | + +### Implementation Tasks + +1. Implement `json.decode()`: + - Depth limit (32) + - String length limit (1 MB) + - Array/object size limits + +2. Implement `json.encode()`: + - Cycle detection + - Output size limit + +3. Implement crypto APIs: + - `crypto.randomBytes(n)` - CSPRNG + - `crypto.hash("sha256", data)` + - `crypto.hmac("sha256", key, data)` + - Remove `math.randomseed`, replace `math.random` with per-app RNG + +### Dependencies + +- Milestone 1 (LuaSandbox) +- nlohmann-json (already in vcpkg) + +### Test Criteria + +```cpp +TEST(JsonApi, DecodesValid); +TEST(JsonApi, RejectsDeepNesting); +TEST(JsonApi, RejectsTooLarge); +TEST(CryptoApi, RandomBytesSecure); +TEST(CryptoApi, HashCorrect); +``` + +--- + +## Milestone 7: Virtual Filesystem + +**Goal**: Per-app isolated storage with quotas. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| VirtualFS | `src/main/cpp/sandbox/virtual_fs.h/cpp` | App storage API | + +### Implementation Tasks + +1. Define virtual path mapping: + ``` + /data/ → /data/data/com.omixlab.mosis/apps//data/ + /cache/ → /data/data/com.omixlab.mosis/apps//cache/ + /temp/ → (session-only, cleared on stop) + /shared/ → requires storage.shared permission + ``` + +2. Implement Lua file API: + ```lua + fs.read(path) -> string + fs.write(path, data) -> bool + fs.append(path, data) -> bool + fs.delete(path) -> bool + fs.exists(path) -> bool + fs.list(dir) -> table + fs.mkdir(path) -> bool + fs.stat(path) -> {size, modified, isDir} + ``` + +3. Enforce limits: + - Per-app quota (50 MB default) + - Max file size (10 MB) + - Max path depth (10) + +4. Cleanup `/temp/` on app stop. + +### Dependencies + +- Milestone 1 (LuaSandbox) +- Milestone 2 (PermissionGate) +- Milestone 4 (Path validation) + +### Test Criteria + +```cpp +TEST(VirtualFS, ReadsWritesInAppDir); +TEST(VirtualFS, BlocksTraversal); +TEST(VirtualFS, EnforcesQuota); +TEST(VirtualFS, CleansUpTemp); +``` + +--- + +## Milestone 8: SQLite Database + +**Goal**: Per-app SQLite with injection prevention. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| DatabaseManager | `src/main/cpp/sandbox/database_manager.h/cpp` | SQLite sandbox | + +### Implementation Tasks + +1. Create per-app database: + - Location: `/data/data/com.omixlab.mosis/apps//db/app.db` + - Max database size: 50 MB + +2. Implement SQLite authorizer: + ```cpp + sqlite3_set_authorizer(db, Authorizer, sandbox); + // Block: ATTACH, DETACH, dangerous PRAGMAs, load_extension + ``` + +3. Implement Lua API: + ```lua + local db = database.open("mydata") + db:execute("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)") + db:execute("INSERT INTO items (name) VALUES (?)", {"Item 1"}) + local rows = db:query("SELECT * FROM items WHERE id > ?", {0}) + db:close() + ``` + +4. Enforce prepared statements only (no raw SQL interpolation). + +### Dependencies + +- Milestone 1 (LuaSandbox) +- Milestone 2 (PermissionGate) +- SQLite3 (add to vcpkg) + +### Test Criteria + +```cpp +TEST(Database, CreatesTables); +TEST(Database, PreparedStatements); +TEST(Database, BlocksAttach); +TEST(Database, BlocksDangerousPragma); +``` + +--- + +## Milestone 9: Network - HTTP + +**Goal**: Secure HTTP requests with domain filtering. +**Estimated Files**: 2 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| HttpRequestValidator | `src/main/cpp/sandbox/http_validator.h/cpp` | URL/domain validation | +| NetworkManager | `src/main/cpp/sandbox/network_manager.h/cpp` | HTTP client wrapper | + +### Implementation Tasks + +1. Implement URL parsing and validation: + - Require HTTPS + - Block private IPs, localhost, metadata IPs + - Domain whitelist from manifest + +2. Implement Lua HTTP API: + ```lua + local response = network.request({ + url = "https://api.example.com/data", + method = "POST", + headers = {...}, + body = "...", + timeout = 30000 + }) + ``` + +3. Integrate with Android HttpURLConnection or libcurl. + +4. Enforce limits: + - Request body: 10 MB + - Response body: 50 MB + - Timeout: 60s + - Concurrent requests: 6 + +### Dependencies + +- Milestone 1 (LuaSandbox) +- Milestone 2 (PermissionGate) +- Milestone 3 (RateLimiter, AuditLog) + +### Test Criteria + +```cpp +TEST(HttpValidator, BlocksPrivateIP); +TEST(HttpValidator, BlocksLocalhost); +TEST(HttpValidator, RequiresHttps); +TEST(HttpValidator, EnforcesDomainWhitelist); +TEST(NetworkManager, MakesRequest); +``` + +--- + +## Milestone 10: Network - WebSocket + +**Goal**: Secure WebSocket connections. +**Estimated Files**: 1 new file (extends NetworkManager) + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| WebSocketManager | `src/main/cpp/sandbox/websocket_manager.h/cpp` | WS connections | + +### Implementation Tasks + +1. Implement connection management: + - Max 5 connections per app + - Idle timeout: 5 minutes + - Message size limit: 1 MB + +2. Implement Lua WebSocket API: + ```lua + local ws = network.websocket("wss://api.example.com/ws") + ws:on("open", function() ... end) + ws:on("message", function(data) ... end) + ws:on("close", function(code, reason) ... end) + ws:send(data) + ws:close() + ``` + +3. Use same URL validation as HTTP. + +4. Cleanup connections on app stop. + +### Dependencies + +- Milestone 9 (HttpRequestValidator) + +### Test Criteria + +```cpp +TEST(WebSocketManager, Connects); +TEST(WebSocketManager, LimitsPerApp); +TEST(WebSocketManager, CleansUpOnStop); +``` + +--- + +## Milestone 11: Virtual Hardware - Camera + +**Goal**: Camera access with mandatory indicators. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| CameraInterface | `src/main/cpp/sandbox/camera_interface.h/cpp` | Camera API | + +### Implementation Tasks + +1. Implement session management: + - `startSession(options)` - requires permission + user gesture + - `capturePhoto()` - returns frame data + - `stopSession()` + +2. Mandatory recording indicator: + - Show in system UI (cannot be hidden by app) + +3. Implement Lua API: + ```lua + local session = camera.start({facing = "back", resolution = "720p"}) + session:on("frame", function(data) ... end) + local photo = session:capture() + session:stop() + ``` + +4. Rate limit: 1 session at a time, max 30 fps. + +### Dependencies + +- Milestone 2 (PermissionGate + user gesture) +- Milestone 3 (AuditLog) +- Game engine camera bridge (future) + +### Test Criteria + +```cpp +TEST(CameraInterface, RequiresPermission); +TEST(CameraInterface, RequiresUserGesture); +TEST(CameraInterface, ShowsIndicator); +TEST(CameraInterface, StopsOnAppStop); +``` + +--- + +## Milestone 12: Virtual Hardware - Microphone + +**Goal**: Audio recording with mandatory indicators. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| MicrophoneInterface | `src/main/cpp/sandbox/microphone_interface.h/cpp` | Mic API | + +### Implementation Tasks + +1. Implement recording session: + - `startRecording(options)` - requires permission + user gesture + - `stopRecording()` - returns audio data + +2. Mandatory recording indicator (same as camera). + +3. Implement Lua API: + ```lua + local recording = microphone.start({sampleRate = 44100, channels = 1}) + recording:on("data", function(samples) ... end) + local audio = recording:stop() + ``` + +4. Rate limit: 1 recording at a time. + +### Dependencies + +- Milestone 2 (PermissionGate + user gesture) +- Milestone 3 (AuditLog) + +### Test Criteria + +```cpp +TEST(MicrophoneInterface, RequiresPermission); +TEST(MicrophoneInterface, RequiresUserGesture); +TEST(MicrophoneInterface, ShowsIndicator); +``` + +--- + +## Milestone 13: Virtual Hardware - Audio Output + +**Goal**: Safe audio playback. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| AudioOutputInterface | `src/main/cpp/sandbox/audio_output.h/cpp` | Speaker API | + +### Implementation Tasks + +1. Implement playback: + - `play(audioData, options)` + - `stop()` + - `setVolume(0.0-1.0)` + +2. System volume limit (app cannot exceed system volume). + +3. Implement Lua API: + ```lua + local player = audio.play(soundData, {loop = false, volume = 0.8}) + player:on("ended", function() ... end) + player:stop() + ``` + +4. Concurrent sound limit: 10 per app. + +### Dependencies + +- Milestone 1 (LuaSandbox) + +### Test Criteria + +```cpp +TEST(AudioOutput, PlaysSound); +TEST(AudioOutput, RespectsVolumeLimit); +TEST(AudioOutput, LimitsConcurrent); +``` + +--- + +## Milestone 14: Virtual Hardware - Location + +**Goal**: Location with privacy controls. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| LocationInterface | `src/main/cpp/sandbox/location_interface.h/cpp` | GPS API | + +### Implementation Tasks + +1. Two permission levels: + - `location.coarse` - city-level (1km accuracy) + - `location.fine` - precise GPS + +2. Implement Lua API: + ```lua + location.getCurrentPosition(function(pos) + print(pos.latitude, pos.longitude, pos.accuracy) + end) + + local watch = location.watchPosition(function(pos) ... end, { + enableHighAccuracy = true, + timeout = 30000 + }) + watch:stop() + ``` + +3. Rate limit: 1 request per second. + +4. Reduce precision for coarse permission. + +### Dependencies + +- Milestone 2 (PermissionGate) +- Milestone 3 (RateLimiter) + +### Test Criteria + +```cpp +TEST(LocationInterface, RequiresPermission); +TEST(LocationInterface, CoarseReducesPrecision); +TEST(LocationInterface, RateLimits); +``` + +--- + +## Milestone 15: Virtual Hardware - Sensors + +**Goal**: Motion sensors with fingerprinting prevention. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| SensorInterface | `src/main/cpp/sandbox/sensor_interface.h/cpp` | Accelerometer, gyro, etc. | + +### Implementation Tasks + +1. Supported sensors: + - Accelerometer (requires `sensors.motion`) + - Gyroscope (requires `sensors.motion`) + - Magnetometer (requires `sensors.motion`) + - Proximity (auto-granted) + - Ambient light (auto-granted) + +2. Implement Lua API: + ```lua + local sub = sensors.subscribe("accelerometer", function(data) + print(data.x, data.y, data.z, data.timestamp) + end, {frequency = 60}) + sub:stop() + ``` + +3. Reduce precision to prevent fingerprinting: + - Round values to 2 decimal places + - Limit update frequency to 60 Hz + +### Dependencies + +- Milestone 2 (PermissionGate) + +### Test Criteria + +```cpp +TEST(SensorInterface, RequiresPermission); +TEST(SensorInterface, ReducesPrecision); +TEST(SensorInterface, LimitsFrequency); +``` + +--- + +## Milestone 16: Virtual Hardware - Bluetooth + +**Goal**: Bluetooth discovery and pairing. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| BluetoothInterface | `src/main/cpp/sandbox/bluetooth_interface.h/cpp` | BT API | + +### Implementation Tasks + +1. Discovery with user consent: + - `startDiscovery()` requires user confirmation + - Return device name/address only (no full UUID list) + +2. Implement Lua API: + ```lua + bluetooth.startDiscovery(function(devices) + for _, device in ipairs(devices) do + print(device.name, device.address) + end + end, {timeout = 30}) + + bluetooth.connect(address, function(connection) + connection:send(data) + end) + ``` + +3. Limit: 5 connections per app. + +### Dependencies + +- Milestone 2 (PermissionGate + user gesture) + +### Test Criteria + +```cpp +TEST(BluetoothInterface, RequiresPermission); +TEST(BluetoothInterface, RequiresUserConsent); +``` + +--- + +## Milestone 17: Virtual Hardware - Contacts + +**Goal**: Contact access with granular permissions. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| ContactsInterface | `src/main/cpp/sandbox/contacts_interface.h/cpp` | Contacts API | + +### Implementation Tasks + +1. Separate read/write permissions: + - `contacts.read` - query contacts + - `contacts.write` - add/modify contacts + +2. Implement Lua API: + ```lua + local contacts = contacts.query({search = "John"}) + for _, c in ipairs(contacts) do + print(c.name, c.phone, c.email) + end + + contacts.add({name = "New Contact", phone = "555-1234"}) + ``` + +3. Limit fields returned (no raw account data). + +### Dependencies + +- Milestone 2 (PermissionGate) + +### Test Criteria + +```cpp +TEST(ContactsInterface, RequiresReadPermission); +TEST(ContactsInterface, RequiresWritePermission); +``` + +--- + +## Milestone 18: Inter-App Communication + +**Goal**: Kernel-mediated message passing. +**Estimated Files**: 1 new file + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| MessageBus | `src/main/cpp/sandbox/message_bus.h/cpp` | App-to-app messaging | + +### Implementation Tasks + +1. Implement intent system: + - Manifest declares received intents + - Sender must have required permissions + +2. Implement Lua API: + ```lua + -- Sending + intents.send({ + action = "share", + type = "text/plain", + data = "Hello world" + }) + + -- Receiving (in manifest + handler) + intents.on("share", function(intent) + print(intent.from, intent.data) + end) + ``` + +3. Validate sender/receiver at runtime. + +### Dependencies + +- Milestone 2 (PermissionGate) +- Milestone 3 (AuditLog) + +### Test Criteria + +```cpp +TEST(MessageBus, SendsToRegisteredReceiver); +TEST(MessageBus, BlocksUnregisteredAction); +``` + +--- + +## Milestone 19: Security Testing Suite + +**Goal**: Comprehensive test coverage. +**Estimated Files**: 3 new files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| Unit tests | `tests/test_sandbox_security.cpp` | Core sandbox tests | +| Integration tests | `tests/test_sandbox_integration.cpp` | Full system tests | +| Fuzzer | `tests/fuzz_sandbox.cpp` | Random input testing | + +### Implementation Tasks + +1. Write unit tests for all milestones (see individual test criteria). + +2. Write integration tests: + - Full app lifecycle + - Permission flow + - Resource cleanup + +3. Implement fuzzer: + - Generate random Lua code + - Verify no crashes + - Verify sandbox integrity after each run + +4. Security audit checklist (from SANDBOX.md). + +### Dependencies + +- All previous milestones + +--- + +## Milestone 20: Kernel Integration + +**Goal**: Wire sandbox into existing kernel. +**Estimated Files**: Modify existing files + +### Deliverables + +| Component | File | Description | +|-----------|------|-------------| +| App lifecycle | `src/main/cpp/kernel.cpp` | App start/stop | +| Sandbox manager | `src/main/cpp/kernel.cpp` | Multi-app management | + +### Implementation Tasks + +1. Replace global `lua_State` with `LuaSandboxManager`: + - Create sandbox per app + - Route events to correct sandbox + +2. Integrate with RmlUi: + - Bridge RmlUi document events to sandbox + - Replace `Rml::Lua::Interpreter` with sandboxed states + +3. Wire up resource cleanup on app stop. + +### Dependencies + +- Milestones 1-18 + +--- + +## Summary + +| Phase | Milestones | Description | +|-------|------------|-------------| +| **Foundation** | 1-4 | Core sandbox, permissions, logging, paths | +| **Core APIs** | 5-8 | Timers, JSON, crypto, filesystem, database | +| **Network** | 9-10 | HTTP and WebSocket | +| **Hardware** | 11-17 | Camera, mic, audio, location, sensors, BT, contacts | +| **System** | 18-20 | IPC, testing, integration | + +### Recommended Order + +1. **Milestone 1** - Must be first (foundation) +2. **Milestone 2** - Permissions (needed by everything) +3. **Milestone 3** - Audit/rate limiting (needed by APIs) +4. **Milestone 4** - Path security (needed by fs/require) +5. **Milestone 5-6** - Timers, JSON (high value) +6. **Milestone 7-8** - Storage, database (app data) +7. **Milestone 9-10** - Network (app connectivity) +8. **Milestones 11-17** - Hardware (as needed) +9. **Milestone 18** - IPC (multi-app) +10. **Milestone 19-20** - Testing & integration + +### Quick Start + +Begin with **Milestone 1** to establish the core sandbox. This is the foundation everything else builds on. diff --git a/SANDBOX_MILESTONE_1.md b/SANDBOX_MILESTONE_1.md new file mode 100644 index 0000000..750b9d6 --- /dev/null +++ b/SANDBOX_MILESTONE_1.md @@ -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 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) diff --git a/SANDBOX_MILESTONE_2.md b/SANDBOX_MILESTONE_2.md new file mode 100644 index 0000000..4a6a98c --- /dev/null +++ b/SANDBOX_MILESTONE_2.md @@ -0,0 +1,488 @@ +# Milestone 2: Permission System + +**Status**: ✅ Complete +**Goal**: Gate API access based on app permissions. + +--- + +## Overview + +This milestone implements the permission system that controls which APIs an app can access. It defines permission categories (Normal, Dangerous, Signature) and provides mechanisms for checking permissions at runtime. + +### Key Deliverables + +1. **PermissionGate class** - Permission checking and enforcement +2. **Permission categories** - Normal/Dangerous/Signature classification +3. **User gesture tracking** - Detect recent user interactions +4. **Manifest parsing** - Read app permissions from manifest.json + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── lua_sandbox.h # (existing) +├── lua_sandbox.cpp # (existing) +├── permission_gate.h # NEW - Permission checking +└── permission_gate.cpp # NEW - Implementation + +sandbox-test/ +├── scripts/ +│ ├── test_permission_normal.lua # NEW +│ ├── test_permission_dangerous.lua # NEW +│ └── test_permission_signature.lua # NEW +└── src/ + └── main.cpp # Add new tests +``` + +--- + +## Implementation Details + +### 1. Permission Categories + +```cpp +// permission_gate.h +enum class PermissionCategory { + Normal, // Auto-granted (e.g., vibrate, internet) + Dangerous, // Requires user consent (e.g., camera, location) + Signature // System apps only (e.g., system settings, install apps) +}; +``` + +### 2. Permission Definitions + +```cpp +// permission_gate.cpp +static const std::unordered_map PERMISSIONS = { + // Normal permissions (auto-granted) + {"internet", {PermissionCategory::Normal, "Access the internet"}}, + {"vibrate", {PermissionCategory::Normal, "Vibrate the device"}}, + {"wake_lock", {PermissionCategory::Normal, "Keep device awake"}}, + {"notifications", {PermissionCategory::Normal, "Show notifications"}}, + + // Dangerous permissions (require user consent) + {"camera", {PermissionCategory::Dangerous, "Access the camera"}}, + {"microphone", {PermissionCategory::Dangerous, "Record audio"}}, + {"location.fine", {PermissionCategory::Dangerous, "Access precise location"}}, + {"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}}, + {"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}}, + {"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}}, + {"storage.external", {PermissionCategory::Dangerous, "Access external storage"}}, + {"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}}, + {"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}}, + {"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}}, + {"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}}, + + // Signature permissions (system apps only) + {"system.settings", {PermissionCategory::Signature, "Modify system settings"}}, + {"system.install", {PermissionCategory::Signature, "Install apps"}}, + {"system.uninstall", {PermissionCategory::Signature, "Uninstall apps"}}, + {"system.admin", {PermissionCategory::Signature, "Device administrator"}}, +}; +``` + +### 3. PermissionGate Class + +```cpp +// permission_gate.h +#pragma once + +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +struct SandboxContext; // Forward declaration + +enum class PermissionCategory { + Normal, + Dangerous, + Signature +}; + +struct PermissionInfo { + PermissionCategory category; + std::string description; +}; + +class PermissionGate { +public: + explicit PermissionGate(const SandboxContext& context); + + // Check if app has permission (throws Lua error if not) + bool Check(lua_State* L, const std::string& permission); + + // Check without throwing (returns false if denied) + bool HasPermission(const std::string& permission) const; + + // Get permission category + static PermissionCategory GetCategory(const std::string& permission); + + // User gesture tracking + void RecordUserGesture(); + bool HasRecentUserGesture(int ms = 5000) const; + + // Runtime permission grant (called after user consent) + void GrantPermission(const std::string& permission); + void RevokePermission(const std::string& permission); + + // Get all declared permissions + const std::vector& GetDeclaredPermissions() const; + + // Get all granted permissions + std::vector GetGrantedPermissions() const; + +private: + const SandboxContext& m_context; + std::unordered_set m_runtime_grants; // Runtime-granted dangerous perms + std::chrono::steady_clock::time_point m_last_gesture; + + bool CheckNormalPermission(const std::string& permission) const; + bool CheckDangerousPermission(const std::string& permission) const; + bool CheckSignaturePermission(const std::string& permission) const; +}; + +// Lua helper - throws error if permission denied +int RequirePermission(lua_State* L, const char* permission); + +} // namespace mosis +``` + +### 4. Implementation + +```cpp +// permission_gate.cpp +#include "permission_gate.h" +#include "lua_sandbox.h" +#include +#include + +namespace mosis { + +// Permission database +static const std::unordered_map PERMISSIONS = { + // Normal + {"internet", {PermissionCategory::Normal, "Access the internet"}}, + {"vibrate", {PermissionCategory::Normal, "Vibrate the device"}}, + {"wake_lock", {PermissionCategory::Normal, "Keep device awake"}}, + {"notifications", {PermissionCategory::Normal, "Show notifications"}}, + + // Dangerous + {"camera", {PermissionCategory::Dangerous, "Access the camera"}}, + {"microphone", {PermissionCategory::Dangerous, "Record audio"}}, + {"location.fine", {PermissionCategory::Dangerous, "Access precise location"}}, + {"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}}, + {"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}}, + {"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}}, + {"storage.external", {PermissionCategory::Dangerous, "Access external storage"}}, + {"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}}, + {"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}}, + + // Signature + {"system.settings", {PermissionCategory::Signature, "Modify system settings"}}, + {"system.install", {PermissionCategory::Signature, "Install apps"}}, + {"system.admin", {PermissionCategory::Signature, "Device administrator"}}, +}; + +PermissionGate::PermissionGate(const SandboxContext& context) + : m_context(context) + , m_last_gesture(std::chrono::steady_clock::time_point::min()) +{ +} + +PermissionCategory PermissionGate::GetCategory(const std::string& permission) { + auto it = PERMISSIONS.find(permission); + if (it != PERMISSIONS.end()) { + return it->second.category; + } + // Unknown permissions default to Dangerous + return PermissionCategory::Dangerous; +} + +bool PermissionGate::HasPermission(const std::string& permission) const { + auto category = GetCategory(permission); + + switch (category) { + case PermissionCategory::Normal: + return CheckNormalPermission(permission); + case PermissionCategory::Dangerous: + return CheckDangerousPermission(permission); + case PermissionCategory::Signature: + return CheckSignaturePermission(permission); + } + return false; +} + +bool PermissionGate::Check(lua_State* L, const std::string& permission) { + if (!HasPermission(permission)) { + luaL_error(L, "permission denied: %s", permission.c_str()); + return false; + } + return true; +} + +bool PermissionGate::CheckNormalPermission(const std::string& permission) const { + // Normal permissions are auto-granted if declared in manifest + const auto& declared = m_context.permissions; + return std::find(declared.begin(), declared.end(), permission) != declared.end(); +} + +bool PermissionGate::CheckDangerousPermission(const std::string& permission) const { + // Must be declared in manifest + const auto& declared = m_context.permissions; + if (std::find(declared.begin(), declared.end(), permission) == declared.end()) { + return false; + } + + // Must be granted at runtime (or be a system app) + if (m_context.is_system_app) { + return true; + } + + return m_runtime_grants.count(permission) > 0; +} + +bool PermissionGate::CheckSignaturePermission(const std::string& permission) const { + // Only system apps get signature permissions + if (!m_context.is_system_app) { + return false; + } + + // Must still be declared + const auto& declared = m_context.permissions; + return std::find(declared.begin(), declared.end(), permission) != declared.end(); +} + +void PermissionGate::RecordUserGesture() { + m_last_gesture = std::chrono::steady_clock::now(); +} + +bool PermissionGate::HasRecentUserGesture(int ms) const { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - m_last_gesture); + return elapsed.count() < ms; +} + +void PermissionGate::GrantPermission(const std::string& permission) { + m_runtime_grants.insert(permission); +} + +void PermissionGate::RevokePermission(const std::string& permission) { + m_runtime_grants.erase(permission); +} + +const std::vector& PermissionGate::GetDeclaredPermissions() const { + return m_context.permissions; +} + +std::vector PermissionGate::GetGrantedPermissions() const { + std::vector granted; + + for (const auto& perm : m_context.permissions) { + if (HasPermission(perm)) { + granted.push_back(perm); + } + } + + return granted; +} + +int RequirePermission(lua_State* L, const char* permission) { + // Get sandbox from registry + lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox"); + auto* sandbox = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + if (!sandbox) { + return luaL_error(L, "sandbox not initialized"); + } + + // TODO: Get PermissionGate from sandbox once integrated + // For now, this is a placeholder + return 0; +} + +} // namespace mosis +``` + +--- + +## Test Cases + +### Test 1: Normal Permission Auto-Granted + +**Script**: `scripts/test_permission_normal.lua` +```lua +-- Test that normal permissions are auto-granted when declared +-- This is called from C++ which sets up context with "internet" permission + +-- If we get here, permission check passed +print("PASS: Normal permission granted") +``` + +**C++ Test**: +```cpp +bool Test_NormalPermissionAutoGranted(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {"internet"}; // Declare normal permission + + LuaSandbox sandbox(ctx); + PermissionGate gate(ctx); + + // Normal permissions should be auto-granted + EXPECT_TRUE(gate.HasPermission("internet")); + + return true; +} +``` + +### Test 2: Dangerous Permission Requires Grant + +**C++ Test**: +```cpp +bool Test_DangerousPermissionRequiresGrant(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {"camera"}; // Declare dangerous permission + + PermissionGate gate(ctx); + + // Not granted yet + EXPECT_FALSE(gate.HasPermission("camera")); + + // Grant at runtime + gate.GrantPermission("camera"); + + // Now should have it + EXPECT_TRUE(gate.HasPermission("camera")); + + // Revoke + gate.RevokePermission("camera"); + EXPECT_FALSE(gate.HasPermission("camera")); + + return true; +} +``` + +### Test 3: Signature Permission System Only + +**C++ Test**: +```cpp +bool Test_SignaturePermissionSystemOnly(std::string& error_msg) { + // Non-system app + SandboxContext ctx = TestContext(); + ctx.permissions = {"system.settings"}; + ctx.is_system_app = false; + + PermissionGate gate(ctx); + EXPECT_FALSE(gate.HasPermission("system.settings")); + + // System app + SandboxContext sys_ctx = TestContext(); + sys_ctx.permissions = {"system.settings"}; + sys_ctx.is_system_app = true; + + PermissionGate sys_gate(sys_ctx); + EXPECT_TRUE(sys_gate.HasPermission("system.settings")); + + return true; +} +``` + +### Test 4: User Gesture Tracking + +**C++ Test**: +```cpp +bool Test_UserGestureRequired(std::string& error_msg) { + SandboxContext ctx = TestContext(); + PermissionGate gate(ctx); + + // No recent gesture + EXPECT_FALSE(gate.HasRecentUserGesture(5000)); + + // Record gesture + gate.RecordUserGesture(); + + // Should have recent gesture + EXPECT_TRUE(gate.HasRecentUserGesture(5000)); + + return true; +} +``` + +### Test 5: Undeclared Permission Denied + +**C++ Test**: +```cpp +bool Test_UndeclaredPermissionDenied(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {}; // No permissions declared + + PermissionGate gate(ctx); + + // Even normal permissions need to be declared + EXPECT_FALSE(gate.HasPermission("internet")); + + return true; +} +``` + +--- + +## 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 Permission Tests Only + +```bash +./build/Debug/sandbox-test.exe --test Permission +``` + +--- + +## Acceptance Criteria + +All tests must pass: + +- [x] `Test_NormalPermissionAutoGranted` - Normal perms auto-granted when declared +- [x] `Test_DangerousPermissionRequiresGrant` - Dangerous perms need runtime grant +- [x] `Test_SignaturePermissionSystemOnly` - Signature perms only for system apps +- [x] `Test_UserGestureTracking` - User gesture tracking works +- [x] `Test_UndeclaredPermissionDenied` - Undeclared perms always denied +- [x] `Test_SystemAppGetsDangerousAuto` - System apps get dangerous perms auto +- [x] `Test_PermissionCategoryCheck` - Permission categories are correct + +--- + +## Integration Notes + +After Milestone 2: +1. Add `PermissionGate` to `LuaSandbox` class +2. Call `RequirePermission()` before sensitive operations +3. Wire up user gesture recording from touch events + +--- + +## Next Steps + +After Milestone 2 passes: +1. Milestone 3: Audit Logging & Rate Limiting +2. Use permission gate in all subsequent API implementations diff --git a/SANDBOX_MILESTONE_3.md b/SANDBOX_MILESTONE_3.md new file mode 100644 index 0000000..7831653 --- /dev/null +++ b/SANDBOX_MILESTONE_3.md @@ -0,0 +1,357 @@ +# Milestone 3: Audit Logging & Rate Limiting + +**Status**: Complete ✓ +**Goal**: Track security events and prevent API abuse. + +--- + +## Overview + +This milestone adds security event logging and rate limiting to prevent abuse. The audit log tracks permission checks, sandbox violations, and resource usage. The rate limiter uses a token bucket algorithm to limit API call frequency. + +### Key Deliverables + +1. **AuditLog class** - Security event logging with ring buffer +2. **RateLimiter class** - Token bucket rate limiting +3. **Thread safety** - Concurrent access support + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── lua_sandbox.h # (existing) +├── lua_sandbox.cpp # (existing) +├── permission_gate.h # (existing) +├── permission_gate.cpp # (existing) +├── audit_log.h # NEW - Audit logging +├── audit_log.cpp # NEW - Implementation +├── rate_limiter.h # NEW - Rate limiting +└── rate_limiter.cpp # NEW - Implementation +``` + +--- + +## Implementation Details + +### 1. AuditEvent Enum + +```cpp +// audit_log.h +enum class AuditEvent { + // Lifecycle + AppStart, + AppStop, + + // Permissions + PermissionCheck, + PermissionGranted, + PermissionDenied, + + // Network + NetworkRequest, + NetworkBlocked, + + // Storage + FileAccess, + FileBlocked, + DatabaseAccess, + + // Hardware + CameraAccess, + MicrophoneAccess, + LocationAccess, + + // Security + SandboxViolation, + ResourceLimitHit, + RateLimitHit, + + // Other + Custom +}; +``` + +### 2. AuditLog Class + +```cpp +// audit_log.h +#pragma once + +#include +#include +#include +#include + +namespace mosis { + +enum class AuditEvent { /* ... */ }; + +struct AuditEntry { + std::chrono::system_clock::time_point timestamp; + AuditEvent event; + std::string app_id; + std::string details; + bool success; +}; + +class AuditLog { +public: + explicit AuditLog(size_t max_entries = 10000); + + // Log an event + void Log(AuditEvent event, const std::string& app_id, + const std::string& details = "", bool success = true); + + // Query entries + std::vector GetEntries(size_t count = 100) const; + std::vector GetEntriesForApp(const std::string& app_id, + size_t count = 100) const; + std::vector GetEntriesByEvent(AuditEvent event, + size_t count = 100) const; + + // Statistics + size_t GetTotalEntries() const; + size_t CountEvents(AuditEvent event, const std::string& app_id = "") const; + + // Clear + void Clear(); + +private: + mutable std::mutex m_mutex; + std::vector m_entries; + size_t m_max_entries; + size_t m_write_index = 0; + size_t m_total_logged = 0; +}; + +// Global audit log (singleton pattern) +AuditLog& GetAuditLog(); + +} // namespace mosis +``` + +### 3. RateLimiter Class (Token Bucket) + +```cpp +// rate_limiter.h +#pragma once + +#include +#include +#include +#include + +namespace mosis { + +struct RateLimitConfig { + double tokens_per_second; // Refill rate + double max_tokens; // Bucket capacity +}; + +class RateLimiter { +public: + // Default limits for common operations + RateLimiter(); + + // Check if operation is allowed (consumes token if yes) + bool Check(const std::string& app_id, const std::string& operation); + + // Check without consuming + bool CanProceed(const std::string& app_id, const std::string& operation) const; + + // Configure limits for an operation + void SetLimit(const std::string& operation, const RateLimitConfig& config); + + // Get current token count + double GetTokens(const std::string& app_id, const std::string& operation) const; + + // Reset an app's tokens (e.g., on app restart) + void ResetApp(const std::string& app_id); + +private: + struct Bucket { + double tokens; + std::chrono::steady_clock::time_point last_refill; + }; + + void Refill(Bucket& bucket, const RateLimitConfig& config) const; + Bucket& GetBucket(const std::string& app_id, const std::string& operation); + + mutable std::mutex m_mutex; + std::unordered_map m_configs; + std::unordered_map m_buckets; // Key: app_id:operation +}; + +// Global rate limiter +RateLimiter& GetRateLimiter(); + +} // namespace mosis +``` + +### 4. Default Rate Limits + +```cpp +// rate_limiter.cpp +RateLimiter::RateLimiter() { + // Network + SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100 + SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10 + + // Storage + SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500 + SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100 + SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200 + + // Hardware + SetLimit("camera.capture", {30.0, 30.0}); // 30 fps max + SetLimit("microphone.record", {1.0, 1.0}); // 1 session at a time + SetLimit("location.request", {1.0, 5.0}); // 1/sec, burst 5 + + // Timers + SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100 +} +``` + +--- + +## Test Cases + +### Test 1: Audit Log Basic + +```cpp +bool Test_AuditLogBasic(std::string& error_msg) { + mosis::AuditLog log(1000); + + log.Log(mosis::AuditEvent::AppStart, "test.app", "App started"); + log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true); + log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false); + + auto entries = log.GetEntries(10); + EXPECT_TRUE(entries.size() == 3); + + auto app_entries = log.GetEntriesForApp("test.app", 10); + EXPECT_TRUE(app_entries.size() == 3); + + return true; +} +``` + +### Test 2: Audit Log Ring Buffer + +```cpp +bool Test_AuditLogRingBuffer(std::string& error_msg) { + mosis::AuditLog log(100); // Small buffer + + // Log more than capacity + for (int i = 0; i < 200; i++) { + log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i)); + } + + // Should only have latest 100 + auto entries = log.GetEntries(200); + EXPECT_TRUE(entries.size() == 100); + + // Total logged should be 200 + EXPECT_TRUE(log.GetTotalEntries() == 200); + + return true; +} +``` + +### Test 3: Rate Limiter Basic + +```cpp +bool Test_RateLimiterBasic(std::string& error_msg) { + mosis::RateLimiter limiter; + + // Should succeed initially (has tokens) + EXPECT_TRUE(limiter.Check("test.app", "network.request")); + + return true; +} +``` + +### Test 4: Rate Limiter Exhaustion + +```cpp +bool Test_RateLimiterExhaustion(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {0.0, 5.0}); // 5 tokens, no refill + + // Use all tokens + for (int i = 0; i < 5; i++) { + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + } + + // Should be denied now + EXPECT_FALSE(limiter.Check("test.app", "test.op")); + + return true; +} +``` + +### Test 5: Rate Limiter Refill + +```cpp +bool Test_RateLimiterRefill(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {1000.0, 1.0}); // 1000/sec, max 1 + + // Use the token + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + EXPECT_FALSE(limiter.Check("test.app", "test.op")); + + // Wait a bit for refill (1ms = 1 token at 1000/sec) + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + + // Should have token again + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + + return true; +} +``` + +### Test 6: App Isolation + +```cpp +bool Test_RateLimiterAppIsolation(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {0.0, 1.0}); // 1 token, no refill + + // App 1 uses its token + EXPECT_TRUE(limiter.Check("app1", "test.op")); + EXPECT_FALSE(limiter.Check("app1", "test.op")); + + // App 2 should still have its token + EXPECT_TRUE(limiter.Check("app2", "test.op")); + + return true; +} +``` + +--- + +## Acceptance Criteria + +All tests must pass: + +- [x] `Test_AuditLogBasic` - Logging and retrieval work +- [x] `Test_AuditLogRingBuffer` - Ring buffer wraps correctly +- [x] `Test_AuditLogThreadSafe` - Thread safety verified +- [x] `Test_RateLimiterBasic` - Basic rate limiting works +- [x] `Test_RateLimiterExhaustion` - Tokens exhaust correctly +- [x] `Test_RateLimiterRefill` - Tokens refill over time +- [x] `Test_RateLimiterAppIsolation` - Apps have separate buckets +- [x] `Test_RateLimiterReset` - App reset clears buckets +- [x] `Test_RateLimiterNoConfig` - Unconfigured ops allowed + +--- + +## Next Steps + +After Milestone 3 passes: +1. Integrate AuditLog into LuaSandbox and PermissionGate +2. Integrate RateLimiter into API implementations +3. Milestone 4: Safe Path & Require diff --git a/SANDBOX_MILESTONE_4.md b/SANDBOX_MILESTONE_4.md new file mode 100644 index 0000000..c3d784f --- /dev/null +++ b/SANDBOX_MILESTONE_4.md @@ -0,0 +1,241 @@ +# Milestone 4: Safe Path & Require + +**Status**: Complete ✓ +**Goal**: Secure file access within app sandbox. + +--- + +## Overview + +This milestone implements path validation to prevent directory traversal attacks and a safe `require()` function that loads Lua modules only from the app's scripts directory. + +### Key Deliverables + +1. **PathSandbox class** - Path validation and canonicalization +2. **SafeRequire function** - Secure module loader +3. **Module caching** - Registry-based cache for loaded modules + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── lua_sandbox.h # (existing) +├── lua_sandbox.cpp # (existing) +├── permission_gate.h # (existing) +├── permission_gate.cpp # (existing) +├── audit_log.h # (existing) +├── audit_log.cpp # (existing) +├── rate_limiter.h # (existing) +├── rate_limiter.cpp # (existing) +├── path_sandbox.h # NEW - Path validation +└── path_sandbox.cpp # NEW - Implementation +``` + +--- + +## Implementation Details + +### 1. PathSandbox Class + +```cpp +// path_sandbox.h +#pragma once + +#include +#include + +namespace mosis { + +class PathSandbox { +public: + explicit PathSandbox(const std::string& app_path); + + // Validate a path is within the sandbox + bool ValidatePath(const std::string& path, std::string& out_canonical); + + // Check if path contains traversal attempts + static bool ContainsTraversal(const std::string& path); + + // Check if path is absolute + static bool IsAbsolutePath(const std::string& path); + + // Normalize path separators and remove redundant components + static std::string NormalizePath(const std::string& path); + + // Get the app's base path + const std::string& GetAppPath() const { return m_app_path; } + + // Resolve a relative path to full path within sandbox + std::string ResolvePath(const std::string& relative_path); + +private: + std::string m_app_path; +}; + +} // namespace mosis +``` + +### 2. SafeRequire Function + +```cpp +// path_sandbox.cpp + +// Safe require implementation for Lua +int SafeRequire(lua_State* L); + +// Register safe require as global +void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox); +``` + +### 3. Module Name Validation + +Valid module names contain only: +- Alphanumeric characters (a-z, A-Z, 0-9) +- Underscores (_) +- Dots (.) for submodules + +Examples: +- `utils` → loads `scripts/utils.lua` +- `ui.button` → loads `scripts/ui/button.lua` + +--- + +## Test Cases + +### Test 1: Rejects Directory Traversal + +```cpp +bool Test_PathRejectsTraversal(std::string& error_msg) { + mosis::PathSandbox sandbox("D:/test/app"); + + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("../etc/passwd")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("foo/../../../bar")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..\\windows\\system32")); + + std::string canonical; + EXPECT_FALSE(sandbox.ValidatePath("../etc/passwd", canonical)); + EXPECT_FALSE(sandbox.ValidatePath("data/../../../etc/passwd", canonical)); + + return true; +} +``` + +### Test 2: Rejects Absolute Paths + +```cpp +bool Test_PathRejectsAbsolute(std::string& error_msg) { + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("/etc/passwd")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("C:\\Windows\\System32")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("D:/test/file.txt")); + + EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("scripts/utils.lua")); + EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("./data/file.txt")); + + return true; +} +``` + +### Test 3: Accepts Valid Paths + +```cpp +bool Test_PathAcceptsValid(std::string& error_msg) { + mosis::PathSandbox sandbox("D:/test/app"); + + std::string canonical; + EXPECT_TRUE(sandbox.ValidatePath("scripts/utils.lua", canonical)); + EXPECT_TRUE(sandbox.ValidatePath("data/config.json", canonical)); + EXPECT_TRUE(sandbox.ValidatePath("./scripts/ui/button.lua", canonical)); + + return true; +} +``` + +### Test 4: Safe Require Loads Modules + +```cpp +bool Test_SafeRequireLoads(std::string& error_msg) { + // Create sandbox with test scripts directory + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); // Use scripts/ as app path + + LuaSandbox sandbox(ctx); + + // Should be able to require a test module + std::string script = R"( + local m = require("test_module") + return m.value == 42 + )"; + + EXPECT_TRUE(sandbox.LoadString(script, "require_test")); + + return true; +} +``` + +### Test 5: Safe Require Caches Modules + +```cpp +bool Test_SafeRequireCaches(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); + + LuaSandbox sandbox(ctx); + + std::string script = R"( + local m1 = require("test_module") + local m2 = require("test_module") + return m1 == m2 -- Should be same table (cached) + )"; + + EXPECT_TRUE(sandbox.LoadString(script, "cache_test")); + + return true; +} +``` + +### Test 6: Safe Require Rejects Invalid Names + +```cpp +bool Test_SafeRequireRejectsInvalid(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); + + LuaSandbox sandbox(ctx); + + // Should reject path traversal in module name + EXPECT_FALSE(sandbox.LoadString("require('../evil')", "evil_require")); + + // Should reject absolute paths + EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')", "abs_require")); + + // Should reject special characters + EXPECT_FALSE(sandbox.LoadString("require('foo;bar')", "special_require")); + + return true; +} +``` + +--- + +## Acceptance Criteria + +All tests pass: + +- [x] `Test_PathRejectsTraversal` - Block .. traversal +- [x] `Test_PathRejectsAbsolute` - Block absolute paths +- [x] `Test_PathAcceptsValid` - Allow valid relative paths +- [x] `Test_ModuleNameValidation` - Validate module names +- [x] `Test_ModuleToPath` - Convert module names to paths +- [x] `Test_SafeRequireLoads` - Load modules from scripts/ +- [x] `Test_SafeRequireCaches` - Cache loaded modules +- [x] `Test_SafeRequireRejectsInvalid` - Reject malicious require calls + +--- + +## Next Steps + +After Milestone 4 passes: +1. Milestone 5: Timer & Callback System +2. Milestone 6: JSON & Crypto APIs diff --git a/SANDBOX_MILESTONE_5.md b/SANDBOX_MILESTONE_5.md new file mode 100644 index 0000000..1a4649e --- /dev/null +++ b/SANDBOX_MILESTONE_5.md @@ -0,0 +1,333 @@ +# Milestone 5: Timer & Callback System + +**Status**: Complete +**Goal**: Safe timer APIs managed by kernel. + +--- + +## Overview + +This milestone implements JavaScript-style timer APIs (`setTimeout`, `setInterval`) that are: +- Managed by the kernel (not Lua coroutines) +- Subject to per-app limits +- Properly cleaned up when apps stop +- Integrated with sandbox instruction counting + +### Key Deliverables + +1. **TimerManager class** - Central timer management +2. **Lua timer APIs** - setTimeout, setInterval, clearTimeout, clearInterval +3. **Kernel integration** - Fire timers from main loop + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── lua_sandbox.h # (existing) +├── lua_sandbox.cpp # (existing) +├── timer_manager.h # NEW - Timer management +└── timer_manager.cpp # NEW - Implementation +``` + +--- + +## Implementation Details + +### 1. TimerManager Class + +```cpp +// timer_manager.h +#pragma once + +#include +#include +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +using TimerId = uint64_t; +using TimePoint = std::chrono::steady_clock::time_point; +using Duration = std::chrono::milliseconds; + +struct Timer { + TimerId id; + std::string app_id; + TimePoint fire_time; + Duration interval; // 0 for setTimeout, >0 for setInterval + int callback_ref; // Lua registry reference + lua_State* L; // Lua state that owns the callback + bool cancelled = false; +}; + +class TimerManager { +public: + TimerManager(); + ~TimerManager(); + + // Create timers (returns timer ID) + TimerId SetTimeout(lua_State* L, const std::string& app_id, + int callback_ref, int delay_ms); + TimerId SetInterval(lua_State* L, const std::string& app_id, + int callback_ref, int interval_ms); + + // Cancel timers + bool ClearTimer(const std::string& app_id, TimerId id); + + // Cancel all timers for an app + void ClearAppTimers(const std::string& app_id); + + // Process timers (call from main loop) + // Returns number of timers fired + int ProcessTimers(); + + // Get timer count for an app + size_t GetTimerCount(const std::string& app_id) const; + + // Configuration + static constexpr size_t MAX_TIMERS_PER_APP = 100; + static constexpr int MIN_INTERVAL_MS = 10; + static constexpr int MIN_TIMEOUT_MS = 0; + +private: + struct TimerCompare { + bool operator()(const Timer& a, const Timer& b) const { + return a.fire_time > b.fire_time; // Min-heap + } + }; + + TimerId m_next_id = 1; + std::priority_queue, TimerCompare> m_timers; + std::unordered_map m_app_timer_counts; + mutable std::mutex m_mutex; + + void FireTimer(Timer& timer); +}; + +// Lua API registration +void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id); + +} // namespace mosis +``` + +### 2. Lua Timer APIs + +```lua +-- setTimeout: fire callback once after delay +local id = setTimeout(function() + print("Fired after 1000ms") +end, 1000) + +-- clearTimeout: cancel a pending timeout +clearTimeout(id) + +-- setInterval: fire callback repeatedly +local id = setInterval(function() + print("Fires every 500ms") +end, 500) + +-- clearInterval: stop an interval +clearInterval(id) +``` + +### 3. Timer Limits + +| Limit | Value | Reason | +|-------|-------|--------| +| Max timers per app | 100 | Prevent resource exhaustion | +| Min interval | 10ms | Prevent CPU spinning | +| Min timeout | 0ms | Allow immediate callbacks | + +--- + +## Test Cases + +### Test 1: SetTimeout Fires + +```cpp +bool Test_SetTimeoutFires(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + // Register timer API + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Set a timeout + sandbox.LoadString(R"( + fired = false + setTimeout(function() fired = true end, 50) + )", "timeout_test"); + + // Process timers after delay + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + manager.ProcessTimers(); + + // Check if callback fired + sandbox.LoadString("assert(fired == true)", "check"); + + return true; +} +``` + +### Test 2: SetInterval Fires Multiple Times + +```cpp +bool Test_SetIntervalFires(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + sandbox.LoadString(R"( + count = 0 + setInterval(function() count = count + 1 end, 30) + )", "interval_test"); + + // Process multiple times + for (int i = 0; i < 5; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + manager.ProcessTimers(); + } + + // Should have fired multiple times + sandbox.LoadString("assert(count >= 3)", "check"); + + return true; +} +``` + +### Test 3: ClearTimeout Cancels + +```cpp +bool Test_ClearTimeoutCancels(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + sandbox.LoadString(R"( + fired = false + local id = setTimeout(function() fired = true end, 100) + clearTimeout(id) + )", "clear_test"); + + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + manager.ProcessTimers(); + + // Should NOT have fired + sandbox.LoadString("assert(fired == false)", "check"); + + return true; +} +``` + +### Test 4: Timer Limit Enforced + +```cpp +bool Test_TimerLimitEnforced(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Try to create too many timers + sandbox.LoadString(R"( + for i = 1, 150 do + setTimeout(function() end, 1000000) + end + )", "limit_test"); + + // Should be capped at MAX_TIMERS_PER_APP + EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) <= 100); + + return true; +} +``` + +### Test 5: ClearAppTimers Cleanup + +```cpp +bool Test_ClearAppTimersCleanup(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + sandbox.LoadString(R"( + for i = 1, 10 do + setTimeout(function() end, 1000000) + end + )", "cleanup_test"); + + EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 10); + + // Clear all timers for app + manager.ClearAppTimers(ctx.app_id); + + EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 0); + + return true; +} +``` + +### Test 6: Minimum Interval Enforced + +```cpp +bool Test_MinIntervalEnforced(std::string& error_msg) { + mosis::TimerManager manager; + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Try to set interval less than minimum + sandbox.LoadString(R"( + count = 0 + setInterval(function() count = count + 1 end, 1) -- 1ms, should be clamped to 10ms + )", "min_interval_test"); + + // With 1ms interval, in 50ms we'd get 50 callbacks + // With 10ms minimum, we should get ~5 + std::this_thread::sleep_for(std::chrono::milliseconds(55)); + for (int i = 0; i < 10; i++) { + manager.ProcessTimers(); + } + + sandbox.LoadString("assert(count <= 10)", "check"); + + return true; +} +``` + +--- + +## Acceptance Criteria + +All tests must pass: + +- [x] `Test_SetTimeoutFires` - Timeout fires after delay +- [x] `Test_SetIntervalFires` - Interval fires repeatedly +- [x] `Test_ClearTimeoutCancels` - Cancelled timeout doesn't fire +- [x] `Test_ClearIntervalCancels` - Cancelled interval stops +- [x] `Test_TimerLimitEnforced` - Max 100 timers per app +- [x] `Test_ClearAppTimersCleanup` - All app timers cleared on stop +- [x] `Test_MinIntervalEnforced` - Interval clamped to 10ms minimum + +--- + +## Next Steps + +After Milestone 5 passes: +1. Milestone 6: JSON & Crypto APIs +2. Milestone 7: Virtual Filesystem diff --git a/sandbox-test/CMakeLists.txt b/sandbox-test/CMakeLists.txt new file mode 100644 index 0000000..f4ae34a --- /dev/null +++ b/sandbox-test/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.22.1) +project(sandbox-test) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find dependencies via vcpkg +find_package(Lua REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) + +# Sandbox library (the code being tested) +add_library(mosis-sandbox STATIC + ../src/main/cpp/sandbox/lua_sandbox.cpp + ../src/main/cpp/sandbox/permission_gate.cpp + ../src/main/cpp/sandbox/audit_log.cpp + ../src/main/cpp/sandbox/rate_limiter.cpp + ../src/main/cpp/sandbox/path_sandbox.cpp + ../src/main/cpp/sandbox/timer_manager.cpp +) +target_include_directories(mosis-sandbox PUBLIC + ../src/main/cpp/sandbox + ${LUA_INCLUDE_DIR} +) +target_link_libraries(mosis-sandbox PUBLIC + ${LUA_LIBRARIES} +) + +# Test executable +add_executable(sandbox-test + src/main.cpp + src/test_harness.cpp +) + +target_include_directories(sandbox-test PRIVATE + src + ../src/main/cpp/sandbox +) + +target_link_libraries(sandbox-test PRIVATE + mosis-sandbox + nlohmann_json::nlohmann_json +) + +# Copy test scripts to build directory +add_custom_command(TARGET sandbox-test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/scripts + $/scripts +) + +# Windows-specific +if(WIN32) + target_compile_definitions(sandbox-test PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() diff --git a/sandbox-test/README.md b/sandbox-test/README.md new file mode 100644 index 0000000..cc3688c --- /dev/null +++ b/sandbox-test/README.md @@ -0,0 +1,132 @@ +# Sandbox Security Tests + +Automated tests for the Mosis Lua sandbox security implementation. + +## Prerequisites + +- CMake 3.22+ +- vcpkg with packages: `lua`, `nlohmann-json` +- MSVC or compatible C++23 compiler + +## Build + +```bash +# From sandbox-test directory +cd D:\Dev\Mosis\MosisService\sandbox-test + +# Configure with vcpkg +cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake + +# Build +cmake --build build --config Debug +``` + +## Run Tests + +### Run All Tests (Uber Command) + +```bash +# Windows +.\run_tests.bat + +# Or directly +.\build\Debug\sandbox-test.exe +``` + +### Run Specific Test + +```bash +.\build\Debug\sandbox-test.exe --test DangerousGlobals +.\build\Debug\sandbox-test.exe --test Memory +.\build\Debug\sandbox-test.exe --test CPU +``` + +### Custom Output File + +```bash +.\build\Debug\sandbox-test.exe --output my_results.json +``` + +## Test List + +| Test Name | Description | Script | +|-----------|-------------|--------| +| `DangerousGlobalsRemoved` | Verifies os, io, debug, etc. are nil | `test_globals_removed.lua` | +| `BytecodeRejected` | Verifies binary Lua chunks are rejected | (C++ only) | +| `MemoryLimitEnforced` | Verifies memory allocation limit works | `test_memory_limit.lua` | +| `CPULimitEnforced` | Verifies instruction count limit works | `test_cpu_limit.lua` | +| `MetatableProtected` | Verifies _G and string metatable are frozen | `test_metatable_protected.lua` | +| `SafeOperationsWork` | Verifies normal Lua operations still work | `test_safe_operations.lua` | +| `StringDumpRemoved` | Verifies string.dump is nil | `test_string_dump_removed.lua` | +| `MemoryTracking` | Verifies memory usage is tracked | (C++ only) | +| `InstructionCounting` | Verifies instruction count is tracked | (C++ only) | +| `MultipleLoads` | Verifies multiple scripts can be loaded | (C++ only) | +| `ErrorRecovery` | Verifies sandbox recovers from errors | (C++ only) | + +## Output Format + +Tests produce a JSON report at `test_results.json`: + +```json +{ + "name": "Lua Sandbox Security Tests", + "timestamp": "2024-01-15T10:30:00Z", + "summary": { + "passed": 11, + "failed": 0, + "total": 11 + }, + "tests": [ + { + "name": "DangerousGlobalsRemoved", + "status": "passed", + "duration_ms": 5 + } + ] +} +``` + +## Exit Codes + +- `0` - All tests passed +- `1` - One or more tests failed + +## Adding New Tests + +1. Create Lua script in `scripts/` directory +2. Add C++ test function in `main.cpp`: + ```cpp + bool Test_MyNewTest(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + // ... test logic + return true; + } + ``` +3. Register in `main()`: + ```cpp + harness.AddTest("MyNewTest", Test_MyNewTest); + ``` + +## Debugging Failed Tests + +1. Run specific test: `--test TestName` +2. Check Lua script in `scripts/` for expected behavior +3. Check `test_results.json` for error details +4. Add print statements to Lua scripts (output goes to console) + +## CI Integration + +```bash +# In CI script +cd sandbox-test +cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake +cmake --build build --config Release +./build/Release/sandbox-test.exe --output ci_results.json + +# Check exit code +if [ $? -ne 0 ]; then + echo "Sandbox tests failed!" + cat ci_results.json + exit 1 +fi +``` diff --git a/sandbox-test/run_tests.bat b/sandbox-test/run_tests.bat new file mode 100644 index 0000000..36c13f6 --- /dev/null +++ b/sandbox-test/run_tests.bat @@ -0,0 +1,59 @@ +@echo off +setlocal + +echo ======================================== +echo MOSIS SANDBOX TEST RUNNER +echo ======================================== +echo. + +REM Check if build exists +if not exist "build\Debug\sandbox-test.exe" ( + echo Build not found. Building... + echo. + + REM Check VCPKG_ROOT + if "%VCPKG_ROOT%"=="" ( + echo ERROR: VCPKG_ROOT environment variable not set + exit /b 1 + ) + + REM Configure + echo Configuring CMake... + cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake + if errorlevel 1 ( + echo ERROR: CMake configure failed + exit /b 1 + ) + + REM Build + echo Building... + cmake --build build --config Debug + if errorlevel 1 ( + echo ERROR: Build failed + exit /b 1 + ) + echo. +) + +REM Run tests +echo Running tests... +echo. + +cd build\Debug +sandbox-test.exe %* +set TEST_RESULT=%errorlevel% + +cd ..\.. + +echo. +if %TEST_RESULT% equ 0 ( + echo ======================================== + echo ALL TESTS PASSED + echo ======================================== +) else ( + echo ======================================== + echo SOME TESTS FAILED + echo ======================================== +) + +exit /b %TEST_RESULT% diff --git a/sandbox-test/run_tests.ps1 b/sandbox-test/run_tests.ps1 new file mode 100644 index 0000000..4883235 --- /dev/null +++ b/sandbox-test/run_tests.ps1 @@ -0,0 +1,67 @@ +#!/usr/bin/env pwsh +# Sandbox Test Runner for PowerShell + +Write-Host "========================================" +Write-Host " MOSIS SANDBOX TEST RUNNER" +Write-Host "========================================" +Write-Host "" + +$ErrorActionPreference = "Stop" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Push-Location $scriptDir + +try { + # Check if build exists + if (-not (Test-Path "build/Debug/sandbox-test.exe")) { + Write-Host "Build not found. Building..." + Write-Host "" + + # Check VCPKG_ROOT + if (-not $env:VCPKG_ROOT) { + Write-Error "VCPKG_ROOT environment variable not set" + exit 1 + } + + # Configure + Write-Host "Configuring CMake..." + cmake -B build "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + if ($LASTEXITCODE -ne 0) { + Write-Error "CMake configure failed" + exit 1 + } + + # Build + Write-Host "Building..." + cmake --build build --config Debug + if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed" + exit 1 + } + Write-Host "" + } + + # Run tests + Write-Host "Running tests..." + Write-Host "" + + Push-Location "build/Debug" + & ./sandbox-test.exe @args + $testResult = $LASTEXITCODE + Pop-Location + + Write-Host "" + if ($testResult -eq 0) { + Write-Host "========================================" + Write-Host " ALL TESTS PASSED" + Write-Host "========================================" + } else { + Write-Host "========================================" + Write-Host " SOME TESTS FAILED" + Write-Host "========================================" + } + + exit $testResult +} +finally { + Pop-Location +} diff --git a/sandbox-test/scripts/scripts/test_module.lua b/sandbox-test/scripts/scripts/test_module.lua new file mode 100644 index 0000000..ecb6577 --- /dev/null +++ b/sandbox-test/scripts/scripts/test_module.lua @@ -0,0 +1,11 @@ +-- Test module for SafeRequire tests +local M = {} + +M.value = 42 +M.name = "test_module" + +function M.add(a, b) + return a + b +end + +return M diff --git a/sandbox-test/scripts/test_bytecode_rejected.lua b/sandbox-test/scripts/test_bytecode_rejected.lua new file mode 100644 index 0000000..10b88ba --- /dev/null +++ b/sandbox-test/scripts/test_bytecode_rejected.lua @@ -0,0 +1,5 @@ +-- This script tests that text loading works +-- The actual bytecode rejection test is done from C++ side +-- by attempting to load a bytecode string directly + +print("PASS: Text loading works") diff --git a/sandbox-test/scripts/test_cpu_limit.lua b/sandbox-test/scripts/test_cpu_limit.lua new file mode 100644 index 0000000..3521767 --- /dev/null +++ b/sandbox-test/scripts/test_cpu_limit.lua @@ -0,0 +1,12 @@ +-- This script runs an infinite loop +-- It should be stopped by the instruction limit hook + +local count = 0 + +while true do + count = count + 1 + -- This loop should be interrupted by instruction hook +end + +-- Should never reach here +error("FAIL: CPU limit not enforced - loop completed") diff --git a/sandbox-test/scripts/test_globals_removed.lua b/sandbox-test/scripts/test_globals_removed.lua new file mode 100644 index 0000000..dc043ea --- /dev/null +++ b/sandbox-test/scripts/test_globals_removed.lua @@ -0,0 +1,26 @@ +-- Test that dangerous globals are nil +-- This script should run successfully if sandbox is properly configured +-- Note: 'require' is intentionally NOT in this list because the sandbox +-- provides a safe version when app_path is configured + +local dangerous = { + "os", "io", "debug", "package", "ffi", "jit", + "dofile", "loadfile", "load", "loadstring", + "rawget", "rawset", "rawequal", "rawlen", + "collectgarbage", "newproxy" +} + +local failed = {} + +for _, name in ipairs(dangerous) do + local value = _G[name] + if value ~= nil then + table.insert(failed, name .. " (is " .. type(value) .. ")") + end +end + +if #failed > 0 then + error("FAIL: These globals should be nil: " .. table.concat(failed, ", ")) +end + +print("PASS: All dangerous globals removed") diff --git a/sandbox-test/scripts/test_memory_limit.lua b/sandbox-test/scripts/test_memory_limit.lua new file mode 100644 index 0000000..fc3a688 --- /dev/null +++ b/sandbox-test/scripts/test_memory_limit.lua @@ -0,0 +1,20 @@ +-- This script intentionally tries to exhaust memory +-- When run with a 512KB limit, it should fail before completing + +local t = {} +local i = 0 + +while true do + i = i + 1 + -- Each string is 100KB + t[i] = string.rep("x", 100000) + + -- Safety check - if we get past 100 iterations with 512KB limit, + -- something is wrong + if i > 100 then + error("FAIL: Should have hit memory limit by now (allocated ~10MB)") + end +end + +-- Should never reach here +error("FAIL: Memory limit not enforced") diff --git a/sandbox-test/scripts/test_metatable_protected.lua b/sandbox-test/scripts/test_metatable_protected.lua new file mode 100644 index 0000000..8ed81c7 --- /dev/null +++ b/sandbox-test/scripts/test_metatable_protected.lua @@ -0,0 +1,33 @@ +-- Test that metatables are protected from manipulation + +-- Test 1: String metatable should return protection value, not actual metatable +local mt = getmetatable("") +if mt ~= "string" then + error("FAIL: string metatable should return 'string', got " .. tostring(mt)) +end + +-- Test 2: Cannot add new globals +local ok, err = pcall(function() + _G.my_new_global = "test" +end) +if ok then + error("FAIL: Should not be able to add new 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 + +-- Test 4: Cannot replace math table +local ok3, err3 = pcall(function() + _G.math = {} +end) +if ok3 then + error("FAIL: Should not be able to replace math") +end + +print("PASS: Metatables protected") diff --git a/sandbox-test/scripts/test_safe_operations.lua b/sandbox-test/scripts/test_safe_operations.lua new file mode 100644 index 0000000..fc112fe --- /dev/null +++ b/sandbox-test/scripts/test_safe_operations.lua @@ -0,0 +1,158 @@ +-- Test that safe/normal Lua operations still work correctly + +local function check(cond, msg) + if not cond then + error("FAIL: " .. msg) + end +end + +-- ============================================ +-- MATH OPERATIONS +-- ============================================ +local x = math.sin(1.5) + math.floor(3.7) +check(type(x) == "number", "Math operations failed") + +check(math.abs(-5) == 5, "math.abs failed") +check(math.max(1, 2, 3) == 3, "math.max failed") +check(math.min(1, 2, 3) == 1, "math.min failed") +check(math.floor(3.9) == 3, "math.floor failed") +check(math.ceil(3.1) == 4, "math.ceil failed") + +-- ============================================ +-- STRING OPERATIONS +-- ============================================ +local s = string.format("hello %d", 42) +check(s == "hello 42", "string.format failed") + +local upper = string.upper("test") +check(upper == "TEST", "string.upper failed") + +local lower = string.lower("TEST") +check(lower == "test", "string.lower failed") + +local sub = string.sub("hello", 2, 4) +check(sub == "ell", "string.sub failed") + +local len = string.len("hello") +check(len == 5, "string.len failed") + +local rep = string.rep("ab", 3) +check(rep == "ababab", "string.rep failed") + +local rev = string.reverse("hello") +check(rev == "olleh", "string.reverse failed") + +-- ============================================ +-- TABLE OPERATIONS +-- ============================================ +local t = {1, 2, 3} +table.insert(t, 4) +check(#t == 4, "table.insert failed") +check(t[4] == 4, "table.insert value failed") + +local removed = table.remove(t) +check(removed == 4, "table.remove failed") +check(#t == 3, "table.remove length failed") + +local t2 = {3, 1, 2} +table.sort(t2) +check(t2[1] == 1 and t2[2] == 2 and t2[3] == 3, "table.sort failed") + +local concat = table.concat({"a", "b", "c"}, ",") +check(concat == "a,b,c", "table.concat failed") + +-- ============================================ +-- ITERATION +-- ============================================ +local count = 0 +for i, v in ipairs({1, 2, 3, 4}) do + count = count + 1 +end +check(count == 4, "ipairs iteration failed") + +count = 0 +for k, v in pairs({a=1, b=2, c=3}) do + count = count + 1 +end +check(count == 3, "pairs iteration failed") + +-- next function +local t3 = {a=1, b=2} +local k, v = next(t3) +check(k ~= nil and v ~= nil, "next function failed") + +-- ============================================ +-- ERROR HANDLING +-- ============================================ +local ok, err = pcall(function() + error("test error") +end) +check(not ok, "pcall should return false for error") +check(err:find("test error"), "Error message should contain 'test error'") + +local ok2, result = pcall(function() + return 42 +end) +check(ok2 and result == 42, "pcall should return success value") + +-- xpcall with traceback +local ok3, err3 = xpcall(function() + error("xpcall test") +end, function(e) + return "caught: " .. tostring(e) +end) +check(not ok3, "xpcall should return false for error") +check(err3:find("caught"), "xpcall error handler should run") + +-- ============================================ +-- TYPE CHECKS +-- ============================================ +check(type({}) == "table", "type table failed") +check(type("") == "string", "type string failed") +check(type(123) == "number", "type number failed") +check(type(true) == "boolean", "type boolean failed") +check(type(nil) == "nil", "type nil failed") +check(type(function() end) == "function", "type function failed") + +-- ============================================ +-- CONVERSION +-- ============================================ +check(tonumber("42") == 42, "tonumber string failed") +check(tonumber("3.14") == 3.14, "tonumber float failed") +check(tonumber("abc") == nil, "tonumber invalid failed") +check(tonumber(42) == 42, "tonumber number failed") + +check(tostring(42) == "42", "tostring number failed") +check(tostring(true) == "true", "tostring boolean failed") +check(type(tostring({})) == "string", "tostring table failed") + +-- ============================================ +-- SELECT +-- ============================================ +local a, b = select(2, 1, 2, 3) +check(a == 2 and b == 3, "select failed") +check(select("#", 1, 2, 3, 4) == 4, "select # failed") + +-- ============================================ +-- ASSERT +-- ============================================ +local ok4, err4 = pcall(function() + assert(true, "should not fail") +end) +check(ok4, "assert true failed") + +local ok5, err5 = pcall(function() + assert(false, "intentional fail") +end) +check(not ok5, "assert false should fail") +check(err5:find("intentional fail"), "assert message wrong") + +-- ============================================ +-- UTF8 (if available) +-- ============================================ +if utf8 then + local len = utf8.len("hello") + check(len == 5, "utf8.len failed") +end + +print("PASS: All safe operations work correctly") diff --git a/sandbox-test/scripts/test_string_dump_removed.lua b/sandbox-test/scripts/test_string_dump_removed.lua new file mode 100644 index 0000000..f66556e --- /dev/null +++ b/sandbox-test/scripts/test_string_dump_removed.lua @@ -0,0 +1,18 @@ +-- Test that string.dump is removed +-- string.dump can be used to create bytecode from functions, +-- which could be used to bypass sandbox restrictions + +if string.dump ~= nil then + error("FAIL: string.dump should be nil but exists") +end + +-- Also verify string table exists and other functions work +if string.upper == nil then + error("FAIL: string.upper should exist") +end + +if string.format == nil then + error("FAIL: string.format should exist") +end + +print("PASS: string.dump removed, other string functions intact") diff --git a/sandbox-test/src/main.cpp b/sandbox-test/src/main.cpp new file mode 100644 index 0000000..e1a2748 --- /dev/null +++ b/sandbox-test/src/main.cpp @@ -0,0 +1,984 @@ +#include "test_harness.h" +#include "lua_sandbox.h" +#include "permission_gate.h" +#include +#include +#include "audit_log.h" +#include "rate_limiter.h" +#include "path_sandbox.h" +#include "timer_manager.h" +#include +#include +#include +#include +#include + +// Get path to scripts directory +std::string GetScriptsDir() { + // Scripts are copied to build directory by CMake + return "scripts"; +} + +// Helper to create test context +SandboxContext TestContext() { + return SandboxContext{ + .app_id = "test.app", + .app_path = ".", + .permissions = {}, + .is_system_app = false + }; +} + +// Helper to read file contents +std::string ReadFile(const std::string& path) { + std::ifstream f(path); + if (!f) return ""; + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +// Helper to setup a _test table in real _G for timer tests +// This allows test scripts to store state without triggering the proxy's __newindex +void SetupTestTable(lua_State* L) { + lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + if (lua_getmetatable(L, -1)) { + lua_getfield(L, -1, "__index"); + if (lua_istable(L, -1)) { + // Found real _G through proxy's __index + lua_newtable(L); // Create _test table + lua_setfield(L, -2, "_test"); + lua_pop(L, 3); // pop real _G, metatable, proxy + return; + } + lua_pop(L, 2); // pop __index, metatable + } + // No proxy, use directly + lua_newtable(L); + lua_setfield(L, -2, "_test"); + lua_pop(L, 1); // pop _G +} + +//============================================================================= +// TEST DEFINITIONS +//============================================================================= + +bool Test_DangerousGlobalsRemoved(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + std::string script = ReadFile(GetScriptsDir() + "/test_globals_removed.lua"); + EXPECT_FALSE(script.empty()); + EXPECT_TRUE(sandbox.LoadString(script, "test_globals_removed.lua")); + return true; +} + +bool Test_BytecodeRejected(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + + // Lua 5.4 bytecode signature + std::string bytecode = "\x1bLua\x54\x00\x19\x93\r\n\x1a\n"; + + EXPECT_FALSE(sandbox.LoadString(bytecode, "bytecode_test")); + + // Error should mention binary/bytecode + std::string err = sandbox.GetLastError(); + bool mentions_binary = (err.find("binary") != std::string::npos || + err.find("attempt to load") != std::string::npos || + err.find("text") != std::string::npos); + EXPECT_TRUE(mentions_binary); + + return true; +} + +bool Test_MemoryLimitEnforced(std::string& error_msg) { + SandboxLimits limits; + limits.memory_bytes = 512 * 1024; // 512 KB - very small + + LuaSandbox sandbox(TestContext(), limits); + + std::string script = ReadFile(GetScriptsDir() + "/test_memory_limit.lua"); + EXPECT_FALSE(script.empty()); + + // Should fail due to memory exhaustion + EXPECT_FALSE(sandbox.LoadString(script, "test_memory_limit.lua")); + + return true; +} + +bool Test_CPULimitEnforced(std::string& error_msg) { + SandboxLimits limits; + limits.instructions_per_call = 10000; // Very low + + LuaSandbox sandbox(TestContext(), limits); + + std::string script = ReadFile(GetScriptsDir() + "/test_cpu_limit.lua"); + EXPECT_FALSE(script.empty()); + + // Should fail due to instruction limit + EXPECT_FALSE(sandbox.LoadString(script, "test_cpu_limit.lua")); + + // Error should mention instructions + std::string err = sandbox.GetLastError(); + EXPECT_CONTAINS(err, "instruction"); + + return true; +} + +bool Test_MetatableProtected(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + std::string script = ReadFile(GetScriptsDir() + "/test_metatable_protected.lua"); + EXPECT_FALSE(script.empty()); + if (!sandbox.LoadString(script, "test_metatable_protected.lua")) { + error_msg = "Script failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_SafeOperationsWork(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + std::string script = ReadFile(GetScriptsDir() + "/test_safe_operations.lua"); + EXPECT_FALSE(script.empty()); + EXPECT_TRUE(sandbox.LoadString(script, "test_safe_operations.lua")); + return true; +} + +bool Test_StringDumpRemoved(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + std::string script = ReadFile(GetScriptsDir() + "/test_string_dump_removed.lua"); + EXPECT_FALSE(script.empty()); + EXPECT_TRUE(sandbox.LoadString(script, "test_string_dump_removed.lua")); + return true; +} + +bool Test_MemoryTracking(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + + // Initially should have some baseline memory + size_t initial = sandbox.GetMemoryUsed(); + EXPECT_TRUE(initial > 0); + + // Allocate some data + sandbox.LoadString("local t = {}; for i=1,1000 do t[i] = string.rep('x', 100) end", "alloc"); + + // Memory should have increased + size_t after = sandbox.GetMemoryUsed(); + EXPECT_TRUE(after > initial); + + return true; +} + +bool Test_InstructionCounting(std::string& error_msg) { + SandboxLimits limits; + limits.instructions_per_call = 1000000; // 1M instructions + + LuaSandbox sandbox(TestContext(), limits); + + // Run some code + sandbox.LoadString("for i=1,10000 do local x = i * 2 end", "counting"); + + // Should have used some instructions + EXPECT_TRUE(sandbox.GetInstructionsUsed() > 0); + + return true; +} + +bool Test_MultipleLoads(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + + // Should be able to load multiple scripts + EXPECT_TRUE(sandbox.LoadString("local a = 1", "script1")); + EXPECT_TRUE(sandbox.LoadString("local b = 2", "script2")); + EXPECT_TRUE(sandbox.LoadString("local c = 3", "script3")); + + return true; +} + +bool Test_ErrorRecovery(std::string& error_msg) { + LuaSandbox sandbox(TestContext()); + + // Script with error + EXPECT_FALSE(sandbox.LoadString("error('test error')", "error_script")); + + // Should still be able to run more code after error + EXPECT_TRUE(sandbox.LoadString("local x = 1", "after_error")); + + return true; +} + +//============================================================================= +// PERMISSION SYSTEM TESTS (Milestone 2) +//============================================================================= + +bool Test_NormalPermissionAutoGranted(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {"internet", "vibrate"}; // Declare normal permissions + + mosis::PermissionGate gate(ctx); + + // Normal permissions should be auto-granted when declared + EXPECT_TRUE(gate.HasPermission("internet")); + EXPECT_TRUE(gate.HasPermission("vibrate")); + + return true; +} + +bool Test_DangerousPermissionRequiresGrant(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {"camera"}; // Declare dangerous permission + + mosis::PermissionGate gate(ctx); + + // Not granted yet (regular app) + EXPECT_FALSE(gate.HasPermission("camera")); + + // Grant at runtime + gate.GrantPermission("camera"); + + // Now should have it + EXPECT_TRUE(gate.HasPermission("camera")); + + // Revoke + gate.RevokePermission("camera"); + EXPECT_FALSE(gate.HasPermission("camera")); + + return true; +} + +bool Test_SignaturePermissionSystemOnly(std::string& error_msg) { + // Non-system app + SandboxContext ctx = TestContext(); + ctx.permissions = {"system.settings"}; + ctx.is_system_app = false; + + mosis::PermissionGate gate(ctx); + EXPECT_FALSE(gate.HasPermission("system.settings")); + + // System app + SandboxContext sys_ctx = TestContext(); + sys_ctx.permissions = {"system.settings"}; + sys_ctx.is_system_app = true; + + mosis::PermissionGate sys_gate(sys_ctx); + EXPECT_TRUE(sys_gate.HasPermission("system.settings")); + + return true; +} + +bool Test_UserGestureTracking(std::string& error_msg) { + SandboxContext ctx = TestContext(); + mosis::PermissionGate gate(ctx); + + // No recent gesture + EXPECT_FALSE(gate.HasRecentUserGesture(5000)); + + // Record gesture + gate.RecordUserGesture(); + + // Should have recent gesture + EXPECT_TRUE(gate.HasRecentUserGesture(5000)); + + // Wait for gesture to expire (use short window) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_FALSE(gate.HasRecentUserGesture(50)); // 50ms window, we waited 100ms + + return true; +} + +bool Test_UndeclaredPermissionDenied(std::string& error_msg) { + SandboxContext ctx = TestContext(); + ctx.permissions = {}; // No permissions declared + + mosis::PermissionGate gate(ctx); + + // Even normal permissions need to be declared + EXPECT_FALSE(gate.HasPermission("internet")); + + // Dangerous permissions also denied + EXPECT_FALSE(gate.HasPermission("camera")); + + return true; +} + +bool Test_SystemAppGetsDangerousAuto(std::string& error_msg) { + // System apps get dangerous permissions automatically (no runtime grant needed) + SandboxContext ctx = TestContext(); + ctx.permissions = {"camera", "microphone"}; + ctx.is_system_app = true; + + mosis::PermissionGate gate(ctx); + + // System app should have dangerous perms without explicit grant + EXPECT_TRUE(gate.HasPermission("camera")); + EXPECT_TRUE(gate.HasPermission("microphone")); + + return true; +} + +bool Test_PermissionCategoryCheck(std::string& error_msg) { + // Check that permission categories are correct + EXPECT_TRUE(mosis::PermissionGate::GetCategory("internet") == mosis::PermissionCategory::Normal); + EXPECT_TRUE(mosis::PermissionGate::GetCategory("camera") == mosis::PermissionCategory::Dangerous); + EXPECT_TRUE(mosis::PermissionGate::GetCategory("system.settings") == mosis::PermissionCategory::Signature); + + // Unknown permissions default to Dangerous + EXPECT_TRUE(mosis::PermissionGate::GetCategory("unknown.perm") == mosis::PermissionCategory::Dangerous); + + return true; +} + +//============================================================================= +// AUDIT LOG TESTS (Milestone 3) +//============================================================================= + +bool Test_AuditLogBasic(std::string& error_msg) { + mosis::AuditLog log(1000); + + log.Log(mosis::AuditEvent::AppStart, "test.app", "App started"); + log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true); + log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false); + + auto entries = log.GetEntries(10); + EXPECT_TRUE(entries.size() == 3); + + auto app_entries = log.GetEntriesForApp("test.app", 10); + EXPECT_TRUE(app_entries.size() == 3); + + // Check event filtering + auto denied_entries = log.GetEntriesByEvent(mosis::AuditEvent::PermissionDenied, 10); + EXPECT_TRUE(denied_entries.size() == 1); + + return true; +} + +bool Test_AuditLogRingBuffer(std::string& error_msg) { + mosis::AuditLog log(100); // Small buffer + + // Log more than capacity + for (int i = 0; i < 200; i++) { + log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i)); + } + + // Should only have latest 100 stored + auto entries = log.GetEntries(200); + EXPECT_TRUE(entries.size() == 100); + + // Total logged should be 200 + EXPECT_TRUE(log.GetTotalEntries() == 200); + + // Most recent should be "199" + EXPECT_TRUE(entries[0].details == "199"); + + return true; +} + +bool Test_AuditLogThreadSafe(std::string& error_msg) { + mosis::AuditLog log(10000); + + // Spawn multiple threads logging concurrently + std::vector threads; + for (int t = 0; t < 4; t++) { + threads.emplace_back([&log, t]() { + for (int i = 0; i < 1000; i++) { + log.Log(mosis::AuditEvent::Custom, "app" + std::to_string(t), std::to_string(i)); + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + // Should have logged 4000 entries + EXPECT_TRUE(log.GetTotalEntries() == 4000); + + return true; +} + +//============================================================================= +// RATE LIMITER TESTS (Milestone 3) +//============================================================================= + +bool Test_RateLimiterBasic(std::string& error_msg) { + mosis::RateLimiter limiter; + + // Should succeed initially (has tokens) + EXPECT_TRUE(limiter.Check("test.app", "network.request")); + + return true; +} + +bool Test_RateLimiterExhaustion(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {0.0, 5.0}); // 5 tokens, no refill + + // Use all tokens + for (int i = 0; i < 5; i++) { + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + } + + // Should be denied now + EXPECT_FALSE(limiter.Check("test.app", "test.op")); + + return true; +} + +bool Test_RateLimiterRefill(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {1000.0, 1.0}); // 1000/sec, max 1 token + + // Use the token + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + EXPECT_FALSE(limiter.Check("test.app", "test.op")); + + // Wait a bit for refill (2ms = ~2 tokens at 1000/sec, but max is 1) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + // Should have token again + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + + return true; +} + +bool Test_RateLimiterAppIsolation(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {0.0, 1.0}); // 1 token, no refill + + // App 1 uses its token + EXPECT_TRUE(limiter.Check("app1", "test.op")); + EXPECT_FALSE(limiter.Check("app1", "test.op")); + + // App 2 should still have its token + EXPECT_TRUE(limiter.Check("app2", "test.op")); + + return true; +} + +bool Test_RateLimiterReset(std::string& error_msg) { + mosis::RateLimiter limiter; + limiter.SetLimit("test.op", {0.0, 2.0}); // 2 tokens, no refill + + // Use all tokens + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + EXPECT_FALSE(limiter.Check("test.app", "test.op")); + + // Reset the app + limiter.ResetApp("test.app"); + + // Should have tokens again + EXPECT_TRUE(limiter.Check("test.app", "test.op")); + + return true; +} + +bool Test_RateLimiterNoConfig(std::string& error_msg) { + mosis::RateLimiter limiter; + + // Operation with no config should always succeed + for (int i = 0; i < 100; i++) { + EXPECT_TRUE(limiter.Check("test.app", "unconfigured.operation")); + } + + return true; +} + +//============================================================================= +// PATH SANDBOX TESTS (Milestone 4) +//============================================================================= + +bool Test_PathRejectsTraversal(std::string& error_msg) { + mosis::PathSandbox sandbox("D:/test/app"); + + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("../etc/passwd")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("foo/../../../bar")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..\\windows\\system32")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("data/..")); + EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..")); + + // Should not match ".." in filenames + EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("file..txt")); + EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("test...name")); + + std::string canonical; + EXPECT_FALSE(sandbox.ValidatePath("../etc/passwd", canonical)); + EXPECT_FALSE(sandbox.ValidatePath("data/../../../etc/passwd", canonical)); + + return true; +} + +bool Test_PathRejectsAbsolute(std::string& error_msg) { + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("/etc/passwd")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("C:\\Windows\\System32")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("D:/test/file.txt")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("\\\\server\\share")); + EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("//server/share")); + + EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("scripts/utils.lua")); + EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("./data/file.txt")); + EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("data/config.json")); + + mosis::PathSandbox sandbox("D:/test/app"); + std::string canonical; + EXPECT_FALSE(sandbox.ValidatePath("/etc/passwd", canonical)); + EXPECT_FALSE(sandbox.ValidatePath("C:\\Windows\\System32\\file.dll", canonical)); + + return true; +} + +bool Test_PathAcceptsValid(std::string& error_msg) { + mosis::PathSandbox sandbox(GetScriptsDir()); + + std::string canonical; + EXPECT_TRUE(sandbox.ValidatePath("test_globals_removed.lua", canonical)); + EXPECT_TRUE(sandbox.ValidatePath("./test_memory_limit.lua", canonical)); + + return true; +} + +bool Test_ModuleNameValidation(std::string& error_msg) { + // Valid names + EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("utils")); + EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("my_module")); + EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("ui.button")); + EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("a.b.c")); + EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("Module123")); + + // Invalid names + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName(".utils")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("utils.")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("ui..button")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("../evil")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("/etc/passwd")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo;bar")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo/bar")); + EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo\\bar")); + + return true; +} + +bool Test_ModuleToPath(std::string& error_msg) { + EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("utils") == "scripts/utils.lua"); + EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("ui.button") == "scripts/ui/button.lua"); + EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("a.b.c") == "scripts/a/b/c.lua"); + + return true; +} + +bool Test_SafeRequireLoads(std::string& error_msg) { + // Create sandbox with scripts directory as app path + // The test_module.lua is in scripts/scripts/ so after ModuleToPath + // it becomes scripts/scripts/test_module.lua + // Safe require is auto-registered by LuaSandbox when app_path is set + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); // "scripts" + + LuaSandbox sandbox(ctx); + + // Should be able to require a test module + std::string script = + "local m = require('test_module')\n" + "if m.value ~= 42 then\n" + " error('module value mismatch')\n" + "end\n" + "return true\n"; + + if (!sandbox.LoadString(script, "require_test")) { + error_msg = "Failed to load module: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_SafeRequireCaches(std::string& error_msg) { + // Safe require is auto-registered by LuaSandbox when app_path is set + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); + + LuaSandbox sandbox(ctx); + + std::string script = + "local m1 = require('test_module')\n" + "local m2 = require('test_module')\n" + "if m1 ~= m2 then\n" + " error('modules should be same (cached)')\n" + "end\n" + "return true\n"; + + if (!sandbox.LoadString(script, "cache_test")) { + error_msg = "Cache test failed: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_SafeRequireRejectsInvalid(std::string& error_msg) { + // Safe require is auto-registered by LuaSandbox when app_path is set + SandboxContext ctx = TestContext(); + ctx.app_path = GetScriptsDir(); + + LuaSandbox sandbox(ctx); + + // Should reject path traversal in module name + EXPECT_FALSE(sandbox.LoadString("require('../evil')", "evil_require")); + + // Should reject absolute paths + EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')", "abs_require")); + + // Should reject special characters + EXPECT_FALSE(sandbox.LoadString("require('foo;bar')", "special_require")); + + // Should reject empty + EXPECT_FALSE(sandbox.LoadString("require('')", "empty_require")); + + return true; +} + +//============================================================================= +// TIMER MANAGER TESTS (Milestone 5) +//============================================================================= + +bool Test_SetTimeoutFires(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + // Setup _test table for storing state (bypasses proxy __newindex) + SetupTestTable(sandbox.GetState()); + + // Register timer API + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Set a timeout that modifies _test table + std::string script = + "_test.fired = false\n" + "setTimeout(function() _test.fired = true end, 50)\n"; + + if (!sandbox.LoadString(script, "timeout_test")) { + error_msg = "Failed to set timeout: " + sandbox.GetLastError(); + return false; + } + + // Process timers after delay + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + manager.ProcessTimers(); + + // Check if callback fired + if (!sandbox.LoadString("assert(_test.fired == true, 'callback did not fire')", "check")) { + error_msg = "Timeout callback did not fire: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_SetIntervalFires(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + // Setup _test table for storing state + SetupTestTable(sandbox.GetState()); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + std::string script = + "_test.count = 0\n" + "setInterval(function() _test.count = _test.count + 1 end, 30)\n"; + + if (!sandbox.LoadString(script, "interval_test")) { + error_msg = "Failed to set interval: " + sandbox.GetLastError(); + return false; + } + + // Process multiple times + for (int i = 0; i < 5; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + manager.ProcessTimers(); + } + + // Should have fired multiple times + if (!sandbox.LoadString("assert(_test.count >= 3, 'interval fired only ' .. _test.count .. ' times')", "check")) { + error_msg = "Interval did not fire enough times: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_ClearTimeoutCancels(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + // Setup _test table for storing state + SetupTestTable(sandbox.GetState()); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + std::string script = + "_test.fired = false\n" + "local id = setTimeout(function() _test.fired = true end, 100)\n" + "clearTimeout(id)\n"; + + if (!sandbox.LoadString(script, "clear_test")) { + error_msg = "Failed to clear timeout: " + sandbox.GetLastError(); + return false; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + manager.ProcessTimers(); + + // Should NOT have fired + if (!sandbox.LoadString("assert(_test.fired == false, 'callback should not have fired')", "check")) { + error_msg = "Cancelled timeout still fired: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_ClearIntervalCancels(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + // Setup _test table for storing state + SetupTestTable(sandbox.GetState()); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Store both count and interval ID in _test table so they persist across LoadString calls + std::string script = + "_test.count = 0\n" + "_test.id = setInterval(function() _test.count = _test.count + 1 end, 30)\n"; + + if (!sandbox.LoadString(script, "interval_setup")) { + error_msg = "Failed to set interval: " + sandbox.GetLastError(); + return false; + } + + // Let it fire once + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + manager.ProcessTimers(); + + // Now cancel it + sandbox.LoadString("clearInterval(_test.id)", "cancel"); + + // Wait and process more + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + manager.ProcessTimers(); + + // Should have fired only once (or maybe twice due to timing) + if (!sandbox.LoadString("assert(_test.count <= 2, 'interval fired too many times: ' .. _test.count)", "check")) { + error_msg = "Interval kept firing after cancel: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +bool Test_TimerLimitEnforced(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Try to create more than MAX_TIMERS_PER_APP (100) timers + std::string script = + "created = 0\n" + "for i = 1, 150 do\n" + " local ok, err = pcall(function()\n" + " setTimeout(function() end, 1000000)\n" + " end)\n" + " if ok then created = created + 1 end\n" + "end\n"; + + sandbox.LoadString(script, "limit_test"); + + // Should be capped at MAX_TIMERS_PER_APP + size_t count = manager.GetTimerCount(ctx.app_id); + if (count > 100) { + error_msg = "Timer limit not enforced: " + std::to_string(count) + " timers created"; + return false; + } + + return true; +} + +bool Test_ClearAppTimersCleanup(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + std::string script = + "for i = 1, 10 do\n" + " setTimeout(function() end, 1000000)\n" + "end\n"; + + sandbox.LoadString(script, "cleanup_test"); + + size_t before = manager.GetTimerCount(ctx.app_id); + EXPECT_TRUE(before == 10); + + // Clear all timers for app (simulating app stop) + manager.ClearAppTimers(ctx.app_id); + + size_t after = manager.GetTimerCount(ctx.app_id); + EXPECT_TRUE(after == 0); + + return true; +} + +bool Test_MinIntervalEnforced(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox + mosis::TimerManager manager; + + // Setup _test table for storing state + SetupTestTable(sandbox.GetState()); + + mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); + + // Try to set interval less than minimum (10ms) + std::string script = + "_test.count = 0\n" + "setInterval(function() _test.count = _test.count + 1 end, 1)\n"; // 1ms, should be clamped to 10ms + + sandbox.LoadString(script, "min_interval_test"); + + // With 1ms interval, in 50ms we'd get 50 callbacks + // With 10ms minimum, we should get ~5 + std::this_thread::sleep_for(std::chrono::milliseconds(55)); + for (int i = 0; i < 10; i++) { + manager.ProcessTimers(); + } + + if (!sandbox.LoadString("assert(_test.count <= 10, 'interval fired too often: ' .. _test.count)", "check")) { + error_msg = "Minimum interval not enforced: " + sandbox.GetLastError(); + return false; + } + + return true; +} + +//============================================================================= +// MAIN +//============================================================================= + +int main(int argc, char* argv[]) { + std::string filter; + std::string output_file = "test_results.json"; + + // Parse args + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg == "--test" && i + 1 < argc) { + filter = argv[++i]; + } else if (arg == "--output" && i + 1 < argc) { + output_file = argv[++i]; + } else if (arg == "--help") { + std::cout << "Usage: sandbox-test [options]\n"; + std::cout << "Options:\n"; + std::cout << " --test Run only tests containing \n"; + std::cout << " --output Write JSON report to \n"; + std::cout << " --help Show this help\n"; + return 0; + } + } + + std::cout << "========================================\n"; + std::cout << " LUA SANDBOX SECURITY TESTS\n"; + std::cout << "========================================\n\n"; + + // Check scripts directory exists + if (!std::filesystem::exists(GetScriptsDir())) { + std::cerr << "ERROR: Scripts directory not found: " << GetScriptsDir() << "\n"; + std::cerr << "Make sure to run from the build directory.\n"; + return 1; + } + + // Register tests + TestHarness harness; + + // Milestone 1: Core Sandbox + harness.AddTest("DangerousGlobalsRemoved", Test_DangerousGlobalsRemoved); + harness.AddTest("BytecodeRejected", Test_BytecodeRejected); + harness.AddTest("MemoryLimitEnforced", Test_MemoryLimitEnforced); + harness.AddTest("CPULimitEnforced", Test_CPULimitEnforced); + harness.AddTest("MetatableProtected", Test_MetatableProtected); + harness.AddTest("SafeOperationsWork", Test_SafeOperationsWork); + harness.AddTest("StringDumpRemoved", Test_StringDumpRemoved); + harness.AddTest("MemoryTracking", Test_MemoryTracking); + harness.AddTest("InstructionCounting", Test_InstructionCounting); + harness.AddTest("MultipleLoads", Test_MultipleLoads); + harness.AddTest("ErrorRecovery", Test_ErrorRecovery); + + // Milestone 2: Permission System + harness.AddTest("NormalPermissionAutoGranted", Test_NormalPermissionAutoGranted); + harness.AddTest("DangerousPermissionRequiresGrant", Test_DangerousPermissionRequiresGrant); + harness.AddTest("SignaturePermissionSystemOnly", Test_SignaturePermissionSystemOnly); + harness.AddTest("UserGestureTracking", Test_UserGestureTracking); + harness.AddTest("UndeclaredPermissionDenied", Test_UndeclaredPermissionDenied); + harness.AddTest("SystemAppGetsDangerousAuto", Test_SystemAppGetsDangerousAuto); + harness.AddTest("PermissionCategoryCheck", Test_PermissionCategoryCheck); + + // Milestone 3: Audit Logging & Rate Limiting + harness.AddTest("AuditLogBasic", Test_AuditLogBasic); + harness.AddTest("AuditLogRingBuffer", Test_AuditLogRingBuffer); + harness.AddTest("AuditLogThreadSafe", Test_AuditLogThreadSafe); + harness.AddTest("RateLimiterBasic", Test_RateLimiterBasic); + harness.AddTest("RateLimiterExhaustion", Test_RateLimiterExhaustion); + harness.AddTest("RateLimiterRefill", Test_RateLimiterRefill); + harness.AddTest("RateLimiterAppIsolation", Test_RateLimiterAppIsolation); + harness.AddTest("RateLimiterReset", Test_RateLimiterReset); + harness.AddTest("RateLimiterNoConfig", Test_RateLimiterNoConfig); + + // Milestone 4: Safe Path & Require + harness.AddTest("PathRejectsTraversal", Test_PathRejectsTraversal); + harness.AddTest("PathRejectsAbsolute", Test_PathRejectsAbsolute); + harness.AddTest("PathAcceptsValid", Test_PathAcceptsValid); + harness.AddTest("ModuleNameValidation", Test_ModuleNameValidation); + harness.AddTest("ModuleToPath", Test_ModuleToPath); + harness.AddTest("SafeRequireLoads", Test_SafeRequireLoads); + harness.AddTest("SafeRequireCaches", Test_SafeRequireCaches); + harness.AddTest("SafeRequireRejectsInvalid", Test_SafeRequireRejectsInvalid); + + // Milestone 5: Timer & Callback System + harness.AddTest("SetTimeoutFires", Test_SetTimeoutFires); + harness.AddTest("SetIntervalFires", Test_SetIntervalFires); + harness.AddTest("ClearTimeoutCancels", Test_ClearTimeoutCancels); + harness.AddTest("ClearIntervalCancels", Test_ClearIntervalCancels); + harness.AddTest("TimerLimitEnforced", Test_TimerLimitEnforced); + harness.AddTest("ClearAppTimersCleanup", Test_ClearAppTimersCleanup); + harness.AddTest("MinIntervalEnforced", Test_MinIntervalEnforced); + + // Run tests + auto results = harness.Run(filter); + + // Output + harness.PrintResults(results); + harness.WriteJsonReport(results, output_file); + + std::cout << "\nJSON report written to: " << output_file << "\n"; + + // Return non-zero if any tests failed + int failed = 0; + for (const auto& r : results) { + if (!r.passed) failed++; + } + + return failed > 0 ? 1 : 0; +} diff --git a/sandbox-test/src/test_harness.cpp b/sandbox-test/src/test_harness.cpp new file mode 100644 index 0000000..d1e24bc --- /dev/null +++ b/sandbox-test/src/test_harness.cpp @@ -0,0 +1,128 @@ +#include "test_harness.h" +#include +#include +#include +#include + +void TestHarness::AddTest(const std::string& name, std::function func) { + m_tests.push_back({name, func}); +} + +std::vector TestHarness::Run(const std::string& filter) { + std::vector results; + + for (const auto& test : m_tests) { + // Filter check + if (!filter.empty() && test.name.find(filter) == std::string::npos) { + continue; + } + + TestResult result; + result.name = test.name; + + std::cout << "Running: " << test.name << "... " << std::flush; + + auto start = std::chrono::steady_clock::now(); + + try { + std::string error; + result.passed = test.func(error); + result.error_message = error; + } catch (const std::exception& e) { + result.passed = false; + result.error_message = std::string("Exception: ") + e.what(); + } catch (...) { + result.passed = false; + result.error_message = "Unknown exception"; + } + + auto end = std::chrono::steady_clock::now(); + result.duration_ms = std::chrono::duration_cast(end - start).count(); + + if (result.passed) { + std::cout << "PASSED (" << result.duration_ms << "ms)\n"; + } else { + std::cout << "FAILED\n"; + std::cout << " Error: " << result.error_message << "\n"; + } + + results.push_back(result); + } + + return results; +} + +void TestHarness::WriteJsonReport(const std::vector& results, const std::string& path) { + nlohmann::json report; + + // Get timestamp + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%SZ"); + + report["name"] = "Lua Sandbox Security Tests"; + report["timestamp"] = ss.str(); + + int passed = 0, failed = 0; + for (const auto& r : results) { + if (r.passed) passed++; + else failed++; + } + + report["summary"]["passed"] = passed; + report["summary"]["failed"] = failed; + report["summary"]["total"] = static_cast(results.size()); + + nlohmann::json tests = nlohmann::json::array(); + for (const auto& r : results) { + nlohmann::json t; + t["name"] = r.name; + t["status"] = r.passed ? "passed" : "failed"; + t["duration_ms"] = r.duration_ms; + if (!r.passed && !r.error_message.empty()) { + t["error"] = r.error_message; + } + tests.push_back(t); + } + report["tests"] = tests; + + std::ofstream f(path); + f << report.dump(2); +} + +void TestHarness::PrintResults(const std::vector& results) { + std::cout << "\n"; + std::cout << "========================================\n"; + std::cout << " TEST RESULTS\n"; + std::cout << "========================================\n\n"; + + int passed = 0, failed = 0; + for (const auto& r : results) { + if (r.passed) passed++; + else failed++; + } + + std::cout << "Total: " << results.size() << "\n"; + std::cout << "Passed: " << passed << "\n"; + std::cout << "Failed: " << failed << "\n\n"; + + if (failed > 0) { + std::cout << "FAILED TESTS:\n"; + for (const auto& r : results) { + if (!r.passed) { + std::cout << " - " << r.name << "\n"; + std::cout << " " << r.error_message << "\n"; + } + } + std::cout << "\n"; + } + + if (failed == 0) { + std::cout << "ALL TESTS PASSED!\n"; + } else { + std::cout << "SOME TESTS FAILED!\n"; + } + + std::cout << "========================================\n"; +} diff --git a/sandbox-test/src/test_harness.h b/sandbox-test/src/test_harness.h new file mode 100644 index 0000000..86ce4b3 --- /dev/null +++ b/sandbox-test/src/test_harness.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Simple test result +struct TestResult { + std::string name; + bool passed; + std::string error_message; + int64_t duration_ms; +}; + +// Test case definition +struct TestCase { + std::string name; + std::function func; // Returns true if passed, error in string +}; + +// Test runner +class TestHarness { +public: + void AddTest(const std::string& name, std::function func); + + // Run all tests or filter by name + std::vector Run(const std::string& filter = ""); + + // Output results as JSON + void WriteJsonReport(const std::vector& results, const std::string& path); + + // Print results to console + void PrintResults(const std::vector& results); + +private: + std::vector m_tests; +}; + +// Assertion macros +#define EXPECT_TRUE(cond) \ + do { \ + if (!(cond)) { \ + error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": EXPECT_TRUE(" #cond ") failed"; \ + return false; \ + } \ + } while(0) + +#define EXPECT_FALSE(cond) \ + do { \ + if (cond) { \ + error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": EXPECT_FALSE(" #cond ") failed"; \ + return false; \ + } \ + } while(0) + +#define EXPECT_EQ(a, b) \ + do { \ + if ((a) != (b)) { \ + error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": EXPECT_EQ failed: " + std::to_string(a) + " != " + std::to_string(b); \ + return false; \ + } \ + } while(0) + +#define EXPECT_NE(a, b) \ + do { \ + if ((a) == (b)) { \ + error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": EXPECT_NE failed: values are equal"; \ + return false; \ + } \ + } while(0) + +#define EXPECT_CONTAINS(haystack, needle) \ + do { \ + if ((haystack).find(needle) == std::string::npos) { \ + error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \ + ": EXPECT_CONTAINS failed: '" + (haystack) + "' does not contain '" + (needle) + "'"; \ + return false; \ + } \ + } while(0) diff --git a/sandbox-test/test_results.json b/sandbox-test/test_results.json new file mode 100644 index 0000000..eeb5096 --- /dev/null +++ b/sandbox-test/test_results.json @@ -0,0 +1,221 @@ +{ + "name": "Lua Sandbox Security Tests", + "summary": { + "failed": 0, + "passed": 42, + "total": 42 + }, + "tests": [ + { + "duration_ms": 0, + "name": "DangerousGlobalsRemoved", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "BytecodeRejected", + "status": "passed" + }, + { + "duration_ms": 2, + "name": "MemoryLimitEnforced", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "CPULimitEnforced", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "MetatableProtected", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SafeOperationsWork", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "StringDumpRemoved", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "MemoryTracking", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "InstructionCounting", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "MultipleLoads", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "ErrorRecovery", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "NormalPermissionAutoGranted", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "DangerousPermissionRequiresGrant", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SignaturePermissionSystemOnly", + "status": "passed" + }, + { + "duration_ms": 111, + "name": "UserGestureTracking", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "UndeclaredPermissionDenied", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SystemAppGetsDangerousAuto", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "PermissionCategoryCheck", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "AuditLogBasic", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "AuditLogRingBuffer", + "status": "passed" + }, + { + "duration_ms": 14, + "name": "AuditLogThreadSafe", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "RateLimiterBasic", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "RateLimiterExhaustion", + "status": "passed" + }, + { + "duration_ms": 16, + "name": "RateLimiterRefill", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "RateLimiterAppIsolation", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "RateLimiterReset", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "RateLimiterNoConfig", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "PathRejectsTraversal", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "PathRejectsAbsolute", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "PathAcceptsValid", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "ModuleNameValidation", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "ModuleToPath", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SafeRequireLoads", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SafeRequireCaches", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "SafeRequireRejectsInvalid", + "status": "passed" + }, + { + "duration_ms": 107, + "name": "SetTimeoutFires", + "status": "passed" + }, + { + "duration_ms": 237, + "name": "SetIntervalFires", + "status": "passed" + }, + { + "duration_ms": 155, + "name": "ClearTimeoutCancels", + "status": "passed" + }, + { + "duration_ms": 158, + "name": "ClearIntervalCancels", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "TimerLimitEnforced", + "status": "passed" + }, + { + "duration_ms": 0, + "name": "ClearAppTimersCleanup", + "status": "passed" + }, + { + "duration_ms": 62, + "name": "MinIntervalEnforced", + "status": "passed" + } + ], + "timestamp": "2026-01-18T13:19:38Z" +} \ No newline at end of file diff --git a/src/main/cpp/sandbox/audit_log.cpp b/src/main/cpp/sandbox/audit_log.cpp new file mode 100644 index 0000000..0002311 --- /dev/null +++ b/src/main/cpp/sandbox/audit_log.cpp @@ -0,0 +1,188 @@ +#include "audit_log.h" + +#include + +namespace mosis { + +//============================================================================= +// CONSTRUCTOR +//============================================================================= + +AuditLog::AuditLog(size_t max_entries) + : m_max_entries(max_entries) +{ + m_entries.resize(max_entries); +} + +//============================================================================= +// LOGGING +//============================================================================= + +void AuditLog::Log(AuditEvent event, const std::string& app_id, + const std::string& details, bool success) { + std::lock_guard lock(m_mutex); + + AuditEntry entry{ + .timestamp = std::chrono::system_clock::now(), + .event = event, + .app_id = app_id, + .details = details, + .success = success + }; + + m_entries[m_write_index] = std::move(entry); + m_write_index = (m_write_index + 1) % m_max_entries; + m_total_logged++; + + if (m_total_logged > m_max_entries) { + m_wrapped = true; + } +} + +//============================================================================= +// QUERIES +//============================================================================= + +std::vector AuditLog::GetEntries(size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector result; + size_t stored = GetStoredEntries(); + count = std::min(count, stored); + + result.reserve(count); + + // Read from most recent backwards + for (size_t i = 0; i < count; i++) { + size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries; + result.push_back(m_entries[idx]); + } + + return result; +} + +std::vector AuditLog::GetEntriesForApp(const std::string& app_id, + size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector result; + result.reserve(count); + + size_t stored = GetStoredEntries(); + + for (size_t i = 0; i < stored && result.size() < count; i++) { + size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries; + if (m_entries[idx].app_id == app_id) { + result.push_back(m_entries[idx]); + } + } + + return result; +} + +std::vector AuditLog::GetEntriesByEvent(AuditEvent event, + size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector result; + result.reserve(count); + + size_t stored = GetStoredEntries(); + + for (size_t i = 0; i < stored && result.size() < count; i++) { + size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries; + if (m_entries[idx].event == event) { + result.push_back(m_entries[idx]); + } + } + + return result; +} + +//============================================================================= +// STATISTICS +//============================================================================= + +size_t AuditLog::GetTotalEntries() const { + std::lock_guard lock(m_mutex); + return m_total_logged; +} + +size_t AuditLog::GetStoredEntries() const { + // Note: caller should hold lock + if (m_wrapped) { + return m_max_entries; + } + return m_write_index; +} + +size_t AuditLog::CountEvents(AuditEvent event, const std::string& app_id) const { + std::lock_guard lock(m_mutex); + + size_t count = 0; + size_t stored = GetStoredEntries(); + + for (size_t i = 0; i < stored; i++) { + const auto& entry = m_entries[i]; + if (entry.event == event) { + if (app_id.empty() || entry.app_id == app_id) { + count++; + } + } + } + + return count; +} + +//============================================================================= +// CLEAR +//============================================================================= + +void AuditLog::Clear() { + std::lock_guard lock(m_mutex); + m_write_index = 0; + m_total_logged = 0; + m_wrapped = false; + // Clear all entries + for (auto& entry : m_entries) { + entry = AuditEntry{}; + } +} + +//============================================================================= +// UTILITIES +//============================================================================= + +const char* AuditLog::EventToString(AuditEvent event) { + switch (event) { + case AuditEvent::AppStart: return "AppStart"; + case AuditEvent::AppStop: return "AppStop"; + case AuditEvent::PermissionCheck: return "PermissionCheck"; + case AuditEvent::PermissionGranted: return "PermissionGranted"; + case AuditEvent::PermissionDenied: return "PermissionDenied"; + case AuditEvent::NetworkRequest: return "NetworkRequest"; + case AuditEvent::NetworkBlocked: return "NetworkBlocked"; + case AuditEvent::FileAccess: return "FileAccess"; + case AuditEvent::FileBlocked: return "FileBlocked"; + case AuditEvent::DatabaseAccess: return "DatabaseAccess"; + case AuditEvent::CameraAccess: return "CameraAccess"; + case AuditEvent::MicrophoneAccess: return "MicrophoneAccess"; + case AuditEvent::LocationAccess: return "LocationAccess"; + case AuditEvent::SandboxViolation: return "SandboxViolation"; + case AuditEvent::ResourceLimitHit: return "ResourceLimitHit"; + case AuditEvent::RateLimitHit: return "RateLimitHit"; + case AuditEvent::Custom: return "Custom"; + default: return "Unknown"; + } +} + +//============================================================================= +// GLOBAL INSTANCE +//============================================================================= + +AuditLog& GetAuditLog() { + static AuditLog instance(10000); + return instance; +} + +} // namespace mosis diff --git a/src/main/cpp/sandbox/audit_log.h b/src/main/cpp/sandbox/audit_log.h new file mode 100644 index 0000000..c42333a --- /dev/null +++ b/src/main/cpp/sandbox/audit_log.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +namespace mosis { + +enum class AuditEvent { + // Lifecycle + AppStart, + AppStop, + + // Permissions + PermissionCheck, + PermissionGranted, + PermissionDenied, + + // Network + NetworkRequest, + NetworkBlocked, + + // Storage + FileAccess, + FileBlocked, + DatabaseAccess, + + // Hardware + CameraAccess, + MicrophoneAccess, + LocationAccess, + + // Security + SandboxViolation, + ResourceLimitHit, + RateLimitHit, + + // Other + Custom +}; + +struct AuditEntry { + std::chrono::system_clock::time_point timestamp; + AuditEvent event; + std::string app_id; + std::string details; + bool success; +}; + +class AuditLog { +public: + explicit AuditLog(size_t max_entries = 10000); + + // Log an event + void Log(AuditEvent event, const std::string& app_id, + const std::string& details = "", bool success = true); + + // Query entries (returns most recent first) + std::vector GetEntries(size_t count = 100) const; + std::vector GetEntriesForApp(const std::string& app_id, + size_t count = 100) const; + std::vector GetEntriesByEvent(AuditEvent event, + size_t count = 100) const; + + // Statistics + size_t GetTotalEntries() const; + size_t GetStoredEntries() const; + size_t CountEvents(AuditEvent event, const std::string& app_id = "") const; + + // Clear all entries + void Clear(); + + // Convert event to string for logging + static const char* EventToString(AuditEvent event); + +private: + mutable std::mutex m_mutex; + std::vector m_entries; + size_t m_max_entries; + size_t m_write_index = 0; + size_t m_total_logged = 0; + bool m_wrapped = false; +}; + +// Global audit log (singleton) +AuditLog& GetAuditLog(); + +} // namespace mosis + +// Convenience alias +using AuditLog = mosis::AuditLog; +using AuditEvent = mosis::AuditEvent; +using AuditEntry = mosis::AuditEntry; diff --git a/src/main/cpp/sandbox/lua_sandbox.cpp b/src/main/cpp/sandbox/lua_sandbox.cpp new file mode 100644 index 0000000..60f9c4c --- /dev/null +++ b/src/main/cpp/sandbox/lua_sandbox.cpp @@ -0,0 +1,448 @@ +#include "lua_sandbox.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace mosis { + +//============================================================================= +// ALLOCATOR +//============================================================================= + +void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { + auto* sandbox = static_cast(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(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(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(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 diff --git a/src/main/cpp/sandbox/lua_sandbox.h b/src/main/cpp/sandbox/lua_sandbox.h new file mode 100644 index 0000000..3f16da1 --- /dev/null +++ b/src/main/cpp/sandbox/lua_sandbox.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +// Forward declare lua_State to avoid including lua.h in header +struct lua_State; +struct lua_Debug; + +namespace mosis { + +// Resource limits for sandbox +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 +}; + +// Context for sandbox (app identity, permissions, etc.) +struct SandboxContext { + std::string app_id; + std::string app_path; + std::vector permissions; + bool is_system_app = false; +}; + +// Isolated Lua execution environment +class LuaSandbox { +public: + explicit LuaSandbox(const SandboxContext& context, + const SandboxLimits& limits = {}); + ~LuaSandbox(); + + // Non-copyable, non-movable + LuaSandbox(const LuaSandbox&) = delete; + LuaSandbox& operator=(const LuaSandbox&) = delete; + LuaSandbox(LuaSandbox&&) = delete; + LuaSandbox& operator=(LuaSandbox&&) = delete; + + // Load and execute Lua code (text only, bytecode rejected) + 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 SandboxLimits& GetLimits() const { return m_limits; } + const std::string& app_id() const { return m_context.app_id; } + + // Reset instruction counter (call before each event handler) + void ResetInstructionCount(); + + // Check if sandbox is in valid state + bool IsValid() const { return m_L != nullptr; } + +private: + // Setup functions + void SetupSandbox(); + void RemoveDangerousGlobals(); + void ProtectBuiltinTables(); + void SetupInstructionHook(); + void SetupSafeGlobals(); + void SetupSafeRequire(); + + // Allocator callback (static for C compatibility) + static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize); + + // Instruction hook callback (static for C compatibility) + static void InstructionHook(lua_State* L, lua_Debug* ar); + + // Safe print function + static int SafePrint(lua_State* L); + + // Safe require function + static int SafeRequire(lua_State* L); + + 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; +}; + +} // namespace mosis + +// Convenience alias for tests +using SandboxContext = mosis::SandboxContext; +using SandboxLimits = mosis::SandboxLimits; +using LuaSandbox = mosis::LuaSandbox; diff --git a/src/main/cpp/sandbox/path_sandbox.cpp b/src/main/cpp/sandbox/path_sandbox.cpp new file mode 100644 index 0000000..562c33a --- /dev/null +++ b/src/main/cpp/sandbox/path_sandbox.cpp @@ -0,0 +1,344 @@ +#include "path_sandbox.h" + +#include +#include +#include +#include +#include + +namespace mosis { + +//============================================================================= +// CONSTRUCTOR +//============================================================================= + +PathSandbox::PathSandbox(const std::string& app_path) + : m_app_path(app_path) +{ + // Normalize the app path + if (!m_app_path.empty()) { + // Ensure trailing separator for prefix matching + if (m_app_path.back() != '/' && m_app_path.back() != '\\') { + m_app_path += '/'; + } + // Normalize separators to forward slash + std::replace(m_app_path.begin(), m_app_path.end(), '\\', '/'); + } +} + +//============================================================================= +// PATH VALIDATION +//============================================================================= + +bool PathSandbox::ContainsTraversal(const std::string& path) { + std::string normalized = NormalizePath(path); + + // Check for .. anywhere in the path + size_t pos = 0; + while ((pos = normalized.find("..", pos)) != std::string::npos) { + // Make sure it's actually a parent directory reference, not part of a filename + bool at_start = (pos == 0); + bool before_is_sep = (pos > 0 && (normalized[pos-1] == '/' || normalized[pos-1] == '\\')); + + size_t after_pos = pos + 2; + bool at_end = (after_pos >= normalized.size()); + bool after_is_sep = (!at_end && (normalized[after_pos] == '/' || normalized[after_pos] == '\\')); + + if ((at_start || before_is_sep) && (at_end || after_is_sep)) { + return true; + } + pos++; + } + + return false; +} + +bool PathSandbox::IsAbsolutePath(const std::string& path) { + if (path.empty()) return false; + + // Unix absolute path + if (path[0] == '/') return true; + + // Windows absolute path (C:\ or C:/) + if (path.length() >= 2) { + char first = path[0]; + if (std::isalpha(static_cast(first)) && path[1] == ':') { + return true; + } + } + + // UNC path (\\server\share or //server/share) + if (path.length() >= 2) { + if ((path[0] == '\\' && path[1] == '\\') || + (path[0] == '/' && path[1] == '/')) { + return true; + } + } + + return false; +} + +std::string PathSandbox::NormalizePath(const std::string& path) { + std::string result = path; + + // Convert backslashes to forward slashes + std::replace(result.begin(), result.end(), '\\', '/'); + + // Remove leading ./ + while (result.length() >= 2 && result[0] == '.' && result[1] == '/') { + result = result.substr(2); + } + + // Remove duplicate slashes + std::string cleaned; + bool last_was_slash = false; + for (char c : result) { + if (c == '/') { + if (!last_was_slash) { + cleaned += c; + } + last_was_slash = true; + } else { + cleaned += c; + last_was_slash = false; + } + } + + return cleaned; +} + +bool PathSandbox::ValidatePath(const std::string& path, std::string& out_canonical) { + // Reject empty paths + if (path.empty()) { + return false; + } + + // Reject absolute paths + if (IsAbsolutePath(path)) { + return false; + } + + // Reject traversal attempts + if (ContainsTraversal(path)) { + return false; + } + + // Normalize and resolve the path + std::string normalized = NormalizePath(path); + std::string resolved = ResolvePath(normalized); + + // Use filesystem to get canonical path (resolves any remaining .) + try { + std::filesystem::path fs_path(resolved); + + // If the file exists, use canonical path for strict checking + if (std::filesystem::exists(fs_path)) { + std::filesystem::path canonical = std::filesystem::canonical(fs_path); + std::string canonical_str = canonical.string(); + std::replace(canonical_str.begin(), canonical_str.end(), '\\', '/'); + + // Verify the canonical path is still within app_path + std::string app_canonical = std::filesystem::canonical( + std::filesystem::path(m_app_path)).string(); + std::replace(app_canonical.begin(), app_canonical.end(), '\\', '/'); + if (!app_canonical.empty() && app_canonical.back() != '/') { + app_canonical += '/'; + } + + if (canonical_str.rfind(app_canonical, 0) != 0) { + return false; // Path escaped sandbox via symlink + } + + out_canonical = canonical_str; + } else { + // File doesn't exist, just use the resolved path + out_canonical = resolved; + } + } catch (const std::filesystem::filesystem_error&) { + // Filesystem error, use the resolved path as-is + out_canonical = resolved; + } + + return true; +} + +std::string PathSandbox::ResolvePath(const std::string& relative_path) { + std::string normalized = NormalizePath(relative_path); + + // Combine with app path + std::string result = m_app_path + normalized; + + return result; +} + +//============================================================================= +// MODULE NAME VALIDATION +//============================================================================= + +bool PathSandbox::IsValidModuleName(const std::string& name) { + if (name.empty()) { + return false; + } + + // Check each character + for (size_t i = 0; i < name.length(); i++) { + char c = name[i]; + + // Allow alphanumeric + if (std::isalnum(static_cast(c))) { + continue; + } + + // Allow underscore + if (c == '_') { + continue; + } + + // Allow dot for submodules, but not at start/end or consecutive + if (c == '.') { + if (i == 0 || i == name.length() - 1) { + return false; // Dot at start or end + } + if (i > 0 && name[i-1] == '.') { + return false; // Consecutive dots + } + continue; + } + + // Any other character is invalid + return false; + } + + // Reject names that look like traversal + if (name.find("..") != std::string::npos) { + return false; + } + + return true; +} + +std::string PathSandbox::ModuleToPath(const std::string& module_name) { + // Convert dots to path separators + std::string path = module_name; + std::replace(path.begin(), path.end(), '.', '/'); + + // Add scripts/ prefix and .lua suffix + return "scripts/" + path + ".lua"; +} + +//============================================================================= +// SAFE REQUIRE +//============================================================================= + +// Registry key for PathSandbox pointer +static const char* SANDBOX_KEY = "mosis.path_sandbox"; + +// Registry key for loaded modules cache +static const char* LOADED_KEY = "mosis.loaded_modules"; + +int SafeRequire(lua_State* L) { + // Get module name + const char* module_name = luaL_checkstring(L, 1); + + // Validate module name + if (!PathSandbox::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)) { + // Module already loaded, return cached value + return 1; + } + lua_pop(L, 1); // Pop nil + } + lua_pop(L, 1); // Pop cache table (or nil if not exists) + + // Get PathSandbox from registry + lua_getfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY); + if (!lua_islightuserdata(L, -1)) { + return luaL_error(L, "require not properly initialized"); + } + PathSandbox* sandbox = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + // Convert module name to path + std::string relative_path = PathSandbox::ModuleToPath(module_name); + + // Validate the path + std::string canonical; + if (!sandbox->ValidatePath(relative_path, canonical)) { + return luaL_error(L, "cannot load module '%s': path validation failed", module_name); + } + + // Read the file + std::ifstream file(canonical); + if (!file.is_open()) { + // Try with the resolved path directly (in case canonical check failed) + std::string resolved = sandbox->ResolvePath(relative_path); + file.open(resolved); + if (!file.is_open()) { + return luaL_error(L, "module '%s' not found", module_name); + } + } + + 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); // Propagate error + } + + // 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)) { + // Create cache table if it doesn't exist + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY); + } + + // cache[module_name] = result + lua_pushvalue(L, -2); // Push the result + lua_setfield(L, -2, module_name); + lua_pop(L, 1); // Pop cache table + + // Return the module + return 1; +} + +void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox) { + // Store PathSandbox pointer in registry + lua_pushlightuserdata(L, sandbox); + lua_setfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY); + + // Create loaded modules cache + lua_newtable(L); + lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY); + + // Register require function + lua_pushcfunction(L, SafeRequire); + lua_setglobal(L, "require"); +} + +} // namespace mosis diff --git a/src/main/cpp/sandbox/path_sandbox.h b/src/main/cpp/sandbox/path_sandbox.h new file mode 100644 index 0000000..3eb7d59 --- /dev/null +++ b/src/main/cpp/sandbox/path_sandbox.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +struct lua_State; + +namespace mosis { + +class PathSandbox { +public: + explicit PathSandbox(const std::string& app_path); + + // Validate a path is within the sandbox + // Returns true if valid, sets out_canonical to the resolved path + bool ValidatePath(const std::string& path, std::string& out_canonical); + + // Check if path contains traversal attempts (..) + static bool ContainsTraversal(const std::string& path); + + // Check if path is absolute + static bool IsAbsolutePath(const std::string& path); + + // Normalize path separators and remove redundant ./ components + static std::string NormalizePath(const std::string& path); + + // Validate module name for require() - alphanumeric, underscore, dots only + static bool IsValidModuleName(const std::string& name); + + // Convert module name to relative path (e.g., "ui.button" -> "scripts/ui/button.lua") + static std::string ModuleToPath(const std::string& module_name); + + // Get the app's base path + const std::string& GetAppPath() const { return m_app_path; } + + // Resolve a relative path to full path within sandbox + std::string ResolvePath(const std::string& relative_path); + +private: + std::string m_app_path; +}; + +// Safe require implementation for Lua +// Loads modules only from app_path/scripts/.lua +// Caches modules in registry +int SafeRequire(lua_State* L); + +// Register safe require as global "require" +// The PathSandbox pointer is stored in registry for use by SafeRequire +void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox); + +} // namespace mosis diff --git a/src/main/cpp/sandbox/permission_gate.cpp b/src/main/cpp/sandbox/permission_gate.cpp new file mode 100644 index 0000000..517031f --- /dev/null +++ b/src/main/cpp/sandbox/permission_gate.cpp @@ -0,0 +1,197 @@ +#include "permission_gate.h" +#include "lua_sandbox.h" + +#include +#include + +namespace mosis { + +//============================================================================= +// PERMISSION DATABASE +//============================================================================= + +static const std::unordered_map PERMISSIONS = { + // Normal permissions (auto-granted when declared) + {"internet", {PermissionCategory::Normal, "Access the internet"}}, + {"vibrate", {PermissionCategory::Normal, "Vibrate the device"}}, + {"wake_lock", {PermissionCategory::Normal, "Keep device awake"}}, + {"notifications", {PermissionCategory::Normal, "Show notifications"}}, + {"alarms", {PermissionCategory::Normal, "Set alarms"}}, + {"nfc", {PermissionCategory::Normal, "Use NFC"}}, + + // Dangerous permissions (require user consent) + {"camera", {PermissionCategory::Dangerous, "Access the camera"}}, + {"microphone", {PermissionCategory::Dangerous, "Record audio"}}, + {"location.fine", {PermissionCategory::Dangerous, "Access precise location"}}, + {"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}}, + {"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}}, + {"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}}, + {"storage.external", {PermissionCategory::Dangerous, "Access external storage"}}, + {"storage.shared", {PermissionCategory::Dangerous, "Access shared storage"}}, + {"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}}, + {"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}}, + {"bluetooth.scan", {PermissionCategory::Dangerous, "Scan for Bluetooth devices"}}, + {"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}}, + {"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}}, + {"phone.call", {PermissionCategory::Dangerous, "Make phone calls"}}, + {"phone.read_state", {PermissionCategory::Dangerous, "Read phone state"}}, + {"sms.read", {PermissionCategory::Dangerous, "Read SMS messages"}}, + {"sms.send", {PermissionCategory::Dangerous, "Send SMS messages"}}, + + // Signature permissions (system apps only) + {"system.settings", {PermissionCategory::Signature, "Modify system settings"}}, + {"system.install", {PermissionCategory::Signature, "Install apps"}}, + {"system.uninstall", {PermissionCategory::Signature, "Uninstall apps"}}, + {"system.admin", {PermissionCategory::Signature, "Device administrator"}}, + {"system.overlay", {PermissionCategory::Signature, "Display over other apps"}}, + {"system.wallpaper", {PermissionCategory::Signature, "Set wallpaper"}}, +}; + +//============================================================================= +// CONSTRUCTOR +//============================================================================= + +PermissionGate::PermissionGate(const SandboxContext& context) + : m_context(context) + , m_last_gesture(std::chrono::steady_clock::time_point::min()) +{ +} + +//============================================================================= +// PERMISSION INFO +//============================================================================= + +PermissionCategory PermissionGate::GetCategory(const std::string& permission) { + auto it = PERMISSIONS.find(permission); + if (it != PERMISSIONS.end()) { + return it->second.category; + } + // Unknown permissions default to Dangerous for safety + return PermissionCategory::Dangerous; +} + +const PermissionInfo* PermissionGate::GetPermissionInfo(const std::string& permission) { + auto it = PERMISSIONS.find(permission); + if (it != PERMISSIONS.end()) { + return &it->second; + } + return nullptr; +} + +//============================================================================= +// PERMISSION CHECKING +//============================================================================= + +bool PermissionGate::HasPermission(const std::string& permission) const { + auto category = GetCategory(permission); + + switch (category) { + case PermissionCategory::Normal: + return CheckNormalPermission(permission); + case PermissionCategory::Dangerous: + return CheckDangerousPermission(permission); + case PermissionCategory::Signature: + return CheckSignaturePermission(permission); + } + return false; +} + +bool PermissionGate::Check(lua_State* L, const std::string& permission) { + if (!HasPermission(permission)) { + luaL_error(L, "permission denied: %s", permission.c_str()); + return false; + } + return true; +} + +bool PermissionGate::IsDeclared(const std::string& permission) const { + const auto& declared = m_context.permissions; + return std::find(declared.begin(), declared.end(), permission) != declared.end(); +} + +bool PermissionGate::CheckNormalPermission(const std::string& permission) const { + // Normal permissions are auto-granted if declared in manifest + return IsDeclared(permission); +} + +bool PermissionGate::CheckDangerousPermission(const std::string& permission) const { + // Must be declared in manifest + if (!IsDeclared(permission)) { + return false; + } + + // System apps get dangerous permissions automatically + if (m_context.is_system_app) { + return true; + } + + // Regular apps need runtime grant + return m_runtime_grants.count(permission) > 0; +} + +bool PermissionGate::CheckSignaturePermission(const std::string& permission) const { + // Only system apps get signature permissions + if (!m_context.is_system_app) { + return false; + } + + // Must still be declared + return IsDeclared(permission); +} + +//============================================================================= +// USER GESTURE TRACKING +//============================================================================= + +void PermissionGate::RecordUserGesture() { + m_last_gesture = std::chrono::steady_clock::now(); +} + +bool PermissionGate::HasRecentUserGesture(int ms) const { + // If no gesture has been recorded, return false + if (m_last_gesture == std::chrono::steady_clock::time_point::min()) { + return false; + } + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - m_last_gesture); + return elapsed.count() < ms; +} + +//============================================================================= +// RUNTIME GRANTS +//============================================================================= + +void PermissionGate::GrantPermission(const std::string& permission) { + // Can only grant dangerous permissions + auto category = GetCategory(permission); + if (category == PermissionCategory::Dangerous) { + m_runtime_grants.insert(permission); + } +} + +void PermissionGate::RevokePermission(const std::string& permission) { + m_runtime_grants.erase(permission); +} + +//============================================================================= +// QUERIES +//============================================================================= + +const std::vector& PermissionGate::GetDeclaredPermissions() const { + return m_context.permissions; +} + +std::vector PermissionGate::GetGrantedPermissions() const { + std::vector granted; + + for (const auto& perm : m_context.permissions) { + if (HasPermission(perm)) { + granted.push_back(perm); + } + } + + return granted; +} + +} // namespace mosis diff --git a/src/main/cpp/sandbox/permission_gate.h b/src/main/cpp/sandbox/permission_gate.h new file mode 100644 index 0000000..0160353 --- /dev/null +++ b/src/main/cpp/sandbox/permission_gate.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +struct SandboxContext; // Forward declaration + +enum class PermissionCategory { + Normal, // Auto-granted when declared (e.g., internet, vibrate) + Dangerous, // Requires user consent (e.g., camera, location) + Signature // System apps only (e.g., system.settings) +}; + +struct PermissionInfo { + PermissionCategory category; + std::string description; +}; + +class PermissionGate { +public: + explicit PermissionGate(const SandboxContext& context); + + // Check if app has permission (throws Lua error if not) + bool Check(lua_State* L, const std::string& permission); + + // Check without throwing (returns false if denied) + bool HasPermission(const std::string& permission) const; + + // Get permission category + static PermissionCategory GetCategory(const std::string& permission); + + // Get permission info (returns nullptr if unknown) + static const PermissionInfo* GetPermissionInfo(const std::string& permission); + + // User gesture tracking + void RecordUserGesture(); + bool HasRecentUserGesture(int ms = 5000) const; + + // Runtime permission grant (called after user consent) + void GrantPermission(const std::string& permission); + void RevokePermission(const std::string& permission); + + // Get all declared permissions + const std::vector& GetDeclaredPermissions() const; + + // Get all granted permissions + std::vector GetGrantedPermissions() const; + + // Check if permission is declared in manifest + bool IsDeclared(const std::string& permission) const; + +private: + const SandboxContext& m_context; + std::unordered_set m_runtime_grants; // Runtime-granted dangerous perms + std::chrono::steady_clock::time_point m_last_gesture; + + bool CheckNormalPermission(const std::string& permission) const; + bool CheckDangerousPermission(const std::string& permission) const; + bool CheckSignaturePermission(const std::string& permission) const; +}; + +} // namespace mosis + +// Convenience alias +using PermissionGate = mosis::PermissionGate; +using PermissionCategory = mosis::PermissionCategory; diff --git a/src/main/cpp/sandbox/rate_limiter.cpp b/src/main/cpp/sandbox/rate_limiter.cpp new file mode 100644 index 0000000..1eaf29a --- /dev/null +++ b/src/main/cpp/sandbox/rate_limiter.cpp @@ -0,0 +1,209 @@ +#include "rate_limiter.h" + +#include + +namespace mosis { + +//============================================================================= +// CONSTRUCTOR (with default limits) +//============================================================================= + +RateLimiter::RateLimiter() { + // Network operations + SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100 + SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10 + SetLimit("network.download", {5.0, 20.0}); // 5/sec, burst 20 + + // Storage operations + SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500 + SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100 + SetLimit("storage.delete", {10.0, 50.0}); // 10/sec, burst 50 + SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200 + + // Hardware access + SetLimit("camera.capture", {30.0, 30.0}); // 30 fps max + SetLimit("microphone.record", {1.0, 1.0}); // 1 session at a time + SetLimit("location.request", {1.0, 5.0}); // 1/sec, burst 5 + SetLimit("sensor.read", {60.0, 60.0}); // 60 Hz max + + // Timers + SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100 + + // Crypto + SetLimit("crypto.random", {100.0, 1000.0}); // 100/sec, burst 1000 + SetLimit("crypto.hash", {100.0, 1000.0}); // 100/sec, burst 1000 +} + +//============================================================================= +// CONFIGURATION +//============================================================================= + +void RateLimiter::SetLimit(const std::string& operation, const RateLimitConfig& config) { + std::lock_guard lock(m_mutex); + m_configs[operation] = config; +} + +const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const { + std::lock_guard lock(m_mutex); + auto it = m_configs.find(operation); + if (it != m_configs.end()) { + return &it->second; + } + return nullptr; +} + +//============================================================================= +// CHECKING +//============================================================================= + +bool RateLimiter::Check(const std::string& app_id, const std::string& operation) { + std::lock_guard lock(m_mutex); + + // Find config + auto config_it = m_configs.find(operation); + if (config_it == m_configs.end()) { + // No limit configured, allow by default + return true; + } + + const auto& config = config_it->second; + auto& bucket = GetBucket(app_id, operation); + + // Refill based on elapsed time + Refill(bucket, config); + + // Check if we have a token + if (bucket.tokens >= 1.0) { + bucket.tokens -= 1.0; + return true; + } + + return false; +} + +bool RateLimiter::CanProceed(const std::string& app_id, const std::string& operation) const { + std::lock_guard lock(m_mutex); + + // Find config + auto config_it = m_configs.find(operation); + if (config_it == m_configs.end()) { + return true; // No limit + } + + const auto& config = config_it->second; + std::string key = MakeKey(app_id, operation); + + auto bucket_it = m_buckets.find(key); + if (bucket_it == m_buckets.end()) { + return true; // New bucket would have full tokens + } + + // Make a copy to check without modifying + Bucket bucket = bucket_it->second; + Refill(bucket, config); + + return bucket.tokens >= 1.0; +} + +double RateLimiter::GetTokens(const std::string& app_id, const std::string& operation) const { + std::lock_guard lock(m_mutex); + + std::string key = MakeKey(app_id, operation); + auto bucket_it = m_buckets.find(key); + + if (bucket_it == m_buckets.end()) { + // Check if there's a config + auto config_it = m_configs.find(operation); + if (config_it != m_configs.end()) { + return config_it->second.max_tokens; // Would start with full + } + return 0.0; + } + + // Find config to refill + auto config_it = m_configs.find(operation); + if (config_it == m_configs.end()) { + return bucket_it->second.tokens; + } + + // Make a copy to check without modifying + Bucket bucket = bucket_it->second; + Refill(bucket, config_it->second); + + return bucket.tokens; +} + +//============================================================================= +// RESET +//============================================================================= + +void RateLimiter::ResetApp(const std::string& app_id) { + std::lock_guard lock(m_mutex); + + // Find and remove all buckets for this app + std::string prefix = app_id + ":"; + for (auto it = m_buckets.begin(); it != m_buckets.end(); ) { + if (it->first.rfind(prefix, 0) == 0) { // starts with app_id: + it = m_buckets.erase(it); + } else { + ++it; + } + } +} + +void RateLimiter::ClearAll() { + std::lock_guard lock(m_mutex); + m_buckets.clear(); +} + +//============================================================================= +// INTERNAL +//============================================================================= + +void RateLimiter::Refill(Bucket& bucket, const RateLimitConfig& config) const { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration(now - bucket.last_refill); + + // Add tokens based on elapsed time + double new_tokens = bucket.tokens + (elapsed.count() * config.tokens_per_second); + + // Cap at max + bucket.tokens = std::min(new_tokens, config.max_tokens); + bucket.last_refill = now; +} + +RateLimiter::Bucket& RateLimiter::GetBucket(const std::string& app_id, + const std::string& operation) { + std::string key = MakeKey(app_id, operation); + + auto it = m_buckets.find(key); + if (it != m_buckets.end()) { + return it->second; + } + + // Create new bucket with full tokens + auto config_it = m_configs.find(operation); + double initial = (config_it != m_configs.end()) ? config_it->second.max_tokens : 1.0; + + m_buckets[key] = Bucket{ + .tokens = initial, + .last_refill = std::chrono::steady_clock::now() + }; + + return m_buckets[key]; +} + +std::string RateLimiter::MakeKey(const std::string& app_id, const std::string& operation) { + return app_id + ":" + operation; +} + +//============================================================================= +// GLOBAL INSTANCE +//============================================================================= + +RateLimiter& GetRateLimiter() { + static RateLimiter instance; + return instance; +} + +} // namespace mosis diff --git a/src/main/cpp/sandbox/rate_limiter.h b/src/main/cpp/sandbox/rate_limiter.h new file mode 100644 index 0000000..a068e0b --- /dev/null +++ b/src/main/cpp/sandbox/rate_limiter.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +namespace mosis { + +struct RateLimitConfig { + double tokens_per_second; // Refill rate + double max_tokens; // Bucket capacity +}; + +class RateLimiter { +public: + // Default limits for common operations + RateLimiter(); + + // Check if operation is allowed (consumes token if yes) + bool Check(const std::string& app_id, const std::string& operation); + + // Check without consuming token + bool CanProceed(const std::string& app_id, const std::string& operation) const; + + // Configure limits for an operation + void SetLimit(const std::string& operation, const RateLimitConfig& config); + + // Get config for an operation + const RateLimitConfig* GetLimit(const std::string& operation) const; + + // Get current token count for app+operation + double GetTokens(const std::string& app_id, const std::string& operation) const; + + // Reset all buckets for an app (e.g., on app restart) + void ResetApp(const std::string& app_id); + + // Clear all buckets + void ClearAll(); + +private: + struct Bucket { + double tokens; + std::chrono::steady_clock::time_point last_refill; + }; + + // Refill bucket based on elapsed time + void Refill(Bucket& bucket, const RateLimitConfig& config) const; + + // Get or create bucket for app+operation + Bucket& GetBucket(const std::string& app_id, const std::string& operation); + + // Get bucket key + static std::string MakeKey(const std::string& app_id, const std::string& operation); + + mutable std::mutex m_mutex; + std::unordered_map m_configs; + mutable std::unordered_map m_buckets; +}; + +// Global rate limiter (singleton) +RateLimiter& GetRateLimiter(); + +} // namespace mosis + +// Convenience alias +using RateLimiter = mosis::RateLimiter; +using RateLimitConfig = mosis::RateLimitConfig; diff --git a/src/main/cpp/sandbox/timer_manager.cpp b/src/main/cpp/sandbox/timer_manager.cpp new file mode 100644 index 0000000..a581bc4 --- /dev/null +++ b/src/main/cpp/sandbox/timer_manager.cpp @@ -0,0 +1,440 @@ +#include "timer_manager.h" + +#include +#include + +namespace mosis { + +//============================================================================= +// CONSTRUCTOR / DESTRUCTOR +//============================================================================= + +TimerManager::TimerManager() = default; + +TimerManager::~TimerManager() { + std::lock_guard lock(m_mutex); + + // Release all Lua callback references + for (auto& timer : m_timers) { + if (timer.callback_ref != LUA_NOREF && timer.L) { + luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref); + } + } + m_timers.clear(); +} + +//============================================================================= +// TIMER CREATION +//============================================================================= + +TimerId TimerManager::SetTimeout(lua_State* L, const std::string& app_id, + int callback_ref, int delay_ms) { + std::lock_guard lock(m_mutex); + + // Check per-app limit + if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) { + // Release the callback reference since we're not using it + luaL_unref(L, LUA_REGISTRYINDEX, callback_ref); + return 0; + } + + // Clamp delay + if (delay_ms < MIN_TIMEOUT_MS) { + delay_ms = MIN_TIMEOUT_MS; + } + + Timer timer; + timer.id = m_next_id++; + timer.app_id = app_id; + timer.fire_time = std::chrono::steady_clock::now() + Duration(delay_ms); + timer.interval = Duration(0); + timer.callback_ref = callback_ref; + timer.L = L; + timer.cancelled = false; + timer.is_interval = false; + + m_timers.push_back(timer); + m_app_timer_counts[app_id]++; + m_app_timer_ids[app_id].insert(timer.id); + + return timer.id; +} + +TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id, + int callback_ref, int interval_ms) { + std::lock_guard lock(m_mutex); + + // Check per-app limit + if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) { + luaL_unref(L, LUA_REGISTRYINDEX, callback_ref); + return 0; + } + + // Clamp interval to minimum + if (interval_ms < MIN_INTERVAL_MS) { + interval_ms = MIN_INTERVAL_MS; + } + + Timer timer; + timer.id = m_next_id++; + timer.app_id = app_id; + timer.fire_time = std::chrono::steady_clock::now() + Duration(interval_ms); + timer.interval = Duration(interval_ms); + timer.callback_ref = callback_ref; + timer.L = L; + timer.cancelled = false; + timer.is_interval = true; + + m_timers.push_back(timer); + m_app_timer_counts[app_id]++; + m_app_timer_ids[app_id].insert(timer.id); + + return timer.id; +} + +//============================================================================= +// TIMER CANCELLATION +//============================================================================= + +bool TimerManager::ClearTimer(const std::string& app_id, TimerId id) { + std::lock_guard lock(m_mutex); + + // Find the timer + auto it = std::find_if(m_timers.begin(), m_timers.end(), + [id, &app_id](const Timer& t) { + return t.id == id && t.app_id == app_id && !t.cancelled; + }); + + if (it == m_timers.end()) { + return false; + } + + // Mark as cancelled (will be removed during ProcessTimers) + it->cancelled = true; + + // Release the Lua callback reference + if (it->callback_ref != LUA_NOREF && it->L) { + luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref); + it->callback_ref = LUA_NOREF; + } + + // Update counts + if (m_app_timer_counts[app_id] > 0) { + m_app_timer_counts[app_id]--; + } + m_app_timer_ids[app_id].erase(id); + + return true; +} + +void TimerManager::ClearAppTimers(const std::string& app_id) { + std::lock_guard lock(m_mutex); + + // Get all timer IDs for this app + auto it = m_app_timer_ids.find(app_id); + if (it == m_app_timer_ids.end()) { + return; + } + + // Mark all timers as cancelled and release references + for (auto& timer : m_timers) { + if (timer.app_id == app_id && !timer.cancelled) { + timer.cancelled = true; + if (timer.callback_ref != LUA_NOREF && timer.L) { + luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref); + timer.callback_ref = LUA_NOREF; + } + } + } + + // Clear tracking + m_app_timer_counts[app_id] = 0; + m_app_timer_ids[app_id].clear(); +} + +//============================================================================= +// TIMER PROCESSING +//============================================================================= + +void TimerManager::FireTimer(Timer& timer) { + if (timer.cancelled || timer.callback_ref == LUA_NOREF || !timer.L) { + return; + } + + lua_State* L = timer.L; + + // Get the callback from registry + lua_rawgeti(L, LUA_REGISTRYINDEX, timer.callback_ref); + + if (lua_isfunction(L, -1)) { + // Call the callback with protected call + int result = lua_pcall(L, 0, 0, 0); + if (result != LUA_OK) { + // Log error but don't propagate + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } +} + +void TimerManager::RescheduleInterval(Timer& timer) { + // Update fire time for next interval + timer.fire_time = std::chrono::steady_clock::now() + timer.interval; +} + +int TimerManager::ProcessTimers() { + // We need to be careful here - firing a timer might cause + // new timers to be added or timers to be cancelled + + std::vector to_fire; + std::vector to_reschedule; + std::vector to_remove; + + auto now = std::chrono::steady_clock::now(); + + { + std::lock_guard lock(m_mutex); + + // Find all timers that should fire + for (auto& timer : m_timers) { + if (timer.cancelled) { + to_remove.push_back(timer.id); + } else if (timer.fire_time <= now) { + to_fire.push_back(timer); + if (timer.is_interval) { + to_reschedule.push_back(timer.id); + } else { + to_remove.push_back(timer.id); + } + } + } + } + + // Fire timers outside the lock to allow callbacks to create new timers + int fired_count = 0; + for (auto& timer : to_fire) { + FireTimer(timer); + fired_count++; + } + + { + std::lock_guard lock(m_mutex); + + // Reschedule intervals + for (TimerId id : to_reschedule) { + auto it = std::find_if(m_timers.begin(), m_timers.end(), + [id](const Timer& t) { return t.id == id && !t.cancelled; }); + if (it != m_timers.end()) { + RescheduleInterval(*it); + } + } + + // Remove completed/cancelled timers + for (TimerId id : to_remove) { + auto it = std::find_if(m_timers.begin(), m_timers.end(), + [id](const Timer& t) { return t.id == id; }); + if (it != m_timers.end()) { + // Release reference if not already released + if (it->callback_ref != LUA_NOREF && it->L && !it->is_interval) { + luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref); + } + + // Update counts only for non-cancelled (timeout) timers + if (!it->cancelled && !it->is_interval) { + const std::string& app_id = it->app_id; + if (m_app_timer_counts[app_id] > 0) { + m_app_timer_counts[app_id]--; + } + m_app_timer_ids[app_id].erase(id); + } + + m_timers.erase(it); + } + } + } + + return fired_count; +} + +size_t TimerManager::GetTimerCount(const std::string& app_id) const { + std::lock_guard lock(m_mutex); + + auto it = m_app_timer_counts.find(app_id); + if (it == m_app_timer_counts.end()) { + return 0; + } + return it->second; +} + +//============================================================================= +// LUA API +//============================================================================= + +// Registry keys for storing manager pointer and app_id +static const char* TIMER_MANAGER_KEY = "__mosis_timer_manager"; +static const char* TIMER_APP_ID_KEY = "__mosis_timer_app_id"; + +// setTimeout(callback, delay_ms) -> timer_id +static int lua_setTimeout(lua_State* L) { + // Check arguments + luaL_checktype(L, 1, LUA_TFUNCTION); + int delay_ms = static_cast(luaL_checkinteger(L, 2)); + + // Get timer manager from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY); + if (!lua_islightuserdata(L, -1)) { + return luaL_error(L, "timer system not initialized"); + } + TimerManager* manager = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + // Get app_id from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY); + if (!lua_isstring(L, -1)) { + return luaL_error(L, "app_id not set"); + } + std::string app_id = lua_tostring(L, -1); + lua_pop(L, 1); + + // Store the callback in registry + lua_pushvalue(L, 1); // Push the callback + int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + // Create the timer + TimerId id = manager->SetTimeout(L, app_id, callback_ref, delay_ms); + + if (id == 0) { + return luaL_error(L, "timer limit exceeded"); + } + + lua_pushinteger(L, static_cast(id)); + return 1; +} + +// clearTimeout(timer_id) +static int lua_clearTimeout(lua_State* L) { + TimerId id = static_cast(luaL_checkinteger(L, 1)); + + // Get timer manager from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY); + if (!lua_islightuserdata(L, -1)) { + return luaL_error(L, "timer system not initialized"); + } + TimerManager* manager = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + // Get app_id from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY); + if (!lua_isstring(L, -1)) { + return luaL_error(L, "app_id not set"); + } + std::string app_id = lua_tostring(L, -1); + lua_pop(L, 1); + + manager->ClearTimer(app_id, id); + + return 0; +} + +// setInterval(callback, interval_ms) -> timer_id +static int lua_setInterval(lua_State* L) { + // Check arguments + luaL_checktype(L, 1, LUA_TFUNCTION); + int interval_ms = static_cast(luaL_checkinteger(L, 2)); + + // Get timer manager from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY); + if (!lua_islightuserdata(L, -1)) { + return luaL_error(L, "timer system not initialized"); + } + TimerManager* manager = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + + // Get app_id from registry + lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY); + if (!lua_isstring(L, -1)) { + return luaL_error(L, "app_id not set"); + } + std::string app_id = lua_tostring(L, -1); + lua_pop(L, 1); + + // Store the callback in registry + lua_pushvalue(L, 1); // Push the callback + int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + // Create the timer + TimerId id = manager->SetInterval(L, app_id, callback_ref, interval_ms); + + if (id == 0) { + return luaL_error(L, "timer limit exceeded"); + } + + lua_pushinteger(L, static_cast(id)); + return 1; +} + +// clearInterval(timer_id) +static int lua_clearInterval(lua_State* L) { + // Same as clearTimeout + return lua_clearTimeout(L); +} + +void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id) { + // Store timer manager pointer in registry + lua_pushlightuserdata(L, manager); + lua_setfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY); + + // Store app_id in registry + lua_pushstring(L, app_id.c_str()); + lua_setfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY); + + // Get the real _G (not the proxy) + // We need to set these in the real global table that the proxy reads from + lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + + // Check if we're dealing with a proxy (has __index metatable) + if (lua_getmetatable(L, -1)) { + lua_getfield(L, -1, "__index"); + if (lua_istable(L, -1)) { + // We have a proxy, use the __index table as the real _G + lua_remove(L, -2); // Remove metatable + lua_remove(L, -2); // Remove proxy + + // Now top of stack is real _G + lua_pushcfunction(L, lua_setTimeout); + lua_setfield(L, -2, "setTimeout"); + + lua_pushcfunction(L, lua_clearTimeout); + lua_setfield(L, -2, "clearTimeout"); + + lua_pushcfunction(L, lua_setInterval); + lua_setfield(L, -2, "setInterval"); + + lua_pushcfunction(L, lua_clearInterval); + lua_setfield(L, -2, "clearInterval"); + + lua_pop(L, 1); // Pop real _G + return; + } + lua_pop(L, 2); // Pop __index and metatable + } + + // No proxy, just use _G directly + lua_pop(L, 1); // Pop whatever we got from LUA_RIDX_GLOBALS + + // Register as globals + lua_pushcfunction(L, lua_setTimeout); + lua_setglobal(L, "setTimeout"); + + lua_pushcfunction(L, lua_clearTimeout); + lua_setglobal(L, "clearTimeout"); + + lua_pushcfunction(L, lua_setInterval); + lua_setglobal(L, "setInterval"); + + lua_pushcfunction(L, lua_clearInterval); + lua_setglobal(L, "clearInterval"); +} + +} // namespace mosis diff --git a/src/main/cpp/sandbox/timer_manager.h b/src/main/cpp/sandbox/timer_manager.h new file mode 100644 index 0000000..99585f5 --- /dev/null +++ b/src/main/cpp/sandbox/timer_manager.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +using TimerId = uint64_t; +using TimePoint = std::chrono::steady_clock::time_point; +using Duration = std::chrono::milliseconds; + +struct Timer { + TimerId id; + std::string app_id; + TimePoint fire_time; + Duration interval; // 0 for setTimeout, >0 for setInterval + int callback_ref; // Lua registry reference + lua_State* L; // Lua state that owns the callback + bool cancelled = false; + bool is_interval = false; +}; + +class TimerManager { +public: + TimerManager(); + ~TimerManager(); + + // Non-copyable + TimerManager(const TimerManager&) = delete; + TimerManager& operator=(const TimerManager&) = delete; + + // Create timers (returns timer ID, 0 on failure) + TimerId SetTimeout(lua_State* L, const std::string& app_id, + int callback_ref, int delay_ms); + TimerId SetInterval(lua_State* L, const std::string& app_id, + int callback_ref, int interval_ms); + + // Cancel timers + bool ClearTimer(const std::string& app_id, TimerId id); + + // Cancel all timers for an app (call on app stop) + void ClearAppTimers(const std::string& app_id); + + // Process timers (call from main loop) + // Returns number of timers fired + int ProcessTimers(); + + // Get timer count for an app + size_t GetTimerCount(const std::string& app_id) const; + + // Configuration + static constexpr size_t MAX_TIMERS_PER_APP = 100; + static constexpr int MIN_INTERVAL_MS = 10; + static constexpr int MIN_TIMEOUT_MS = 0; + +private: + TimerId m_next_id = 1; + + // All timers (we use a vector and sort/search as needed) + std::vector m_timers; + + // Track timer count per app + std::unordered_map m_app_timer_counts; + + // Track which timer IDs belong to which app (for fast cancellation) + std::unordered_map> m_app_timer_ids; + + mutable std::mutex m_mutex; + + void FireTimer(Timer& timer); + void RemoveTimer(TimerId id); + void RescheduleInterval(Timer& timer); +}; + +// Lua API registration +// Registers: setTimeout, clearTimeout, setInterval, clearInterval +void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id); + +} // namespace mosis