add Lua sandbox with timer system (milestones 1-5 complete)
This commit is contained in:
188
src/main/cpp/sandbox/audit_log.cpp
Normal file
188
src/main/cpp/sandbox/audit_log.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "audit_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<std::mutex> 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<AuditEntry> AuditLog::GetEntries(size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> 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<AuditEntry> AuditLog::GetEntriesForApp(const std::string& app_id,
|
||||
size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> 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<AuditEntry> AuditLog::GetEntriesByEvent(AuditEvent event,
|
||||
size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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
|
||||
94
src/main/cpp/sandbox/audit_log.h
Normal file
94
src/main/cpp/sandbox/audit_log.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
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<AuditEntry> GetEntries(size_t count = 100) const;
|
||||
std::vector<AuditEntry> GetEntriesForApp(const std::string& app_id,
|
||||
size_t count = 100) const;
|
||||
std::vector<AuditEntry> 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<AuditEntry> 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;
|
||||
448
src/main/cpp/sandbox/lua_sandbox.cpp
Normal file
448
src/main/cpp/sandbox/lua_sandbox.cpp
Normal file
@@ -0,0 +1,448 @@
|
||||
#include "lua_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// ALLOCATOR
|
||||
//=============================================================================
|
||||
|
||||
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
|
||||
auto* sandbox = static_cast<LuaSandbox*>(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<LuaSandbox*>(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<unsigned char>(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<LuaSandbox*>(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
|
||||
101
src/main/cpp/sandbox/lua_sandbox.h
Normal file
101
src/main/cpp/sandbox/lua_sandbox.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// 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<std::string> 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;
|
||||
344
src/main/cpp/sandbox/path_sandbox.cpp
Normal file
344
src/main/cpp/sandbox/path_sandbox.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "path_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
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<unsigned char>(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<unsigned char>(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<PathSandbox*>(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
|
||||
52
src/main/cpp/sandbox/path_sandbox.h
Normal file
52
src/main/cpp/sandbox/path_sandbox.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
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/<module>.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
|
||||
197
src/main/cpp/sandbox/permission_gate.cpp
Normal file
197
src/main/cpp/sandbox/permission_gate.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
#include "permission_gate.h"
|
||||
#include "lua_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// PERMISSION DATABASE
|
||||
//=============================================================================
|
||||
|
||||
static const std::unordered_map<std::string, PermissionInfo> 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<std::chrono::milliseconds>(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<std::string>& PermissionGate::GetDeclaredPermissions() const {
|
||||
return m_context.permissions;
|
||||
}
|
||||
|
||||
std::vector<std::string> PermissionGate::GetGrantedPermissions() const {
|
||||
std::vector<std::string> granted;
|
||||
|
||||
for (const auto& perm : m_context.permissions) {
|
||||
if (HasPermission(perm)) {
|
||||
granted.push_back(perm);
|
||||
}
|
||||
}
|
||||
|
||||
return granted;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
73
src/main/cpp/sandbox/permission_gate.h
Normal file
73
src/main/cpp/sandbox/permission_gate.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
|
||||
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<std::string>& GetDeclaredPermissions() const;
|
||||
|
||||
// Get all granted permissions
|
||||
std::vector<std::string> GetGrantedPermissions() const;
|
||||
|
||||
// Check if permission is declared in manifest
|
||||
bool IsDeclared(const std::string& permission) const;
|
||||
|
||||
private:
|
||||
const SandboxContext& m_context;
|
||||
std::unordered_set<std::string> 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;
|
||||
209
src/main/cpp/sandbox/rate_limiter.cpp
Normal file
209
src/main/cpp/sandbox/rate_limiter.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "rate_limiter.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<std::mutex> lock(m_mutex);
|
||||
m_configs[operation] = config;
|
||||
}
|
||||
|
||||
const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<double>(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
|
||||
68
src/main/cpp/sandbox/rate_limiter.h
Normal file
68
src/main/cpp/sandbox/rate_limiter.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
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<std::string, RateLimitConfig> m_configs;
|
||||
mutable std::unordered_map<std::string, Bucket> m_buckets;
|
||||
};
|
||||
|
||||
// Global rate limiter (singleton)
|
||||
RateLimiter& GetRateLimiter();
|
||||
|
||||
} // namespace mosis
|
||||
|
||||
// Convenience alias
|
||||
using RateLimiter = mosis::RateLimiter;
|
||||
using RateLimitConfig = mosis::RateLimitConfig;
|
||||
440
src/main/cpp/sandbox/timer_manager.cpp
Normal file
440
src/main/cpp/sandbox/timer_manager.cpp
Normal file
@@ -0,0 +1,440 @@
|
||||
#include "timer_manager.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR / DESTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
TimerManager::TimerManager() = default;
|
||||
|
||||
TimerManager::~TimerManager() {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Timer> to_fire;
|
||||
std::vector<TimerId> to_reschedule;
|
||||
std::vector<TimerId> to_remove;
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<int>(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<TimerManager*>(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<lua_Integer>(id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// clearTimeout(timer_id)
|
||||
static int lua_clearTimeout(lua_State* L) {
|
||||
TimerId id = static_cast<TimerId>(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<TimerManager*>(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<int>(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<TimerManager*>(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<lua_Integer>(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
|
||||
87
src/main/cpp/sandbox/timer_manager.h
Normal file
87
src/main/cpp/sandbox/timer_manager.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <cstdint>
|
||||
|
||||
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<Timer> m_timers;
|
||||
|
||||
// Track timer count per app
|
||||
std::unordered_map<std::string, size_t> m_app_timer_counts;
|
||||
|
||||
// Track which timer IDs belong to which app (for fast cancellation)
|
||||
std::unordered_map<std::string, std::unordered_set<TimerId>> 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
|
||||
Reference in New Issue
Block a user